diff --git a/apps/web-antd/src/api/finance/index.ts b/apps/web-antd/src/api/finance/index.ts index 44f9611..5171a66 100644 --- a/apps/web-antd/src/api/finance/index.ts +++ b/apps/web-antd/src/api/finance/index.ts @@ -3,6 +3,8 @@ */ import { requestClient } from '#/api/request'; +export * from './settlement'; + /** 交易类型筛选值。 */ export type FinanceTransactionTypeFilter = | 'all' diff --git a/apps/web-antd/src/api/finance/settlement.ts b/apps/web-antd/src/api/finance/settlement.ts new file mode 100644 index 0000000..ccd15a7 --- /dev/null +++ b/apps/web-antd/src/api/finance/settlement.ts @@ -0,0 +1,137 @@ +/** + * 文件职责:财务中心到账查询 API 契约与请求封装。 + */ +import { requestClient } from '#/api/request'; + +/** 到账渠道筛选值。 */ +export type FinanceSettlementChannelFilter = 'alipay' | 'all' | 'wechat'; + +/** 到账查询筛选参数。 */ +export interface FinanceSettlementFilterQuery { + channel?: FinanceSettlementChannelFilter; + endDate?: string; + startDate?: string; + storeId: string; +} + +/** 到账查询列表参数。 */ +export interface FinanceSettlementListQuery extends FinanceSettlementFilterQuery { + page: number; + pageSize: number; +} + +/** 到账统计结果。 */ +export interface FinanceSettlementStatsDto { + currentMonthArrivedAmount: number; + currentMonthTransactionCount: number; + todayArrivedAmount: number; + yesterdayArrivedAmount: number; +} + +/** 到账账户信息。 */ +export interface FinanceSettlementAccountDto { + alipayPidMasked: string; + bankAccountName: string; + bankAccountNoMasked: string; + bankName: string; + settlementPeriodText: string; + wechatMerchantNoMasked: string; +} + +/** 到账列表行。 */ +export interface FinanceSettlementListItemDto { + arrivedAmount: number; + arrivedDate: string; + channel: 'alipay' | 'wechat'; + channelText: string; + transactionCount: number; +} + +/** 到账列表结果。 */ +export interface FinanceSettlementListResultDto { + items: FinanceSettlementListItemDto[]; + page: number; + pageSize: number; + total: number; +} + +/** 到账明细查询参数。 */ +export interface FinanceSettlementDetailQuery { + arrivedDate: string; + channel: 'alipay' | 'wechat'; + storeId: string; +} + +/** 到账明细行。 */ +export interface FinanceSettlementDetailItemDto { + amount: number; + orderNo: string; + paidAt: string; +} + +/** 到账明细结果。 */ +export interface FinanceSettlementDetailResultDto { + items: FinanceSettlementDetailItemDto[]; +} + +/** 到账导出结果。 */ +export interface FinanceSettlementExportDto { + fileContentBase64: string; + fileName: string; + totalCount: number; +} + +/** 查询到账统计。 */ +export async function getFinanceSettlementStatsApi(params: { + storeId: string; +}) { + return requestClient.get( + '/finance/settlement/stats', + { + params, + }, + ); +} + +/** 查询到账账户信息。 */ +export async function getFinanceSettlementAccountApi() { + return requestClient.get( + '/finance/settlement/account', + ); +} + +/** 查询到账列表。 */ +export async function getFinanceSettlementListApi( + params: FinanceSettlementListQuery, +) { + return requestClient.get( + '/finance/settlement/list', + { + params, + }, + ); +} + +/** 查询到账明细。 */ +export async function getFinanceSettlementDetailApi( + params: FinanceSettlementDetailQuery, +) { + return requestClient.get( + '/finance/settlement/detail', + { + params, + }, + ); +} + +/** 导出到账汇总 CSV。 */ +export async function exportFinanceSettlementCsvApi( + params: FinanceSettlementFilterQuery, +) { + return requestClient.get( + '/finance/settlement/export', + { + params, + }, + ); +} diff --git a/apps/web-antd/src/views/finance/settlement/components/SettlementAccountBar.vue b/apps/web-antd/src/views/finance/settlement/components/SettlementAccountBar.vue new file mode 100644 index 0000000..abab674 --- /dev/null +++ b/apps/web-antd/src/views/finance/settlement/components/SettlementAccountBar.vue @@ -0,0 +1,60 @@ + + + diff --git a/apps/web-antd/src/views/finance/settlement/components/SettlementDetailTable.vue b/apps/web-antd/src/views/finance/settlement/components/SettlementDetailTable.vue new file mode 100644 index 0000000..6f1c1a5 --- /dev/null +++ b/apps/web-antd/src/views/finance/settlement/components/SettlementDetailTable.vue @@ -0,0 +1,51 @@ + + + diff --git a/apps/web-antd/src/views/finance/settlement/components/SettlementFilterBar.vue b/apps/web-antd/src/views/finance/settlement/components/SettlementFilterBar.vue new file mode 100644 index 0000000..7b18f6e --- /dev/null +++ b/apps/web-antd/src/views/finance/settlement/components/SettlementFilterBar.vue @@ -0,0 +1,105 @@ + + + diff --git a/apps/web-antd/src/views/finance/settlement/components/SettlementStatsCards.vue b/apps/web-antd/src/views/finance/settlement/components/SettlementStatsCards.vue new file mode 100644 index 0000000..f5a0387 --- /dev/null +++ b/apps/web-antd/src/views/finance/settlement/components/SettlementStatsCards.vue @@ -0,0 +1,63 @@ + + + diff --git a/apps/web-antd/src/views/finance/settlement/components/SettlementTableCard.vue b/apps/web-antd/src/views/finance/settlement/components/SettlementTableCard.vue new file mode 100644 index 0000000..2440989 --- /dev/null +++ b/apps/web-antd/src/views/finance/settlement/components/SettlementTableCard.vue @@ -0,0 +1,131 @@ + + + diff --git a/apps/web-antd/src/views/finance/settlement/composables/settlement-page/constants.ts b/apps/web-antd/src/views/finance/settlement/composables/settlement-page/constants.ts new file mode 100644 index 0000000..f43e891 --- /dev/null +++ b/apps/web-antd/src/views/finance/settlement/composables/settlement-page/constants.ts @@ -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: '', +}; diff --git a/apps/web-antd/src/views/finance/settlement/composables/settlement-page/data-actions.ts b/apps/web-antd/src/views/finance/settlement/composables/settlement-page/data-actions.ts new file mode 100644 index 0000000..4749371 --- /dev/null +++ b/apps/web-antd/src/views/finance/settlement/composables/settlement-page/data-actions.ts @@ -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, + }; +} diff --git a/apps/web-antd/src/views/finance/settlement/composables/settlement-page/detail-actions.ts b/apps/web-antd/src/views/finance/settlement/composables/settlement-page/detail-actions.ts new file mode 100644 index 0000000..ef39012 --- /dev/null +++ b/apps/web-antd/src/views/finance/settlement/composables/settlement-page/detail-actions.ts @@ -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, + }; +} diff --git a/apps/web-antd/src/views/finance/settlement/composables/settlement-page/export-actions.ts b/apps/web-antd/src/views/finance/settlement/composables/settlement-page/export-actions.ts new file mode 100644 index 0000000..68fd0f4 --- /dev/null +++ b/apps/web-antd/src/views/finance/settlement/composables/settlement-page/export-actions.ts @@ -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, + }; +} diff --git a/apps/web-antd/src/views/finance/settlement/composables/settlement-page/filter-actions.ts b/apps/web-antd/src/views/finance/settlement/composables/settlement-page/filter-actions.ts new file mode 100644 index 0000000..98321f8 --- /dev/null +++ b/apps/web-antd/src/views/finance/settlement/composables/settlement-page/filter-actions.ts @@ -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; + 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, + }; +} diff --git a/apps/web-antd/src/views/finance/settlement/composables/settlement-page/helpers.ts b/apps/web-antd/src/views/finance/settlement/composables/settlement-page/helpers.ts new file mode 100644 index 0000000..735aba8 --- /dev/null +++ b/apps/web-antd/src/views/finance/settlement/composables/settlement-page/helpers.ts @@ -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); +} diff --git a/apps/web-antd/src/views/finance/settlement/composables/useFinanceSettlementPage.ts b/apps/web-antd/src/views/finance/settlement/composables/useFinanceSettlementPage.ts new file mode 100644 index 0000000..93e9f62 --- /dev/null +++ b/apps/web-antd/src/views/finance/settlement/composables/useFinanceSettlementPage.ts @@ -0,0 +1,201 @@ +import type { FinanceSettlementDetailStateMap } from '../types'; + +/** + * 文件职责:到账查询页面状态与动作编排。 + */ +import type { + FinanceSettlementAccountDto, + FinanceSettlementListItemDto, + FinanceSettlementStatsDto, +} from '#/api/finance'; +import type { StoreListItemDto } from '#/api/store'; + +import { computed, onActivated, onMounted, reactive, ref, watch } from 'vue'; + +import { useAccessStore } from '@vben/stores'; + +import { + createDefaultFilters, + DEFAULT_STATS, + FINANCE_SETTLEMENT_EXPORT_PERMISSION, + FINANCE_SETTLEMENT_VIEW_PERMISSION, +} from './settlement-page/constants'; +import { createDataActions } from './settlement-page/data-actions'; +import { createDetailActions } from './settlement-page/detail-actions'; +import { createExportActions } from './settlement-page/export-actions'; +import { createFilterActions } from './settlement-page/filter-actions'; + +/** 创建到账查询页面组合状态。 */ +export function useFinanceSettlementPage() { + const accessStore = useAccessStore(); + + const stores = ref([]); + const selectedStoreId = ref(''); + const isStoreLoading = ref(false); + + const filters = reactive(createDefaultFilters()); + const rows = ref([]); + const pagination = reactive({ + page: 1, + pageSize: 20, + total: 0, + }); + + const stats = reactive({ ...DEFAULT_STATS }); + const account = ref(null); + + const isListLoading = ref(false); + const isStatsLoading = ref(false); + const isAccountLoading = ref(false); + const isExporting = ref(false); + + const expandedRowKeys = ref([]); + const detailStates = reactive({}); + + const storeOptions = computed(() => + stores.value.map((item) => ({ + label: item.name, + value: item.id, + })), + ); + + const accessCodeSet = computed( + () => new Set((accessStore.accessCodes ?? []).map(String)), + ); + + const canView = computed(() => + accessCodeSet.value.has(FINANCE_SETTLEMENT_VIEW_PERMISSION), + ); + + const canExport = computed(() => + accessCodeSet.value.has(FINANCE_SETTLEMENT_EXPORT_PERMISSION), + ); + + const { clearPageData, loadAccount, loadPageData, loadStores, resetStats } = + createDataActions({ + stores, + selectedStoreId, + filters, + rows, + pagination, + stats, + account, + isStoreLoading, + isListLoading, + isStatsLoading, + isAccountLoading, + }); + + const { + handlePageChange, + handleSearch, + setChannel, + setEndDate, + setStartDate, + } = createFilterActions({ + filters, + pagination, + loadPageData, + }); + + const { clearDetailStates, handleExpand } = createDetailActions({ + selectedStoreId, + expandedRowKeys, + detailStates, + }); + + const { handleExport } = createExportActions({ + canExport, + selectedStoreId, + filters, + isExporting, + }); + + function setSelectedStoreId(value: string) { + selectedStoreId.value = value; + } + + function clearByPermission() { + stores.value = []; + selectedStoreId.value = ''; + account.value = null; + clearPageData(); + clearDetailStates(); + resetStats(); + } + + watch(selectedStoreId, async (storeId) => { + clearDetailStates(); + + if (!storeId) { + clearPageData(); + return; + } + + pagination.page = 1; + await loadPageData(); + }); + + watch(canView, async (value, oldValue) => { + if (value === oldValue) { + return; + } + + if (!value) { + clearByPermission(); + return; + } + + await Promise.all([loadStores(), loadAccount()]); + }); + + onMounted(async () => { + if (!canView.value) { + clearByPermission(); + return; + } + + await Promise.all([loadStores(), loadAccount()]); + }); + + onActivated(() => { + if (!canView.value) { + return; + } + + if (stores.value.length === 0 || !selectedStoreId.value) { + void loadStores(); + } + + if (!account.value && !isAccountLoading.value) { + void loadAccount(); + } + }); + + return { + account, + canExport, + canView, + detailStates, + expandedRowKeys, + filters, + handleExpand, + handleExport, + handlePageChange, + handleSearch, + isAccountLoading, + isExporting, + isListLoading, + isStatsLoading, + isStoreLoading, + pagination, + rows, + selectedStoreId, + setChannel, + setEndDate, + setSelectedStoreId, + setStartDate, + stats, + storeOptions, + }; +} diff --git a/apps/web-antd/src/views/finance/settlement/index.vue b/apps/web-antd/src/views/finance/settlement/index.vue new file mode 100644 index 0000000..f62710a --- /dev/null +++ b/apps/web-antd/src/views/finance/settlement/index.vue @@ -0,0 +1,83 @@ + + + + + diff --git a/apps/web-antd/src/views/finance/settlement/styles/base.less b/apps/web-antd/src/views/finance/settlement/styles/base.less new file mode 100644 index 0000000..77ea294 --- /dev/null +++ b/apps/web-antd/src/views/finance/settlement/styles/base.less @@ -0,0 +1,18 @@ +/** + * 文件职责:到账查询页面基础容器样式。 + */ +.page-finance-settlement { + .ant-card { + border-radius: 10px; + } +} + +.fst-page { + display: flex; + flex-direction: column; + gap: 12px; +} + +.fst-mono { + font-family: ui-monospace, sfmono-regular, menlo, consolas, monospace; +} diff --git a/apps/web-antd/src/views/finance/settlement/styles/index.less b/apps/web-antd/src/views/finance/settlement/styles/index.less new file mode 100644 index 0000000..f98468c --- /dev/null +++ b/apps/web-antd/src/views/finance/settlement/styles/index.less @@ -0,0 +1,7 @@ +/** + * 文件职责:到账查询页面样式聚合入口。 + */ +@import './base.less'; +@import './layout.less'; +@import './table.less'; +@import './responsive.less'; diff --git a/apps/web-antd/src/views/finance/settlement/styles/layout.less b/apps/web-antd/src/views/finance/settlement/styles/layout.less new file mode 100644 index 0000000..cb5980e --- /dev/null +++ b/apps/web-antd/src/views/finance/settlement/styles/layout.less @@ -0,0 +1,134 @@ +/** + * 文件职责:到账查询页面布局与筛选区域样式。 + */ +.fst-stats { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 12px; +} + +.fst-stat-card { + padding: 18px 20px; + background: #fff; + border: 1px solid #f0f0f0; + border-radius: 10px; + box-shadow: 0 2px 8px rgb(15 23 42 / 6%); + transition: all 0.2s ease; + + &:hover { + box-shadow: 0 6px 14px rgb(15 23 42 / 10%); + transform: translateY(-1px); + } +} + +.fst-stat-label { + display: flex; + gap: 6px; + align-items: center; + margin-bottom: 6px; + font-size: 13px; + color: rgb(0 0 0 / 45%); +} + +.fst-stat-icon { + width: 16px; + height: 16px; +} + +.fst-stat-value { + font-size: 24px; + font-weight: 700; + line-height: 1.2; + color: rgb(0 0 0 / 88%); + + &.is-green { + color: #52c41a; + } +} + +.fst-account-bar { + display: flex; + flex-wrap: wrap; + gap: 14px; + align-items: center; + padding: 14px 20px; + font-size: 13px; + color: rgb(0 0 0 / 65%); + background: #fff; + border: 1px solid #f0f0f0; + border-radius: 10px; + box-shadow: 0 2px 8px rgb(15 23 42 / 6%); + + &.is-loading { + opacity: 0.8; + } + + strong { + font-weight: 600; + color: rgb(0 0 0 / 88%); + } +} + +.fst-account-icon { + width: 18px; + height: 18px; + color: #1677ff; +} + +.fst-account-sep { + width: 1px; + height: 20px; + background: #f0f0f0; +} + +.fst-toolbar { + display: flex; + flex-wrap: wrap; + gap: 10px; + align-items: center; + padding: 14px 18px; + background: #fff; + border: 1px solid #f0f0f0; + border-radius: 10px; + box-shadow: 0 2px 8px rgb(15 23 42 / 6%); + + .fst-store-select { + width: 220px; + } + + .fst-date-input { + width: 145px; + } + + .fst-channel-select { + width: 130px; + } + + .fst-date-sep { + font-size: 13px; + line-height: 32px; + color: rgb(0 0 0 / 45%); + } + + .fst-toolbar-right { + margin-left: auto; + } + + .fst-export-btn { + display: inline-flex; + gap: 4px; + align-items: center; + height: 32px; + } + + .ant-select-selector, + .ant-input, + .ant-input-affix-wrapper { + height: 32px; + font-size: 13px; + } + + .ant-input-affix-wrapper .ant-input { + height: 100%; + } +} diff --git a/apps/web-antd/src/views/finance/settlement/styles/responsive.less b/apps/web-antd/src/views/finance/settlement/styles/responsive.less new file mode 100644 index 0000000..52c4996 --- /dev/null +++ b/apps/web-antd/src/views/finance/settlement/styles/responsive.less @@ -0,0 +1,48 @@ +/** + * 文件职责:到账查询页面响应式样式。 + */ +@media (max-width: 1600px) { + .fst-stats { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@media (max-width: 992px) { + .fst-account-sep { + display: none; + } +} + +@media (max-width: 768px) { + .fst-stats { + grid-template-columns: 1fr; + } + + .fst-toolbar { + padding: 14px 12px; + + .fst-store-select, + .fst-date-input, + .fst-channel-select { + width: 100%; + } + + .fst-date-sep { + display: none; + } + + .fst-toolbar-right { + width: 100%; + margin-left: 0; + } + + .fst-export-btn { + justify-content: center; + width: 100%; + } + } + + .fst-detail-wrap { + padding: 12px 12px 12px 24px; + } +} diff --git a/apps/web-antd/src/views/finance/settlement/styles/table.less b/apps/web-antd/src/views/finance/settlement/styles/table.less new file mode 100644 index 0000000..f2eca5a --- /dev/null +++ b/apps/web-antd/src/views/finance/settlement/styles/table.less @@ -0,0 +1,98 @@ +/** + * 文件职责:到账查询表格与展开明细样式。 + */ +.fst-table-card { + overflow: hidden; + background: #fff; + border: 1px solid #f0f0f0; + border-radius: 10px; + + .ant-table-wrapper { + .ant-table-thead > tr > th { + font-size: 13px; + white-space: nowrap; + } + + .ant-table-tbody > tr > td { + font-size: 13px; + vertical-align: middle; + } + + .ant-table-expanded-row > td { + padding: 0 !important; + background: #f8f9fb !important; + } + } + + .ant-pagination { + margin: 14px 16px; + } +} + +.fst-channel-icon { + display: inline-flex; + gap: 6px; + align-items: center; +} + +.fst-channel-dot { + width: 8px; + height: 8px; + border-radius: 50%; + + &.is-wechat { + background: #07c160; + } + + &.is-alipay { + background: #1677ff; + } +} + +.fst-amount { + font-weight: 600; + white-space: nowrap; +} + +.fst-detail-wrap { + padding: 14px 20px 14px 40px; +} + +.fst-detail-title { + padding-left: 10px; + margin-bottom: 10px; + font-size: 13px; + font-weight: 600; + color: rgb(0 0 0 / 88%); + border-left: 3px solid #1677ff; +} + +.fst-mini-table { + width: 100%; + font-size: 12px; + border-collapse: collapse; + + th { + padding: 8px 10px; + font-weight: 500; + color: rgb(0 0 0 / 45%); + text-align: left; + background: #fff; + border-bottom: 1px solid #f0f0f0; + } + + td { + padding: 8px 10px; + color: rgb(0 0 0 / 88%); + border-bottom: 1px solid #f3f4f6; + } + + tbody tr:last-child td { + border-bottom: none; + } + + tbody tr td[colspan] { + color: rgb(0 0 0 / 45%); + text-align: center; + } +} diff --git a/apps/web-antd/src/views/finance/settlement/types.ts b/apps/web-antd/src/views/finance/settlement/types.ts new file mode 100644 index 0000000..ee151d3 --- /dev/null +++ b/apps/web-antd/src/views/finance/settlement/types.ts @@ -0,0 +1,60 @@ +/** + * 文件职责:到账查询页面本地状态类型定义。 + */ +import type { + FinanceSettlementChannelFilter, + FinanceSettlementDetailItemDto, + FinanceSettlementListItemDto, +} from '#/api/finance'; + +/** 到账查询筛选状态。 */ +export interface FinanceSettlementFilterState { + channel: FinanceSettlementChannelFilter; + endDate: string; + startDate: string; +} + +/** 到账查询分页状态。 */ +export interface FinanceSettlementPaginationState { + page: number; + pageSize: number; + total: number; +} + +/** 通用选项项。 */ +export interface OptionItem { + label: string; + value: string; +} + +/** 到账筛选请求负载。 */ +export interface FinanceSettlementQueryPayload { + channel?: Exclude; + endDate?: string; + startDate?: string; + storeId: string; +} + +/** 到账列表请求负载。 */ +export interface FinanceSettlementListQueryPayload extends FinanceSettlementQueryPayload { + page: number; + pageSize: number; +} + +/** 展开行明细状态。 */ +export interface FinanceSettlementDetailState { + items: FinanceSettlementDetailItemDto[]; + loading: boolean; +} + +/** 展开行明细缓存映射。 */ +export type FinanceSettlementDetailStateMap = Record< + string, + FinanceSettlementDetailState +>; + +/** 表格展开动作参数。 */ +export interface FinanceSettlementExpandAction { + expanded: boolean; + row: FinanceSettlementListItemDto; +}