diff --git a/apps/web-antd/src/api/finance/index.ts b/apps/web-antd/src/api/finance/index.ts index 44f9611..93c8515 100644 --- a/apps/web-antd/src/api/finance/index.ts +++ b/apps/web-antd/src/api/finance/index.ts @@ -1,159 +1,5 @@ /** - * 文件职责:财务中心交易流水 API 契约与请求封装。 + * 文件职责:财务中心 API 聚合导出。 */ -import { requestClient } from '#/api/request'; - -/** 交易类型筛选值。 */ -export type FinanceTransactionTypeFilter = - | 'all' - | 'income' - | 'point_redeem' - | 'refund' - | 'stored_card_recharge'; - -/** 交易渠道筛选值。 */ -export type FinanceTransactionChannelFilter = - | 'all' - | 'delivery' - | 'dine_in' - | 'pickup'; - -/** 交易支付方式筛选值。 */ -export type FinanceTransactionPaymentFilter = - | 'alipay' - | 'all' - | 'balance' - | 'card' - | 'cash' - | 'wechat'; - -/** 交易流水筛选参数。 */ -export interface FinanceTransactionFilterQuery { - channel?: FinanceTransactionChannelFilter; - endDate?: string; - keyword?: string; - paymentMethod?: FinanceTransactionPaymentFilter; - startDate?: string; - storeId: string; - type?: FinanceTransactionTypeFilter; -} - -/** 交易流水列表查询参数。 */ -export interface FinanceTransactionListQuery extends FinanceTransactionFilterQuery { - page: number; - pageSize: number; -} - -/** 交易流水列表行。 */ -export interface FinanceTransactionListItemDto { - amount: number; - channel: string; - isIncome: boolean; - occurredAt: string; - orderNo?: string; - paymentMethod: string; - remark: string; - transactionId: string; - transactionNo: string; - type: string; - typeText: string; -} - -/** 交易流水列表结果。 */ -export interface FinanceTransactionListResultDto { - items: FinanceTransactionListItemDto[]; - page: number; - pageIncomeAmount: number; - pageRefundAmount: number; - pageSize: number; - total: number; -} - -/** 交易流水统计结果。 */ -export interface FinanceTransactionStatsDto { - totalCount: number; - totalIncome: number; - totalRefund: number; -} - -/** 交易流水详情。 */ -export interface FinanceTransactionDetailDto { - amount: number; - arrivedAmount?: number; - channel: string; - customerName: string; - customerPhone: string; - giftAmount?: number; - memberMobileMasked?: string; - memberName?: string; - occurredAt: string; - orderNo?: string; - paymentMethod: string; - pointBalanceAfterChange?: number; - pointChangeAmount?: number; - rechargeAmount?: number; - refundNo?: string; - refundReason?: string; - remark: string; - storeId: string; - transactionId: string; - transactionNo: string; - type: string; - typeText: string; -} - -/** 交易流水导出结果。 */ -export interface FinanceTransactionExportDto { - fileContentBase64: string; - fileName: string; - totalCount: number; -} - -/** 查询交易流水列表。 */ -export async function getFinanceTransactionListApi( - params: FinanceTransactionListQuery, -) { - return requestClient.get( - '/finance/transaction/list', - { - params, - }, - ); -} - -/** 查询交易流水统计。 */ -export async function getFinanceTransactionStatsApi( - params: FinanceTransactionFilterQuery, -) { - return requestClient.get( - '/finance/transaction/stats', - { - params, - }, - ); -} - -/** 查询交易流水详情。 */ -export async function getFinanceTransactionDetailApi(params: { - storeId: string; - transactionId: string; -}) { - return requestClient.get( - '/finance/transaction/detail', - { - params, - }, - ); -} - -/** 导出交易流水 CSV。 */ -export async function exportFinanceTransactionCsvApi( - params: FinanceTransactionFilterQuery, -) { - return requestClient.get( - '/finance/transaction/export', - { - params, - }, - ); -} +export * from './report'; +export * from './transaction'; diff --git a/apps/web-antd/src/api/finance/report.ts b/apps/web-antd/src/api/finance/report.ts new file mode 100644 index 0000000..455be3e --- /dev/null +++ b/apps/web-antd/src/api/finance/report.ts @@ -0,0 +1,156 @@ +/** + * 文件职责:财务中心经营报表 API 契约与请求封装。 + */ +import { requestClient } from '#/api/request'; + +/** 报表周期筛选值。 */ +export type FinanceBusinessReportPeriodType = 'daily' | 'monthly' | 'weekly'; + +/** 经营报表状态值。 */ +export type FinanceBusinessReportStatus = + | 'failed' + | 'queued' + | 'running' + | 'succeeded'; + +/** 经营报表列表查询参数。 */ +export interface FinanceBusinessReportListQuery { + page: number; + pageSize: number; + periodType?: FinanceBusinessReportPeriodType; + storeId: string; +} + +/** 经营报表详情查询参数。 */ +export interface FinanceBusinessReportDetailQuery { + reportId: string; + storeId: string; +} + +/** 经营报表批量导出查询参数。 */ +export interface FinanceBusinessReportBatchExportQuery { + page: number; + pageSize: number; + periodType?: FinanceBusinessReportPeriodType; + storeId: string; +} + +/** 经营报表列表行。 */ +export interface FinanceBusinessReportListItemDto { + averageOrderValue: number; + canDownload: boolean; + costTotalAmount: number; + dateText: string; + netProfitAmount: number; + orderCount: number; + profitRatePercent: number; + refundRatePercent: number; + reportId: string; + revenueAmount: number; + status: FinanceBusinessReportStatus; + statusText: string; +} + +/** 经营报表列表结果。 */ +export interface FinanceBusinessReportListResultDto { + items: FinanceBusinessReportListItemDto[]; + page: number; + pageSize: number; + total: number; +} + +/** 经营报表 KPI 项。 */ +export interface FinanceBusinessReportKpiDto { + key: string; + label: string; + momChangeRate: number; + valueText: string; + yoyChangeRate: number; +} + +/** 经营报表明细项。 */ +export interface FinanceBusinessReportBreakdownItemDto { + amount: number; + key: string; + label: string; + ratioPercent: number; +} + +/** 经营报表详情。 */ +export interface FinanceBusinessReportDetailDto { + costBreakdowns: FinanceBusinessReportBreakdownItemDto[]; + incomeBreakdowns: FinanceBusinessReportBreakdownItemDto[]; + kpis: FinanceBusinessReportKpiDto[]; + periodType: FinanceBusinessReportPeriodType; + reportId: string; + status: FinanceBusinessReportStatus; + statusText: string; + title: string; +} + +/** 经营报表导出结果。 */ +export interface FinanceBusinessReportExportDto { + fileContentBase64: string; + fileName: string; + totalCount: number; +} + +/** 查询经营报表列表。 */ +export async function getFinanceBusinessReportListApi( + params: FinanceBusinessReportListQuery, +) { + return requestClient.get( + '/finance/report/list', + { + params, + }, + ); +} + +/** 查询经营报表详情。 */ +export async function getFinanceBusinessReportDetailApi( + params: FinanceBusinessReportDetailQuery, +) { + return requestClient.get( + '/finance/report/detail', + { + params, + }, + ); +} + +/** 导出经营报表 PDF。 */ +export async function exportFinanceBusinessReportPdfApi( + params: FinanceBusinessReportDetailQuery, +) { + return requestClient.get( + '/finance/report/export/pdf', + { + params, + }, + ); +} + +/** 导出经营报表 Excel。 */ +export async function exportFinanceBusinessReportExcelApi( + params: FinanceBusinessReportDetailQuery, +) { + return requestClient.get( + '/finance/report/export/excel', + { + params, + }, + ); +} + +/** 批量导出经营报表(ZIP)。 */ +export async function exportFinanceBusinessReportBatchApi( + params: FinanceBusinessReportBatchExportQuery, +) { + return requestClient.get( + '/finance/report/export/batch', + { + params, + }, + ); +} diff --git a/apps/web-antd/src/api/finance/transaction.ts b/apps/web-antd/src/api/finance/transaction.ts new file mode 100644 index 0000000..44f9611 --- /dev/null +++ b/apps/web-antd/src/api/finance/transaction.ts @@ -0,0 +1,159 @@ +/** + * 文件职责:财务中心交易流水 API 契约与请求封装。 + */ +import { requestClient } from '#/api/request'; + +/** 交易类型筛选值。 */ +export type FinanceTransactionTypeFilter = + | 'all' + | 'income' + | 'point_redeem' + | 'refund' + | 'stored_card_recharge'; + +/** 交易渠道筛选值。 */ +export type FinanceTransactionChannelFilter = + | 'all' + | 'delivery' + | 'dine_in' + | 'pickup'; + +/** 交易支付方式筛选值。 */ +export type FinanceTransactionPaymentFilter = + | 'alipay' + | 'all' + | 'balance' + | 'card' + | 'cash' + | 'wechat'; + +/** 交易流水筛选参数。 */ +export interface FinanceTransactionFilterQuery { + channel?: FinanceTransactionChannelFilter; + endDate?: string; + keyword?: string; + paymentMethod?: FinanceTransactionPaymentFilter; + startDate?: string; + storeId: string; + type?: FinanceTransactionTypeFilter; +} + +/** 交易流水列表查询参数。 */ +export interface FinanceTransactionListQuery extends FinanceTransactionFilterQuery { + page: number; + pageSize: number; +} + +/** 交易流水列表行。 */ +export interface FinanceTransactionListItemDto { + amount: number; + channel: string; + isIncome: boolean; + occurredAt: string; + orderNo?: string; + paymentMethod: string; + remark: string; + transactionId: string; + transactionNo: string; + type: string; + typeText: string; +} + +/** 交易流水列表结果。 */ +export interface FinanceTransactionListResultDto { + items: FinanceTransactionListItemDto[]; + page: number; + pageIncomeAmount: number; + pageRefundAmount: number; + pageSize: number; + total: number; +} + +/** 交易流水统计结果。 */ +export interface FinanceTransactionStatsDto { + totalCount: number; + totalIncome: number; + totalRefund: number; +} + +/** 交易流水详情。 */ +export interface FinanceTransactionDetailDto { + amount: number; + arrivedAmount?: number; + channel: string; + customerName: string; + customerPhone: string; + giftAmount?: number; + memberMobileMasked?: string; + memberName?: string; + occurredAt: string; + orderNo?: string; + paymentMethod: string; + pointBalanceAfterChange?: number; + pointChangeAmount?: number; + rechargeAmount?: number; + refundNo?: string; + refundReason?: string; + remark: string; + storeId: string; + transactionId: string; + transactionNo: string; + type: string; + typeText: string; +} + +/** 交易流水导出结果。 */ +export interface FinanceTransactionExportDto { + fileContentBase64: string; + fileName: string; + totalCount: number; +} + +/** 查询交易流水列表。 */ +export async function getFinanceTransactionListApi( + params: FinanceTransactionListQuery, +) { + return requestClient.get( + '/finance/transaction/list', + { + params, + }, + ); +} + +/** 查询交易流水统计。 */ +export async function getFinanceTransactionStatsApi( + params: FinanceTransactionFilterQuery, +) { + return requestClient.get( + '/finance/transaction/stats', + { + params, + }, + ); +} + +/** 查询交易流水详情。 */ +export async function getFinanceTransactionDetailApi(params: { + storeId: string; + transactionId: string; +}) { + return requestClient.get( + '/finance/transaction/detail', + { + params, + }, + ); +} + +/** 导出交易流水 CSV。 */ +export async function exportFinanceTransactionCsvApi( + params: FinanceTransactionFilterQuery, +) { + return requestClient.get( + '/finance/transaction/export', + { + params, + }, + ); +} diff --git a/apps/web-antd/src/views/finance/report/components/ReportBatchExportModal.vue b/apps/web-antd/src/views/finance/report/components/ReportBatchExportModal.vue new file mode 100644 index 0000000..7658164 --- /dev/null +++ b/apps/web-antd/src/views/finance/report/components/ReportBatchExportModal.vue @@ -0,0 +1,96 @@ + + + diff --git a/apps/web-antd/src/views/finance/report/components/ReportPreviewDrawer.vue b/apps/web-antd/src/views/finance/report/components/ReportPreviewDrawer.vue new file mode 100644 index 0000000..af2d576 --- /dev/null +++ b/apps/web-antd/src/views/finance/report/components/ReportPreviewDrawer.vue @@ -0,0 +1,208 @@ + + + diff --git a/apps/web-antd/src/views/finance/report/components/ReportTableCard.vue b/apps/web-antd/src/views/finance/report/components/ReportTableCard.vue new file mode 100644 index 0000000..2d1f4be --- /dev/null +++ b/apps/web-antd/src/views/finance/report/components/ReportTableCard.vue @@ -0,0 +1,205 @@ + + + diff --git a/apps/web-antd/src/views/finance/report/components/ReportToolbar.vue b/apps/web-antd/src/views/finance/report/components/ReportToolbar.vue new file mode 100644 index 0000000..baa1c7e --- /dev/null +++ b/apps/web-antd/src/views/finance/report/components/ReportToolbar.vue @@ -0,0 +1,80 @@ + + + diff --git a/apps/web-antd/src/views/finance/report/composables/report-page/constants.ts b/apps/web-antd/src/views/finance/report/composables/report-page/constants.ts new file mode 100644 index 0000000..bb7f7da --- /dev/null +++ b/apps/web-antd/src/views/finance/report/composables/report-page/constants.ts @@ -0,0 +1,32 @@ +import type { + FinanceBusinessReportPaginationState, + FinanceBusinessReportPeriodOption, +} from '../../types'; + +/** + * 文件职责:经营报表页面常量与默认状态定义。 + */ + +/** 经营报表查看权限。 */ +export const FINANCE_REPORT_VIEW_PERMISSION = 'tenant:statistics:report:view'; + +/** 经营报表导出权限。 */ +export const FINANCE_REPORT_EXPORT_PERMISSION = + 'tenant:statistics:report:export'; + +/** 报表周期选项。 */ +export const REPORT_PERIOD_OPTIONS: FinanceBusinessReportPeriodOption[] = [ + { label: '日报', value: 'daily' }, + { label: '周报', value: 'weekly' }, + { label: '月报', value: 'monthly' }, +]; + +/** 默认报表周期。 */ +export const DEFAULT_PERIOD_TYPE = 'daily' as const; + +/** 默认分页状态。 */ +export const DEFAULT_PAGINATION: FinanceBusinessReportPaginationState = { + page: 1, + pageSize: 20, + total: 0, +}; diff --git a/apps/web-antd/src/views/finance/report/composables/report-page/data-actions.ts b/apps/web-antd/src/views/finance/report/composables/report-page/data-actions.ts new file mode 100644 index 0000000..46766f4 --- /dev/null +++ b/apps/web-antd/src/views/finance/report/composables/report-page/data-actions.ts @@ -0,0 +1,84 @@ +import type { FinanceBusinessReportPaginationState } from '../../types'; + +/** + * 文件职责:经营报表页面数据加载动作。 + */ +import type { FinanceBusinessReportListItemDto } from '#/api/finance'; +import type { StoreListItemDto } from '#/api/store'; + +import { getFinanceBusinessReportListApi } from '#/api/finance'; +import { getStoreListApi } from '#/api/store'; + +import { buildReportListQueryPayload } from './helpers'; + +interface DataActionOptions { + isListLoading: { value: boolean }; + isStoreLoading: { value: boolean }; + pagination: FinanceBusinessReportPaginationState; + periodType: { value: 'daily' | 'monthly' | 'weekly' }; + rows: { value: FinanceBusinessReportListItemDto[] }; + selectedStoreId: { value: string }; + stores: { value: StoreListItemDto[] }; +} + +/** 创建数据相关动作。 */ +export function createDataActions(options: DataActionOptions) { + function clearPageData() { + options.rows.value = []; + options.pagination.total = 0; + } + + 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 loadReportList() { + if (!options.selectedStoreId.value) { + clearPageData(); + return; + } + + options.isListLoading.value = true; + try { + const payload = buildReportListQueryPayload( + options.selectedStoreId.value, + options.periodType.value, + options.pagination.page, + options.pagination.pageSize, + ); + const result = await getFinanceBusinessReportListApi(payload); + + options.rows.value = result.items; + options.pagination.total = result.total; + options.pagination.page = result.page; + options.pagination.pageSize = result.pageSize; + } finally { + options.isListLoading.value = false; + } + } + + return { + clearPageData, + loadReportList, + loadStores, + }; +} diff --git a/apps/web-antd/src/views/finance/report/composables/report-page/detail-actions.ts b/apps/web-antd/src/views/finance/report/composables/report-page/detail-actions.ts new file mode 100644 index 0000000..0976825 --- /dev/null +++ b/apps/web-antd/src/views/finance/report/composables/report-page/detail-actions.ts @@ -0,0 +1,136 @@ +/** + * 文件职责:经营报表预览抽屉与单条导出动作。 + */ +import type { FinanceBusinessReportDetailDto } from '#/api/finance'; + +import { message } from 'ant-design-vue'; + +import { + exportFinanceBusinessReportExcelApi, + exportFinanceBusinessReportPdfApi, + getFinanceBusinessReportDetailApi, +} from '#/api/finance'; + +import { buildReportDetailQueryPayload, downloadBase64File } from './helpers'; + +interface DetailActionOptions { + canExport: { value: boolean }; + currentPreviewReportId: { value: string }; + isDownloadingExcel: { value: boolean }; + isDownloadingPdf: { value: boolean }; + isPreviewDrawerOpen: { value: boolean }; + isPreviewLoading: { value: boolean }; + previewDetail: { value: FinanceBusinessReportDetailDto | null }; + selectedStoreId: { value: string }; +} + +/** 创建预览与单条导出动作。 */ +export function createDetailActions(options: DetailActionOptions) { + function setPreviewDrawerOpen(value: boolean) { + options.isPreviewDrawerOpen.value = value; + if (!value) { + options.previewDetail.value = null; + options.currentPreviewReportId.value = ''; + } + } + + async function openPreview(reportId: string) { + if (!options.selectedStoreId.value || !reportId) { + return; + } + + options.currentPreviewReportId.value = reportId; + options.isPreviewDrawerOpen.value = true; + options.previewDetail.value = null; + options.isPreviewLoading.value = true; + try { + const payload = buildReportDetailQueryPayload( + options.selectedStoreId.value, + reportId, + ); + options.previewDetail.value = + await getFinanceBusinessReportDetailApi(payload); + options.currentPreviewReportId.value = + options.previewDetail.value.reportId || reportId; + } finally { + options.isPreviewLoading.value = false; + } + } + + function resolveTargetReportId(reportId?: string) { + const explicit = String(reportId ?? '').trim(); + if (explicit) { + return explicit; + } + + const current = String(options.currentPreviewReportId.value || '').trim(); + if (current) { + return current; + } + + return String(options.previewDetail.value?.reportId ?? '').trim(); + } + + async function downloadPdf(reportId?: string) { + if (!options.canExport.value || !options.selectedStoreId.value) { + return; + } + + const targetReportId = resolveTargetReportId(reportId); + if (!targetReportId) { + return; + } + + options.isDownloadingPdf.value = true; + try { + const payload = buildReportDetailQueryPayload( + options.selectedStoreId.value, + targetReportId, + ); + const result = await exportFinanceBusinessReportPdfApi(payload); + downloadBase64File( + result.fileName, + result.fileContentBase64, + 'application/pdf', + ); + message.success('PDF 下载成功'); + } finally { + options.isDownloadingPdf.value = false; + } + } + + async function downloadExcel(reportId?: string) { + if (!options.canExport.value || !options.selectedStoreId.value) { + return; + } + + const targetReportId = resolveTargetReportId(reportId); + if (!targetReportId) { + return; + } + + options.isDownloadingExcel.value = true; + try { + const payload = buildReportDetailQueryPayload( + options.selectedStoreId.value, + targetReportId, + ); + const result = await exportFinanceBusinessReportExcelApi(payload); + downloadBase64File( + result.fileName, + result.fileContentBase64, + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + ); + message.success('Excel 下载成功'); + } finally { + options.isDownloadingExcel.value = false; + } + } + + return { + downloadExcel, + downloadPdf, + openPreview, + setPreviewDrawerOpen, + }; +} diff --git a/apps/web-antd/src/views/finance/report/composables/report-page/export-actions.ts b/apps/web-antd/src/views/finance/report/composables/report-page/export-actions.ts new file mode 100644 index 0000000..da5a768 --- /dev/null +++ b/apps/web-antd/src/views/finance/report/composables/report-page/export-actions.ts @@ -0,0 +1,71 @@ +import type { FinanceBusinessReportPaginationState } from '../../types'; + +/** + * 文件职责:经营报表批量导出弹窗与导出动作。 + */ +import type { FinanceBusinessReportPeriodType } from '#/api/finance'; + +import { message } from 'ant-design-vue'; + +import { exportFinanceBusinessReportBatchApi } from '#/api/finance'; + +import { + buildReportBatchExportQueryPayload, + downloadBase64File, +} from './helpers'; + +interface ExportActionOptions { + canExport: { value: boolean }; + isBatchExportModalOpen: { value: boolean }; + isBatchExporting: { value: boolean }; + pagination: FinanceBusinessReportPaginationState; + periodType: { value: FinanceBusinessReportPeriodType }; + selectedStoreId: { value: string }; +} + +/** 创建批量导出动作。 */ +export function createExportActions(options: ExportActionOptions) { + function setBatchExportModalOpen(value: boolean) { + options.isBatchExportModalOpen.value = value; + } + + function openBatchExportModal() { + if (!options.canExport.value || !options.selectedStoreId.value) { + return; + } + + options.isBatchExportModalOpen.value = true; + } + + async function handleConfirmBatchExport() { + if (!options.canExport.value || !options.selectedStoreId.value) { + return; + } + + options.isBatchExporting.value = true; + try { + const payload = buildReportBatchExportQueryPayload( + options.selectedStoreId.value, + options.periodType.value, + options.pagination.page, + options.pagination.pageSize, + ); + const result = await exportFinanceBusinessReportBatchApi(payload); + downloadBase64File( + result.fileName, + result.fileContentBase64, + 'application/zip', + ); + message.success(`批量导出成功,共 ${result.totalCount} 份报表`); + setBatchExportModalOpen(false); + } finally { + options.isBatchExporting.value = false; + } + } + + return { + handleConfirmBatchExport, + openBatchExportModal, + setBatchExportModalOpen, + }; +} diff --git a/apps/web-antd/src/views/finance/report/composables/report-page/filter-actions.ts b/apps/web-antd/src/views/finance/report/composables/report-page/filter-actions.ts new file mode 100644 index 0000000..dfc5867 --- /dev/null +++ b/apps/web-antd/src/views/finance/report/composables/report-page/filter-actions.ts @@ -0,0 +1,30 @@ +import type { FinanceBusinessReportPaginationState } from '../../types'; + +/** + * 文件职责:经营报表页面筛选与分页行为。 + */ +import type { FinanceBusinessReportPeriodType } from '#/api/finance'; + +interface FilterActionOptions { + loadReportList: () => Promise; + pagination: FinanceBusinessReportPaginationState; + periodType: { value: FinanceBusinessReportPeriodType }; +} + +/** 创建筛选与分页行为。 */ +export function createFilterActions(options: FilterActionOptions) { + function setPeriodType(value: FinanceBusinessReportPeriodType) { + options.periodType.value = value || 'daily'; + } + + async function handlePageChange(page: number, pageSize: number) { + options.pagination.page = page; + options.pagination.pageSize = pageSize; + await options.loadReportList(); + } + + return { + handlePageChange, + setPeriodType, + }; +} diff --git a/apps/web-antd/src/views/finance/report/composables/report-page/helpers.ts b/apps/web-antd/src/views/finance/report/composables/report-page/helpers.ts new file mode 100644 index 0000000..17f79bb --- /dev/null +++ b/apps/web-antd/src/views/finance/report/composables/report-page/helpers.ts @@ -0,0 +1,130 @@ +import type { + FinanceBusinessReportBatchExportQuery, + FinanceBusinessReportDetailQuery, + FinanceBusinessReportPeriodType, + FinanceBusinessReportStatus, +} from '#/api/finance'; + +/** + * 文件职责:经营报表页面纯函数与数据转换工具。 + */ + +/** 构建经营报表列表查询请求。 */ +export function buildReportListQueryPayload( + storeId: string, + periodType: FinanceBusinessReportPeriodType, + page: number, + pageSize: number, +) { + return { + storeId, + periodType, + page, + pageSize, + }; +} + +/** 构建经营报表详情查询请求。 */ +export function buildReportDetailQueryPayload( + storeId: string, + reportId: string, +): FinanceBusinessReportDetailQuery { + return { + storeId, + reportId, + }; +} + +/** 构建经营报表批量导出查询请求。 */ +export function buildReportBatchExportQueryPayload( + storeId: string, + periodType: FinanceBusinessReportPeriodType, + page: number, + pageSize: number, +): FinanceBusinessReportBatchExportQuery { + return { + storeId, + periodType, + page, + pageSize, + }; +} + +/** 报表周期文案。 */ +export function resolvePeriodTypeLabel(value: FinanceBusinessReportPeriodType) { + if (value === 'weekly') return '周报'; + if (value === 'monthly') return '月报'; + return '日报'; +} + +/** 报表状态标签色。 */ +export function resolveReportStatusTagColor( + status: FinanceBusinessReportStatus, +) { + if (status === 'succeeded') return 'green'; + if (status === 'failed') return 'red'; + if (status === 'running' || status === 'queued') return 'blue'; + return 'default'; +} + +/** 货币格式化(人民币)。 */ +export function formatCurrency(value: number, fractionDigits = 0) { + const digits = Math.max(0, Math.min(2, fractionDigits)); + return new Intl.NumberFormat('zh-CN', { + style: 'currency', + currency: 'CNY', + minimumFractionDigits: digits, + maximumFractionDigits: digits, + }).format(Number.isFinite(value) ? value : 0); +} + +/** 订单数格式化。 */ +export function formatOrderCount(value: number) { + return `${Math.max(0, Math.round(value || 0))}单`; +} + +/** 客单价格式化。 */ +export function formatAverageOrderValue(value: number) { + return formatCurrency(value, 1); +} + +/** 百分比格式化。 */ +export function formatPercent(value: number, digits = 1) { + const safeDigits = Math.max(0, Math.min(2, digits)); + const normalized = Number.isFinite(value) ? value : 0; + return `${normalized.toFixed(safeDigits)}%`; +} + +/** 数值符号格式化。 */ +export function formatSignedRate(value: number, digits = 1) { + const normalized = Number.isFinite(value) ? value : 0; + if (normalized === 0) { + return `${normalized.toFixed(digits)}%`; + } + + return `${normalized > 0 ? '↑' : '↓'}${Math.abs(normalized).toFixed(digits)}%`; +} + +function decodeBase64ToBlob(base64: string, mimeType: 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: mimeType }); +} + +/** 下载 Base64 编码文件。 */ +export function downloadBase64File( + fileName: string, + fileContentBase64: string, + mimeType = 'application/octet-stream', +) { + const blob = decodeBase64ToBlob(fileContentBase64, mimeType); + 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/report/composables/useFinanceReportPage.ts b/apps/web-antd/src/views/finance/report/composables/useFinanceReportPage.ts new file mode 100644 index 0000000..1288220 --- /dev/null +++ b/apps/web-antd/src/views/finance/report/composables/useFinanceReportPage.ts @@ -0,0 +1,219 @@ +/** + * 文件职责:经营报表页面状态与动作编排。 + */ +import type { + FinanceBusinessReportDetailDto, + FinanceBusinessReportListItemDto, + FinanceBusinessReportPeriodType, +} from '#/api/finance'; +import type { StoreListItemDto } from '#/api/store'; + +import { computed, onActivated, onMounted, reactive, ref, watch } from 'vue'; + +import { useAccessStore } from '@vben/stores'; + +import { + DEFAULT_PAGINATION, + DEFAULT_PERIOD_TYPE, + FINANCE_REPORT_EXPORT_PERMISSION, + FINANCE_REPORT_VIEW_PERMISSION, +} from './report-page/constants'; +import { createDataActions } from './report-page/data-actions'; +import { createDetailActions } from './report-page/detail-actions'; +import { createExportActions } from './report-page/export-actions'; +import { createFilterActions } from './report-page/filter-actions'; + +/** 创建经营报表页面组合状态。 */ +export function useFinanceReportPage() { + const accessStore = useAccessStore(); + + const stores = ref([]); + const selectedStoreId = ref(''); + const isStoreLoading = ref(false); + + const periodType = ref(DEFAULT_PERIOD_TYPE); + const rows = ref([]); + const pagination = reactive({ ...DEFAULT_PAGINATION }); + const isListLoading = ref(false); + + const previewDetail = ref(null); + const currentPreviewReportId = ref(''); + const isPreviewDrawerOpen = ref(false); + const isPreviewLoading = ref(false); + const isDownloadingPdf = ref(false); + const isDownloadingExcel = ref(false); + + const isBatchExportModalOpen = ref(false); + const isBatchExporting = ref(false); + + const storeOptions = computed(() => + stores.value.map((item) => ({ + label: item.name, + value: item.id, + })), + ); + + const selectedStoreName = computed( + () => + storeOptions.value.find((item) => item.value === selectedStoreId.value) + ?.label ?? '--', + ); + + const accessCodeSet = computed( + () => new Set((accessStore.accessCodes ?? []).map(String)), + ); + + const canView = computed(() => + accessCodeSet.value.has(FINANCE_REPORT_VIEW_PERMISSION), + ); + + const canExport = computed(() => + accessCodeSet.value.has(FINANCE_REPORT_EXPORT_PERMISSION), + ); + + const { clearPageData, loadReportList, loadStores } = createDataActions({ + stores, + selectedStoreId, + periodType, + rows, + pagination, + isStoreLoading, + isListLoading, + }); + + const { handlePageChange, setPeriodType } = createFilterActions({ + periodType, + pagination, + loadReportList, + }); + + const { downloadExcel, downloadPdf, openPreview, setPreviewDrawerOpen } = + createDetailActions({ + canExport, + selectedStoreId, + previewDetail, + currentPreviewReportId, + isPreviewDrawerOpen, + isPreviewLoading, + isDownloadingPdf, + isDownloadingExcel, + }); + + const { + handleConfirmBatchExport, + openBatchExportModal, + setBatchExportModalOpen, + } = createExportActions({ + canExport, + selectedStoreId, + periodType, + pagination, + isBatchExportModalOpen, + isBatchExporting, + }); + + function setSelectedStoreId(value: string) { + selectedStoreId.value = value; + } + + function clearByPermission() { + stores.value = []; + selectedStoreId.value = ''; + clearPageData(); + setPreviewDrawerOpen(false); + setBatchExportModalOpen(false); + } + + watch(selectedStoreId, async (storeId) => { + setPreviewDrawerOpen(false); + setBatchExportModalOpen(false); + + if (!storeId) { + clearPageData(); + return; + } + + pagination.page = 1; + await loadReportList(); + }); + + watch(periodType, async (value, oldValue) => { + if (value === oldValue) { + return; + } + + setPreviewDrawerOpen(false); + setBatchExportModalOpen(false); + + if (!selectedStoreId.value) { + clearPageData(); + return; + } + + pagination.page = 1; + await loadReportList(); + }); + + watch(canView, async (value, oldValue) => { + if (value === oldValue) { + return; + } + + if (!value) { + clearByPermission(); + return; + } + + await loadStores(); + }); + + onMounted(async () => { + if (!canView.value) { + clearByPermission(); + return; + } + + await loadStores(); + }); + + onActivated(() => { + if (!canView.value) { + return; + } + + if (stores.value.length === 0 || !selectedStoreId.value) { + void loadStores(); + } + }); + + return { + canExport, + canView, + currentPreviewReportId, + downloadPreviewExcel: downloadExcel, + downloadPreviewPdf: downloadPdf, + handleConfirmBatchExport, + handlePageChange, + isBatchExportModalOpen, + isBatchExporting, + isDownloadingExcel, + isDownloadingPdf, + isListLoading, + isPreviewDrawerOpen, + isPreviewLoading, + isStoreLoading, + openBatchExportModal, + openPreview, + pagination, + periodType, + previewDetail, + rows, + selectedStoreId, + selectedStoreName, + setBatchExportModalOpen, + setPeriodType, + setPreviewDrawerOpen, + setSelectedStoreId, + storeOptions, + }; +} diff --git a/apps/web-antd/src/views/finance/report/index.vue b/apps/web-antd/src/views/finance/report/index.vue new file mode 100644 index 0000000..8e3d949 --- /dev/null +++ b/apps/web-antd/src/views/finance/report/index.vue @@ -0,0 +1,104 @@ + + + + + diff --git a/apps/web-antd/src/views/finance/report/styles/base.less b/apps/web-antd/src/views/finance/report/styles/base.less new file mode 100644 index 0000000..e903133 --- /dev/null +++ b/apps/web-antd/src/views/finance/report/styles/base.less @@ -0,0 +1,14 @@ +/** + * 文件职责:经营报表页面基础容器样式。 + */ +.page-finance-report { + .ant-card { + border-radius: 10px; + } +} + +.frp-page { + display: flex; + flex-direction: column; + gap: 12px; +} diff --git a/apps/web-antd/src/views/finance/report/styles/drawer.less b/apps/web-antd/src/views/finance/report/styles/drawer.less new file mode 100644 index 0000000..808207e --- /dev/null +++ b/apps/web-antd/src/views/finance/report/styles/drawer.less @@ -0,0 +1,162 @@ +/** + * 文件职责:经营报表预览抽屉样式。 + */ +.page-finance-report { + .ant-drawer { + .ant-drawer-header { + padding: 14px 18px; + border-bottom: 1px solid #f0f0f0; + } + + .ant-drawer-body { + padding: 16px 20px; + } + + .ant-drawer-footer { + padding: 12px 20px; + border-top: 1px solid #f0f0f0; + } + } +} + +.frp-section { + margin-bottom: 22px; +} + +.frp-section-hd { + padding-left: 10px; + margin-bottom: 14px; + font-size: 15px; + font-weight: 600; + color: rgb(0 0 0 / 88%); + border-left: 3px solid #1677ff; +} + +.frp-kpi-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 12px; +} + +.frp-kpi-item { + padding: 14px 16px; + background: #f8f9fb; + border: 1px solid #f0f0f0; + border-radius: 8px; +} + +.frp-kpi-label { + margin-bottom: 6px; + font-size: 12px; + color: rgb(0 0 0 / 45%); +} + +.frp-kpi-val { + font-size: 20px; + font-weight: 700; + line-height: 1.3; + color: rgb(0 0 0 / 88%); + + &.is-profit { + color: #52c41a; + } +} + +.frp-kpi-change { + display: flex; + gap: 8px; + align-items: center; + margin-top: 4px; + font-size: 12px; +} + +.frp-kpi-change-positive { + color: #52c41a; +} + +.frp-kpi-change-negative { + color: #ff4d4f; +} + +.frp-kpi-change-neutral { + color: rgb(0 0 0 / 45%); +} + +.frp-divider { + height: 1px; + margin: 20px 0; + background: linear-gradient(90deg, #f0f0f0, transparent); +} + +.frp-detail-row { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 0; + font-size: 13px; + border-bottom: 1px solid #f5f5f5; + + &:last-child { + border-bottom: none; + } +} + +.frp-detail-name { + display: inline-flex; + gap: 6px; + align-items: center; + color: rgb(0 0 0 / 88%); +} + +.frp-breakdown-icon { + width: 14px; + height: 14px; + + &.is-primary { + color: #1677ff; + } + + &.is-success { + color: #52c41a; + } + + &.is-warning { + color: #faad14; + } + + &.is-muted { + color: rgb(0 0 0 / 45%); + } +} + +.frp-detail-right { + display: flex; + gap: 16px; + align-items: center; +} + +.frp-detail-amount { + min-width: 80px; + font-weight: 600; + color: rgb(0 0 0 / 88%); + text-align: right; +} + +.frp-detail-pct { + min-width: 50px; + font-size: 12px; + color: rgb(0 0 0 / 45%); + text-align: right; +} + +.frp-drawer-footer { + display: flex; + gap: 8px; + justify-content: flex-end; +} + +.frp-drawer-btn { + display: inline-flex; + gap: 4px; + align-items: center; +} diff --git a/apps/web-antd/src/views/finance/report/styles/index.less b/apps/web-antd/src/views/finance/report/styles/index.less new file mode 100644 index 0000000..75b1c93 --- /dev/null +++ b/apps/web-antd/src/views/finance/report/styles/index.less @@ -0,0 +1,9 @@ +/** + * 文件职责:经营报表页面样式聚合入口。 + */ +@import './base.less'; +@import './layout.less'; +@import './table.less'; +@import './drawer.less'; +@import './modal.less'; +@import './responsive.less'; diff --git a/apps/web-antd/src/views/finance/report/styles/layout.less b/apps/web-antd/src/views/finance/report/styles/layout.less new file mode 100644 index 0000000..5003055 --- /dev/null +++ b/apps/web-antd/src/views/finance/report/styles/layout.less @@ -0,0 +1,39 @@ +/** + * 文件职责:经营报表页面布局与工具栏样式。 + */ +.frp-toolbar { + display: flex; + gap: 12px; + align-items: center; + padding: 16px 20px; + background: #fff; + border: 1px solid #f0f0f0; + border-radius: 10px; + box-shadow: 0 2px 8px rgb(15 23 42 / 6%); + + .frp-period-segment { + --ant-segmented-item-selected-bg: #fff; + --ant-segmented-item-selected-color: #1677ff; + } + + .frp-store-select { + width: 200px; + min-width: 200px; + } + + .frp-toolbar-right { + margin-left: auto; + } + + .frp-batch-export-btn { + display: inline-flex; + gap: 4px; + align-items: center; + height: 32px; + } + + .ant-select-selector { + height: 32px; + font-size: 13px; + } +} diff --git a/apps/web-antd/src/views/finance/report/styles/modal.less b/apps/web-antd/src/views/finance/report/styles/modal.less new file mode 100644 index 0000000..5f29ca9 --- /dev/null +++ b/apps/web-antd/src/views/finance/report/styles/modal.less @@ -0,0 +1,51 @@ +/** + * 文件职责:经营报表批量导出弹窗样式。 + */ +.frp-batch-modal { + display: flex; + flex-direction: column; + gap: 12px; +} + +.frp-batch-desc { + margin: 0; + font-size: 13px; + color: rgb(0 0 0 / 65%); +} + +.frp-batch-list { + display: flex; + flex-direction: column; + gap: 8px; + padding: 12px 14px; + background: #fafafa; + border: 1px solid #f0f0f0; + border-radius: 8px; +} + +.frp-batch-line { + display: flex; + gap: 16px; + justify-content: space-between; + font-size: 13px; + + .frp-batch-label { + color: rgb(0 0 0 / 45%); + } + + .frp-batch-value { + color: rgb(0 0 0 / 88%); + text-align: right; + } + + &.is-total { + padding-top: 8px; + margin-top: 4px; + font-weight: 600; + border-top: 1px solid #f0f0f0; + + .frp-batch-value { + color: #1677ff; + } + } +} diff --git a/apps/web-antd/src/views/finance/report/styles/responsive.less b/apps/web-antd/src/views/finance/report/styles/responsive.less new file mode 100644 index 0000000..19af752 --- /dev/null +++ b/apps/web-antd/src/views/finance/report/styles/responsive.less @@ -0,0 +1,58 @@ +/** + * 文件职责:经营报表页面响应式样式。 + */ +@media (max-width: 1200px) { + .frp-kpi-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@media (max-width: 768px) { + .frp-toolbar { + flex-wrap: wrap; + padding: 14px 12px; + + .frp-period-segment, + .frp-store-select { + width: 100%; + min-width: 100%; + } + + .frp-toolbar-right { + width: 100%; + margin-left: 0; + } + + .frp-batch-export-btn { + justify-content: center; + width: 100%; + } + } + + .frp-table-card { + .ant-table-wrapper { + overflow-x: auto; + } + } + + .frp-kpi-grid { + grid-template-columns: 1fr; + } + + .page-finance-report { + .ant-drawer { + .ant-drawer-content-wrapper { + width: 100% !important; + max-width: 100%; + } + + .ant-drawer-body { + padding: 14px 12px; + } + + .ant-drawer-footer { + padding: 12px; + } + } + } +} diff --git a/apps/web-antd/src/views/finance/report/styles/table.less b/apps/web-antd/src/views/finance/report/styles/table.less new file mode 100644 index 0000000..31b146e --- /dev/null +++ b/apps/web-antd/src/views/finance/report/styles/table.less @@ -0,0 +1,59 @@ +/** + * 文件职责:经营报表表格区域样式。 + */ +.frp-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; + white-space: nowrap; + } + } + + .ant-pagination { + margin: 14px 16px; + } +} + +.frp-date { + white-space: nowrap; +} + +.frp-value { + color: rgb(0 0 0 / 88%); + white-space: nowrap; +} + +.frp-amount { + font-weight: 600; + white-space: nowrap; + + &.is-profit { + color: #52c41a; + } + + &.is-loss { + color: #ff4d4f; + } +} + +.frp-action-group { + display: flex; + gap: 2px; + align-items: center; +} + +.frp-action-link { + padding-inline: 0; + white-space: nowrap; +} diff --git a/apps/web-antd/src/views/finance/report/types.ts b/apps/web-antd/src/views/finance/report/types.ts new file mode 100644 index 0000000..dc1284c --- /dev/null +++ b/apps/web-antd/src/views/finance/report/types.ts @@ -0,0 +1,23 @@ +/** + * 文件职责:经营报表页面本地状态类型定义。 + */ +import type { FinanceBusinessReportPeriodType } from '#/api/finance'; + +/** 通用选项项。 */ +export interface OptionItem { + label: string; + value: string; +} + +/** 报表周期选项。 */ +export interface FinanceBusinessReportPeriodOption { + label: string; + value: FinanceBusinessReportPeriodType; +} + +/** 经营报表分页状态。 */ +export interface FinanceBusinessReportPaginationState { + page: number; + pageSize: number; + total: number; +}