diff --git a/src/modules/container-loading/api/platformContainerLoading.test.ts b/src/modules/container-loading/api/platformContainerLoading.test.ts index 1cb86699..d27daf98 100644 --- a/src/modules/container-loading/api/platformContainerLoading.test.ts +++ b/src/modules/container-loading/api/platformContainerLoading.test.ts @@ -3,7 +3,7 @@ import { http } from '@/shared/api/http' import { adjustSmartLoadingItem, assignSmartLoadingItem, createSmartLoadingPlan, getContainer, listContainers, listSmartLoadingCandidates, pushSmartLoadingPreload, removeSmartLoadingItem, - getContainerClearance, uploadContainerClearance, completeContainerClearance, + getContainerClearance, downloadContainerClearance, uploadContainerClearance, completeContainerClearance, getContainerOverview, markContainerException, cancelContainerException, uploadContainerDocument, downloadContainerDocument, downloadContainerPdaPhoto, getCustomerContainerTransitSummary, createContainerSimulation, linkContainerOceanBooking, unlinkContainerOceanBooking, @@ -150,4 +150,12 @@ describe('platform container loading API', () => { '/platform/v1/containers/7/pda/photos/../23/download', '/api/api/platform/v1/containers/7/pda/photos/23/download', ]) expect(() => downloadContainerPdaPhoto(path)).toThrow('invalid container photo path') }) + + it('downloads destination clearance through its exact protected endpoint with string ids', async () => { + const blob = new Blob(['clearance']) + vi.mocked(http.get).mockResolvedValueOnce(blob) + const result = await downloadContainerClearance('2092438843630391297', '2092438843630393502') + expect(http.get).toHaveBeenCalledWith('/platform/v1/containers/2092438843630391297/clearance/document/2092438843630393502/download', { responseType: 'blob' }) + expect(result).toBe(blob) + }) }) diff --git a/src/modules/container-loading/api/platformContainerLoading.ts b/src/modules/container-loading/api/platformContainerLoading.ts index b09c748b..c1aa5393 100644 --- a/src/modules/container-loading/api/platformContainerLoading.ts +++ b/src/modules/container-loading/api/platformContainerLoading.ts @@ -206,6 +206,10 @@ export async function getContainerClearance(containerId: string) { return document(unwrapApiResponse(response)) } +export function downloadContainerClearance(containerId: string, fileId: string) { + return http.get(`${containerPath}/${encodeURIComponent(containerId)}/clearance/document/${encodeURIComponent(fileId)}/download`, { responseType: 'blob' }) +} + export async function uploadContainerClearance(containerId: string, file: File, expectedVersion: number, operationReason?: string | null) { const body = new FormData() body.append('file', file) diff --git a/src/modules/container-loading/components/ContainerOverviewPanel.vue b/src/modules/container-loading/components/ContainerOverviewPanel.vue index 3a79a67b..2861e796 100644 --- a/src/modules/container-loading/components/ContainerOverviewPanel.vue +++ b/src/modules/container-loading/components/ContainerOverviewPanel.vue @@ -34,7 +34,9 @@ const documentsByType = computed(() => new Map(props.overview.documents.map((ite const containerDownloadableTypes = new Set(['DELIVERY_ORDER', 'PICKUP_ORDER']) const customsTypes = new Set(['DESTINATION_CLEARANCE', 'CUSTOMS_DECLARATION']) function canDownloadDocument(documentType: string) { - return containerDownloadableTypes.has(documentType) || (documentType === 'CUSTOMS_DECLARATION' && props.canDownloadCustoms) + return containerDownloadableTypes.has(documentType) + || (documentType === 'CUSTOMS_DECLARATION' && props.canDownloadCustoms) + || (documentType === 'DESTINATION_CLEARANCE' && props.canManageCustoms) } function canUploadDocument(documentType: string) { return containerDownloadableTypes.has(documentType) ? props.canManageDocuments : customsTypes.has(documentType) && props.canManageCustoms diff --git a/src/modules/container-loading/pages/ContainerDetailView.test.ts b/src/modules/container-loading/pages/ContainerDetailView.test.ts index 8dce8c5a..29e2c4b2 100644 --- a/src/modules/container-loading/pages/ContainerDetailView.test.ts +++ b/src/modules/container-loading/pages/ContainerDetailView.test.ts @@ -2,8 +2,8 @@ import ElementPlus, { ElMessageBox } from 'element-plus' import { flushPromises, mount, type VueWrapper } from '@vue/test-utils' import { beforeEach, describe, expect, it, vi } from 'vitest' import ContainerDetailView from './ContainerDetailView.vue' -import { downloadContainerDocument, getContainer, getContainerClearance, getContainerOverview, uploadContainerClearance, uploadContainerDocument } from '../api/platformContainerLoading' -import { completeLoadingDeclaration, downloadLoadingDeclaration, uploadLoadingDeclaration } from '@/modules/loading-lists' +import { downloadContainerClearance, downloadContainerDocument, getContainer, getContainerClearance, getContainerOverview, uploadContainerClearance, uploadContainerDocument } from '../api/platformContainerLoading' +import { completeLoadingDeclaration, downloadLoadingDeclaration, getLoadingDeclaration, uploadLoadingDeclaration } from '@/modules/loading-lists' import { confirmContainerOutboundDispatch, getContainerOutboundDispatch } from '@/modules/outbound-dispatch/api/platformOutboundDispatch' import type { ContainerOutboundDispatchDetail } from '@/modules/outbound-dispatch/types' import { ApiResponseError } from '@/shared/api/http' @@ -12,12 +12,13 @@ const { accessCan, routerPush, carrierMounts } = vi.hoisted(() => ({ accessCan: vi.mock('vue-router', () => ({ useRoute: () => ({ params: { containerId: '2092438843630391297' }, query: {} }), useRouter: () => ({ push: routerPush }) })) vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: accessCan }) })) vi.mock('@/modules/port-schedules', () => ({ CarrierScheduleQueryPanel: { template: '
', mounted: () => { carrierMounts.value += 1 } } })) -vi.mock('@/modules/loading-lists', () => ({ uploadLoadingDeclaration: vi.fn(), completeLoadingDeclaration: vi.fn(), downloadLoadingDeclaration: vi.fn() })) +vi.mock('@/modules/loading-lists', () => ({ uploadLoadingDeclaration: vi.fn(), completeLoadingDeclaration: vi.fn(), downloadLoadingDeclaration: vi.fn(), getLoadingDeclaration: vi.fn() })) vi.mock('@/modules/outbound-dispatch/api/platformOutboundDispatch', () => ({ getContainerOutboundDispatch: vi.fn(), confirmContainerOutboundDispatch: vi.fn() })) vi.mock('../api/platformContainerLoading', () => ({ getContainer: vi.fn(), getContainerOverview: vi.fn(), getContainerClearance: vi.fn(), uploadContainerClearance: vi.fn(), completeContainerClearance: vi.fn(), uploadContainerDocument: vi.fn(), downloadContainerDocument: vi.fn(), downloadContainerPdaPhoto: vi.fn(), markContainerException: vi.fn(), cancelContainerException: vi.fn(), + downloadContainerClearance: vi.fn(), })) const detail = { id: '2092438843630391297', planId: '2092438843630391401', planNo: 'LP-260825-0181', bookingNo: 'CB-1', cabinetNo: 'CAB-1', containerNo: 'C11111111', warehouseId: '193000000000200002', warehouseName: '东莞仓', containerType: '45HC', planStatus: 'COMPLETED', version: 8 } as never @@ -59,6 +60,8 @@ describe('ContainerDetailView', () => { vi.mocked(uploadLoadingDeclaration).mockResolvedValue({ id: '2092438843630391501', documentType: 'CUSTOMS_DECLARATION', fileId: '2092438843630391502', fileName: 'customs-new.pdf', fileHash: 'hash', uploadedBy: '1', uploadedByName: '操作员', uploadedAt: '2026-08-26T12:00:00', completedBy: null, completedByName: null, completedAt: null, version: 6 } as never) vi.mocked(completeLoadingDeclaration).mockResolvedValue({} as never) vi.mocked(downloadLoadingDeclaration).mockResolvedValue(new Blob(['customs'])) + vi.mocked(downloadContainerClearance).mockResolvedValue(new Blob(['clearance'])) + vi.mocked(getLoadingDeclaration).mockResolvedValue(null) vi.mocked(uploadContainerDocument).mockResolvedValue({} as never) vi.mocked(uploadContainerClearance).mockResolvedValue({} as never) vi.mocked(getContainerOutboundDispatch).mockResolvedValue(outboundDetail) @@ -139,6 +142,74 @@ describe('ContainerDetailView', () => { wrapper.unmount() }) + it('refreshes a hidden customs version after A0409 and preserves error evidence without auto retry', async () => { + const hidden = { id: '2092438843630392501', documentType: 'CUSTOMS_DECLARATION', fileId: '2092438843630392502', fileName: 'server-customs.pdf', fileHash: 'server-hash', uploadedBy: '2', uploadedByName: '服务端操作员', uploadedAt: '2026-08-26T12:30:00', completedBy: null, completedByName: null, completedAt: null, version: 7 } + const withoutCustoms = { ...overview, documents: [] } + vi.mocked(getContainerOverview).mockResolvedValue(withoutCustoms as never) + vi.mocked(getLoadingDeclaration).mockResolvedValueOnce(hidden as never) + vi.mocked(uploadLoadingDeclaration).mockRejectedValueOnce(new ApiResponseError('版本冲突', 'A0409', undefined, undefined, 'request-upload-409', 'trace-upload-409')) + const wrapper = mountView(); await flushPromises() + await documentButton(wrapper, '国内出口报关附件', '上传附件').trigger('click'); await flushPromises() + const file = new File(['next'], 'next.pdf', { type: 'application/pdf' }) + ;(wrapper.vm as unknown as { selectDocumentFile: (file: { raw: File }) => void }).selectDocumentFile({ raw: file }) + await (wrapper.vm as unknown as { saveDocument: () => Promise }).saveDocument(); await flushPromises() + expect(uploadLoadingDeclaration).toHaveBeenCalledOnce() + expect(uploadLoadingDeclaration).toHaveBeenCalledWith('2092438843630391401', '193000000000200002', file, 0, '上传国内出口报关附件') + expect(getLoadingDeclaration).toHaveBeenCalledWith('2092438843630391401', '193000000000200002') + expect(documentArticle(wrapper, '国内出口报关附件').text()).toContain('server-customs.pdf') + expect(documentArticle(wrapper, '国内出口报关附件').text()).toContain('服务端操作员') + expect(wrapper.text()).toContain('A0409') + expect(wrapper.text()).toContain('request-upload-409') + wrapper.unmount() + }) + + it('uses the independent destination document version and immediately replaces the table row after update', async () => { + const existing = { id: '301', documentType: 'DESTINATION_CLEARANCE', fileId: '302', fileName: 'clearance-v1.pdf', fileHash: null, uploadedBy: '3', uploadedByName: '清关员', uploadedAt: '2026-08-26T11:00:00', completedBy: null, completedByName: null, completedAt: null, version: 1 } + const updated = { ...existing, fileId: '303', fileName: 'clearance-v2.pdf', uploadedAt: '2026-08-26T13:00:00', version: 2 } + vi.mocked(getContainerClearance).mockResolvedValueOnce(existing as never) + vi.mocked(uploadContainerClearance).mockResolvedValueOnce(updated as never) + const wrapper = mountView(); await flushPromises() + await documentButton(wrapper, '目的港清关资料', '上传附件').trigger('click'); await flushPromises() + const file = new File(['clearance'], 'clearance-v2.pdf', { type: 'application/pdf' }) + ;(wrapper.vm as unknown as { selectClearanceFile: (file: { raw: File }) => void }).selectClearanceFile({ raw: file }) + await (wrapper.vm as unknown as { uploadClearance: () => Promise }).uploadClearance(); await flushPromises() + expect(uploadContainerClearance).toHaveBeenCalledWith('2092438843630391297', file, 1, '上传目的港清关资料') + expect(documentArticle(wrapper, '目的港清关资料').text()).toContain('clearance-v2.pdf') + 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) + const click = vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => {}) + const wrapper = mountView(); await flushPromises() + const row = documentArticle(wrapper, '目的港清关资料') + expect(row.findAll('button').map((button) => button.text())).toEqual(['下载']) + await documentButton(wrapper, '目的港清关资料', '下载').trigger('click'); await flushPromises() + expect(downloadContainerClearance).toHaveBeenCalledWith('2092438843630391297', '2092438843630393502') + expect(downloadLoadingDeclaration).not.toHaveBeenCalled() + expect(downloadContainerDocument).not.toHaveBeenCalled() + expect(click).toHaveBeenCalledOnce() + wrapper.unmount() + }) + + it('refreshes destination clearance after A0409 and preserves the server file and error metadata', async () => { + const existing = { id: '401', documentType: 'DESTINATION_CLEARANCE', fileId: '402', fileName: 'clearance-v1.pdf', fileHash: null, uploadedBy: '3', uploadedByName: '清关员', uploadedAt: '2026-08-26T11:00:00', completedBy: null, completedByName: null, completedAt: null, version: 1 } + const server = { ...existing, fileId: '403', fileName: 'server-clearance-v2.pdf', uploadedByName: '服务端清关员', version: 2 } + vi.mocked(getContainerClearance).mockResolvedValueOnce(existing as never).mockResolvedValueOnce(server as never) + vi.mocked(uploadContainerClearance).mockRejectedValueOnce(new ApiResponseError('版本冲突', 'A0409', undefined, undefined, 'request-clearance-409', 'trace-clearance-409')) + const wrapper = mountView(); await flushPromises() + await documentButton(wrapper, '目的港清关资料', '上传附件').trigger('click'); await flushPromises() + const file = new File(['clearance'], 'clearance-next.pdf', { type: 'application/pdf' }) + ;(wrapper.vm as unknown as { selectClearanceFile: (file: { raw: File }) => void }).selectClearanceFile({ raw: file }) + await (wrapper.vm as unknown as { uploadClearance: () => Promise }).uploadClearance(); await flushPromises() + expect(uploadContainerClearance).toHaveBeenCalledOnce() + expect(documentArticle(wrapper, '目的港清关资料').text()).toContain('server-clearance-v2.pdf') + expect(wrapper.text()).toContain('A0409') + expect(wrapper.text()).toContain('request-clearance-409') + wrapper.unmount() + }) + it('keeps customs attachment upload and completion as two explicit steps with exact payload', async () => { const completedOverview = { ...overview, documents: overview.documents.map((item) => ({ ...item, completed: true })) } vi.mocked(getContainerOverview).mockResolvedValueOnce(overview as never).mockResolvedValue(completedOverview as never) diff --git a/src/modules/container-loading/pages/ContainerDetailView.vue b/src/modules/container-loading/pages/ContainerDetailView.vue index 2235baf9..80f74844 100644 --- a/src/modules/container-loading/pages/ContainerDetailView.vue +++ b/src/modules/container-loading/pages/ContainerDetailView.vue @@ -11,13 +11,14 @@ import AppDialog from '@/shared/components/AppDialog.vue' import LoadErrorState from '@/shared/components/LoadErrorState.vue' import StatusBadge from '@/shared/components/StatusBadge.vue' import { CarrierScheduleQueryPanel } from '@/modules/port-schedules' -import { completeLoadingDeclaration, downloadLoadingDeclaration, uploadLoadingDeclaration } from '@/modules/loading-lists' +import { completeLoadingDeclaration, downloadLoadingDeclaration, getLoadingDeclaration, uploadLoadingDeclaration } from '@/modules/loading-lists' import { confirmContainerOutboundDispatch, getContainerOutboundDispatch } from '@/modules/outbound-dispatch/api/platformOutboundDispatch' import type { ContainerOutboundDispatchDetail, ContainerOutboundGateItem } from '@/modules/outbound-dispatch/types' import ContainerOverviewPanel from '../components/ContainerOverviewPanel.vue' import { cancelContainerException, completeContainerClearance, + downloadContainerClearance, downloadContainerDocument, getContainer, getContainerClearance, @@ -99,6 +100,42 @@ const canConfirmDeparture = computed(() => canConfirmOutboundDispatch.value && outboundDetail.value?.aggregate.dispatchStatus === 'READY_FOR_DISPATCH' && outboundGateSummary.value.blocked === 0) +function toOverviewDocument(document: BusinessDocument, documentType: string, fallback?: ContainerOverviewDocument | null): ContainerOverviewDocument { + return { + id: document.id || fallback?.id || document.fileId, + documentType, fileId: document.fileId, fileName: document.fileName, fileHash: document.fileHash, + uploadedBy: document.uploadedBy, uploadedByName: document.uploadedByName, uploadedAt: document.uploadedAt, + version: document.version, completed: Boolean(document.completedAt), + } +} +function replaceOverviewDocument(document: ContainerOverviewDocument) { + if (!overview.value) return + overview.value = { ...overview.value, documents: [...overview.value.documents.filter((item) => item.documentType !== document.documentType), document] } +} +async function refreshOverviewDocument(documentType: 'CUSTOMS_DECLARATION' | 'DESTINATION_CLEARANCE') { + let refreshCause: unknown + let refreshed: ContainerOverview | null = null + try { refreshed = await getContainerOverview(containerId.value) } catch (cause) { refreshCause = cause } + let current = refreshed?.documents.find((item) => item.documentType === documentType) || null + if (!current) { + try { + const business = documentType === 'CUSTOMS_DECLARATION' + ? overview.value?.loadingPlanId && detail.value ? await getLoadingDeclaration(overview.value.loadingPlanId, detail.value.warehouseId) : null + : await getContainerClearance(containerId.value) + if (business) { + if (documentType === 'DESTINATION_CLEARANCE') clearanceDocument.value = business + current = toOverviewDocument(business, documentType, overview.value?.documents.find((item) => item.documentType === documentType)) + } + } catch (cause) { if (!refreshCause) refreshCause = cause } + } + if (refreshed) overview.value = current && !refreshed.documents.some((item) => item.documentType === documentType) + ? { ...refreshed, documents: [...refreshed.documents, current] } + : refreshed + else if (current) replaceOverviewDocument(current) + else if (refreshCause) throw refreshCause + return current +} + function statusText(status?: ContainerPlanStatus | null) { return status ? ({ WAIT_LOADING: '待装柜', EDITING: '预装柜中', LOADING: '拣货中', COMPLETED: '装柜完成' })[status] : '—' } function formatTime(value?: string | null) { return value?.replace('T', ' ').slice(0, 16) || '—' } @@ -173,13 +210,8 @@ async function saveDocument() { if (!overview.value.loadingPlanId) throw new Error('当前货柜没有装柜清单,无法上传国内出口报关附件') const uploaded = await uploadLoadingDeclaration(overview.value.loadingPlanId, detail.value.warehouseId, documentFile.value, expectedVersion, '上传国内出口报关附件') if (!uploaded) throw new Error('报关附件上传成功但未返回附件信息') - const optimistic: ContainerOverviewDocument = { - id: uploaded.id || selectedDocument.value?.id || uploaded.fileId, - documentType: 'CUSTOMS_DECLARATION', fileId: uploaded.fileId, fileName: uploaded.fileName, - fileHash: uploaded.fileHash, uploadedBy: uploaded.uploadedBy, uploadedByName: uploaded.uploadedByName, - uploadedAt: uploaded.uploadedAt, version: uploaded.version, completed: Boolean(uploaded.completedAt), - } - overview.value = { ...overview.value, documents: [...overview.value.documents.filter((item) => item.documentType !== 'CUSTOMS_DECLARATION'), optimistic] } + const optimistic = toOverviewDocument(uploaded, 'CUSTOMS_DECLARATION', selectedDocument.value) + replaceOverviewDocument(optimistic) try { const refreshed = await getContainerOverview(containerId.value) if (refreshed.documents.some((item) => item.documentType === 'CUSTOMS_DECLARATION')) overview.value = refreshed @@ -194,7 +226,17 @@ async function saveDocument() { overview.value = await getContainerOverview(containerId.value) } documentOpen.value = false; documentFile.value = null; ElMessage.success(`${documentTitle.value}已上传`) - } catch (cause) { documentError.value = getApiErrorDetails(cause, `${documentTitle.value}上传失败`) } + } catch (cause) { + const details = getApiErrorDetails(cause, `${documentTitle.value}上传失败`) + if (details.code === 'A0409') { + if (documentType.value === 'CUSTOMS_DECLARATION') { + try { await refreshOverviewDocument('CUSTOMS_DECLARATION') } catch { /* 保留原并发错误证据 */ } + } else { + try { overview.value = await getContainerOverview(containerId.value) } catch { /* 保留原并发错误证据 */ } + } + } + documentError.value = details + } finally { documentSaving.value = false } } async function refreshCustomsFacts() { @@ -226,7 +268,7 @@ async function completeCustomsDeclaration(document: ContainerOverviewDocument) { } async function saveBlob(blob: Blob, fileName: string) { const url = URL.createObjectURL(blob); const anchor = document.createElement('a'); anchor.href = url; anchor.download = fileName || '货柜资料'; anchor.click(); URL.revokeObjectURL(url) } async function downloadDocument(item: ContainerOverviewDocument) { - if (!['DELIVERY_ORDER', 'PICKUP_ORDER', 'CUSTOMS_DECLARATION'].includes(item.documentType)) return + if (!['DELIVERY_ORDER', 'PICKUP_ORDER', 'CUSTOMS_DECLARATION', 'DESTINATION_CLEARANCE'].includes(item.documentType)) return if (item.documentType === 'CUSTOMS_DECLARATION' && (!overview.value?.loadingPlanId || !detail.value)) { customsActionError.value = { code: 'LOADING_PLAN_REQUIRED', message: '当前货柜没有装柜清单,无法下载国内出口报关附件' } return @@ -235,11 +277,14 @@ async function downloadDocument(item: ContainerOverviewDocument) { try { const blob = item.documentType === 'CUSTOMS_DECLARATION' ? await downloadLoadingDeclaration(overview.value!.loadingPlanId!, detail.value!.warehouseId, item.fileId) - : await downloadContainerDocument(containerId.value, item.documentType as 'DELIVERY_ORDER' | 'PICKUP_ORDER', item.fileId) + : item.documentType === 'DESTINATION_CLEARANCE' + ? await downloadContainerClearance(containerId.value, item.fileId) + : await downloadContainerDocument(containerId.value, item.documentType as 'DELIVERY_ORDER' | 'PICKUP_ORDER', item.fileId) await saveBlob(blob, item.fileName); ElMessage.success('资料下载已开始') } catch (cause) { const details = getApiErrorDetails(cause, '资料下载失败') if (item.documentType === 'CUSTOMS_DECLARATION') customsActionError.value = details + if (item.documentType === 'DESTINATION_CLEARANCE') clearanceError.value = details ElMessage.error(details.message) } finally { documentDownloading.value = false } @@ -268,14 +313,29 @@ function removeClearanceFile() { clearanceFile.value = null } async function uploadClearance() { if (!clearanceFile.value) { clearanceError.value = { code: 'FILE_REQUIRED', message: '请选择目的港清关资料文件' }; return } clearanceSaving.value = true; clearanceError.value = null - try { clearanceDocument.value = await uploadContainerClearance(containerId.value, clearanceFile.value, clearanceDocument.value?.version || 0, '上传目的港清关资料'); overview.value = await getContainerOverview(containerId.value); clearanceFile.value = null; ElMessage.success('目的港清关资料已上传') } - catch (cause) { clearanceError.value = getApiErrorDetails(cause, '目的港清关资料上传失败') } + try { + const uploaded = await uploadContainerClearance(containerId.value, clearanceFile.value, clearanceDocument.value?.version || 0, '上传目的港清关资料') + if (!uploaded) throw new Error('目的港清关资料上传成功但未返回附件信息') + clearanceDocument.value = uploaded + replaceOverviewDocument(toOverviewDocument(uploaded, 'DESTINATION_CLEARANCE', overview.value?.documents.find((item) => item.documentType === 'DESTINATION_CLEARANCE'))) + try { + const refreshed = await getContainerOverview(containerId.value) + overview.value = refreshed.documents.some((item) => item.documentType === 'DESTINATION_CLEARANCE') ? refreshed : { ...refreshed, documents: [...refreshed.documents, toOverviewDocument(uploaded, 'DESTINATION_CLEARANCE')] } + } catch (cause) { documentRefreshWarning.value = getApiErrorDetails(cause, '目的港清关资料已上传,但最新资料刷新失败;当前已保留本次上传结果') } + clearanceFile.value = null; ElMessage.success('目的港清关资料已上传') + } catch (cause) { + const details = getApiErrorDetails(cause, '目的港清关资料上传失败') + if (details.code === 'A0409') { + try { await refreshOverviewDocument('DESTINATION_CLEARANCE') } catch { /* 保留原并发错误证据 */ } + } + clearanceError.value = details + } finally { clearanceSaving.value = false } } async function completeClearance() { if (!clearanceDocument.value) return clearanceSaving.value = true; clearanceError.value = null - try { clearanceDocument.value = await completeContainerClearance(containerId.value, clearanceDocument.value.version, '确认目的港清关完成'); overview.value = await getContainerOverview(containerId.value); ElMessage.success('已完成清关') } + try { const completed = await completeContainerClearance(containerId.value, clearanceDocument.value.version, '确认目的港清关完成'); if (!completed) throw new Error('确认清关完成但未返回附件信息'); clearanceDocument.value = completed; replaceOverviewDocument(toOverviewDocument(completed, 'DESTINATION_CLEARANCE')); overview.value = await getContainerOverview(containerId.value); ElMessage.success('已完成清关') } catch (cause) { clearanceError.value = getApiErrorDetails(cause, '清关完成失败') } finally { clearanceSaving.value = false } } @@ -315,9 +375,9 @@ onMounted(async () => { await load(); if (route.query.action === 'documents') op
- 选择文件 + 选择文件 - +