fix(admin): refresh container document versions
This commit is contained in:
@@ -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
|
||||
<div class="surface-panel carrier-panel"><CarrierScheduleQueryPanel :key="carrierRefreshKey" :ocean-booking-id="detail.oceanBookingId" :container-booking-id="detail.id" /></div>
|
||||
</template>
|
||||
|
||||
<AppDialog v-model="documentOpen" :title="documentTitle" width="min(520px, calc(100vw - 28px))" confirm-text="上传资料" :confirm-loading="documentSaving" :confirm-disabled="!documentFile" @confirm="saveDocument"><el-form label-position="top"><el-form-item :label="documentTitle"><el-upload :auto-upload="false" :limit="1" :on-change="selectDocumentFile" :on-remove="removeDocumentFile"><el-button><FileUp :size="16" />选择文件</el-button><template #tip><div class="el-upload__tip">{{ selectedDocument ? '上传新文件将替换当前资料并保留历史。' : `请选择需要提交的${documentTitle}。` }}</div></template></el-upload></el-form-item></el-form><LoadErrorState v-if="documentError" compact title="资料上传失败" :message="documentError.message" :show-retry="false" /></AppDialog>
|
||||
<AppDialog v-model="documentOpen" :title="documentTitle" width="min(520px, calc(100vw - 28px))" confirm-text="上传资料" :confirm-loading="documentSaving" :confirm-disabled="!documentFile" @confirm="saveDocument"><el-form label-position="top"><el-form-item :label="documentTitle"><el-upload :auto-upload="false" :limit="1" :on-change="selectDocumentFile" :on-remove="removeDocumentFile"><el-button><FileUp :size="16" />选择文件</el-button><template #tip><div class="el-upload__tip">{{ selectedDocument ? '上传新文件将替换当前资料并保留历史。' : `请选择需要提交的${documentTitle}。` }}</div></template></el-upload></el-form-item></el-form><LoadErrorState v-if="documentError" compact title="资料上传失败" :message="documentError.message" :show-retry="false"><template #details><ApiErrorMeta :details="documentError" /></template></LoadErrorState></AppDialog>
|
||||
<AppDialog v-model="exceptionOpen" :title="exceptionTitle" width="min(520px, calc(100vw - 28px))" :confirm-text="overview?.exception.active ? '确认取消' : '确认标记'" :confirm-loading="exceptionSaving" :confirm-disabled="!exceptionReason.trim()" @confirm="saveException"><el-alert :title="overview?.exception.active ? '取消备注为必填项,提交后将保留历史记录。' : '异常原因将同步记录在货柜详情和操作日志中。'" :type="overview?.exception.active ? 'warning' : 'error'" :closable="false" /><el-form label-position="top"><el-form-item :label="overview?.exception.active ? '取消备注' : '异常原因'" required><el-input v-model="exceptionReason" type="textarea" :rows="4" maxlength="500" show-word-limit /></el-form-item></el-form><LoadErrorState v-if="exceptionError" compact title="异常处理失败" :message="exceptionError.message" :show-retry="false" /></AppDialog>
|
||||
<AppDialog v-model="clearanceOpen" title="目的港清关资料" width="min(520px, calc(100vw - 28px))" :loading="clearanceLoading" :show-confirm="!clearanceDocument?.completedAt" confirm-text="上传资料" :confirm-loading="clearanceSaving" :confirm-disabled="!clearanceFile" @confirm="uploadClearance"><el-alert v-if="clearanceDocument?.completedAt" type="success" :closable="false" :title="`目的港清关已完成:${clearanceDocument.fileName}`" /><template v-else><el-form label-position="top"><el-form-item label="目的港清关资料"><el-upload :auto-upload="false" :limit="1" :on-change="selectClearanceFile" :on-remove="removeClearanceFile"><el-button><FileUp :size="16" />选择文件</el-button></el-upload></el-form-item></el-form><el-button v-if="clearanceDocument" type="success" plain :loading="clearanceSaving" @click="completeClearance">确认目的港清关完成</el-button></template><LoadErrorState v-if="clearanceError" compact title="目的港清关资料操作失败" :message="clearanceError.message" :show-retry="false" /></AppDialog>
|
||||
<AppDialog v-model="clearanceOpen" title="目的港清关资料" width="min(520px, calc(100vw - 28px))" :loading="clearanceLoading" :show-confirm="!clearanceDocument?.completedAt" confirm-text="上传资料" :confirm-loading="clearanceSaving" :confirm-disabled="!clearanceFile" @confirm="uploadClearance"><el-alert v-if="clearanceDocument?.completedAt" type="success" :closable="false" :title="`目的港清关已完成:${clearanceDocument.fileName}`" /><template v-else><el-form label-position="top"><el-form-item label="目的港清关资料"><el-upload :auto-upload="false" :limit="1" :on-change="selectClearanceFile" :on-remove="removeClearanceFile"><el-button><FileUp :size="16" />选择文件</el-button></el-upload></el-form-item></el-form><el-button v-if="clearanceDocument" type="success" plain :loading="clearanceSaving" @click="completeClearance">确认目的港清关完成</el-button></template><LoadErrorState v-if="clearanceError" compact title="目的港清关资料操作失败" :message="clearanceError.message" :show-retry="false"><template #details><ApiErrorMeta :details="clearanceError" /></template></LoadErrorState></AppDialog>
|
||||
</AdminPage>
|
||||
</template>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user