198 lines
5.0 KiB
TypeScript
198 lines
5.0 KiB
TypeScript
import type {
|
||
FinanceTransactionFilterState,
|
||
FinanceTransactionListQueryPayload,
|
||
FinanceTransactionQueryPayload,
|
||
QuickDateRangeKey,
|
||
} from '../../types';
|
||
|
||
/**
|
||
* 文件职责:交易流水页面纯函数与数据转换工具。
|
||
*/
|
||
import type {
|
||
FinanceTransactionChannelFilter,
|
||
FinanceTransactionPaymentFilter,
|
||
FinanceTransactionTypeFilter,
|
||
} from '#/api/finance';
|
||
|
||
function formatDate(date: Date) {
|
||
const year = date.getFullYear();
|
||
const month = `${date.getMonth() + 1}`.padStart(2, '0');
|
||
const day = `${date.getDate()}`.padStart(2, '0');
|
||
return `${year}-${month}-${day}`;
|
||
}
|
||
|
||
function toDateOnly(date: Date) {
|
||
return new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
||
}
|
||
|
||
function shiftDate(date: Date, dayOffset: number) {
|
||
const next = new Date(date);
|
||
next.setDate(next.getDate() + dayOffset);
|
||
return next;
|
||
}
|
||
|
||
function normalizeType(
|
||
value: FinanceTransactionTypeFilter,
|
||
): FinanceTransactionTypeFilter | undefined {
|
||
return value === 'all' ? undefined : value;
|
||
}
|
||
|
||
function normalizeChannel(
|
||
value: FinanceTransactionChannelFilter,
|
||
): FinanceTransactionChannelFilter | undefined {
|
||
return value === 'all' ? undefined : value;
|
||
}
|
||
|
||
function normalizePayment(
|
||
value: FinanceTransactionPaymentFilter,
|
||
): FinanceTransactionPaymentFilter | undefined {
|
||
return value === 'all' ? undefined : value;
|
||
}
|
||
|
||
/** 获取今天日期字符串(yyyy-MM-dd)。 */
|
||
export function getTodayDateString() {
|
||
return formatDate(new Date());
|
||
}
|
||
|
||
/** 根据快捷日期键计算筛选起止日期。 */
|
||
export function resolveQuickRangeDateRange(value: QuickDateRangeKey) {
|
||
const today = toDateOnly(new Date());
|
||
let start = today;
|
||
let end = today;
|
||
|
||
switch (value) {
|
||
case '7d': {
|
||
start = shiftDate(today, -6);
|
||
|
||
break;
|
||
}
|
||
case '30d': {
|
||
start = shiftDate(today, -29);
|
||
|
||
break;
|
||
}
|
||
case 'month': {
|
||
start = new Date(today.getFullYear(), today.getMonth(), 1);
|
||
|
||
break;
|
||
}
|
||
case 'yesterday': {
|
||
start = shiftDate(today, -1);
|
||
end = start;
|
||
|
||
break;
|
||
}
|
||
// No default
|
||
}
|
||
|
||
return {
|
||
startDate: formatDate(start),
|
||
endDate: formatDate(end),
|
||
};
|
||
}
|
||
|
||
/** 构建交易流水筛选请求。 */
|
||
export function buildFilterQueryPayload(
|
||
storeId: string,
|
||
filters: FinanceTransactionFilterState,
|
||
): FinanceTransactionQueryPayload {
|
||
return {
|
||
storeId,
|
||
startDate: filters.startDate || undefined,
|
||
endDate: filters.endDate || undefined,
|
||
type: normalizeType(filters.type),
|
||
channel: normalizeChannel(filters.channel),
|
||
paymentMethod: normalizePayment(filters.paymentMethod),
|
||
keyword: filters.keyword.trim() || undefined,
|
||
};
|
||
}
|
||
|
||
/** 构建交易流水列表请求。 */
|
||
export function buildListQueryPayload(
|
||
storeId: string,
|
||
filters: FinanceTransactionFilterState,
|
||
page: number,
|
||
pageSize: number,
|
||
): FinanceTransactionListQueryPayload {
|
||
return {
|
||
...buildFilterQueryPayload(storeId, filters),
|
||
page,
|
||
pageSize,
|
||
};
|
||
}
|
||
|
||
/** 判断日期范围是否合法。 */
|
||
export function isDateRangeInvalid(filters: FinanceTransactionFilterState) {
|
||
if (!filters.startDate || !filters.endDate) {
|
||
return false;
|
||
}
|
||
return filters.startDate > filters.endDate;
|
||
}
|
||
|
||
/** 货币格式化(人民币)。 */
|
||
export function formatCurrency(value: number) {
|
||
return new Intl.NumberFormat('zh-CN', {
|
||
style: 'currency',
|
||
currency: 'CNY',
|
||
minimumFractionDigits: 2,
|
||
maximumFractionDigits: 2,
|
||
}).format(Number.isFinite(value) ? value : 0);
|
||
}
|
||
|
||
/** 交易金额(带符号)格式化。 */
|
||
export function formatSignedAmount(value: number, isIncome: boolean) {
|
||
const normalized = Number.isFinite(value) ? value : 0;
|
||
if (normalized === 0) {
|
||
return formatCurrency(0);
|
||
}
|
||
|
||
if (normalized > 0 || isIncome) {
|
||
return `+${formatCurrency(Math.abs(normalized))}`;
|
||
}
|
||
|
||
return `-${formatCurrency(Math.abs(normalized))}`;
|
||
}
|
||
|
||
/** 金额视觉色调。 */
|
||
export function resolveAmountToneClass(value: number, isIncome: boolean) {
|
||
if (value > 0 || isIncome) {
|
||
return 'income';
|
||
}
|
||
if (value < 0) {
|
||
return 'expense';
|
||
}
|
||
return 'neutral';
|
||
}
|
||
|
||
/** 交易类型标签颜色。 */
|
||
export function resolveTransactionTypeTagColor(type: string) {
|
||
if (type === 'income') return 'green';
|
||
if (type === 'refund') return 'red';
|
||
if (type === 'stored_card_recharge') return 'blue';
|
||
if (type === 'point_redeem') return 'orange';
|
||
return 'default';
|
||
}
|
||
|
||
function decodeBase64ToBlob(base64: string) {
|
||
const binary = atob(base64);
|
||
const bytes = new Uint8Array(binary.length);
|
||
for (let index = 0; index < binary.length; index += 1) {
|
||
bytes[index] = binary.codePointAt(index) ?? 0;
|
||
}
|
||
return new Blob([bytes], { type: 'text/csv;charset=utf-8;' });
|
||
}
|
||
|
||
/** 下载 Base64 编码文件。 */
|
||
export function downloadBase64File(
|
||
fileName: string,
|
||
fileContentBase64: string,
|
||
) {
|
||
const blob = decodeBase64ToBlob(fileContentBase64);
|
||
const url = URL.createObjectURL(blob);
|
||
const anchor = document.createElement('a');
|
||
anchor.href = url;
|
||
anchor.download = fileName;
|
||
anchor.click();
|
||
URL.revokeObjectURL(url);
|
||
}
|