feat(@vben/web-antd): add finance settlement page module
This commit is contained in:
@@ -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: '',
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user