feat: 新增财务交易流水模块页面与接口

This commit is contained in:
2026-03-04 11:03:37 +08:00
parent 645a3beb47
commit 1e141d3ce0
23 changed files with 2703 additions and 0 deletions

View File

@@ -0,0 +1,197 @@
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);
}