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'
2026-10-20 10:11:31 +08:00
import AppDialog from '@/shared/components/AppDialog.vue'
import { containerDialogTitle } from '../containerDialogTitle'
2026-08-28 16:54:02 +08:00
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-28 13:21:59 +08:00
sendContainerCustomsToBroker ,
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-29 04:20:58 +01:00
type ClearanceResultType ,
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-09-29 04:20:58 +01:00
const clearanceDialogVisible = ref ( false )
const clearanceBatch = ref < DeclarationBatch | null >( null )
const clearanceResultType = ref < ClearanceResultType >( 'INSPECTION' )
const clearanceRemark = ref ( '' )
const clearanceResultOptions : Array < { value : ClearanceResultType ; label : string } > = [
{ value : 'INSPECTION' , label : '查验' },
{ value : 'SUPPLEMENT_REQUIRED' , label : '补资料' },
{ value : 'DETAINED' , label : '扣货' },
{ value : 'RELEASED' , label : '放行' },
{ value : 'OTHER' , label : '其他' },
]
2026-08-28 16:54:02 +08:00
2026-09-29 13:50:05 +08:00
function clearanceResultLabel ( value : ClearanceResultType ) {
return clearanceResultOptions . find (( option ) => option . value === value ) ? . label || '未知结果'
}
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-28 13:21:59 +08:00
const brokerSubmission = computed (() => customs . value ? . brokerSubmission || null )
2026-09-26 14:22:45 +08:00
const brokerSubmitted = computed (() => Boolean ( customs . value ? . brokerSubmittedAt ))
2026-09-28 13:21:59 +08:00
const allReceiptsConfirmed = computed (() => Boolean ( customs . value ? . documents . length ) && customs . value ? . documents . every (( document ) => document . receipt && document . receiptStatus === 'CONFIRMED' ) === true )
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-28 09:49:53 +08:00
function receiptHistoryLabel ( document : ContainerCustomsDocument ) {
return ( document . receiptHistory || []). map (( item ) => `v ${ item . version } ` ). join ( '、' ) || '—'
}
2026-09-28 13:21:59 +08:00
function confirmationActionLabel ( action : ContainerCustomsDocument [ 'confirmationHistory' ][ number ][ 'action' ]) {
return action === 'CONFIRM' ? '确认' : '打回'
}
function formatDateTime ( value : string | null | undefined ) {
if ( ! value ) return '—'
const date = new Date ( value )
return Number . isNaN ( date . getTime ()) ? value : date . toLocaleString ( 'zh-CN' , { hour12 : false })
}
2026-09-26 13:28:48 +08:00
function selectReceipt ( documentId : string , upload : UploadFile ) {
2026-09-28 13:21:59 +08:00
if ( ! props . canManage || customs . value ? . status !== 'DECLARING' || ! brokerSubmission . value || brokerSubmitted . value ) 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 )
2026-09-28 13:21:59 +08:00
if ( customs . value ? . status === 'COMPLETED' || customs . value ? . brokerSubmittedAt ) {
2026-09-27 20:41:39 +08:00
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-10-20 10:11:31 +08:00
function customsDialogTitle ( action : string , batch ?: DeclarationBatch | null ) {
const code = batch ? . batchNo ? . trim ()
return code ? ` ${ action } · ${ code } ` : containerDialogTitle ( action , customs . value )
}
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
2026-10-20 10:11:31 +08:00
try { await ElMessageBox . confirm ( '当前柜的资料、回执已确认并已发送报关行执行,确认创建正式申报批次?完税文件仍按报关行完成后回传。' , customsDialogTitle ( '创建正式申报批次' ), { 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 {
2026-10-20 10:11:31 +08:00
const answer = await ElMessageBox . prompt ( '请输入报关单号' , customsDialogTitle ( '提交正式申报' , batch ), { customClass : 'admin-density-confirm' , inputValidator : ( value ) => value . trim () ? true : '请输入报关单号' , confirmButtonText : '确认提交' })
2026-09-27 20:41:39 +08:00
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 }
}
2026-09-29 04:20:58 +01:00
function openFormalResultDialog ( batch : DeclarationBatch ) {
clearanceBatch . value = batch
clearanceResultType . value = 'INSPECTION'
clearanceRemark . value = ''
clearanceDialogVisible . value = true
}
async function recordFormalResult () {
const batch = clearanceBatch . value
if ( ! batch ) return
if ( clearanceResultType . value === 'OTHER' && ! clearanceRemark . value . trim ()) {
ElMessage . warning ( '选择“其他”时请填写备注' )
return
}
actionLoading . value = true ; actionError . value = null
2026-09-27 20:41:39 +08:00
try {
2026-09-29 04:20:58 +01:00
await recordClearanceResult ( batch , clearanceResultType . value , clearanceRemark . value . trim () || undefined )
clearanceDialogVisible . value = false
2026-09-29 13:50:05 +08:00
await load (); ElMessage . success ( '海关结果已记录' )
} catch ( error ) { actionError . value = getApiErrorDetails ( error , '海关结果记录失败' ) }
2026-09-29 04:20:58 +01:00
finally { actionLoading . value = false }
2026-09-27 20:41:39 +08:00
}
async function resolveFormalResult ( batch : DeclarationBatch , result : DeclarationBatch [ 'results' ][ number ]) {
try {
2026-10-20 10:11:31 +08:00
const answer = await ElMessageBox . prompt ( '请输入处理说明' , customsDialogTitle ( '处理海关结果' , batch ), { customClass : 'admin-density-confirm' , inputValue : result . resolution || '' , inputValidator : ( value ) => value . trim () ? true : '请输入处理说明' , confirmButtonText : '确认处理' })
2026-09-27 20:41:39 +08:00
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-10-20 10:11:31 +08:00
await ElMessageBox . confirm ( '开始后将冻结客户当前退税资料版本、关联入仓码覆盖范围及平台买单准备事实,确认开始整柜报关?' , customsDialogTitle ( '开始整柜报关' ), { 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-28 13:21:59 +08:00
if ( ! props . canManage || customs . value ? . status !== 'DECLARING' || ! brokerSubmission . value || 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 () {
2026-09-28 13:21:59 +08:00
if ( ! customs . value || customs . value . status !== 'DECLARING' || ! brokerSubmission . value || brokerSubmitted . value || ! allReceiptsConfirmed . value ) return
2026-10-20 10:11:31 +08:00
try { await ElMessageBox . confirm ( '确认已核对全部回执并把确认结果发送给报关行执行?' , customsDialogTitle ( '发送报关行执行' ), { customClass : 'admin-density-confirm' , type : 'warning' , confirmButtonText : '确认发送' }) } catch { return }
2026-09-26 13:28:48 +08:00
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-09-28 13:21:59 +08:00
async function sendToBroker () {
if ( ! customs . value || customs . value . status !== 'DECLARING' || brokerSubmission . value || brokerSubmitted . value ) return
2026-10-20 10:11:31 +08:00
try { await ElMessageBox . confirm ( '确认将当前冻结的退税报关资料首次发送给报关行?发送后由报关行返回回执,资料版本和关联范围按当前快照处理。' , customsDialogTitle ( '首次发送报关行' ), { customClass : 'admin-density-confirm' , type : 'warning' , confirmButtonText : '确认发送' }) } catch { return }
2026-09-28 13:21:59 +08:00
actionLoading . value = true ; actionError . value = null
try { customs . value = await sendContainerCustomsToBroker ( 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-10-20 10:11:31 +08:00
try { await ElMessageBox . confirm ( '确认全买单柜报关已经完成?报关完成不等同于国内离仓。' , customsDialogTitle ( '完成全买单柜报关' ), { 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" >
2026-10-20 15:48:23 +08:00
< div >< h2 > 整柜报关 </ h2 ></ div >
2026-08-28 16:54:02 +08:00
< 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-28 13:21:59 +08:00
< el-button v-if = "canManage && customs.hasActualCargo && customs.status === 'DECLARING' && !brokerSubmission" type="primary" :loading="actionLoading" @click="sendToBroker" > 首次发送报关行 </ el -button >
< el-button v-if = "canManage && customs.hasActualCargo && customs.status === 'DECLARING' && brokerSubmission && !brokerSubmitted" type="primary" plain :disabled="!allReceiptsConfirmed" :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-10-09 11:11:20 +08:00
< dl class = "density-fields customs-summary" aria -label =" 报关概况 " >
< div >< dt > 实际箱数 </ dt >< dd >{{ customs . actualBoxCount }}</ dd ></ div >< div >< dt > 退税总票 </ dt >< dd >{{ customs . taxRefundCount }}</ dd ></ div >< div >< dt > 已覆盖 </ dt >< dd >{{ customs . coveredTaxRefundCount }}</ dd ></ div >< div >< dt > 缺失 </ dt >< dd : class = "{ 'coverage-missing': missingCoverageCount }" >{{ missingCoverageCount }}</ dd ></ div >
< template v-if = "brokerSubmission" >
< div class = "submission-batch" >< dt > 首次发送批次 </ dt >< dd >{{ brokerSubmission . batchNo }}</ dd ></ div >< div >< dt > 发送状态 </ dt >< dd >{{ brokerSubmission . status === 'SENT' ? '已发送' : brokerSubmission . status }}</ dd ></ div >< div >< dt > 资料版本 </ dt >< dd > v {{ brokerSubmission . sourceVersion }}</ dd ></ div >
< div class = "submission-batch" >< dt > 发送人 </ dt >< dd >{{ brokerSubmission . sentByName || '—' }}</ dd ></ div >< div class = "submission-batch" >< dt > 发送时间 </ dt >< dd >{{ formatDateTime ( brokerSubmission . sentAt ) }}</ dd ></ div >
</ template >
</ dl >
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-28 13:21:59 +08:00
< 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 >
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-28 13:21:59 +08:00
< td >{{ document . inboundCodes . join ( '、' ) || '—' }}</ td >< td >{{ brokerSubmitted ? '已发送报关行执行' : brokerSubmission ? '已首次发送,等待回执确认' : '待首次发送报关行' }}</ 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><small v-if="document.receiptHistory?.length" class="receipt-history-note">回执历史:{{ receiptHistoryLabel(document) }}</small><small v-for="history in document.confirmationHistory" :key="history.id" class="confirmation-history-note">{{ confirmationActionLabel(history.action) }} · {{ formatDateTime(history.createdAt) }} · {{ history.operatorName || '—' }}<span v-if="history.reason" > · {{ history.reason }}</ span ></ small ></ 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" >
2026-10-20 15:48:23 +08:00
< div class = "declaration-batches-heading" >< div >< h3 > 正式申报与海关结果 </ h3 ></ 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 >
2026-09-29 04:20:58 +01:00
< 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="openFormalResultDialog(batch)" > 标记海关异常 / 结果 </ el -button ></ div >
2026-09-29 13:50:05 +08:00
< 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>{{ clearanceResultLabel(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 >
2026-09-27 20:41:39 +08:00
</ 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-10-20 10:11:31 +08:00
< AppDialog v-model = "clearanceDialogVisible" class="admin-density-dialog" :title="customsDialogTitle('标记海关异常/结果', clearanceBatch)" width="460px" :close-on-click-modal="false" :lock-while-submitting="false" :confirm-loading="actionLoading" confirm-text="确认标记" @confirm="recordFormalResult" >
< el-form label -position =" top " class = "density-form density-form--flush-end" >
2026-09-29 04:20:58 +01:00
< el-form-item label = "选择理由" required >
2026-10-20 10:11:31 +08:00
< el-select v-model = "clearanceResultType" placeholder="请选择理由" >
2026-09-29 04:20:58 +01:00
< el-option v-for = "option in clearanceResultOptions" :key="option.value" :label="option.label" :value="option.value" />
</ el-select >
</ el-form-item >
< el-form-item label = "备注" : required = "clearanceResultType === 'OTHER'" >
< el-input v-model = "clearanceRemark" type="textarea" :rows="4" maxlength="1000" show-word-limit :placeholder="clearanceResultType === 'OTHER' ? '请填写具体情况' : '可填写短信内容或人工处理说明(可选)'" />
</ el-form-item >
</ el-form >
2026-10-20 10:11:31 +08:00
</ AppDialog >
2026-08-28 16:54:02 +08:00
</ section >
</ template >
< style scoped >
2026-10-20 10:11:31 +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 - summary { margin - top : var ( -- admin - space - 4 ) ! important }. customs - summary . submission - batch { grid - column : span 2 }. coverage - missing { color : var ( -- el - color - danger ) ! important }. compact - table - wrap { margin - top : var ( -- admin - space - 5 ); overflow - x : auto }. confirmation - history - note ,. receipt - history - note { display : block ; margin - top : var ( -- admin - space - 2 ); color : var ( -- cc - muted )}. 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 >