From 4687f17d09891ed3588934888af20998eea74567 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Fri, 28 Aug 2026 09:19:24 +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 --- .../pages/ContainerDetailView.test.ts | 59 +++++++++++++++++++ .../pages/ContainerDetailView.vue | 27 +++++++-- 2 files changed, 81 insertions(+), 5 deletions(-) diff --git a/src/modules/container-loading/pages/ContainerDetailView.test.ts b/src/modules/container-loading/pages/ContainerDetailView.test.ts index b6dcdefe..8f1e25f5 100644 --- a/src/modules/container-loading/pages/ContainerDetailView.test.ts +++ b/src/modules/container-loading/pages/ContainerDetailView.test.ts @@ -54,6 +54,8 @@ function deferred() { } type ClearanceVm = { clearanceOpen: boolean; clearanceFile: File | null; clearanceError: { code?: string; requestId?: string } | null; documentRefreshWarning: unknown; selectClearanceFile: (file: { raw: File }) => void; uploadClearance: () => Promise; completeClearance: () => Promise } function clearanceVm(wrapper: VueWrapper) { return wrapper.vm as unknown as ClearanceVm } +type DocumentVm = { documentOpen: boolean; documentFile: File | null; documentError: { code?: string; requestId?: string } | null; documentUploadKey: number; documentRefreshWarning: { code?: string } | null; selectDocumentFile: (file: { raw: File }) => void; saveDocument: () => Promise } +function documentVm(wrapper: VueWrapper) { return wrapper.vm as unknown as DocumentVm } describe('ContainerDetailView', () => { beforeEach(() => { @@ -193,6 +195,63 @@ describe('ContainerDetailView', () => { wrapper.unmount() }) + it.each([ + ['DELIVERY_ORDER', 'DO 资料'], + ['PICKUP_ORDER', 'PU 资料'], + ] as const)('optimistically replaces %s, closes before overview refresh and preserves the returned file over a stale overview', async (documentType, label) => { + const existing = { id: `old-${documentType}`, documentType, fileId: `old-file-${documentType}`, fileName: `old-${documentType}.pdf`, fileHash: null, uploadedBy: '1', uploadedByName: '旧操作员', uploadedAt: '2026-08-26T10:00:00', version: 1, completed: false } + const initial = { ...overview, documents: [...overview.documents, existing] } + const uploaded = { id: `new-${documentType}`, documentType, fileId: `new-file-${documentType}`, fileName: `new-${documentType}.pdf`, fileHash: 'hash', uploadedBy: '2', uploadedByName: '新操作员', uploadedAt: '2026-08-27T13:00:00', completedBy: null, completedByName: null, completedAt: null, version: 2 } + const refresh = deferred() + vi.mocked(getContainerOverview).mockResolvedValueOnce(initial as never).mockReturnValueOnce(refresh.promise as never) + vi.mocked(uploadContainerDocument).mockResolvedValueOnce(uploaded as never) + const wrapper = mountView(); await flushPromises() + await documentButton(wrapper, label, '更新附件').trigger('click'); await flushPromises() + const file = new File([documentType], `new-${documentType}.pdf`, { type: 'application/pdf' }) + documentVm(wrapper).selectDocumentFile({ raw: file }) + const request = documentVm(wrapper).saveDocument(); await flushPromises() + expect(uploadContainerDocument).toHaveBeenCalledWith('2092438843630391297', documentType, file, 1, `上传${label}`) + expect(documentVm(wrapper).documentOpen).toBe(false) + expect(documentVm(wrapper).documentFile).toBeNull() + expect(documentArticle(wrapper, label).text()).toContain(`new-${documentType}.pdf`) + expect(documentArticle(wrapper, label).text()).toContain('新操作员') + refresh.resolve(initial); await request; await flushPromises() + expect(documentArticle(wrapper, label).text()).toContain(`new-${documentType}.pdf`) + expect(documentVm(wrapper).documentRefreshWarning).toMatchObject({ code: 'DOCUMENT_OVERVIEW_STALE' }) + wrapper.unmount() + }) + + it('rebuilds the upload picker on reopen and keeps DO failures open with the selected file and API evidence', async () => { + const existing = { id: 'do-old', documentType: 'DELIVERY_ORDER', fileId: 'do-file-old', fileName: 'old-do.pdf', fileHash: null, uploadedBy: '1', uploadedByName: '旧操作员', uploadedAt: '2026-08-26T10:00:00', version: 1, completed: false } + vi.mocked(getContainerOverview).mockResolvedValue({ ...overview, documents: [...overview.documents, existing] } as never) + vi.mocked(uploadContainerDocument) + .mockRejectedValueOnce(new ApiResponseError('版本冲突', 'A0409', undefined, undefined, 'request-do-409', 'trace-do-409')) + .mockResolvedValueOnce(null) + const wrapper = mountView(); await flushPromises() + await documentButton(wrapper, 'DO 资料', '更新附件').trigger('click'); await flushPromises() + const firstKey = documentVm(wrapper).documentUploadKey + const file = new File(['do'], 'next-do.pdf', { type: 'application/pdf' }) + documentVm(wrapper).selectDocumentFile({ raw: file }) + await documentVm(wrapper).saveDocument(); await flushPromises() + expect(documentVm(wrapper).documentOpen).toBe(true) + expect(documentVm(wrapper).documentFile).toBe(file) + expect(documentVm(wrapper).documentError).toMatchObject({ code: 'A0409', requestId: 'request-do-409' }) + expect(wrapper.text()).toContain('trace-do-409') + documentVm(wrapper).documentOpen = false; await flushPromises() + await documentButton(wrapper, 'PU 资料', '上传附件').trigger('click'); await flushPromises() + expect(documentVm(wrapper).documentUploadKey).toBeGreaterThan(firstKey) + expect(documentVm(wrapper).documentFile).toBeNull() + expect(documentVm(wrapper).documentError).toBeNull() + expect(wrapper.text()).not.toContain('request-do-409') + const puFile = new File(['pu'], 'next-pu.pdf', { type: 'application/pdf' }) + documentVm(wrapper).selectDocumentFile({ raw: puFile }) + await documentVm(wrapper).saveDocument(); await flushPromises() + expect(documentVm(wrapper).documentOpen).toBe(true) + expect(documentVm(wrapper).documentFile).toBe(puFile) + expect(uploadContainerDocument).toHaveBeenCalledTimes(2) + wrapper.unmount() + }) + it('downloads destination clearance through its dedicated endpoint and keeps completed rows download-only', async () => { const completed = { id: '351', documentType: 'DESTINATION_CLEARANCE', fileId: '2092438843630393502', fileName: 'destination-clearance.pdf', fileHash: null, uploadedBy: '3', uploadedByName: '清关员', uploadedAt: '2026-08-26T11:00:00', version: 2, completed: true } vi.mocked(getContainerOverview).mockResolvedValueOnce({ ...overview, documents: [...overview.documents, completed] } as never) diff --git a/src/modules/container-loading/pages/ContainerDetailView.vue b/src/modules/container-loading/pages/ContainerDetailView.vue index 5f2a9743..7b0d4d36 100644 --- a/src/modules/container-loading/pages/ContainerDetailView.vue +++ b/src/modules/container-loading/pages/ContainerDetailView.vue @@ -47,6 +47,7 @@ const documentOpen = ref(false) type UploadDocumentType = 'DELIVERY_ORDER' | 'PICKUP_ORDER' | 'CUSTOMS_DECLARATION' const documentType = ref('DELIVERY_ORDER') const documentFile = ref(null) +const documentUploadKey = ref(0) const documentSaving = ref(false) const documentDownloading = ref(false) const documentError = ref(null) @@ -194,7 +195,7 @@ function openDocument(type: string) { if (!['DELIVERY_ORDER', 'PICKUP_ORDER', 'CUSTOMS_DECLARATION'].includes(type)) return if (type === 'CUSTOMS_DECLARATION' ? !canManageCustoms.value : !canEdit.value) return documentType.value = type as UploadDocumentType - documentFile.value = null; documentError.value = null; documentOpen.value = true + documentFile.value = null; documentError.value = null; documentRefreshWarning.value = null; documentUploadKey.value += 1; documentOpen.value = true if (type === 'CUSTOMS_DECLARATION' && !overview.value?.loadingPlanId) { documentError.value = { code: 'LOADING_PLAN_REQUIRED', message: '当前货柜没有装柜清单,无法上传国内出口报关附件' } } @@ -222,10 +223,26 @@ async function saveDocument() { } catch (cause) { documentRefreshWarning.value = getApiErrorDetails(cause, '附件已上传,但最新资料刷新失败;当前已保留本次上传结果') } } else { - await uploadContainerDocument(containerId.value, documentType.value, documentFile.value, expectedVersion, `上传${documentTitle.value}`) - overview.value = await getContainerOverview(containerId.value) + const targetType = documentType.value + const targetTitle = documentTitle.value + const uploaded = await uploadContainerDocument(containerId.value, targetType, documentFile.value, expectedVersion, `上传${targetTitle}`) + if (!uploaded) throw new Error(`${targetTitle}上传成功但未返回附件信息`) + const optimistic = toOverviewDocument(uploaded, targetType, selectedDocument.value) + replaceOverviewDocument(optimistic) + documentOpen.value = false; documentFile.value = null; documentError.value = null; documentUploadKey.value += 1 + ElMessage.success(`${targetTitle}已上传`) + try { + const refreshed = await getContainerOverview(containerId.value) + const current = refreshed.documents.find((item) => item.documentType === targetType) + if (current && (current.fileId === optimistic.fileId || current.version > optimistic.version)) overview.value = refreshed + else { + overview.value = { ...refreshed, documents: [...refreshed.documents.filter((item) => item.documentType !== targetType), optimistic] } + documentRefreshWarning.value = { code: 'DOCUMENT_OVERVIEW_STALE', message: `${targetTitle}已上传,但资料汇总尚未同步;当前已保留本次上传结果` } + } + } catch (cause) { documentRefreshWarning.value = getApiErrorDetails(cause, `${targetTitle}已上传,但最新资料刷新失败;当前已保留本次上传结果`) } + return } - documentOpen.value = false; documentFile.value = null; ElMessage.success(`${documentTitle.value}已上传`) + documentOpen.value = false; documentFile.value = null; documentUploadKey.value += 1; ElMessage.success(`${documentTitle.value}已上传`) } catch (cause) { const details = getApiErrorDetails(cause, `${documentTitle.value}上传失败`) if (details.code === 'A0409') { @@ -388,7 +405,7 @@ onMounted(async () => { await load(); if (route.query.action === 'documents') op
- 选择文件 + 选择文件