diff --git a/apps/web-antd/src/api/finance/index.ts b/apps/web-antd/src/api/finance/index.ts index 5171a66..e6b37ec 100644 --- a/apps/web-antd/src/api/finance/index.ts +++ b/apps/web-antd/src/api/finance/index.ts @@ -1,161 +1,6 @@ /** - * 文件职责:财务中心交易流水 API 契约与请求封装。 + * 文件职责:财务中心 API 聚合导出。 */ -import { requestClient } from '#/api/request'; - +export * from './invoice'; export * from './settlement'; - -/** 交易类型筛选值。 */ -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 './transaction'; diff --git a/apps/web-antd/src/api/finance/invoice.ts b/apps/web-antd/src/api/finance/invoice.ts new file mode 100644 index 0000000..e8b85e3 --- /dev/null +++ b/apps/web-antd/src/api/finance/invoice.ts @@ -0,0 +1,206 @@ +/** + * 文件职责:财务中心发票管理 API 契约与请求封装。 + */ +import { requestClient } from '#/api/request'; + +/** 发票状态筛选值。 */ +export type FinanceInvoiceStatusFilter = 'all' | 'issued' | 'pending' | 'voided'; + +/** 发票类型筛选值。 */ +export type FinanceInvoiceTypeFilter = 'all' | 'normal' | 'special'; + +/** 发票设置详情。 */ +export interface FinanceInvoiceSettingDto { + autoIssueMaxAmount: number; + bankAccount?: string; + bankName?: string; + companyName: string; + enableAutoIssue: boolean; + enableElectronicNormalInvoice: boolean; + enableElectronicSpecialInvoice: boolean; + registeredAddress?: string; + registeredPhone?: string; + taxpayerNumber: string; +} + +/** 保存发票设置请求。 */ +export interface SaveFinanceInvoiceSettingPayload + extends FinanceInvoiceSettingDto {} + +/** 发票记录列表查询参数。 */ +export interface FinanceInvoiceRecordListQuery { + endDate?: string; + invoiceType?: Exclude; + keyword?: string; + page: number; + pageSize: number; + startDate?: string; + status?: Exclude; +} + +/** 发票统计结果。 */ +export interface FinanceInvoiceStatsDto { + currentMonthIssuedAmount: number; + currentMonthIssuedCount: number; + pendingCount: number; + voidedCount: number; +} + +/** 发票记录列表项。 */ +export interface FinanceInvoiceRecordListItemDto { + amount: number; + applicantName: string; + appliedAt: string; + companyName: string; + invoiceNo: string; + invoiceType: string; + invoiceTypeText: string; + orderNo: string; + recordId: string; + status: string; + statusText: string; +} + +/** 发票记录分页结果。 */ +export interface FinanceInvoiceRecordListResultDto { + items: FinanceInvoiceRecordListItemDto[]; + page: number; + pageSize: number; + stats: FinanceInvoiceStatsDto; + totalCount: number; +} + +/** 发票记录详情。 */ +export interface FinanceInvoiceRecordDetailDto { + amount: number; + applicantName: string; + appliedAt: string; + applyRemark?: string; + companyName: string; + contactEmail?: string; + contactPhone?: string; + invoiceNo: string; + invoiceType: string; + invoiceTypeText: string; + issueRemark?: string; + issuedAt?: string; + issuedByUserId?: string; + orderNo: string; + recordId: string; + status: string; + statusText: string; + taxpayerNumber?: string; + voidReason?: string; + voidedAt?: string; + voidedByUserId?: string; +} + +/** 发票开票请求。 */ +export interface FinanceInvoiceIssuePayload { + contactEmail?: string; + issueRemark?: string; + recordId: string; +} + +/** 发票开票结果。 */ +export interface FinanceInvoiceIssueResultDto { + amount: number; + companyName: string; + contactEmail?: string; + invoiceNo: string; + issuedAt: string; + recordId: string; + status: string; + statusText: string; +} + +/** 发票作废请求。 */ +export interface FinanceInvoiceVoidPayload { + recordId: string; + voidReason: string; +} + +/** 发票申请请求。 */ +export interface FinanceInvoiceApplyPayload { + amount: number; + applicantName: string; + appliedAt?: string; + applyRemark?: string; + companyName: string; + contactEmail?: string; + contactPhone?: string; + invoiceType: Exclude; + orderNo: string; + taxpayerNumber?: string; +} + +/** 查询发票设置。 */ +export async function getFinanceInvoiceSettingDetailApi() { + return requestClient.get( + '/finance/invoice/settings/detail', + ); +} + +/** 保存发票设置。 */ +export async function saveFinanceInvoiceSettingApi( + payload: SaveFinanceInvoiceSettingPayload, +) { + return requestClient.post( + '/finance/invoice/settings/save', + payload, + ); +} + +/** 查询发票记录列表。 */ +export async function getFinanceInvoiceRecordListApi( + params: FinanceInvoiceRecordListQuery, +) { + return requestClient.get( + '/finance/invoice/record/list', + { + params, + }, + ); +} + +/** 查询发票记录详情。 */ +export async function getFinanceInvoiceRecordDetailApi(params: { + recordId: string; +}) { + return requestClient.get( + '/finance/invoice/record/detail', + { + params, + }, + ); +} + +/** 执行发票开票。 */ +export async function issueFinanceInvoiceRecordApi( + payload: FinanceInvoiceIssuePayload, +) { + return requestClient.post( + '/finance/invoice/record/issue', + payload, + ); +} + +/** 执行发票作废。 */ +export async function voidFinanceInvoiceRecordApi( + payload: FinanceInvoiceVoidPayload, +) { + return requestClient.post( + '/finance/invoice/record/void', + payload, + ); +} + +/** 发起发票申请。 */ +export async function applyFinanceInvoiceRecordApi( + payload: FinanceInvoiceApplyPayload, +) { + return requestClient.post( + '/finance/invoice/record/apply', + payload, + ); +} 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/invoice/components/InvoiceDetailDrawer.vue b/apps/web-antd/src/views/finance/invoice/components/InvoiceDetailDrawer.vue new file mode 100644 index 0000000..7ed69be --- /dev/null +++ b/apps/web-antd/src/views/finance/invoice/components/InvoiceDetailDrawer.vue @@ -0,0 +1,136 @@ + + + diff --git a/apps/web-antd/src/views/finance/invoice/components/InvoiceFilterBar.vue b/apps/web-antd/src/views/finance/invoice/components/InvoiceFilterBar.vue new file mode 100644 index 0000000..56590b7 --- /dev/null +++ b/apps/web-antd/src/views/finance/invoice/components/InvoiceFilterBar.vue @@ -0,0 +1,101 @@ + + + diff --git a/apps/web-antd/src/views/finance/invoice/components/InvoiceIssueDrawer.vue b/apps/web-antd/src/views/finance/invoice/components/InvoiceIssueDrawer.vue new file mode 100644 index 0000000..b6805a7 --- /dev/null +++ b/apps/web-antd/src/views/finance/invoice/components/InvoiceIssueDrawer.vue @@ -0,0 +1,121 @@ + + + diff --git a/apps/web-antd/src/views/finance/invoice/components/InvoiceIssueResultModal.vue b/apps/web-antd/src/views/finance/invoice/components/InvoiceIssueResultModal.vue new file mode 100644 index 0000000..78965ac --- /dev/null +++ b/apps/web-antd/src/views/finance/invoice/components/InvoiceIssueResultModal.vue @@ -0,0 +1,62 @@ + + + diff --git a/apps/web-antd/src/views/finance/invoice/components/InvoiceSegmentTabs.vue b/apps/web-antd/src/views/finance/invoice/components/InvoiceSegmentTabs.vue new file mode 100644 index 0000000..41a4113 --- /dev/null +++ b/apps/web-antd/src/views/finance/invoice/components/InvoiceSegmentTabs.vue @@ -0,0 +1,29 @@ + + + diff --git a/apps/web-antd/src/views/finance/invoice/components/InvoiceSettingsConfirmModal.vue b/apps/web-antd/src/views/finance/invoice/components/InvoiceSettingsConfirmModal.vue new file mode 100644 index 0000000..7131334 --- /dev/null +++ b/apps/web-antd/src/views/finance/invoice/components/InvoiceSettingsConfirmModal.vue @@ -0,0 +1,72 @@ + + + diff --git a/apps/web-antd/src/views/finance/invoice/components/InvoiceSettingsPanel.vue b/apps/web-antd/src/views/finance/invoice/components/InvoiceSettingsPanel.vue new file mode 100644 index 0000000..eebcb93 --- /dev/null +++ b/apps/web-antd/src/views/finance/invoice/components/InvoiceSettingsPanel.vue @@ -0,0 +1,171 @@ + + + diff --git a/apps/web-antd/src/views/finance/invoice/components/InvoiceStatsBar.vue b/apps/web-antd/src/views/finance/invoice/components/InvoiceStatsBar.vue new file mode 100644 index 0000000..c6a0673 --- /dev/null +++ b/apps/web-antd/src/views/finance/invoice/components/InvoiceStatsBar.vue @@ -0,0 +1,47 @@ + + + diff --git a/apps/web-antd/src/views/finance/invoice/components/InvoiceTableCard.vue b/apps/web-antd/src/views/finance/invoice/components/InvoiceTableCard.vue new file mode 100644 index 0000000..c6b6894 --- /dev/null +++ b/apps/web-antd/src/views/finance/invoice/components/InvoiceTableCard.vue @@ -0,0 +1,206 @@ + + + diff --git a/apps/web-antd/src/views/finance/invoice/components/InvoiceVoidConfirmModal.vue b/apps/web-antd/src/views/finance/invoice/components/InvoiceVoidConfirmModal.vue new file mode 100644 index 0000000..07cb1a5 --- /dev/null +++ b/apps/web-antd/src/views/finance/invoice/components/InvoiceVoidConfirmModal.vue @@ -0,0 +1,71 @@ + + + diff --git a/apps/web-antd/src/views/finance/invoice/composables/invoice-page/constants.ts b/apps/web-antd/src/views/finance/invoice/composables/invoice-page/constants.ts new file mode 100644 index 0000000..bed2d41 --- /dev/null +++ b/apps/web-antd/src/views/finance/invoice/composables/invoice-page/constants.ts @@ -0,0 +1,108 @@ +import type { + FinanceInvoiceSettingFormState, + FinanceInvoiceTabKey, + OptionItem, +} from '../../types'; + +/** + * 文件职责:发票管理页面常量定义。 + */ +import type { FinanceInvoiceStatsDto } from '#/api/finance'; + +import { getMonthFirstDateString, getTodayDateString } from './helpers'; + +/** 发票管理页面查看权限。 */ +export const FINANCE_INVOICE_VIEW_PERMISSION = 'tenant:finance:invoice:view'; + +/** 发票开票权限。 */ +export const FINANCE_INVOICE_ISSUE_PERMISSION = 'tenant:finance:invoice:issue'; + +/** 发票作废权限。 */ +export const FINANCE_INVOICE_VOID_PERMISSION = 'tenant:finance:invoice:void'; + +/** 发票设置权限。 */ +export const FINANCE_INVOICE_SETTINGS_PERMISSION = + 'tenant:finance:invoice:settings'; + +/** 分段选项。 */ +export const INVOICE_TAB_OPTIONS: Array<{ + label: string; + value: FinanceInvoiceTabKey; +}> = [ + { label: '开票记录', value: 'records' }, + { label: '发票设置', value: 'settings' }, +]; + +/** 发票状态筛选项。 */ +export const INVOICE_STATUS_OPTIONS: OptionItem[] = [ + { label: '全部状态', value: 'all' }, + { label: '待开票', value: 'pending' }, + { label: '已开票', value: 'issued' }, + { label: '已作废', value: 'voided' }, +]; + +/** 发票类型筛选项。 */ +export const INVOICE_TYPE_OPTIONS: OptionItem[] = [ + { label: '全部类型', value: 'all' }, + { label: '普通发票', value: 'normal' }, + { label: '专用发票', value: 'special' }, +]; + +/** 默认统计数据。 */ +export const DEFAULT_INVOICE_STATS: FinanceInvoiceStatsDto = { + currentMonthIssuedAmount: 0, + currentMonthIssuedCount: 0, + pendingCount: 0, + voidedCount: 0, +}; + +/** 创建默认筛选条件。 */ +export function createDefaultFilters() { + return { + startDate: getMonthFirstDateString(), + endDate: getTodayDateString(), + status: 'all', + invoiceType: 'all', + keyword: '', + }; +} + +/** 创建默认分页。 */ +export function createDefaultPagination() { + return { + page: 1, + pageSize: 10, + totalCount: 0, + }; +} + +/** 创建默认设置表单。 */ +export function createDefaultSettingsForm(): FinanceInvoiceSettingFormState { + return { + companyName: '', + taxpayerNumber: '', + registeredAddress: '', + registeredPhone: '', + bankName: '', + bankAccount: '', + enableElectronicNormalInvoice: true, + enableElectronicSpecialInvoice: false, + enableAutoIssue: false, + autoIssueMaxAmount: 10_000, + }; +} + +/** 创建默认开票表单。 */ +export function createDefaultIssueForm() { + return { + contactEmail: '', + issueRemark: '', + }; +} + +/** 创建默认作废表单。 */ +export function createDefaultVoidForm() { + return { + voidReason: '', + }; +} diff --git a/apps/web-antd/src/views/finance/invoice/composables/invoice-page/data-actions.ts b/apps/web-antd/src/views/finance/invoice/composables/invoice-page/data-actions.ts new file mode 100644 index 0000000..b56c6b5 --- /dev/null +++ b/apps/web-antd/src/views/finance/invoice/composables/invoice-page/data-actions.ts @@ -0,0 +1,126 @@ +import type { + FinanceInvoiceRecordListItemDto, + FinanceInvoiceSettingDto, + FinanceInvoiceStatsDto, +} from '#/api/finance'; +import type { + FinanceInvoiceFilterState, + FinanceInvoicePaginationState, + FinanceInvoiceSettingFormState, +} from '../../types'; + +/** + * 文件职责:发票管理页面数据加载动作。 + */ +import { + getFinanceInvoiceRecordListApi, + getFinanceInvoiceSettingDetailApi, +} from '#/api/finance'; + +import { + applySettingDtoToForm, + buildInvoiceListQuery, + cloneSettingsForm, +} from './helpers'; + +interface DataActionOptions { + canViewRecords: { value: boolean }; + canViewSettings: { value: boolean }; + createDefaultSettingsForm: () => FinanceInvoiceSettingFormState; + filters: FinanceInvoiceFilterState; + isListLoading: { value: boolean }; + isSettingsLoading: { value: boolean }; + pagination: FinanceInvoicePaginationState; + rows: { value: FinanceInvoiceRecordListItemDto[] }; + settingForm: FinanceInvoiceSettingFormState; + settingSnapshot: { value: FinanceInvoiceSettingFormState }; + stats: FinanceInvoiceStatsDto; +} + +/** 创建页面数据动作。 */ +export function createDataActions(options: DataActionOptions) { + function resetStats() { + options.stats.currentMonthIssuedAmount = 0; + options.stats.currentMonthIssuedCount = 0; + options.stats.pendingCount = 0; + options.stats.voidedCount = 0; + } + + function clearRecordData() { + options.rows.value = []; + options.pagination.totalCount = 0; + resetStats(); + } + + function resetSettings() { + const defaults = options.createDefaultSettingsForm(); + Object.assign(options.settingForm, defaults); + options.settingSnapshot.value = cloneSettingsForm(defaults); + } + + function applySettingsResult(result: FinanceInvoiceSettingDto) { + applySettingDtoToForm(options.settingForm, result); + options.settingSnapshot.value = cloneSettingsForm(options.settingForm); + } + + async function loadRecordList() { + if (!options.canViewRecords.value) { + clearRecordData(); + return; + } + + options.isListLoading.value = true; + try { + const result = await getFinanceInvoiceRecordListApi( + buildInvoiceListQuery(options.filters, options.pagination), + ); + + options.rows.value = result.items; + options.pagination.page = result.page; + options.pagination.pageSize = result.pageSize; + options.pagination.totalCount = result.totalCount; + + options.stats.currentMonthIssuedAmount = + result.stats.currentMonthIssuedAmount; + options.stats.currentMonthIssuedCount = result.stats.currentMonthIssuedCount; + options.stats.pendingCount = result.stats.pendingCount; + options.stats.voidedCount = result.stats.voidedCount; + } finally { + options.isListLoading.value = false; + } + } + + async function loadSettings() { + if (!options.canViewSettings.value) { + resetSettings(); + return; + } + + options.isSettingsLoading.value = true; + try { + const result = await getFinanceInvoiceSettingDetailApi(); + applySettingsResult(result); + } finally { + options.isSettingsLoading.value = false; + } + } + + function clearByPermission() { + if (!options.canViewRecords.value) { + clearRecordData(); + } + + if (!options.canViewSettings.value) { + resetSettings(); + } + } + + return { + applySettingsResult, + clearByPermission, + clearRecordData, + loadRecordList, + loadSettings, + resetSettings, + }; +} diff --git a/apps/web-antd/src/views/finance/invoice/composables/invoice-page/drawer-actions.ts b/apps/web-antd/src/views/finance/invoice/composables/invoice-page/drawer-actions.ts new file mode 100644 index 0000000..b3a4e35 --- /dev/null +++ b/apps/web-antd/src/views/finance/invoice/composables/invoice-page/drawer-actions.ts @@ -0,0 +1,253 @@ +import type { + FinanceInvoiceIssueResultDto, + FinanceInvoiceRecordDetailDto, + FinanceInvoiceRecordListItemDto, +} from '#/api/finance'; +import type { + FinanceInvoiceIssueFormState, + FinanceInvoiceVoidFormState, +} from '../../types'; + +/** + * 文件职责:发票管理页面抽屉与弹窗动作。 + */ +import { + getFinanceInvoiceRecordDetailApi, + issueFinanceInvoiceRecordApi, + voidFinanceInvoiceRecordApi, +} from '#/api/finance'; + +import { message } from 'ant-design-vue'; + +import { isPendingInvoice, toOptionalTrimmed } from './helpers'; + +interface DrawerActionOptions { + canIssue: { value: boolean }; + canVoid: { value: boolean }; + detail: { value: FinanceInvoiceRecordDetailDto | null }; + isDetailDrawerOpen: { value: boolean }; + isDetailLoading: { value: boolean }; + isIssueDetailLoading: { value: boolean }; + isIssueDrawerOpen: { value: boolean }; + isIssueResultModalOpen: { value: boolean }; + isIssueSubmitting: { value: boolean }; + isVoidModalOpen: { value: boolean }; + isVoidSubmitting: { value: boolean }; + issueDetail: { value: FinanceInvoiceRecordDetailDto | null }; + issueForm: FinanceInvoiceIssueFormState; + issueResult: { value: FinanceInvoiceIssueResultDto | null }; + issueTargetRecord: { value: FinanceInvoiceRecordListItemDto | null }; + reloadRecordList: () => Promise; + voidForm: FinanceInvoiceVoidFormState; + voidTargetRecord: { value: FinanceInvoiceRecordListItemDto | null }; +} + +/** 创建抽屉与弹窗动作。 */ +export function createDrawerActions(options: DrawerActionOptions) { + async function loadDetail(recordId: string) { + const result = await getFinanceInvoiceRecordDetailApi({ recordId }); + options.detail.value = result; + return result; + } + + function setDetailDrawerOpen(value: boolean) { + options.isDetailDrawerOpen.value = value; + if (!value) { + options.detail.value = null; + } + } + + function setIssueDrawerOpen(value: boolean) { + options.isIssueDrawerOpen.value = value; + if (!value) { + options.issueTargetRecord.value = null; + options.issueDetail.value = null; + options.issueForm.contactEmail = ''; + options.issueForm.issueRemark = ''; + } + } + + function setVoidModalOpen(value: boolean) { + options.isVoidModalOpen.value = value; + if (!value) { + options.voidTargetRecord.value = null; + options.voidForm.voidReason = ''; + } + } + + function setIssueResultModalOpen(value: boolean) { + options.isIssueResultModalOpen.value = value; + if (!value) { + options.issueResult.value = null; + } + } + + async function openDetail(recordId: string) { + if (!recordId) { + return; + } + + options.isDetailDrawerOpen.value = true; + options.isDetailLoading.value = true; + try { + await loadDetail(recordId); + } finally { + options.isDetailLoading.value = false; + } + } + + async function openIssue(record: FinanceInvoiceRecordListItemDto) { + if (!options.canIssue.value) { + message.warning('当前账号暂无开票权限'); + return; + } + + if (!isPendingInvoice(record.status)) { + message.warning('仅待开票记录可执行开票'); + return; + } + + options.issueTargetRecord.value = record; + options.issueForm.contactEmail = ''; + options.issueForm.issueRemark = ''; + options.issueDetail.value = null; + options.isIssueDrawerOpen.value = true; + + options.isIssueDetailLoading.value = true; + try { + const detail = await getFinanceInvoiceRecordDetailApi({ + recordId: record.recordId, + }); + options.issueDetail.value = detail; + options.issueForm.contactEmail = detail.contactEmail ?? ''; + } finally { + options.isIssueDetailLoading.value = false; + } + } + + async function refreshOpenDetailAfterMutation(recordId: string) { + if (!options.isDetailDrawerOpen.value) { + return; + } + + if (options.detail.value?.recordId !== recordId) { + return; + } + + options.isDetailLoading.value = true; + try { + await loadDetail(recordId); + } finally { + options.isDetailLoading.value = false; + } + } + + async function submitIssue() { + if (!options.canIssue.value) { + message.warning('当前账号暂无开票权限'); + return; + } + + const target = options.issueTargetRecord.value; + if (!target?.recordId) { + message.warning('请选择待开票记录'); + return; + } + + if (!isPendingInvoice(target.status)) { + message.warning('当前记录状态不可开票'); + return; + } + + const email = options.issueForm.contactEmail.trim(); + if (email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { + message.warning('请填写正确的邮箱地址'); + return; + } + + options.isIssueSubmitting.value = true; + try { + const result = await issueFinanceInvoiceRecordApi({ + recordId: target.recordId, + contactEmail: toOptionalTrimmed(options.issueForm.contactEmail), + issueRemark: toOptionalTrimmed(options.issueForm.issueRemark), + }); + + options.issueResult.value = result; + options.isIssueDrawerOpen.value = false; + options.isIssueResultModalOpen.value = true; + message.success('发票开具成功'); + + await options.reloadRecordList(); + await refreshOpenDetailAfterMutation(target.recordId); + } finally { + options.isIssueSubmitting.value = false; + } + } + + function openVoid(record: FinanceInvoiceRecordListItemDto) { + if (!options.canVoid.value) { + message.warning('当前账号暂无作废权限'); + return; + } + + options.voidTargetRecord.value = record; + options.voidForm.voidReason = ''; + options.isVoidModalOpen.value = true; + } + + async function submitVoid() { + if (!options.canVoid.value) { + message.warning('当前账号暂无作废权限'); + return; + } + + const target = options.voidTargetRecord.value; + if (!target?.recordId) { + message.warning('请选择需要作废的发票'); + return; + } + + const reason = options.voidForm.voidReason.trim(); + if (!reason) { + message.warning('请填写作废原因'); + return; + } + + options.isVoidSubmitting.value = true; + try { + const detail = await voidFinanceInvoiceRecordApi({ + recordId: target.recordId, + voidReason: reason, + }); + + message.success('发票已作废'); + options.isVoidModalOpen.value = false; + options.voidTargetRecord.value = null; + options.voidForm.voidReason = ''; + + if ( + options.isDetailDrawerOpen.value && + options.detail.value?.recordId === detail.recordId + ) { + options.detail.value = detail; + } + + await options.reloadRecordList(); + } finally { + options.isVoidSubmitting.value = false; + } + } + + return { + openDetail, + openIssue, + openVoid, + setDetailDrawerOpen, + setIssueDrawerOpen, + setIssueResultModalOpen, + setVoidModalOpen, + submitIssue, + submitVoid, + }; +} diff --git a/apps/web-antd/src/views/finance/invoice/composables/invoice-page/helpers.ts b/apps/web-antd/src/views/finance/invoice/composables/invoice-page/helpers.ts new file mode 100644 index 0000000..e05c28e --- /dev/null +++ b/apps/web-antd/src/views/finance/invoice/composables/invoice-page/helpers.ts @@ -0,0 +1,173 @@ +import type { + FinanceInvoiceRecordListQuery, + FinanceInvoiceSettingDto, + FinanceInvoiceStatusFilter, + FinanceInvoiceTypeFilter, +} from '#/api/finance'; +import type { + FinanceInvoiceFilterState, + FinanceInvoicePaginationState, + FinanceInvoiceSettingFormState, +} from '../../types'; + +/** + * 文件职责:发票管理页面纯函数工具。 + */ + +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 normalizeStatus( + status: FinanceInvoiceStatusFilter, +): Exclude | undefined { + return status === 'all' ? undefined : status; +} + +function normalizeInvoiceType( + invoiceType: FinanceInvoiceTypeFilter, +): Exclude | undefined { + return invoiceType === 'all' ? undefined : invoiceType; +} + +/** 获取今天日期字符串(yyyy-MM-dd)。 */ +export function getTodayDateString() { + return formatDate(new Date()); +} + +/** 获取当月第一天日期字符串(yyyy-MM-dd)。 */ +export function getMonthFirstDateString() { + const now = new Date(); + return formatDate(new Date(now.getFullYear(), now.getMonth(), 1)); +} + +/** 构建发票记录列表查询参数。 */ +export function buildInvoiceListQuery( + filters: FinanceInvoiceFilterState, + pagination: FinanceInvoicePaginationState, +): FinanceInvoiceRecordListQuery { + return { + page: pagination.page, + pageSize: pagination.pageSize, + startDate: filters.startDate || undefined, + endDate: filters.endDate || undefined, + status: normalizeStatus(filters.status), + invoiceType: normalizeInvoiceType(filters.invoiceType), + keyword: filters.keyword.trim() || undefined, + }; +} + +/** 判断日期范围是否合法。 */ +export function isDateRangeInvalid(filters: FinanceInvoiceFilterState) { + 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 resolveInvoiceTypeTagColor(type: string) { + if (type === 'normal') return 'blue'; + if (type === 'special') return 'purple'; + return 'default'; +} + +/** 发票状态标签颜色。 */ +export function resolveInvoiceStatusTagColor(status: string) { + if (status === 'pending') return 'orange'; + if (status === 'issued') return 'green'; + if (status === 'voided') return 'default'; + return 'default'; +} + +/** 是否为待开票状态。 */ +export function isPendingInvoice(status: string) { + return status === 'pending'; +} + +/** 是否为已开票状态。 */ +export function isIssuedInvoice(status: string) { + return status === 'issued'; +} + +/** 应用设置 DTO 到表单。 */ +export function applySettingDtoToForm( + target: FinanceInvoiceSettingFormState, + source: FinanceInvoiceSettingDto, +) { + target.companyName = source.companyName || ''; + target.taxpayerNumber = source.taxpayerNumber || ''; + target.registeredAddress = source.registeredAddress || ''; + target.registeredPhone = source.registeredPhone || ''; + target.bankName = source.bankName || ''; + target.bankAccount = source.bankAccount || ''; + target.enableElectronicNormalInvoice = Boolean( + source.enableElectronicNormalInvoice, + ); + target.enableElectronicSpecialInvoice = Boolean( + source.enableElectronicSpecialInvoice, + ); + target.enableAutoIssue = Boolean(source.enableAutoIssue); + target.autoIssueMaxAmount = Number(source.autoIssueMaxAmount || 0); +} + +/** 深拷贝设置表单快照。 */ +export function cloneSettingsForm( + source: FinanceInvoiceSettingFormState, +): FinanceInvoiceSettingFormState { + return { + companyName: source.companyName, + taxpayerNumber: source.taxpayerNumber, + registeredAddress: source.registeredAddress, + registeredPhone: source.registeredPhone, + bankName: source.bankName, + bankAccount: source.bankAccount, + enableElectronicNormalInvoice: source.enableElectronicNormalInvoice, + enableElectronicSpecialInvoice: source.enableElectronicSpecialInvoice, + enableAutoIssue: source.enableAutoIssue, + autoIssueMaxAmount: source.autoIssueMaxAmount, + }; +} + +/** 比较设置表单是否发生变化。 */ +export function isSettingsChanged( + current: FinanceInvoiceSettingFormState, + snapshot: FinanceInvoiceSettingFormState, +) { + return JSON.stringify(current) !== JSON.stringify(snapshot); +} + +/** 生成保存设置请求体。 */ +export function buildSettingSavePayload(form: FinanceInvoiceSettingFormState) { + return { + companyName: form.companyName.trim(), + taxpayerNumber: form.taxpayerNumber.trim(), + registeredAddress: form.registeredAddress.trim() || undefined, + registeredPhone: form.registeredPhone.trim() || undefined, + bankName: form.bankName.trim() || undefined, + bankAccount: form.bankAccount.trim() || undefined, + enableElectronicNormalInvoice: form.enableElectronicNormalInvoice, + enableElectronicSpecialInvoice: form.enableElectronicSpecialInvoice, + enableAutoIssue: form.enableAutoIssue, + autoIssueMaxAmount: Number(form.autoIssueMaxAmount || 0), + }; +} + +/** 文本转空值。 */ +export function toOptionalTrimmed(value: string) { + const normalized = value.trim(); + return normalized ? normalized : undefined; +} diff --git a/apps/web-antd/src/views/finance/invoice/composables/invoice-page/settings-actions.ts b/apps/web-antd/src/views/finance/invoice/composables/invoice-page/settings-actions.ts new file mode 100644 index 0000000..9ecb15d --- /dev/null +++ b/apps/web-antd/src/views/finance/invoice/composables/invoice-page/settings-actions.ts @@ -0,0 +1,120 @@ +import type { FinanceInvoiceSettingFormState } from '../../types'; + +/** + * 文件职责:发票设置保存与确认动作。 + */ +import type { FinanceInvoiceSettingDto } from '#/api/finance'; + +import { saveFinanceInvoiceSettingApi } from '#/api/finance'; + +import { message } from 'ant-design-vue'; + +import { + buildSettingSavePayload, + cloneSettingsForm, + isSettingsChanged, +} from './helpers'; + +interface SettingsActionOptions { + applySettingsResult: (result: FinanceInvoiceSettingDto) => void; + canManageSettings: { value: boolean }; + isSettingsConfirmModalOpen: { value: boolean }; + isSettingsSaving: { value: boolean }; + settingForm: FinanceInvoiceSettingFormState; + settingSnapshot: { value: FinanceInvoiceSettingFormState }; +} + +/** 创建发票设置动作。 */ +export function createSettingsActions(options: SettingsActionOptions) { + function setSettingsConfirmModalOpen(value: boolean) { + options.isSettingsConfirmModalOpen.value = value; + } + + function resetSettings() { + Object.assign(options.settingForm, cloneSettingsForm(options.settingSnapshot.value)); + } + + function hasSettingsChanged() { + return isSettingsChanged(options.settingForm, options.settingSnapshot.value); + } + + function validateSettings() { + if (!options.settingForm.companyName.trim()) { + message.warning('请输入企业名称'); + return false; + } + + if (!options.settingForm.taxpayerNumber.trim()) { + message.warning('请输入纳税人识别号'); + return false; + } + + if ( + !options.settingForm.enableElectronicNormalInvoice && + !options.settingForm.enableElectronicSpecialInvoice + ) { + message.warning('请至少启用一种发票类型'); + return false; + } + + if (options.settingForm.enableAutoIssue) { + const maxAmount = Number(options.settingForm.autoIssueMaxAmount || 0); + if (!Number.isFinite(maxAmount) || maxAmount <= 0) { + message.warning('自动开票最大金额必须大于 0'); + return false; + } + } + + return true; + } + + function openSettingsConfirmModal() { + if (!options.canManageSettings.value) { + message.warning('当前账号暂无发票设置权限'); + return; + } + + if (!validateSettings()) { + return; + } + + if (!hasSettingsChanged()) { + message.info('未检测到设置变更'); + return; + } + + options.isSettingsConfirmModalOpen.value = true; + } + + async function submitSettings() { + if (!options.canManageSettings.value) { + message.warning('当前账号暂无发票设置权限'); + return; + } + + if (!validateSettings()) { + return; + } + + options.isSettingsSaving.value = true; + try { + const result = await saveFinanceInvoiceSettingApi( + buildSettingSavePayload(options.settingForm), + ); + options.applySettingsResult(result); + options.isSettingsConfirmModalOpen.value = false; + message.success('发票设置已保存'); + } finally { + options.isSettingsSaving.value = false; + } + } + + return { + hasSettingsChanged, + openSettingsConfirmModal, + resetSettings, + setSettingsConfirmModalOpen, + submitSettings, + validateSettings, + }; +} diff --git a/apps/web-antd/src/views/finance/invoice/composables/useFinanceInvoicePage.ts b/apps/web-antd/src/views/finance/invoice/composables/useFinanceInvoicePage.ts new file mode 100644 index 0000000..e9b49a1 --- /dev/null +++ b/apps/web-antd/src/views/finance/invoice/composables/useFinanceInvoicePage.ts @@ -0,0 +1,397 @@ +/** + * 文件职责:发票管理页面状态与动作编排。 + */ +import type { + FinanceInvoiceIssueResultDto, + FinanceInvoiceRecordDetailDto, + FinanceInvoiceRecordListItemDto, + FinanceInvoiceStatsDto, +} from '#/api/finance'; +import type { + FinanceInvoiceFilterState, + FinanceInvoiceIssueFormState, + FinanceInvoicePaginationState, + FinanceInvoiceSettingFormState, + FinanceInvoiceTabKey, + FinanceInvoiceVoidFormState, +} from '../types'; + +import { computed, onActivated, onMounted, reactive, ref, watch } from 'vue'; + +import { useAccessStore } from '@vben/stores'; + +import { message } from 'ant-design-vue'; + +import { + createDefaultFilters, + createDefaultIssueForm, + createDefaultPagination, + createDefaultSettingsForm, + createDefaultVoidForm, + DEFAULT_INVOICE_STATS, + FINANCE_INVOICE_ISSUE_PERMISSION, + FINANCE_INVOICE_SETTINGS_PERMISSION, + FINANCE_INVOICE_VIEW_PERMISSION, + FINANCE_INVOICE_VOID_PERMISSION, + INVOICE_TAB_OPTIONS, +} from './invoice-page/constants'; +import { createDataActions } from './invoice-page/data-actions'; +import { createDrawerActions } from './invoice-page/drawer-actions'; +import { isDateRangeInvalid } from './invoice-page/helpers'; +import { createSettingsActions } from './invoice-page/settings-actions'; + +/** 创建发票管理页面组合状态。 */ +export function useFinanceInvoicePage() { + const accessStore = useAccessStore(); + + const activeTab = ref('records'); + + const filters = reactive( + createDefaultFilters() as FinanceInvoiceFilterState, + ); + const pagination = reactive( + createDefaultPagination(), + ); + const rows = ref([]); + + const stats = reactive({ ...DEFAULT_INVOICE_STATS }); + + const settingForm = reactive( + createDefaultSettingsForm(), + ); + const settingSnapshot = ref( + createDefaultSettingsForm(), + ); + + const issueForm = reactive( + createDefaultIssueForm(), + ); + const voidForm = reactive(createDefaultVoidForm()); + + const detail = ref(null); + const issueDetail = ref(null); + const issueResult = ref(null); + + const issueTargetRecord = ref(null); + const voidTargetRecord = ref(null); + + const isListLoading = ref(false); + const isSettingsLoading = ref(false); + const isDetailLoading = ref(false); + const isIssueDetailLoading = ref(false); + const isIssueSubmitting = ref(false); + const isVoidSubmitting = ref(false); + const isSettingsSaving = ref(false); + + const isDetailDrawerOpen = ref(false); + const isIssueDrawerOpen = ref(false); + const isVoidModalOpen = ref(false); + const isIssueResultModalOpen = ref(false); + const isSettingsConfirmModalOpen = ref(false); + + const accessCodeSet = computed( + () => new Set((accessStore.accessCodes ?? []).map(String)), + ); + + const canViewRecords = computed(() => { + const accessCodes = accessCodeSet.value; + return ( + accessCodes.has(FINANCE_INVOICE_VIEW_PERMISSION) || + accessCodes.has(FINANCE_INVOICE_ISSUE_PERMISSION) || + accessCodes.has(FINANCE_INVOICE_VOID_PERMISSION) + ); + }); + + const canIssue = computed(() => + accessCodeSet.value.has(FINANCE_INVOICE_ISSUE_PERMISSION), + ); + + const canVoid = computed(() => + accessCodeSet.value.has(FINANCE_INVOICE_VOID_PERMISSION), + ); + + const canViewSettings = computed(() => { + const accessCodes = accessCodeSet.value; + return ( + accessCodes.has(FINANCE_INVOICE_VIEW_PERMISSION) || + accessCodes.has(FINANCE_INVOICE_SETTINGS_PERMISSION) + ); + }); + + const canManageSettings = computed(() => + accessCodeSet.value.has(FINANCE_INVOICE_SETTINGS_PERMISSION), + ); + + const canViewPage = computed( + () => canViewRecords.value || canViewSettings.value, + ); + + const tabOptions = computed(() => + INVOICE_TAB_OPTIONS.filter((item) => { + if (item.value === 'records') { + return canViewRecords.value; + } + if (item.value === 'settings') { + return canViewSettings.value; + } + return false; + }), + ); + + const { + applySettingsResult, + clearByPermission, + clearRecordData, + loadRecordList, + loadSettings, + } = createDataActions({ + canViewRecords, + canViewSettings, + createDefaultSettingsForm, + filters, + isListLoading, + isSettingsLoading, + pagination, + rows, + settingForm, + settingSnapshot, + stats, + }); + + const { + openDetail, + openIssue, + openVoid, + setDetailDrawerOpen, + setIssueDrawerOpen, + setIssueResultModalOpen, + setVoidModalOpen, + submitIssue, + submitVoid, + } = createDrawerActions({ + canIssue, + canVoid, + detail, + isDetailDrawerOpen, + isDetailLoading, + isIssueDetailLoading, + isIssueDrawerOpen, + isIssueResultModalOpen, + isIssueSubmitting, + isVoidModalOpen, + isVoidSubmitting, + issueDetail, + issueForm, + issueResult, + issueTargetRecord, + reloadRecordList: loadRecordList, + voidForm, + voidTargetRecord, + }); + + const { + hasSettingsChanged, + openSettingsConfirmModal, + resetSettings, + setSettingsConfirmModalOpen, + submitSettings, + } = createSettingsActions({ + applySettingsResult, + canManageSettings, + isSettingsConfirmModalOpen, + isSettingsSaving, + settingForm, + settingSnapshot, + }); + + function setActiveTab(value: FinanceInvoiceTabKey) { + activeTab.value = value; + } + + function setStartDate(value: string) { + filters.startDate = value; + } + + function setEndDate(value: string) { + filters.endDate = value; + } + + function setStatus(value: string) { + filters.status = (value || 'all') as FinanceInvoiceFilterState['status']; + } + + function setInvoiceType(value: string) { + filters.invoiceType = (value || 'all') as FinanceInvoiceFilterState['invoiceType']; + } + + function setKeyword(value: string) { + filters.keyword = value; + } + + function setIssueFormContactEmail(value: string) { + issueForm.contactEmail = value; + } + + function setIssueFormRemark(value: string) { + issueForm.issueRemark = value; + } + + function setVoidReason(value: string) { + voidForm.voidReason = value; + } + + function setSettingsField( + field: K, + value: FinanceInvoiceSettingFormState[K], + ) { + settingForm[field] = value; + } + + async function handleSearch() { + if (isDateRangeInvalid(filters)) { + message.warning('开始日期不能晚于结束日期'); + return; + } + + pagination.page = 1; + await loadRecordList(); + } + + async function handlePageChange(page: number, pageSize: number) { + pagination.page = page; + pagination.pageSize = pageSize; + await loadRecordList(); + } + + function closeTransientPanels() { + setDetailDrawerOpen(false); + setIssueDrawerOpen(false); + setVoidModalOpen(false); + setIssueResultModalOpen(false); + setSettingsConfirmModalOpen(false); + } + + async function loadByPermission() { + const tasks: Array> = []; + + if (canViewRecords.value) { + tasks.push(loadRecordList()); + } else { + clearRecordData(); + } + + if (canViewSettings.value) { + tasks.push(loadSettings()); + } + + await Promise.all(tasks); + } + + watch(tabOptions, (next) => { + const values = next.map((item) => item.value); + if (values.length === 0) { + return; + } + + if (!values.includes(activeTab.value)) { + activeTab.value = values[0] as FinanceInvoiceTabKey; + } + }); + + watch( + canViewPage, + async (value, oldValue) => { + if (value === oldValue) { + return; + } + + if (!value) { + clearByPermission(); + closeTransientPanels(); + return; + } + + await loadByPermission(); + }, + { immediate: false }, + ); + + onMounted(async () => { + if (!canViewPage.value) { + clearByPermission(); + closeTransientPanels(); + return; + } + + await loadByPermission(); + }); + + onActivated(() => { + if (!canViewPage.value) { + return; + } + + void loadByPermission(); + }); + + return { + activeTab, + canIssue, + canManageSettings, + canViewPage, + canViewRecords, + canViewSettings, + canVoid, + detail, + filters, + handlePageChange, + handleSearch, + hasSettingsChanged, + isDetailDrawerOpen, + isDetailLoading, + isIssueDetailLoading, + isIssueDrawerOpen, + isIssueResultModalOpen, + isIssueSubmitting, + isListLoading, + isSettingsConfirmModalOpen, + isSettingsLoading, + isSettingsSaving, + isVoidModalOpen, + isVoidSubmitting, + issueDetail, + issueForm, + issueResult, + issueTargetRecord, + openDetail, + openIssue, + openSettingsConfirmModal, + openVoid, + pagination, + resetSettings, + rows, + setActiveTab, + setDetailDrawerOpen, + setEndDate, + setInvoiceType, + setIssueDrawerOpen, + setIssueFormContactEmail, + setIssueFormRemark, + setIssueResultModalOpen, + setKeyword, + setSettingsConfirmModalOpen, + setSettingsField, + setStartDate, + setStatus, + setVoidModalOpen, + setVoidReason, + settingForm, + stats, + submitIssue, + submitSettings, + submitVoid, + tabOptions, + voidForm, + voidTargetRecord, + }; +} diff --git a/apps/web-antd/src/views/finance/invoice/index.vue b/apps/web-antd/src/views/finance/invoice/index.vue new file mode 100644 index 0000000..254fffd --- /dev/null +++ b/apps/web-antd/src/views/finance/invoice/index.vue @@ -0,0 +1,198 @@ + + + + + diff --git a/apps/web-antd/src/views/finance/invoice/styles/base.less b/apps/web-antd/src/views/finance/invoice/styles/base.less new file mode 100644 index 0000000..8ef50dd --- /dev/null +++ b/apps/web-antd/src/views/finance/invoice/styles/base.less @@ -0,0 +1,24 @@ +/** + * 文件职责:发票管理页面基础样式。 + */ +.page-finance-invoice { + .ant-card { + border-radius: 10px; + } +} + +.fi-page { + display: flex; + flex-direction: column; + gap: 12px; +} + +.fi-tab-panel { + display: flex; + flex-direction: column; + gap: 12px; +} + +.fi-mono { + font-family: ui-monospace, sfmono-regular, menlo, consolas, monospace; +} diff --git a/apps/web-antd/src/views/finance/invoice/styles/drawer.less b/apps/web-antd/src/views/finance/invoice/styles/drawer.less new file mode 100644 index 0000000..fb9801e --- /dev/null +++ b/apps/web-antd/src/views/finance/invoice/styles/drawer.less @@ -0,0 +1,135 @@ +/** + * 文件职责:发票抽屉样式。 + */ +.page-finance-invoice { + .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; + } + } +} + +.fi-section { + margin-bottom: 20px; +} + +.fi-section-title { + padding-left: 10px; + margin-bottom: 14px; + font-size: 14px; + font-weight: 600; + color: rgb(0 0 0 / 88%); + border-left: 3px solid #1677ff; +} + +.fi-info-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 10px 20px; + font-size: 13px; + + .label { + color: rgb(0 0 0 / 45%); + } + + .full { + grid-column: 1 / -1; + } +} + +.fi-amount-strong { + font-weight: 600; + color: rgb(0 0 0 / 88%); +} + +.fi-drawer-footer { + display: flex; + gap: 8px; + justify-content: flex-end; +} + +.fi-timeline { + position: relative; + padding-left: 22px; +} + +.fi-timeline-item { + position: relative; + display: flex; + gap: 8px; + align-items: center; + padding-bottom: 18px; + font-size: 13px; + + &::before { + position: absolute; + top: 5px; + left: -22px; + width: 10px; + height: 10px; + content: ''; + background: #1677ff; + border: 2px solid #d6e4ff; + border-radius: 50%; + } + + &::after { + position: absolute; + top: 17px; + left: -18px; + width: 2px; + height: calc(100% - 12px); + content: ''; + background: #e8e8e8; + } + + &:last-child { + padding-bottom: 0; + + &::after { + display: none; + } + } + + .text { + font-weight: 500; + color: rgb(0 0 0 / 88%); + } + + .time { + color: rgb(0 0 0 / 45%); + } +} + +.fi-remark-box { + display: flex; + flex-direction: column; + gap: 8px; + padding: 10px 14px; + font-size: 13px; + color: rgb(0 0 0 / 65%); + background: #fafafa; + border: 1px solid #f0f0f0; + border-radius: 8px; +} + +.fi-remark-line { + display: flex; + gap: 8px; + + .label { + width: 70px; + flex-shrink: 0; + color: rgb(0 0 0 / 45%); + } +} diff --git a/apps/web-antd/src/views/finance/invoice/styles/index.less b/apps/web-antd/src/views/finance/invoice/styles/index.less new file mode 100644 index 0000000..364fe8d --- /dev/null +++ b/apps/web-antd/src/views/finance/invoice/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/invoice/styles/layout.less b/apps/web-antd/src/views/finance/invoice/styles/layout.less new file mode 100644 index 0000000..2ae09c9 --- /dev/null +++ b/apps/web-antd/src/views/finance/invoice/styles/layout.less @@ -0,0 +1,166 @@ +/** + * 文件职责:发票管理页面布局样式。 + */ +.fi-seg-wrap { + margin-bottom: 2px; +} + +.fi-stats { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 12px; +} + +.fi-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); + } + + &.warn .fi-stat-val { + color: #fa8c16; + } + + &.danger .fi-stat-val { + color: #ff4d4f; + } +} + +.fi-stat-label { + display: flex; + gap: 6px; + align-items: center; + margin-bottom: 8px; + font-size: 13px; + color: rgb(0 0 0 / 45%); +} + +.fi-stat-icon { + width: 16px; + height: 16px; +} + +.fi-stat-val { + font-size: 24px; + font-weight: 700; + line-height: 1.2; + color: rgb(0 0 0 / 88%); +} + +.fi-stat-sub { + margin-top: 4px; + font-size: 12px; + color: rgb(0 0 0 / 45%); +} + +.fi-toolbar { + display: flex; + flex-wrap: wrap; + gap: 10px; + align-items: center; + padding: 14px 16px; + background: #fff; + border: 1px solid #f0f0f0; + border-radius: 10px; + box-shadow: 0 2px 8px rgb(15 23 42 / 6%); + + .fi-date-input { + width: 146px; + } + + .fi-status-select, + .fi-type-select { + width: 124px; + } + + .fi-date-sep { + font-size: 13px; + line-height: 32px; + color: rgb(0 0 0 / 45%); + } + + .fi-toolbar-spacer { + flex: 1; + } + + .fi-search { + width: 220px; + } + + .fi-search-icon { + width: 14px; + height: 14px; + color: rgb(0 0 0 / 45%); + } + + .ant-select-selector, + .ant-input, + .ant-input-affix-wrapper { + height: 32px; + font-size: 13px; + } + + .ant-input-affix-wrapper .ant-input { + height: 100%; + } +} + +.fi-settings { + display: flex; + flex-direction: column; + gap: 12px; +} + +.fi-section-hd { + padding-left: 10px; + margin-bottom: 16px; + font-size: 15px; + font-weight: 600; + color: rgb(0 0 0 / 88%); + border-left: 3px solid #1677ff; +} + +.fi-form-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 8px 24px; +} + +.fi-toggle-row { + display: flex; + gap: 10px; + align-items: center; + margin-bottom: 14px; +} + +.fi-toggle-label { + font-size: 13px; + font-weight: 500; + color: rgb(0 0 0 / 88%); +} + +.fi-toggle-hint { + font-size: 12px; + color: rgb(0 0 0 / 45%); +} + +.fi-auto-form { + max-width: 340px; +} + +.fi-max-amount-input { + width: 100%; +} + +.fi-save-bar { + display: flex; + gap: 8px; + justify-content: flex-end; +} diff --git a/apps/web-antd/src/views/finance/invoice/styles/modal.less b/apps/web-antd/src/views/finance/invoice/styles/modal.less new file mode 100644 index 0000000..125eb9b --- /dev/null +++ b/apps/web-antd/src/views/finance/invoice/styles/modal.less @@ -0,0 +1,63 @@ +/** + * 文件职责:发票弹窗样式。 + */ +.fi-void-modal, +.fi-issue-result, +.fi-settings-confirm { + display: flex; + flex-direction: column; + gap: 12px; +} + +.fi-void-warning { + margin: 0; + font-size: 13px; + color: #cf1322; +} + +.fi-void-summary, +.fi-result-list, +.fi-settings-list { + display: flex; + flex-direction: column; + gap: 8px; + padding: 12px 14px; + background: #fafafa; + border: 1px solid #f0f0f0; + border-radius: 8px; +} + +.fi-void-line, +.fi-result-line, +.fi-settings-line { + display: flex; + gap: 16px; + justify-content: space-between; + font-size: 13px; + + .label { + color: rgb(0 0 0 / 45%); + } + + .value { + color: rgb(0 0 0 / 88%); + text-align: right; + } +} + +.fi-result-title { + font-size: 15px; + font-weight: 600; + color: #1677ff; +} + +.fi-result-footer { + display: flex; + justify-content: flex-end; +} + +.fi-settings-desc { + margin: 0; + font-size: 13px; + color: rgb(0 0 0 / 65%); +} diff --git a/apps/web-antd/src/views/finance/invoice/styles/responsive.less b/apps/web-antd/src/views/finance/invoice/styles/responsive.less new file mode 100644 index 0000000..e199dda --- /dev/null +++ b/apps/web-antd/src/views/finance/invoice/styles/responsive.less @@ -0,0 +1,47 @@ +/** + * 文件职责:发票管理页面响应式样式。 + */ +@media (max-width: 1600px) { + .fi-stats { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@media (max-width: 768px) { + .fi-stats { + grid-template-columns: 1fr; + } + + .fi-toolbar { + padding: 14px 12px; + + .fi-date-input, + .fi-status-select, + .fi-type-select, + .fi-search { + width: 100%; + } + + .fi-date-sep, + .fi-toolbar-spacer { + display: none; + } + } + + .fi-form-grid, + .fi-info-grid { + grid-template-columns: 1fr; + + .full { + grid-column: auto; + } + } + + .fi-save-bar { + justify-content: stretch; + + .ant-btn { + flex: 1; + } + } +} diff --git a/apps/web-antd/src/views/finance/invoice/styles/table.less b/apps/web-antd/src/views/finance/invoice/styles/table.less new file mode 100644 index 0000000..971724f --- /dev/null +++ b/apps/web-antd/src/views/finance/invoice/styles/table.less @@ -0,0 +1,62 @@ +/** + * 文件职责:发票记录表格样式。 + */ +.fi-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-pagination { + margin: 14px 16px; + } +} + +.fi-company-cell { + display: flex; + flex-direction: column; + gap: 2px; + + .fi-applicant { + font-weight: 500; + color: rgb(0 0 0 / 88%); + } + + .fi-company { + font-size: 12px; + color: rgb(0 0 0 / 45%); + } +} + +.fi-amount { + font-weight: 600; + color: rgb(0 0 0 / 88%); +} + +.fi-time { + font-size: 12px; + color: rgb(0 0 0 / 45%); + white-space: nowrap; +} + +.fi-link-action { + padding-inline: 0; +} + +.fi-action-wrap { + display: inline-flex; + gap: 8px; + align-items: center; +} diff --git a/apps/web-antd/src/views/finance/invoice/types.ts b/apps/web-antd/src/views/finance/invoice/types.ts new file mode 100644 index 0000000..b885452 --- /dev/null +++ b/apps/web-antd/src/views/finance/invoice/types.ts @@ -0,0 +1,57 @@ +/** + * 文件职责:发票管理页面本地状态类型定义。 + */ +import type { + FinanceInvoiceStatusFilter, + FinanceInvoiceTypeFilter, +} from '#/api/finance'; + +/** 页面分段键。 */ +export type FinanceInvoiceTabKey = 'records' | 'settings'; + +/** 发票记录筛选状态。 */ +export interface FinanceInvoiceFilterState { + endDate: string; + invoiceType: FinanceInvoiceTypeFilter; + keyword: string; + startDate: string; + status: FinanceInvoiceStatusFilter; +} + +/** 发票记录分页状态。 */ +export interface FinanceInvoicePaginationState { + page: number; + pageSize: number; + totalCount: number; +} + +/** 发票设置表单状态。 */ +export interface FinanceInvoiceSettingFormState { + autoIssueMaxAmount: number; + bankAccount: string; + bankName: string; + companyName: string; + enableAutoIssue: boolean; + enableElectronicNormalInvoice: boolean; + enableElectronicSpecialInvoice: boolean; + registeredAddress: string; + registeredPhone: string; + taxpayerNumber: string; +} + +/** 发票开票抽屉表单状态。 */ +export interface FinanceInvoiceIssueFormState { + contactEmail: string; + issueRemark: string; +} + +/** 发票作废弹窗表单状态。 */ +export interface FinanceInvoiceVoidFormState { + voidReason: string; +} + +/** 通用选项项。 */ +export interface OptionItem { + label: string; + value: string; +}