From a869467efb8f9ba95652e70b06ef8510fc0cc8c2 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Wed, 9 Sep 2026 15:26:43 +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 --- .../ContainerLastLegFeesPanel.test.ts | 87 ++++++++++++++ .../components/ContainerLastLegFeesPanel.vue | 93 ++++++++++++--- .../components/ContainerSimulationCreate.vue | 106 ++++++++++++++++++ src/modules/container-loading/index.ts | 1 + .../pages/ContainerDetailView.test.ts | 1 - .../pages/ContainersView.test.ts | 12 +- .../pages/ContainersView.vue | 70 ++---------- src/modules/finance/api/finance.ts | 16 +++ src/modules/finance/index.ts | 2 + .../FinanceCustomerStatementsView.test.ts | 35 +++++- .../pages/FinanceCustomerStatementsView.vue | 7 +- .../LoadingListsView.creation.test.ts | 65 +++++++++++ .../components/LoadingListsView.vue | 10 ++ 13 files changed, 420 insertions(+), 85 deletions(-) create mode 100644 src/modules/container-loading/components/ContainerLastLegFeesPanel.test.ts create mode 100644 src/modules/container-loading/components/ContainerSimulationCreate.vue create mode 100644 src/modules/loading-lists/components/LoadingListsView.creation.test.ts diff --git a/src/modules/container-loading/components/ContainerLastLegFeesPanel.test.ts b/src/modules/container-loading/components/ContainerLastLegFeesPanel.test.ts new file mode 100644 index 00000000..f00836f6 --- /dev/null +++ b/src/modules/container-loading/components/ContainerLastLegFeesPanel.test.ts @@ -0,0 +1,87 @@ +import { mount, flushPromises } from '@vue/test-utils' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import ElementPlus from 'element-plus' +import ContainerLastLegFeesPanel from './ContainerLastLegFeesPanel.vue' + +const mocks = vi.hoisted(() => ({ upload: vi.fn(), metadata: vi.fn(), download: vi.fn(), create: vi.fn(), fees: vi.fn() })) +vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: () => true }) })) +vi.mock('@/modules/finance', () => ({ getFinancePaymentVoucher: mocks.metadata, downloadFinancePaymentVoucher: mocks.download })) +vi.mock('@/modules/loading-lists', () => ({ uploadContainerFeePaymentVoucher: mocks.upload, listContainerFeeSupplierOptions: async () => [] })) +vi.mock('../api/overseasContainerFees', () => ({ + listOverseasContainerFees: mocks.fees, listOverseasContainerCustomers: async () => [{ customerId: '11', customerName: '客户甲' }], + createOverseasContainerFee: mocks.create, adjustOverseasContainerFee: vi.fn(), reviewOverseasContainerFee: vi.fn(), submitOverseasContainerFee: vi.fn(), voidOverseasContainerFee: vi.fn(), +})) + +async function setup(readonly = false) { + const wrapper = mount(ContainerLastLegFeesPanel, { props: { containerBookingId: '99', containerNo: 'CONT-99', readonly }, global: { plugins: [ElementPlus], stubs: { + AppDialog: { props: ['modelValue', 'title', 'confirmDisabled'], template: '

{{ title }}

' }, + } } }) + await flushPromises() + return wrapper +} +type FormState = { form: { customerId: string; settlementMode: string }; dialogOpen: boolean } +async function upload(wrapper: Awaited>, name: string) { + const input = wrapper.get('input[type="file"]') + Object.defineProperty(input.element, 'files', { configurable: true, value: [new File(['image'], name, { type: 'image/png' })] }) + await input.trigger('change') + await flushPromises() +} +describe('尾程付款凭证', () => { + beforeEach(() => { + vi.clearAllMocks() + mocks.fees.mockResolvedValue([]) + vi.stubGlobal('ResizeObserver', class { observe() {} unobserve() {} disconnect() {} }) + URL.createObjectURL = vi.fn(() => 'blob:voucher') + URL.revokeObjectURL = vi.fn() + mocks.metadata.mockResolvedValue({ fileId: '91', fileName: '付款.png', contentType: 'image/png' }) + mocks.download.mockResolvedValue(new Blob(['image'])) + }) + + it('显示上传文件名,允许再次上传替换和移除', async () => { + const wrapper = await setup() + await wrapper.get('button').trigger('click') + const state = (wrapper.vm.$ as unknown as { setupState: FormState }).setupState + state.form.settlementMode = 'DIRECT_PAID' + await flushPromises() + mocks.upload.mockResolvedValueOnce({ fileId: '91', fileName: '付款.png' }).mockResolvedValueOnce({ fileId: '92', fileName: '新凭证.png' }) + await upload(wrapper, '付款.png') + expect(mocks.upload).toHaveBeenCalledWith('11', expect.any(File)) + expect(wrapper.text()).toContain('付款.png') + await upload(wrapper, '新凭证.png') + expect(wrapper.text()).toContain('新凭证.png') + expect(wrapper.text()).not.toContain('付款.png') + await wrapper.findAll('button').find(b => b.text() === '移除')!.trigger('click') + expect(wrapper.text()).not.toContain('新凭证.png') + wrapper.unmount() + }) + + it('上传中禁止保存,客户切换后忽略旧客户上传结果', async () => { + const wrapper = await setup() + await wrapper.get('button').trigger('click') + const state = (wrapper.vm.$ as unknown as { setupState: FormState }).setupState + state.form.settlementMode = 'DIRECT_PAID' + await flushPromises() + let complete!: (file: { fileId: string; fileName: string }) => void + mocks.upload.mockReturnValue(new Promise(resolve => { complete = resolve })) + await upload(wrapper, '旧客户.png') + expect(wrapper.findAll('button').find(b => b.text() === '保存')!.attributes('disabled')).toBeDefined() + state.form.customerId = '12' + complete({ fileId: '91', fileName: '旧客户.png' }) + await flushPromises() + expect(wrapper.text()).not.toContain('旧客户.png') + wrapper.unmount() + }) + + it('只读列表能查看已保存的待审核凭证,并在关闭时释放预览资源', async () => { + mocks.fees.mockResolvedValue([{ sourceFeeId: '81', customerId: '11', amount: 10, reviewStatus: 'PENDING', paymentVoucherFileIds: ['91'] }]) + const wrapper = await setup(true) + await wrapper.findAll('button').find(b => b.text() === '付款凭证 1')!.trigger('click') + await flushPromises() + expect(mocks.metadata).toHaveBeenCalledWith('11', '91') + expect(mocks.download).toHaveBeenCalledWith('11', '91') + expect(wrapper.get('img').attributes('src')).toBe('blob:voucher') + expect(wrapper.get('a[download]').attributes('download')).toBe('付款.png') + wrapper.unmount() + expect(URL.revokeObjectURL).toHaveBeenCalledWith('blob:voucher') + }) +}) diff --git a/src/modules/container-loading/components/ContainerLastLegFeesPanel.vue b/src/modules/container-loading/components/ContainerLastLegFeesPanel.vue index 2d9450c2..be5191b4 100644 --- a/src/modules/container-loading/components/ContainerLastLegFeesPanel.vue +++ b/src/modules/container-loading/components/ContainerLastLegFeesPanel.vue @@ -1,6 +1,7 @@ + + + + diff --git a/src/modules/container-loading/index.ts b/src/modules/container-loading/index.ts index d8c0bdbe..da4036f4 100644 --- a/src/modules/container-loading/index.ts +++ b/src/modules/container-loading/index.ts @@ -1,3 +1,4 @@ export * from './api/platformContainerLoading' export * from './types' export { default as SmartLoadingDialog } from './components/SmartLoadingDialog.vue' +export { default as ContainerSimulationCreate } from './components/ContainerSimulationCreate.vue' diff --git a/src/modules/container-loading/pages/ContainerDetailView.test.ts b/src/modules/container-loading/pages/ContainerDetailView.test.ts index 0783ae76..846ee964 100644 --- a/src/modules/container-loading/pages/ContainerDetailView.test.ts +++ b/src/modules/container-loading/pages/ContainerDetailView.test.ts @@ -87,7 +87,6 @@ describe('ContainerDetailView customs integration', () => { expect(lastLegFeesSource).toContain("canEdit.value && access.can('platform:supplier:list') ? listContainerFeeSupplierOptions(null) : Promise.resolve([])") expect(lastLegFeesSource).toContain("sourceType: 'OVERSEAS_CONTAINER_FEE', businessType: 'OVERSEAS_CONTAINER'") expect(lastLegFeesSource).toContain("form.settlementMode === 'DIRECT_PAID'") - expect(lastLegFeesSource).toContain('uploadContainerFeePaymentVoucher(form.customerId, file.raw)') expect(lastLegFeesSource).toContain("access.can('platform:finance:fee-source:edit')") expect(lastLegFeesSource).toContain("access.can('platform:finance:fee-source:review')") expect(source).toContain("if (!overview.value?.loadingPlanId) return '当前货柜尚未关联装柜计划,暂无头程计费归属。'") diff --git a/src/modules/container-loading/pages/ContainersView.test.ts b/src/modules/container-loading/pages/ContainersView.test.ts index 161e5db9..2433a23c 100644 --- a/src/modules/container-loading/pages/ContainersView.test.ts +++ b/src/modules/container-loading/pages/ContainersView.test.ts @@ -609,13 +609,15 @@ describe('ContainersView', () => { it('creates an internal simulation without asking for real booking transport facts', () => { const source = readFileSync(resolve(process.cwd(), 'src/modules/container-loading/pages/ContainersView.vue'), 'utf8') + const simulationSource = readFileSync(resolve(process.cwd(), 'src/modules/container-loading/components/ContainerSimulationCreate.vue'), 'utf8') + expect(source).toContain('ContainerSimulationCreate') expect(source).not.toContain('新增货柜') - expect(source).toContain('新增模拟柜') - expect(source).toContain('createContainerSimulation({ ...simulationForm })') - expect(source).toContain("channelCode: '', warehouseId: '', containerType: '40HC', capacityCbm: 76, maxWeightKg: 28000") + expect(simulationSource).toContain('新增模拟柜') + expect(simulationSource).toContain('createContainerSimulation({ ...simulationForm })') + expect(simulationSource).toContain("channelCode: '', warehouseId: '', containerType: '40HC', capacityCbm: 76, maxWeightKg: 28000") expect(source).toContain('CONTAINER_SPECIFICATIONS') - expect(source).toContain('selectSimulationContainerType') - expect(source).toContain('服务端会生成稳定的柜次号') + expect(simulationSource).toContain('selectSimulationContainerType') + expect(simulationSource).toContain('服务端会生成稳定的柜次号') expect(source).toContain(':model-value="form.cabinetNo" disabled placeholder="系统生成"') expect(source).not.toContain('v-model="form.cabinetNo"') expect(source).not.toContain('cabinetNo: form.cabinetNo.trim()') diff --git a/src/modules/container-loading/pages/ContainersView.vue b/src/modules/container-loading/pages/ContainersView.vue index a73165f7..df69f727 100644 --- a/src/modules/container-loading/pages/ContainersView.vue +++ b/src/modules/container-loading/pages/ContainersView.vue @@ -1,14 +1,15 @@