This commit is contained in:
2026-10-30 11:19:57 +08:00
parent a2ef3542f4
commit 8cc8b4fb94
13 changed files with 218 additions and 74 deletions
@@ -212,7 +212,7 @@ describe('ContainerCustomsPanel', () => {
wrapper = await render('DECLARING', true, true)
expect(wrapper.text()).toContain('发送报关行执行')
expect(wrapper.text()).toContain('覆盖预录单')
expect(wrapper.text()).toContain('选择预录单')
expect(wrapper.text()).toContain('报关行回执.pdf')
expect(wrapper.text()).toContain('真实报关完税文件.pdf')
expect(wrapper.text()).not.toContain('确认 · 2026/8/29 11:15:00 · 客户一')
@@ -262,12 +262,35 @@ describe('ContainerCustomsPanel', () => {
const onChange = wrapper.findAllComponents({ name: 'ElUpload' })[0]!.props('onChange') as (upload: { raw: File }) => void
onChange({ raw: selected })
await flushPromises()
await wrapper.findAll('button').find(button => button.text() === '提交')!.trigger('click')
await wrapper.findAll('button').find(button => button.text() === '提交预录单')!.trigger('click')
await flushPromises()
expect(api.upload).toHaveBeenCalledWith(document.documentId, selected, 7)
wrapper.unmount()
})
it('一次选择的预录单会关联柜内全部客户资料', async () => {
const value = customs('DECLARING', false)
value.documents.push({ ...value.documents[0]!, documentId: '2092438843630391414', source: { ...value.documents[0]!.source, fileId: '2092438843630391415', fileName: '另一份客户资料.pdf', version: 1 }, receipt: null, confirmationHistory: [] })
value.brokerSubmission = { id: '2092438843630391412', customsContainerId: value.aggregateId, loadingPlanId: value.loadingPlanId, batchNo: 'BS-20260829-001', status: 'SENT', sourceVersion: 3, sentByName: '平台员', sentAt: '2026-08-29T10:45:00', version: 1 }
api.get.mockResolvedValue(value)
const wrapper = mount(ContainerCustomsPanel, {
props: { loadingPlanId: value.loadingPlanId, canList: true, canManage: true, departureState: 'WAITING_OTHER_GATES' },
global: { plugins: [ElementPlus] },
})
await flushPromises()
const selected = new File(['receipt'], '整柜预录单.pdf', { type: 'application/pdf' })
const onChange = wrapper.findAllComponents({ name: 'ElUpload' })[0]!.props('onChange') as (upload: { raw: File }) => void
onChange({ raw: selected })
await flushPromises()
await wrapper.findAll('button').find(button => button.text() === '提交预录单')!.trigger('click')
await flushPromises()
expect(api.upload).toHaveBeenCalledTimes(2)
expect(api.upload).toHaveBeenNthCalledWith(1, value.documents[0]!.documentId, selected, 0)
expect(api.upload).toHaveBeenNthCalledWith(2, value.documents[1]!.documentId, selected, 0)
wrapper.unmount()
})
it('keeps COMPLETED strictly read-only with both files visible and downloadable', async () => {
const wrapper = await render('COMPLETED', true, true)
expect(wrapper.text()).toContain('客户退税资料.pdf')
@@ -341,7 +364,7 @@ describe('ContainerCustomsPanel', () => {
const onChange = wrapper.findAllComponents({ name: 'ElUpload' })[0]!.props('onChange') as (upload: { raw: File }) => void
onChange({ raw: selected })
await flushPromises()
const submit = wrapper.findAll('button').find(button => button.text() === '提交')!
const submit = wrapper.findAll('button').find(button => button.text() === '提交预录单')!
await submit.trigger('click')
await flushPromises()
@@ -48,7 +48,7 @@ const actionLoading = ref(false)
const loadError = ref<ApiErrorDetails | null>(null)
const actionError = ref<ApiErrorDetails | null>(null)
const downloadingKey = ref('')
const receiptFiles = reactive<Record<string, File | null>>({})
const receiptFile = ref<File | null>(null)
const finalFiles = reactive<Record<string, File | null>>({})
const clearanceDialogVisible = ref(false)
const clearanceBatch = ref<DeclarationBatch | null>(null)
@@ -84,9 +84,9 @@ function formatDateTime(value: string | null | undefined) {
const date = new Date(value)
return Number.isNaN(date.getTime()) ? value : date.toLocaleString('zh-CN', { hour12: false })
}
function selectReceipt(documentId: string, upload: UploadFile) {
function selectReceipt(upload: UploadFile) {
if (!props.canManage || customs.value?.status !== 'DECLARING' || !brokerSubmission.value || brokerSubmitted.value) return
receiptFiles[documentId] = upload.raw || null
receiptFile.value = upload.raw || null
actionError.value = null
}
function selectFinal(documentId: string, upload: UploadFile) {
@@ -185,18 +185,26 @@ async function start() {
finally { actionLoading.value = false }
}
async function uploadReceipt(document: ContainerCustomsDocument) {
async function uploadReceipt() {
if (!props.canManage || customs.value?.status !== 'DECLARING' || !brokerSubmission.value || brokerSubmitted.value) return
const selected = receiptFiles[document.documentId]
const selected = receiptFile.value
if (!selected) { ElMessage.warning('请选择对应的预录单'); return }
const documents = customs.value.documents
let uploadedCount = 0
actionLoading.value = true; actionError.value = null
try {
await uploadContainerCustomsReceipt(document.documentId, selected, document.receipt?.version || 0)
receiptFiles[document.documentId] = null
for (const document of documents) {
await uploadContainerCustomsReceipt(document.documentId, selected, document.receipt?.version || 0)
uploadedCount += 1
}
receiptFile.value = null
await load()
emit('refreshed')
ElMessage.success('预录单已上传,等待客户确认')
} catch (error) { actionError.value = getApiErrorDetails(error, '预录单上传失败') }
ElMessage.success(`预录单已上传并关联 ${documents.length} 份客户资料`)
} catch (error) {
if (uploadedCount) await load()
actionError.value = getApiErrorDetails(error, uploadedCount ? `预录单已关联 ${uploadedCount}/${documents.length} 份资料,后续上传失败` : '预录单上传失败')
}
finally { actionLoading.value = false }
}
@@ -317,10 +325,10 @@ onMounted(load)
<div class="workbench-evidence"><table class="compact-table density-table"><thead><tr><th>客户</th><th>订单号</th><th>入仓码</th></tr></thead><tbody><tr v-for="item in missingInbounds" :key="item.inboundId"><td>{{ item.customerName }}</td><td>{{ item.orderNo }}</td><td>{{ item.inboundCode }}</td></tr></tbody></table></div>
</div>
<div v-if="customs.documents.length" class="compact-table-wrap">
<h3>已冻结退税资料、预录单与完税文件</h3>
<div class="documents-heading"><h3>已冻结退税资料、预录单与完税文件</h3><div v-if="canManage && customs.status === 'DECLARING'" class="document-actions density-actions"><el-upload :disabled="!brokerSubmission || brokerSubmitted" :auto-upload="false" :limit="1" :show-file-list="false" :on-change="selectReceipt"><el-button class="density-action" size="small" :disabled="!brokerSubmission || brokerSubmitted">{{ !brokerSubmission ? '首次发送后上传预录单' : '选择预录单' }}</el-button></el-upload><span v-if="receiptFile" class="selected-receipt" :title="receiptFile.name">{{ receiptFile.name }}</span><el-button data-tone="primary" class="density-action" size="small" type="primary" :disabled="!brokerSubmission || brokerSubmitted || !receiptFile" :loading="actionLoading" @click="uploadReceipt">提交预录单</el-button></div></div>
<div class="workbench-evidence"><table class="compact-table document-table density-table"><thead><tr><th>操作</th><th>关联入仓码</th><th>报关行状态</th><th>客户退税资料</th><th>预录单</th><th>真实报关完税文件</th><th>客户</th><th>预录单情况</th></tr></thead><tbody>
<tr v-for="document in customs.documents" :key="document.documentId">
<td><div v-if="canManage && customs.status === 'DECLARING'" class="document-actions density-actions"><el-upload :disabled="!brokerSubmission || brokerSubmitted" :auto-upload="false" :limit="1" :show-file-list="false" :on-change="(upload: UploadFile) => selectReceipt(document.documentId, upload)"><el-button class="density-action" size="small" :disabled="!brokerSubmission || brokerSubmitted">{{ !brokerSubmission ? '首次发送后上传预录单' : document.receipt ? '覆盖预录单' : '上传预录单' }}</el-button></el-upload><el-button data-tone="primary" class="density-action" size="small" type="primary" :disabled="!brokerSubmission || brokerSubmitted || !receiptFiles[document.documentId]" :loading="actionLoading" @click="uploadReceipt(document)">提交</el-button><el-upload :disabled="!brokerSubmitted" :auto-upload="false" :limit="1" :show-file-list="false" :on-change="(upload: UploadFile) => selectFinal(document.documentId, upload)"><el-button class="density-action" size="small" :disabled="!brokerSubmitted">{{ document.finalDocument ? '覆盖完税文件' : '上传完税文件' }}</el-button></el-upload><el-button data-tone="primary" class="density-action" size="small" type="primary" :disabled="!brokerSubmitted || !finalFiles[document.documentId]" :loading="actionLoading" @click="uploadFinal(document)">提交</el-button></div><span v-else class="readonly-label">只读</span></td>
<td><div v-if="canManage && customs.status === 'DECLARING'" class="document-actions density-actions"><el-upload :disabled="!brokerSubmitted" :auto-upload="false" :limit="1" :show-file-list="false" :on-change="(upload: UploadFile) => selectFinal(document.documentId, upload)"><el-button class="density-action" size="small" :disabled="!brokerSubmitted">{{ document.finalDocument ? '覆盖完税文件' : '上传完税文件' }}</el-button></el-upload><el-button data-tone="primary" class="density-action" size="small" type="primary" :disabled="!brokerSubmitted || !finalFiles[document.documentId]" :loading="actionLoading" @click="uploadFinal(document)">提交</el-button></div><span v-else class="readonly-label">只读</span></td>
<td>{{ document.inboundCodes.join('、') || '—' }}</td>
<td>{{ brokerSubmitted ? '已发送报关行执行' : brokerSubmission ? '已首次发送,等待预录单确认' : '待首次发送报关行' }}</td>
<td><div class="file-cell"><span :title="document.source.fileName">{{ document.source.fileName }}</span><el-button class="density-action" type="primary" :loading="downloadingKey === `source:${document.documentId}`" :aria-label="`下载客户退税资料 ${document.source.fileName}`" @click="downloadFile(document, 'source')">下载</el-button></div></td>
@@ -361,4 +369,5 @@ onMounted(load)
<style scoped>
.container-customs{padding:0;border:0}.customs-header{display:flex;align-items:center;justify-content:space-between;gap:var(--admin-space-5)}.customs-header h2,.compact-table-wrap h3{margin:0;font-size:var(--admin-font-section)}.customs-header p{margin:3px 0 0;color:var(--workbench-muted);font-size:var(--admin-font-caption)}.customs-actions{display:flex;align-items:center;gap:var(--admin-space-3)}.action-error{margin-top:var(--admin-space-3);padding:var(--admin-space-3) var(--admin-space-4);border:1px solid var(--el-color-danger-light-5);border-radius:var(--admin-radius);background:var(--el-color-danger-light-9);color:var(--el-color-danger);font-size:var(--admin-font-caption)}.action-error :deep(.api-error-meta){margin-top:5px}.customs-summary{margin-top:var(--admin-space-4)!important}.customs-summary .submission-batch{grid-column:span 2}.compact-table-wrap{margin-top:var(--admin-space-5);overflow-x:auto}.compact-table-wrap h3{margin-bottom:var(--admin-space-2)}.compact-table{width:100%}.document-table th:first-child{width:270px}.document-actions,.file-cell{display:flex;align-items:center;gap:var(--admin-space-2)}.file-cell span{display:block;max-width:210px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.readonly-label{color:var(--workbench-muted)}.departure-state{display:flex;align-items:center;gap:var(--admin-space-3);margin-top:var(--admin-space-4);padding-top:var(--admin-space-4);border-top:1px solid var(--workbench-border);font-size:var(--admin-font-body)}.declaration-batches-heading{display:flex;align-items:flex-start;justify-content:space-between;gap:var(--admin-space-4)}.declaration-batches-heading p{margin:3px 0 0;color:var(--workbench-muted);font-size:var(--admin-font-caption)}.declaration-batch-list{display:grid;gap:var(--admin-space-3)}.declaration-batch-card{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:var(--admin-space-2) var(--admin-space-4);padding:var(--admin-space-3) var(--admin-space-4);border:1px solid var(--workbench-border);background:var(--workbench-surface-soft)}.declaration-batch-main strong,.declaration-batch-main span{display:block}.declaration-batch-main span{margin-top:4px;color:var(--workbench-muted);font-size:var(--admin-font-caption)}.declaration-batch-actions,.declaration-result-list{grid-column:1 / -1}.declaration-result-list{display:grid;gap:5px;padding-top:var(--admin-space-2);border-top:1px solid var(--workbench-border)}.declaration-result-row{display:flex;align-items:center;gap:var(--admin-space-2);font-size:var(--admin-font-caption)}.declaration-result-row small{flex:1;color:var(--workbench-muted)}
.documents-heading{display:flex;align-items:center;justify-content:space-between;gap:var(--admin-space-4)}.selected-receipt{max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--workbench-muted);font-size:var(--admin-font-caption)}
</style>