From 1c2dd8797afff1b078fd70f973fe97e5b3341a13 Mon Sep 17 00:00:00 2001 From: liuyihe <2513762445@qq.com> Date: Tue, 3 Nov 2026 11:43:50 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FinanceCustomerStatementsView.test.ts | 25 ++++++++- .../pages/FinanceCustomerStatementsView.vue | 14 ++--- .../pages/FinanceFormalSettlementPage.vue | 9 ++++ .../pages/FinanceSettlementDialogs.test.ts | 15 ++++++ .../overseas-business/PROTOTYPE_MATRIX.md | 11 ++++ .../overseas-business/api/overseasFees.ts | 7 +++ .../overseas-business/api/storageRent.ts | 1 + .../components/BusinessOrderFees.test.ts | 6 +-- .../components/OverseasFeeEditor.vue | 6 +-- .../components/StorageRentFeeDetails.test.ts | 52 +++++++++++++++++++ .../components/StorageRentFeeDetails.vue | 26 +++++++++- .../components/StorageRentPanel.test.ts | 13 +++++ .../components/StorageRentPanel.vue | 2 +- .../components/TruckOrderActions.vue | 3 +- .../pages/OverseasBusinessViews.test.ts | 4 +- .../pages/TruckOrderDetailView.test.ts | 12 +++-- .../storageRentPresentation.test.ts | 1 + .../storageRentPresentation.ts | 5 +- .../transportDemandForm.test.ts | 29 +++++++++++ .../overseas-business/transportDemandForm.ts | 1 + .../overseas-business/truckWorkflow.ts | 10 ++-- .../overseas-warehouse/api/cargoDiscards.ts | 2 +- .../components/DispatchWorkflowDialog.test.ts | 5 +- .../components/DispatchWorkflowDialog.vue | 5 +- .../TransportExecutionPanel.test.ts | 5 +- .../components/TransportExecutionPanel.vue | 4 +- .../pages/CargoDiscardDialogs.test.ts | 14 +++++ .../pages/CargoDiscardsView.vue | 22 ++++++-- .../pages/DispatchesView.test.ts | 35 ++++++------- .../pages/DispatchesView.vue | 25 +++++---- .../pages/LastMileOrderDetailView.test.ts | 31 ++++++++++- .../pages/LastMileOrderDetailView.vue | 6 +-- .../pages/LastMileOrdersView.behavior.test.ts | 18 +++++++ .../pages/LastMileOrdersView.vue | 14 ++++- 34 files changed, 354 insertions(+), 84 deletions(-) create mode 100644 src/modules/overseas-business/components/StorageRentFeeDetails.test.ts create mode 100644 src/modules/overseas-business/transportDemandForm.test.ts diff --git a/src/modules/finance/pages/FinanceCustomerStatementsView.test.ts b/src/modules/finance/pages/FinanceCustomerStatementsView.test.ts index 4b2241bc..ebc5b6b2 100644 --- a/src/modules/finance/pages/FinanceCustomerStatementsView.test.ts +++ b/src/modules/finance/pages/FinanceCustomerStatementsView.test.ts @@ -12,7 +12,7 @@ import { ApiResponseError } from '@/shared/api/http' import { createPlatformExportTask } from '@/modules/downloads' import { listPlatformTenants } from '@/modules/iam' import { showExportTaskCreatedDialog } from '@/shared/components/exportTaskCreatedDialog' -import { createReconciliationStatement, getReconciliationStatement, listFinanceFeeSources, listFinanceStatements } from '../api/finance' +import { actionReconciliationStatement, createReconciliationStatement, getReconciliationStatement, listFinanceFeeSources, listFinanceStatements } from '../api/finance' import type { FinanceFeeSourceSummary, FinanceReconciliationDetail, FinanceStatementReadModel } from '../types' import FinanceCustomerStatementsView from './FinanceCustomerStatementsView.vue' @@ -76,6 +76,29 @@ enableAutoUnmount(afterEach) afterAll(() => vi.unstubAllGlobals()) describe('FinanceCustomerStatementsView', () => { + it('omits reopening voided and rejected statements and ignores stale reopen commands', async () => { + vi.mocked(listFinanceStatements).mockResolvedValue([{ ...statement, status: 'VOIDED' }, { ...statement, id: '502', status: 'REJECTED' }]) + const { wrapper } = await mountPage(['platform:finance:customer-statement:list', 'platform:finance:reconciliation:void']) + const page = wrapper.vm as unknown as { handleStatementAction(command: string, row: FinanceStatementReadModel): void } + expect(wrapper.text()).not.toContain('重开') + page.handleStatementAction('reopen', { ...statement, status: 'VOIDED' }) + await flushPromises() + expect(actionReconciliationStatement).not.toHaveBeenCalled() + }) + + it('keeps statement rows visible and shows only the business error after a failed action', async () => { + const draft = { ...statement, status: 'DRAFT' } + vi.mocked(listFinanceStatements).mockResolvedValue([draft]) + vi.mocked(actionReconciliationStatement).mockRejectedValueOnce(new ApiResponseError('费用已生成新的对账单', 'FIN_409', 409, null, 'REQUEST-FAIL', 'TRACE-FAIL')) + const { wrapper } = await mountPage(['platform:finance:customer-statement:list', 'platform:finance:reconciliation:create']) + const page = wrapper.vm as unknown as { handleStatementAction(command: string, row: FinanceStatementReadModel): void } + page.handleStatementAction('submit', draft) + await flushPromises() + expect(wrapper.text()).toContain('费用已生成新的对账单') + expect(wrapper.text()).toContain('REC-501') + expect(wrapper.text()).not.toMatch(/REQUEST-FAIL|TRACE-FAIL|客户对账单加载失败/) + }) + beforeEach(() => { vi.stubGlobal('ResizeObserver', ResizeObserverStub) vi.stubGlobal('localStorage', memoryStorage()) diff --git a/src/modules/finance/pages/FinanceCustomerStatementsView.vue b/src/modules/finance/pages/FinanceCustomerStatementsView.vue index 5d6cfc57..739139f0 100644 --- a/src/modules/finance/pages/FinanceCustomerStatementsView.vue +++ b/src/modules/finance/pages/FinanceCustomerStatementsView.vue @@ -52,6 +52,7 @@ const page = ref(1) const pageSize = ref(20) const loading = ref(false) const error = ref(null) +const actionError = ref('') const customerError = ref(null) const selectedId = ref('') const detail = ref(null) @@ -263,6 +264,7 @@ async function load(model?: QueryModel) { detail.value = null detailCache.clear() error.value = null + actionError.value = '' loading.value = true try { const statements = await listFinanceStatements(String(appliedFilters.value.customerId || '').trim() || undefined) @@ -516,17 +518,18 @@ async function createStatement() { } async function performAction(row: FinanceStatementReadModel, actionName: ReconciliationActionPayload['action']) { - const title = ({ REJECT: '驳回对账单', VOID: '作废对账单', REOPEN: '重开对账单' } as Partial>)[actionName] || '对账单操作' - const reason = ['REJECT', 'VOID', 'REOPEN'].includes(actionName) + const title = ({ REJECT: '驳回对账单', VOID: '作废对账单' } as Partial>)[actionName] || '对账单操作' + const reason = ['REJECT', 'VOID'].includes(actionName) ? await ElMessageBox.prompt('请输入操作原因', `${title} · ${row.billNo || '—'}`, { customClass: 'admin-density-confirm admin-workbench', inputType: 'textarea', inputPattern: /\S+/, inputErrorMessage: '操作原因不能为空', confirmButtonText: '确认', cancelButtonText: '取消' }).then((result) => result.value).catch(() => null) : undefined if (reason === null) return + actionError.value = '' actionBusy.value = true try { await actionReconciliationStatement(row.id, { action: actionName, expectedVersion: row.version, ...(reason ? { reason } : {}) }) ElMessage.success('对账单状态已更新') await load(appliedFilters.value) - } catch (cause) { error.value = getApiErrorDetails(cause, '对账单操作失败') } + } catch (cause) { actionError.value = getApiErrorMessage(cause, '对账单操作失败') } finally { actionBusy.value = false } } @@ -537,7 +540,6 @@ function statementActions(row: FinanceStatementReadModel): RowActionItem[] { { key: 'approve', label: '通过', priority: 250, tone: 'warning', visible: row.status === 'PENDING_REVIEW' && canReview.value, loading: actionBusy.value }, { key: 'reject', label: '驳回', priority: 200, tone: 'danger', visible: row.status === 'PENDING_REVIEW' && canReview.value, loading: actionBusy.value }, { key: 'issue', label: '出账', priority: 200, visible: row.status === 'APPROVED' && canIssue.value, loading: actionBusy.value }, - { key: 'reopen', label: '重开', priority: 150, visible: ['REJECTED', 'VOIDED'].includes(String(row.status)) && canVoid.value, loading: actionBusy.value }, { key: 'void', label: '作废', tone: 'danger', divided: true, visible: ['DRAFT', 'PENDING_REVIEW'].includes(String(row.status)) && canVoid.value, loading: actionBusy.value }, ] } @@ -546,7 +548,7 @@ function handleStatementAction(command: string, row: FinanceStatementReadModel) if (command === 'detail') void openDetail(row) else { const actions: Record = { - submit: 'SUBMIT', approve: 'APPROVE', reject: 'REJECT', issue: 'ISSUE', reopen: 'REOPEN', void: 'VOID', + submit: 'SUBMIT', approve: 'APPROVE', reject: 'REJECT', issue: 'ISSUE', void: 'VOID', } const action = actions[command] if (action) void performAction(row, action) @@ -686,7 +688,7 @@ watch(detailRouteId, (statementId) => {
- +