feat(@vben/web-antd): add finance settlement page module

This commit is contained in:
2026-03-04 15:52:25 +08:00
parent 15d4272d1f
commit 4690ccdd9d
21 changed files with 1705 additions and 0 deletions

View File

@@ -0,0 +1,55 @@
import type { FinanceSettlementFilterState, OptionItem } from '../../types';
import type {
FinanceSettlementAccountDto,
FinanceSettlementChannelFilter,
FinanceSettlementStatsDto,
} from '#/api/finance';
/**
* 文件职责:到账查询页面常量与默认状态定义。
*/
import { getTodayDateString } from './helpers';
/** 到账查询查看权限。 */
export const FINANCE_SETTLEMENT_VIEW_PERMISSION =
'tenant:finance:settlement:view';
/** 到账查询导出权限。 */
export const FINANCE_SETTLEMENT_EXPORT_PERMISSION =
'tenant:finance:settlement:export';
/** 到账渠道筛选项。 */
export const SETTLEMENT_CHANNEL_OPTIONS: OptionItem[] = [
{ label: '全部渠道', value: 'all' },
{ label: '微信支付', value: 'wechat' },
{ label: '支付宝', value: 'alipay' },
];
/** 默认筛选状态。 */
export function createDefaultFilters(): FinanceSettlementFilterState {
const today = getTodayDateString();
return {
channel: 'all' as FinanceSettlementChannelFilter,
startDate: today,
endDate: today,
};
}
/** 默认统计数据。 */
export const DEFAULT_STATS: FinanceSettlementStatsDto = {
todayArrivedAmount: 0,
yesterdayArrivedAmount: 0,
currentMonthArrivedAmount: 0,
currentMonthTransactionCount: 0,
};
/** 默认账户信息。 */
export const DEFAULT_ACCOUNT: FinanceSettlementAccountDto = {
bankName: '',
bankAccountName: '',
bankAccountNoMasked: '',
wechatMerchantNoMasked: '',
alipayPidMasked: '',
settlementPeriodText: '',
};

View File

@@ -0,0 +1,133 @@
import type {
FinanceSettlementFilterState,
FinanceSettlementPaginationState,
} from '../../types';
import type {
FinanceSettlementAccountDto,
FinanceSettlementListItemDto,
FinanceSettlementStatsDto,
} from '#/api/finance';
import type { StoreListItemDto } from '#/api/store';
/**
* 文件职责:到账查询页面数据加载动作。
*/
import {
getFinanceSettlementAccountApi,
getFinanceSettlementListApi,
getFinanceSettlementStatsApi,
} from '#/api/finance';
import { getStoreListApi } from '#/api/store';
import { buildListQueryPayload } from './helpers';
interface DataActionOptions {
account: { value: FinanceSettlementAccountDto | null };
filters: FinanceSettlementFilterState;
isAccountLoading: { value: boolean };
isListLoading: { value: boolean };
isStatsLoading: { value: boolean };
isStoreLoading: { value: boolean };
pagination: FinanceSettlementPaginationState;
rows: { value: FinanceSettlementListItemDto[] };
selectedStoreId: { value: string };
stats: FinanceSettlementStatsDto;
stores: { value: StoreListItemDto[] };
}
/** 创建数据相关动作。 */
export function createDataActions(options: DataActionOptions) {
function resetStats() {
options.stats.todayArrivedAmount = 0;
options.stats.yesterdayArrivedAmount = 0;
options.stats.currentMonthArrivedAmount = 0;
options.stats.currentMonthTransactionCount = 0;
}
function clearPageData() {
options.rows.value = [];
options.pagination.total = 0;
resetStats();
}
async function loadStores() {
options.isStoreLoading.value = true;
try {
const result = await getStoreListApi({ page: 1, pageSize: 200 });
options.stores.value = result.items;
if (result.items.length === 0) {
options.selectedStoreId.value = '';
clearPageData();
return;
}
const matched = result.items.some(
(item) => item.id === options.selectedStoreId.value,
);
if (!matched) {
options.selectedStoreId.value = result.items[0]?.id ?? '';
}
} finally {
options.isStoreLoading.value = false;
}
}
async function loadAccount() {
options.isAccountLoading.value = true;
try {
options.account.value = await getFinanceSettlementAccountApi();
} finally {
options.isAccountLoading.value = false;
}
}
async function loadPageData() {
if (!options.selectedStoreId.value) {
clearPageData();
return;
}
const storeId = options.selectedStoreId.value;
const listPayload = buildListQueryPayload(
storeId,
options.filters,
options.pagination.page,
options.pagination.pageSize,
);
options.isListLoading.value = true;
options.isStatsLoading.value = true;
try {
const [listResult, statsResult] = await Promise.all([
getFinanceSettlementListApi(listPayload),
getFinanceSettlementStatsApi({ storeId }),
]);
options.rows.value = listResult.items;
options.pagination.total = listResult.total;
options.pagination.page = listResult.page;
options.pagination.pageSize = listResult.pageSize;
options.stats.todayArrivedAmount = statsResult.todayArrivedAmount;
options.stats.yesterdayArrivedAmount = statsResult.yesterdayArrivedAmount;
options.stats.currentMonthArrivedAmount =
statsResult.currentMonthArrivedAmount;
options.stats.currentMonthTransactionCount =
statsResult.currentMonthTransactionCount;
} finally {
options.isListLoading.value = false;
options.isStatsLoading.value = false;
}
}
return {
clearPageData,
loadAccount,
loadPageData,
loadStores,
resetStats,
};
}

