2026-08-28 16:54:02 +08:00
< script setup lang = "ts" >
2026-09-14 18:34:34 +08:00
import '@/styles/adminDensity.css'
2026-08-28 16:54:02 +08:00
import { computed , onMounted , reactive , ref , watch } from 'vue'
import { ElMessage , ElMessageBox , type UploadFile } from 'element-plus'
import { createPlatformExportTask } from '@/modules/downloads'
import { getApiErrorDetails , type ApiErrorDetails } from '@/shared/api/http'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import OperationErrorToast from '@/shared/components/OperationErrorToast.vue'
import {
completeAllBuyContainerCustoms ,
2026-09-27 20:41:39 +08:00
createDeclarationBatch ,
2026-09-26 13:28:48 +08:00
downloadContainerCustomsReceipt ,
downloadContainerCustomsFinal ,
2026-08-29 10:19:35 +08:00
downloadContainerCustomsDocument ,
2026-08-28 16:54:02 +08:00
getContainerCustoms ,
2026-09-27 20:41:39 +08:00
getDeclarationBatches ,
recordClearanceResult ,
resolveClearanceResult ,
submitDeclarationBatch ,
2026-08-28 16:54:02 +08:00
startContainerCustoms ,
2026-09-26 13:28:48 +08:00
submitContainerCustomsToBroker ,
uploadContainerCustomsReceipt ,
uploadContainerCustomsFinal ,
2026-08-28 16:54:02 +08:00
type ContainerCustomsDocument ,
type ContainerCustomsStatus ,
type ContainerCustomsView ,
2026-09-27 20:41:39 +08:00
type DeclarationBatch ,
2026-08-28 16:54:02 +08:00
} from '../api/platformContainerCustoms'
const props = defineProps < {
loadingPlanId : string
canList : boolean
canManage : boolean
2026-09-09 13:00:45 +08:00
departureState : 'DEPARTED' | 'WAITING_OTHER_GATES' | 'UNKNOWN'
2026-08-28 16:54:02 +08:00
} > ()
const emit = defineEmits < { refreshed : [] } > ()
const customs = ref < ContainerCustomsView | null >( null )
2026-09-27 20:41:39 +08:00
const declarationBatches = ref < DeclarationBatch [] >([])
2026-08-28 16:54:02 +08:00
const loading = ref ( false )
const actionLoading = ref ( false )
const loadError = ref < ApiErrorDetails | null >( null )
const actionError = ref < ApiErrorDetails | null >( null )
2026-08-29 10:19:35 +08:00
const downloadingKey = ref ( '' )
2026-09-26 13:28:48 +08:00
const receiptFiles = reactive < Record < string , File | null > > ({})
const finalFiles = reactive < Record < string , File | null > > ({})
2026-08-28 16:54:02 +08:00
const missingInbounds = computed (() => customs . value ? . inbounds . filter (( item ) => item . taxMode === 'TAX_REFUND' && ! item . covered ) || [])
const missingCoverageCount = computed (() => Math . max ( 0 , ( customs . value ? . taxRefundCount || 0 ) - ( customs . value ? . coveredTaxRefundCount || 0 )))
2026-09-10 14:02:37 +08:00
const allBuy = computed (() => customs . value ? . hasActualCargo === true && customs . value . taxRefundCount === 0 )
2026-09-26 14:22:45 +08:00
const brokerSubmitted = computed (() => Boolean ( customs . value ? . brokerSubmittedAt ))
2026-09-27 20:41:39 +08:00
const openDeclarationBatch = computed (() => declarationBatches . value . find (( item ) => [ 'READY' , 'SUBMITTED' , 'ACKNOWLEDGED' , 'RESULT_PENDING' ]. includes ( item . status )) || null )
2026-08-28 16:54:02 +08:00
const statusText : Record < ContainerCustomsStatus , string > = {
2026-09-01 20:41:30 +08:00
WAITING_CUSTOMER_DOCUMENTS : '待客户补齐退税资料' , READY_TO_START : '可开始报关' , DECLARING : '报关中' , COMPLETED : '报关已完成' ,
2026-08-28 16:54:02 +08:00
}
function documentCustomers ( document : ContainerCustomsDocument ) {
return [... new Set (( customs . value ? . inbounds || []). filter (( item ) => document . inboundIds . includes ( item . inboundId )). map (( item ) => item . customerName ))]. join ( '、' ) || '—'
}
2026-09-26 13:28:48 +08:00
function selectReceipt ( documentId : string , upload : UploadFile ) {
2026-08-29 10:19:35 +08:00
if ( ! props . canManage || customs . value ? . status !== 'DECLARING' ) return
2026-09-26 13:28:48 +08:00
receiptFiles [ documentId ] = upload . raw || null
actionError . value = null
}
function selectFinal ( documentId : string , upload : UploadFile ) {
if ( ! props . canManage || customs . value ? . status !== 'DECLARING' ) return
finalFiles [ documentId ] = upload . raw || null
2026-08-28 16:54:02 +08:00
actionError . value = null
}
async function load () {
if ( ! props . canList || ! props . loadingPlanId ) return
loading . value = true
loadError . value = null
2026-09-27 20:41:39 +08:00
try {
customs . value = await getContainerCustoms ( props . loadingPlanId )
if ( customs . value ? . status === 'COMPLETED' ) {
try { declarationBatches . value = await getDeclarationBatches ( props . loadingPlanId ) } catch { declarationBatches . value = [] }
} else declarationBatches . value = []
}
2026-08-28 16:54:02 +08:00
catch ( error ) { customs . value = null ; loadError . value = getApiErrorDetails ( error , '整柜报关状态加载失败' ) }
finally { loading . value = false }
}
2026-09-27 20:41:39 +08:00
function declarationStatusLabel ( value : DeclarationBatch [ 'status' ]) {
return ({ READY : '待提交' , SUBMITTED : '已提交' , ACKNOWLEDGED : '已受理' , RESULT_PENDING : '待处理结果' , CLOSED : '已关闭' , CANCELLED : '已取消' } as Record < string , string >)[ value ] || value
}
async function createFormalDeclaration () {
2026-09-27 20:48:19 +08:00
if ( ! customs . value || ! [ 'DECLARING' , 'COMPLETED' ]. includes ( customs . value . status ) || ! brokerSubmitted . value || openDeclarationBatch . value ) return
try { await ElMessageBox . confirm ( '当前柜的资料、回执已确认并已发送报关行执行,确认创建正式申报批次?完税文件仍按报关行完成后回传。' , '创建正式申报批次' , { customClass : 'admin-density-confirm' , type : 'warning' , confirmButtonText : '确认创建' }) } catch { return }
2026-09-27 20:41:39 +08:00
actionLoading . value = true ; actionError . value = null
try { await createDeclarationBatch ( props . loadingPlanId , customs . value . version ); await load (); ElMessage . success ( '正式申报批次已创建' ) }
catch ( error ) { actionError . value = getApiErrorDetails ( error , '正式申报批次创建失败' ) }
finally { actionLoading . value = false }
}
async function submitFormalDeclaration ( batch : DeclarationBatch ) {
try {
const answer = await ElMessageBox . prompt ( '请输入报关单号' , '提交正式申报' , { inputValidator : ( value ) => value . trim () ? true : '请输入报关单号' , confirmButtonText : '确认提交' })
actionLoading . value = true ; actionError . value = null
await submitDeclarationBatch ( batch , answer . value )
await load (); ElMessage . success ( '正式申报已提交' )
} catch ( error ) {
if ( error === 'cancel' || error === 'close' ) return
actionError . value = getApiErrorDetails ( error , '正式申报提交失败' )
} finally { actionLoading . value = false }
}
async function recordFormalResult ( batch : DeclarationBatch ) {
try {
const answer = await ElMessageBox . prompt ( '可填写查验处理说明,没有说明可直接确认' , '录入海关查验结果' , { inputPlaceholder : '处理说明(可选)' , confirmButtonText : '确认录入' })
actionLoading . value = true ; actionError . value = null
await recordClearanceResult ( batch , 'INSPECTION' , answer . value )
await load (); ElMessage . success ( '海关结果已录入' )
} catch ( error ) {
if ( error === 'cancel' || error === 'close' ) return
actionError . value = getApiErrorDetails ( error , '海关结果录入失败' )
} finally { actionLoading . value = false }
}
async function resolveFormalResult ( batch : DeclarationBatch , result : DeclarationBatch [ 'results' ][ number ]) {
try {
const answer = await ElMessageBox . prompt ( '请输入处理说明' , '处理海关结果' , { inputValue : result . resolution || '' , inputValidator : ( value ) => value . trim () ? true : '请输入处理说明' , confirmButtonText : '确认处理' })
actionLoading . value = true ; actionError . value = null
await resolveClearanceResult ( result , batch . version , answer . value )
await load (); ElMessage . success ( '海关结果已处理' )
} catch ( error ) {
if ( error === 'cancel' || error === 'close' ) return
actionError . value = getApiErrorDetails ( error , '海关结果处理失败' )
} finally { actionLoading . value = false }
}
2026-08-28 16:54:02 +08:00
async function start () {
if ( ! customs . value || customs . value . status !== 'READY_TO_START' ) return
try {
2026-09-14 18:34:34 +08:00
await ElMessageBox . confirm ( '开始后将冻结客户当前退税资料版本、关联入仓码覆盖范围及平台买单准备事实,确认开始整柜报关?' , '开始整柜报关' , { customClass : 'admin-density-confirm' , type : 'warning' , confirmButtonText : '确认开始' })
2026-08-28 16:54:02 +08:00
} catch { return }
actionLoading . value = true ; actionError . value = null
try { customs . value = await startContainerCustoms ( props . loadingPlanId , customs . value . version ); ElMessage . success ( '整柜报关已开始' ) }
catch ( error ) { actionError . value = getApiErrorDetails ( error , '开始整柜报关失败' ) }
finally { actionLoading . value = false }
}
2026-09-26 13:28:48 +08:00
async function uploadReceipt ( document : ContainerCustomsDocument ) {
2026-09-26 17:31:29 +08:00
if ( ! props . canManage || customs . value ? . status !== 'DECLARING' || brokerSubmitted . value ) return
2026-09-26 13:28:48 +08:00
const selected = receiptFiles [ document . documentId ]
if ( ! selected ) { ElMessage . warning ( '请选择对应的报关行回执' ); return }
2026-08-28 16:54:02 +08:00
actionLoading . value = true ; actionError . value = null
try {
2026-09-26 13:28:48 +08:00
await uploadContainerCustomsReceipt ( document . documentId , selected , document . receipt ? . version || 0 )
receiptFiles [ document . documentId ] = null
2026-08-28 16:54:02 +08:00
await load ()
emit ( 'refreshed' )
2026-09-24 16:29:31 +08:00
ElMessage . success ( '报关行回执已上传,等待客户确认' )
2026-09-26 13:28:48 +08:00
} catch ( error ) { actionError . value = getApiErrorDetails ( error , '报关行回执上传失败' ) }
finally { actionLoading . value = false }
}
async function uploadFinal ( document : ContainerCustomsDocument ) {
if ( ! props . canManage || customs . value ? . status !== 'DECLARING' ) return
const selected = finalFiles [ document . documentId ]
if ( ! selected ) { ElMessage . warning ( '请选择真实报关完税文件' ); return }
actionLoading . value = true ; actionError . value = null
try {
await uploadContainerCustomsFinal ( document . documentId , selected , document . finalDocument ? . version || 0 )
finalFiles [ document . documentId ] = null
await load (); emit ( 'refreshed' ); ElMessage . success ( '真实报关完税文件已上传' )
} catch ( error ) { actionError . value = getApiErrorDetails ( error , '真实报关完税文件上传失败' ) }
2026-08-28 16:54:02 +08:00
finally { actionLoading . value = false }
}
2026-08-29 10:19:35 +08:00
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 )
}
2026-09-26 13:28:48 +08:00
async function downloadFile ( document : ContainerCustomsDocument , kind : 'source' | 'receipt' | 'final' ) {
const target = kind === 'source' ? document . source : kind === 'receipt' ? document . receipt : document . finalDocument
2026-08-29 10:19:35 +08:00
if ( ! target ) return
const key = ` ${ kind } : ${ document . documentId } `
downloadingKey . value = key
actionError . value = null
try {
const blob = kind === 'source'
? await downloadContainerCustomsDocument ( document . documentId )
2026-09-26 13:28:48 +08:00
: kind === 'receipt' ? await downloadContainerCustomsReceipt ( document . documentId ) : await downloadContainerCustomsFinal ( document . documentId )
2026-08-29 10:19:35 +08:00
saveBlob ( blob , target . fileName )
2026-09-26 13:28:48 +08:00
ElMessage . success ( ` ${ kind === 'source' ? '客户退税资料' : kind === 'receipt' ? '报关行回执' : '报关完税文件' } 下载已开始` )
2026-08-29 10:19:35 +08:00
} catch ( error ) {
2026-09-01 20:41:30 +08:00
actionError . value = getApiErrorDetails ( error , ` ${ kind === 'source' ? '客户退税资料' : '报关完成文件' } 下载失败` )
2026-08-29 10:19:35 +08:00
} finally {
downloadingKey . value = ''
}
}
2026-09-26 13:28:48 +08:00
async function submitToBroker () {
if ( ! customs . value || customs . value . status !== 'DECLARING' ) return
try { await ElMessageBox . confirm ( '确认已核对全部回执并把确认结果发送给报关行执行?' , '发送报关行执行' , { customClass : 'admin-density-confirm' , type : 'warning' , confirmButtonText : '确认发送' }) } catch { return }
actionLoading . value = true ; actionError . value = null
try { customs . value = await submitContainerCustomsToBroker ( props . loadingPlanId , customs . value . version ); ElMessage . success ( '已发送给报关行执行' ) }
catch ( error ) { actionError . value = getApiErrorDetails ( error , '发送报关行执行失败' ) }
finally { actionLoading . value = false }
}
2026-08-28 16:54:02 +08:00
async function completeAllBuy () {
if ( ! customs . value || customs . value . status !== 'DECLARING' || ! allBuy . value ) return
2026-09-14 18:34:34 +08:00
try { await ElMessageBox . confirm ( '确认全买单柜报关已经完成?报关完成不等同于国内离仓。' , '完成全买单柜报关' , { customClass : 'admin-density-confirm' , type : 'warning' , confirmButtonText : '确认完成' }) }
2026-08-28 16:54:02 +08:00
catch { return }
actionLoading . value = true ; actionError . value = null
try {
customs . value = await completeAllBuyContainerCustoms ( props . loadingPlanId , customs . value . version )
emit ( 'refreshed' )
ElMessage . success ( '全买单柜报关已完成' )
} catch ( error ) { actionError . value = getApiErrorDetails ( error , '完成全买单柜报关失败' ) }
finally { actionLoading . value = false }
}
async function exportAllBuy () {
actionLoading . value = true ; actionError . value = null
try {
const task = await createPlatformExportTask ({
2026-08-30 17:43:03 +08:00
exportType : 'LOADING_LIST_BUY_DECLARATION_ITEMS' , format : 'XLSX' , taskName : `买单报关- ${ customs . value ? . planNo || '柜级明细' } ` ,
2026-08-28 16:54:02 +08:00
parameters : { loadingPlanId : String ( props . loadingPlanId ) },
})
ElMessage . success ( `买单报关导出任务已创建: ${ task . taskNo } ` )
} catch ( error ) { actionError . value = getApiErrorDetails ( error , '买单报关导出任务创建失败' ) }
finally { actionLoading . value = false }
}
watch (() => props . loadingPlanId , () => { void load () })
onMounted ( load )
</ script >
< template >
2026-09-14 18:34:34 +08:00
< section class = "container-customs surface-panel admin-density" aria -label =" 整柜报关 " >
2026-08-28 16:54:02 +08:00
< OperationErrorToast :details = "actionError" />
< header class = "customs-header" >
< div >< h2 > 整柜报关 </ h2 >< p > 客户退税资料与平台买单事实按柜冻结 ; 报关完成和国内离仓分别展示 。 </ p ></ div >
< div v-if = "customs" class="customs-actions" >
2026-09-10 14:02:37 +08:00
< el-tag : type = "!customs.hasActualCargo ? 'info' : customs.status === 'COMPLETED' ? 'success' : customs.status === 'WAITING_CUSTOMER_DOCUMENTS' ? 'danger' : 'warning'" >{{ customs . hasActualCargo ? statusText [ customs . status ] : '无需报关' }}</ el-tag >
2026-08-28 16:54:02 +08:00
< el-button v-if = "canManage && allBuy" plain :loading="actionLoading" @click="exportAllBuy" > 导出买单 XLSX </ el -button >
2026-09-10 14:02:37 +08:00
< el-button v-if = "canManage && customs.hasActualCargo && customs.status === 'READY_TO_START'" type="primary" :loading="actionLoading" @click="start" > 开始报关 </ el -button >
2026-09-26 17:31:29 +08:00
< el-button v-if = "canManage && customs.hasActualCargo && customs.status === 'DECLARING' && !brokerSubmitted" type="primary" plain :loading="actionLoading" @click="submitToBroker" > 发送报关行执行 </ el -button >
2026-08-28 16:54:02 +08:00
< el-button v-if = "canManage && allBuy && customs.status === 'DECLARING'" type="primary" :loading="actionLoading" @click="completeAllBuy" > 确认报关完成 </ el -button >
</ div >
</ header >
2026-08-29 10:19:35 +08:00
< div v-if = "actionError" class="action-error" role="alert"><span>{{ actionError.message }}</span><ApiErrorMeta :details="actionError" /></ div >
2026-08-28 16:54:02 +08:00
< LoadErrorState v-if = "loadError" compact title="整柜报关加载失败" :message="loadError.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="loadError" /></ template ></ LoadErrorState >
< div v-else-if = "loading"><el-skeleton :rows="3" animated /></ div >
< template v-else-if = "customs" >
2026-09-10 14:02:37 +08:00
< el-empty v-if = "!customs.hasActualCargo" description="当前装柜计划没有实际装柜货物,无需报关" :image-size="56" />
< template v-else >
2026-09-24 16:29:31 +08:00
< div class = "customs-metrics" >< span > 实际箱数 < strong >{{ customs . actualBoxCount }}</ strong ></ span >< span > 退税总票 < strong >{{ customs . taxRefundCount }}</ strong ></ span >< span > 已覆盖 < strong >{{ customs . coveredTaxRefundCount }}</ strong ></ span >< span : class = "{ danger: missingCoverageCount }" > 缺失 < strong >{{ missingCoverageCount }}</ strong ></ span ></ div >
2026-08-28 16:54:02 +08:00
< div v-if = "missingInbounds.length" class="compact-table-wrap" >
2026-09-01 20:41:30 +08:00
< h3 > 缺退税资料清单 </ h3 >
2026-09-14 18:34:34 +08:00
< 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 >
2026-08-28 16:54:02 +08:00
</ div >
< div v-if = "customs.documents.length" class="compact-table-wrap" >
2026-09-26 13:28:48 +08:00
< h3 > 已冻结退税资料 、 报关行回执与完税文件 </ h3 >
2026-09-26 17:31:29 +08:00
< 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 >< th > 回执情况 </ th ></ tr ></ thead >< tbody >
2026-08-28 16:54:02 +08:00
< tr v-for = "document in customs.documents" :key="document.documentId" >
2026-09-26 17:31:29 +08:00
< 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) => selectReceipt(document.documentId, upload)"><el-button class="density-action" size="small" :disabled="brokerSubmitted">{{ document.receipt ? '覆盖报关行回执' : '上传报关行回执' }}</el-button></el-upload><el-button data-tone="primary" class="density-action" size="small" type="primary" :disabled="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 >
2026-09-14 18:34:34 +08:00
< 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 >
2026-09-26 13:28:48 +08:00
< td >< div v-if = "document.receipt" class="file-cell"><span :title="document.receipt.fileName">{{ document.receipt.fileName }}</span><el-button class="density-action" type="primary" :loading="downloadingKey === `receipt:${document.documentId}`" :aria-label="`下载报关行回执 ${document.receipt.fileName}`" @click="downloadFile(document, 'receipt')" > 下载 </ el -button ></ div >< span v-else > 未上传 </ span ></ td >
< td >< div v-if = "document.finalDocument" class="file-cell"><span :title="document.finalDocument.fileName">{{ document.finalDocument.fileName }}</span><el-button class="density-action" type="primary" :loading="downloadingKey === `final:${document.documentId}`" :aria-label="`下载报关完税文件 ${document.finalDocument.fileName}`" @click="downloadFile(document, 'final')" > 下载 </ el -button ></ div >< span v-else > 未上传 </ span ></ td >
2026-09-26 17:31:29 +08:00
< td >{{ document . inboundCodes . join ( '、' ) || '—' }}</ td >< td >{{ brokerSubmitted ? '已发送报关行执行' : '待发送报关行执行' }}</ td >< td > 退税资料 v {{ document . source . version }}< span v-if = "document.receipt"> / 回执 v{{ document.receipt.version }}</span><span v-if="document.finalDocument" > / 完税 v {{ document.finalDocument.version }}</ span ></ td >< td >{{ documentCustomers ( document ) }}</ td >< td >{{ document . receipt ? ( document . receiptStatus === 'CONFIRMED' ? '客户已确认回执' : document . receiptStatus === 'REJECTED' ? '客户已打回回执' : '待客户确认回执' ) : '待上传报关行回执' }}</ td >
2026-08-28 16:54:02 +08:00
</ tr >
</ tbody ></ table >
</ div >
2026-09-27 20:48:19 +08:00
< div v-if = "['DECLARING', 'COMPLETED'].includes(customs.status) && brokerSubmitted" class="compact-table-wrap declaration-batches-wrap" >
< div class = "declaration-batches-heading" >< div >< h3 > 正式申报与海关结果 </ h3 >< p > 回执确认并发送报关行执行后建立正式申报批次 ; 完税文件 、 船期和 PDA 节点保持独立 。 </ p ></ div >< el-button v-if = "canManage && !openDeclarationBatch" size="small" type="primary" :loading="actionLoading" @click="createFormalDeclaration" > 创建正式申报批次 </ el -button ></ div >
2026-09-27 20:41:39 +08:00
< el-empty v-if = "!declarationBatches.length" description="尚未创建正式申报批次" :image-size="48" />
< div v-else class = "declaration-batch-list" >
< article v-for = "batch in declarationBatches" :key="batch.id" class="declaration-batch-card" >
< div class = "declaration-batch-main" >< strong >{{ batch . batchNo }}</ strong >< span > 报关单号 : {{ batch . customsEntryNo || '—' }} · 版本 {{ batch . version }}</ span ></ div >
< el-tag size = "small" : type = "batch.status === 'CLOSED' ? 'success' : batch.status === 'CANCELLED' ? 'info' : 'warning'" >{{ declarationStatusLabel ( batch . status ) }}</ el-tag >
< div class = "declaration-batch-actions" v-if = "canManage"><el-button v-if="batch.status === 'READY'" size="small" type="primary" :loading="actionLoading" @click="submitFormalDeclaration(batch)">提交正式申报</el-button><el-button v-if="['SUBMITTED','ACKNOWLEDGED','RESULT_PENDING'].includes(batch.status)" size="small" plain :loading="actionLoading" @click="recordFormalResult(batch)" > 录入查验结果 </ el -button ></ div >
< div v-if = "batch.results.length" class="declaration-result-list"><div v-for="result in batch.results" :key="result.id" class="declaration-result-row"><span>{{ result.resultType === 'INSPECTION' ? '查验' : result.resultType === 'SUPPLEMENT_REQUIRED' ? '补充资料' : result.resultType === 'RELEASED' ? '放行' : result.resultType }}</span><el-tag size="small" :type="result.resultStatus === 'RESOLVED' ? 'success' : 'warning'">{{ result.resultStatus === 'RESOLVED' ? '已处理' : result.resultStatus === 'OPEN' ? '待处理' : '已取消' }}</el-tag><small>{{ result.resolution || '—' }}</small><el-button v-if="canManage && result.resultStatus === 'OPEN'" link type="primary" size="small" :loading="actionLoading" @click="resolveFormalResult(batch, result)" > 处理 </ el -button ></ div ></ div >
</ article >
</ div >
</ div >
2026-09-10 14:02:37 +08:00
</ template >
2026-09-09 13:00:45 +08:00
< div v-if = "customs.status === 'COMPLETED'" class="departure-state"><strong>国内离仓状态:</strong><el-tag :type="departureState === 'DEPARTED' ? 'success' : 'warning'" >{{ departureState = == ' DEPARTED ' ? ' 已自动离仓 ' : departureState = == ' UNKNOWN ' ? ' 离仓状态尚未读取 ' : ' 报关已完成 , 待其他离仓条件 ' }} </ el -tag ></ div >
2026-08-28 16:54:02 +08:00
</ template >
2026-09-10 14:15:44 +08:00
< el-empty v-else-if = "canList" description="当前装柜计划尚未完成,完成装柜后可处理报关" :image-size="56" />
2026-08-28 16:54:02 +08:00
</ section >
</ template >
< style scoped >
2026-09-27 20:41:39 +08:00
. 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 : 3 px 0 0 ; color : var ( -- cc - 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 : 1 px 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 : 5 px }. customs - metrics { display : flex ; gap : var ( -- admin - space - 6 ); margin - top : var ( -- admin - space - 4 ); padding : var ( -- admin - space - 3 ) var ( -- admin - space - 4 ); background : var ( -- cc - surface - muted ); font - size : var ( -- admin - font - caption )}. customs - metrics strong { margin - left : var ( -- admin - space - 1 ); font - size : var ( -- admin - font - body )}. customs - metrics . danger strong { color : var ( -- el - color - danger )}. compact - table - wrap { margin - top : var ( -- admin - space - 4 )}. compact - table - wrap h3 { margin - bottom : var ( -- admin - space - 2 )}. compact - table { width : 100 % }. document - table th : first - child { width : 270 px }. document - actions ,. file - cell { display : flex ; align - items : center ; gap : var ( -- admin - space - 2 )}. file - cell span { display : block ; max - width : 210 px ; overflow : hidden ; text - overflow : ellipsis ; white - space : nowrap }. readonly - label { color : var ( -- cc - 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 : 1 px solid var ( -- cc - 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 : 3 px 0 0 ; color : var ( -- cc - 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 , 1 fr ) auto ; gap : var ( -- admin - space - 2 ) var ( -- admin - space - 4 ); padding : var ( -- admin - space - 3 ) var ( -- admin - space - 4 ); border : 1 px solid var ( -- cc - border ); background : var ( -- cc - surface - muted )}. declaration - batch - main strong ,. declaration - batch - main span { display : block }. declaration - batch - main span { margin - top : 4 px ; color : var ( -- cc - muted ); font - size : var ( -- admin - font - caption )}. declaration - batch - actions ,. declaration - result - list { grid - column : 1 / - 1 }. declaration - result - list { display : grid ; gap : 5 px ; padding - top : var ( -- admin - space - 2 ); border - top : 1 px solid var ( -- cc - 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 ( -- cc - muted )}
2026-08-28 16:54:02 +08:00
</ style >