View File

@@ -0,0 +1,76 @@
import type {
FinanceSettlementDetailStateMap,
FinanceSettlementExpandAction,
} from '../../types';
/**
* 文件职责:到账查询展开明细动作。
*/
import { getFinanceSettlementDetailApi } from '#/api/finance';
import { getSettlementRowKey } from './helpers';
interface DetailActionOptions {
detailStates: FinanceSettlementDetailStateMap;
expandedRowKeys: { value: string[] };
selectedStoreId: { value: string };
}
/** 创建展开明细动作。 */
export function createDetailActions(options: DetailActionOptions) {
function clearDetailStates() {
options.expandedRowKeys.value = [];
for (const key of Object.keys(options.detailStates)) {
Reflect.deleteProperty(options.detailStates, key);
}
}
async function handleExpand(action: FinanceSettlementExpandAction) {
const key = getSettlementRowKey(action.row);
if (!action.expanded) {
options.expandedRowKeys.value = options.expandedRowKeys.value.filter(
(item) => item !== key,
);
return;
}
if (!options.selectedStoreId.value) {
return;
}
if (!options.expandedRowKeys.value.includes(key)) {
options.expandedRowKeys.value = [...options.expandedRowKeys.value, key];
}
const currentState = options.detailStates[key] ?? {
loading: false,
items: [],
};
if (currentState.loading || currentState.items.length > 0) {
options.detailStates[key] = currentState;
return;
}
currentState.loading = true;
options.detailStates[key] = currentState;
try {
const result = await getFinanceSettlementDetailApi({
storeId: options.selectedStoreId.value,
arrivedDate: action.row.arrivedDate,
channel: action.row.channel,
});
currentState.items = result.items;
} finally {
currentState.loading = false;
}
}
return {
clearDetailStates,
handleExpand,
};
}

View File

@@ -0,0 +1,52 @@
import type { FinanceSettlementFilterState } from '../../types';
/**
* 文件职责:到账查询导出动作。
*/
import { message } from 'ant-design-vue';
import { exportFinanceSettlementCsvApi } from '#/api/finance';
import {
buildFilterQueryPayload,
downloadBase64File,
isDateRangeInvalid,
} from './helpers';
interface ExportActionOptions {
canExport: { value: boolean };
filters: FinanceSettlementFilterState;
isExporting: { value: boolean };
selectedStoreId: { value: string };
}
/** 创建导出动作。 */
export function createExportActions(options: ExportActionOptions) {
async function handleExport() {
if (!options.canExport.value || !options.selectedStoreId.value) {
return;
}
if (isDateRangeInvalid(options.filters)) {
message.warning('开始日期不能晚于结束日期');
return;
}
options.isExporting.value = true;
try {
const payload = buildFilterQueryPayload(
options.selectedStoreId.value,
options.filters,
);
const result = await exportFinanceSettlementCsvApi(payload);
downloadBase64File(result.fileName, result.fileContentBase64);
message.success(`导出成功,共 ${result.totalCount} 条记录`);
} finally {
options.isExporting.value = false;
}
}
return {
handleExport,
};
}

View File

@@ -0,0 +1,57 @@
import type {
FinanceSettlementFilterState,
FinanceSettlementPaginationState,
} from '../../types';
/**
* 文件职责:到账查询页面筛选与分页行为。
*/
import { message } from 'ant-design-vue';
import { isDateRangeInvalid } from './helpers';
interface FilterActionOptions {
filters: FinanceSettlementFilterState;
loadPageData: () => Promise<void>;
pagination: FinanceSettlementPaginationState;
}
/** 创建筛选行为。 */
export function createFilterActions(options: FilterActionOptions) {
function setChannel(value: string) {
options.filters.channel = (value ||
'all') as FinanceSettlementFilterState['channel'];
}
function setStartDate(value: string) {
options.filters.startDate = value;
}
function setEndDate(value: string) {
options.filters.endDate = value;
}
async function handleSearch() {
if (isDateRangeInvalid(options.filters)) {
message.warning('开始日期不能晚于结束日期');
return;
}
options.pagination.page = 1;
await options.loadPageData();
}
async function handlePageChange(page: number, pageSize: number) {
options.pagination.page = page;
options.pagination.pageSize = pageSize;
await options.loadPageData();
}
return {
handlePageChange,
handleSearch,
setChannel,
setEndDate,
setStartDate,
};
}

View File

@@ -0,0 +1,134 @@
import type {
FinanceSettlementFilterState,
FinanceSettlementListQueryPayload,
FinanceSettlementQueryPayload,
} from '../../types';
/**
* 文件职责:到账查询页面纯函数与数据转换工具。
*/
import type {
FinanceSettlementChannelFilter,
FinanceSettlementListItemDto,
} 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 normalizeChannel(value: FinanceSettlementChannelFilter) {
return value === 'all' ? undefined : value;
}
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;' });
}
/** 获取今天日期字符串yyyy-MM-dd。 */
export function getTodayDateString() {
return formatDate(new Date());
}
/** 构建到账筛选请求。 */
export function buildFilterQueryPayload(
storeId: string,
filters: FinanceSettlementFilterState,
): FinanceSettlementQueryPayload {
return {
storeId,
startDate: filters.startDate || undefined,
endDate: filters.endDate || undefined,
channel: normalizeChannel(filters.channel),
};
}
/** 构建到账列表请求。 */
export function buildListQueryPayload(
storeId: string,
filters: FinanceSettlementFilterState,
page: number,
pageSize: number,
): FinanceSettlementListQueryPayload {
return {
...buildFilterQueryPayload(storeId, filters),
page,
pageSize,
};
}
/** 判断日期范围是否合法。 */
export function isDateRangeInvalid(filters: FinanceSettlementFilterState) {
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 formatCount(value: number) {
return new Intl.NumberFormat('zh-CN', {
maximumFractionDigits: 0,
}).format(Number.isFinite(value) ? value : 0);
}
/** 表格行唯一键。 */
export function getSettlementRowKey(row: FinanceSettlementListItemDto) {
return `${row.arrivedDate}_${row.channel}`;
}
/** 到账渠道圆点类名。 */
export function resolveChannelDotClass(channel: string) {
return channel === 'wechat' ? 'is-wechat' : 'is-alipay';
}
/** 明细支付时间格式化HH:mm。 */
export function formatPaidTime(value: string) {
const normalized = String(value || '').trim();
if (!normalized) {
return '--';
}
const separatorIndex = normalized.indexOf(' ');
if (separatorIndex !== -1 && normalized.length >= separatorIndex + 6) {
return normalized.slice(separatorIndex + 1, separatorIndex + 6);
}
const timePrefix = normalized.match(/^\d{2}:\d{2}/);
if (timePrefix?.[0]) {
return timePrefix[0];
}
return normalized;
}
/** 下载 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);
}