2026-08-21 18:16:42 +08:00
< script setup lang = "ts" >
2026-09-14 20:03:27 +08:00
import '@/styles/adminDensity.css'
2026-10-19 17:56:31 +08:00
import { computed , onBeforeUnmount , ref , watch } from 'vue'
2026-08-21 18:16:42 +08:00
import { useRoute , useRouter } from 'vue-router'
2026-10-20 10:17:17 +08:00
import { inquiryReturnTarget , vehicleStatusLabels } from '@/modules/overseas-master-data'
2026-10-18 16:37:37 +08:00
import { useAccessPreviewStore } from '@/core/access/accessPreview'
2026-08-21 18:16:42 +08:00
import { getApiErrorDetails , type ApiErrorDetails } from '@/shared/api/http'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import DataTable , { type DataTableColumn } from '@/shared/components/DataTable.vue'
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
2026-10-19 17:56:31 +08:00
import { getDispatch } from '../api/dispatches'
import type { DispatchAction , DispatchDetail } from '../dispatchContracts'
import { stageLabel } from '../dispatchContracts'
import { canDispatchAction , dispatchActionLabels , displayDispatchValue as text } from '../dispatchPresentation'
import DispatchWorkflowDialog from '../components/DispatchWorkflowDialog.vue'
import DispatchEvidenceFiles from '../components/DispatchEvidenceFiles.vue'
2026-08-21 18:16:42 +08:00
import OverseasDetailShell from '../components/OverseasDetailShell.vue'
2026-10-19 17:56:31 +08:00
import { formatOverseasDateTime as time } from '../lastMilePresentation'
2026-10-18 16:37:37 +08:00
import { overseasWarehousePermissions as permission } from '../permissions'
2026-08-21 18:16:42 +08:00
2026-10-19 17:56:31 +08:00
const route = useRoute (), router = useRouter (), access = useAccessPreviewStore ()
2026-08-21 18:16:42 +08:00
const dispatchNo = computed (() => String ( route . params . dispatchNo || '' ))
2026-10-19 17:56:31 +08:00
const detail = ref < DispatchDetail | null >( null ), activeTab = ref ( 'vehicle' ), loading = ref ( false ), loadError = ref < ApiErrorDetails | null >( null )
2026-10-20 11:36:15 +08:00
const rejectionError = ref < ApiErrorDetails | null >( null )
2026-10-19 17:56:31 +08:00
const operation = ref < 'ROUTE' | undefined > ()
const action = ref < DispatchAction >( 'EDIT' ), actionOpen = ref ( false )
const detailActions : DispatchAction [] = [ 'EDIT' , 'ADD_CARGO' , 'REMOVE_CARGO' , 'SUBMIT_REVIEW' , 'DISPATCH_REVIEW' , 'LOADING_REVIEW' , 'RETURN_TO_REVIEW' , 'LOADING_COMPLETE' , 'ARRANGE_DELIVERY' , 'DEPART' , 'CANCEL' ]
const visibleActions = computed (() => detailActions . filter ( value => canDispatchAction ( detail . value ? . actions , value , access . can )))
2026-10-20 10:17:17 +08:00
const vehicleStatus = computed (() => {
const status = detail . value ? . vehicleInfo . vehicle ? . vehicleStatus
return status && Object . hasOwn ( vehicleStatusLabels , status ) ? vehicleStatusLabels [ status as keyof typeof vehicleStatusLabels ] : '—'
})
2026-10-19 17:56:31 +08:00
const vehicleFields = computed (() => [
[ '车牌号' , text ( detail . value ? . vehicleInfo . vehicle ? . plate )], [ '司机' , text ( detail . value ? . vehicleInfo . driver ? . name )],
2026-10-20 10:17:17 +08:00
[ '联系电话' , text ( detail . value ? . vehicleInfo . driver ? . phone )], [ '拖车公司名称' , text ( detail . value ? . vehicleInfo . vehicle ? . carrierName )], [ '车辆状态' , vehicleStatus . value ],
2026-08-21 18:16:42 +08:00
])
2026-10-19 17:56:31 +08:00
const dispatchFields = computed (() => [
[ '路线' , text ( detail . value ? . dispatchInfo . route )], [ '发车时间' , time ( detail . value ? . dispatchInfo . departedAt )],
[ '预计到达时间' , time ( detail . value ? . dispatchInfo . estimatedArrivalAt )],
[ 'FBA预约' , detail . value ? . dispatchInfo . fbaAppointments . map ( item => [ item . destinationWarehouseCode , item . appointmentNo , item . status ]. filter ( Boolean ). join ( ' · ' )). join ( '; ' ) || '—' ],
2026-08-21 18:16:42 +08:00
])
2026-10-19 17:56:31 +08:00
const documentFields = computed (() => [
[ '排车单号' , text ( detail . value ? . documents . dispatchNo )], [ '创建时间' , time ( detail . value ? . documents . createdAt )],
[ '更新时间' , time ( detail . value ? . documents . updatedAt )], [ '备注' , text ( detail . value ? . documents . remark )],
2026-08-21 18:16:42 +08:00
])
2026-10-19 17:56:31 +08:00
const relatedOrders = computed (() => detail . value ? . orderDetails || [])
2026-08-21 18:16:42 +08:00
const orderColumns : DataTableColumn [] = [
2026-10-19 17:56:31 +08:00
{ key : 'customerName' , prop : 'customerName' , label : '客户' , minWidth : 140 },
2026-08-21 18:16:42 +08:00
{ key : 'orderNo' , prop : 'orderNo' , label : '订单号' , minWidth : 210 },
2026-10-20 10:11:31 +08:00
{ key : 'sourceBusinessNo' , prop : 'sourceBusinessNo' , label : '来源业务号' , minWidth : 180 },
2026-10-19 17:56:31 +08:00
{ key : 'inboundCode' , prop : 'inboundCode' , label : '入仓码' , minWidth : 170 },
{ key : 'weight' , prop : 'grossWeightKg' , label : '重量(kg)' , minWidth : 130 , align : 'right' },
2026-08-21 18:16:42 +08:00
{ key : 'palletCount' , prop : 'palletCount' , label : '托数' , minWidth : 100 , align : 'right' },
2026-10-19 17:56:31 +08:00
{ key : 'fbaWarehouse' , prop : 'destinationFbaWarehouse' , label : '目的FBA仓' , minWidth : 150 },
{ key : 'volume' , prop : 'volumeCbm' , label : '体积(m³)' , minWidth : 130 , align : 'right' },
2026-08-21 18:16:42 +08:00
{ key : 'containerNo' , prop : 'containerNo' , label : '集装箱号' , minWidth : 170 },
]
2026-10-19 17:56:31 +08:00
let generation = 0
2026-08-21 18:16:42 +08:00
async function load () {
2026-10-19 17:56:31 +08:00
const current = ++ generation , no = dispatchNo . value
detail . value = null ; loadError . value = null
if ( ! no ) { loadError . value = { message : '详情缺少排车单号,请从排车列表重新进入' }; return }
2026-08-21 18:16:42 +08:00
loading . value = true
2026-10-19 17:56:31 +08:00
try { const result = await getDispatch ( no ); if ( current === generation ) { if ( result . dispatchNo !== no ) throw new Error ( '返回排车单与当前业务号不一致' ); detail . value = result } }
catch ( error ) { if ( current === generation ) loadError . value = getApiErrorDetails ( error , '排车详情加载失败,请重试' ) }
finally { if ( current === generation ) loading . value = false }
2026-08-21 18:16:42 +08:00
}
2026-10-11 15:33:09 +08:00
function back () { void router . push ( inquiryReturnTarget ( route . query ) || { name : 'overseas-dispatches' }) }
2026-10-20 11:36:15 +08:00
function openAction ( value : DispatchAction , requestedOperation ?: 'ROUTE' ) { if ( ! canDispatchAction ( detail . value ? . actions , value , access . can )) return ; rejectionError . value = null ; operation . value = requestedOperation ; action . value = value ; actionOpen . value = true }
async function handleRejection ( details : ApiErrorDetails ) { rejectionError . value = details ; await load () }
async function handleSaved () { rejectionError . value = null ; await load () }
2026-10-19 17:56:31 +08:00
watch ( dispatchNo , load , { immediate : true })
onBeforeUnmount (() => { generation ++ })
2026-08-21 18:16:42 +08:00
</ script >
< template >
2026-10-19 17:56:31 +08:00
< OverseasDetailShell class = "admin-density" dense title = "排车详情" reference -label =" 排车单号 " : reference -value =" detail ? .dispatchNo || dispatchNo " : back -text =" inquiryReturnTarget ( route.query ) ? ' 返回原拼车登记 ' : undefined " : back -label =" inquiryReturnTarget ( route.query ) ? ' 返回原拼车登记 ' : ' 返回排车列表 '" domain = "排车详情" :pending = "false" : status -text =" detail ? detail.status === ' CANCELLED ' ? ' 已取消 ' : stageLabel ( detail.stage ) : ' 加载中 '" @back ="back" >
2026-10-20 11:36:15 +08:00
< LoadErrorState v-if = "rejectionError" title="排车操作未完成" :message="rejectionError.message" :show-retry="false"><template #details><ApiErrorMeta :details="rejectionError" /></ template ></ LoadErrorState >
2026-08-21 18:16:42 +08:00
< LoadErrorState v-if = "loadError" title="排车详情加载失败" :message="loadError.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="loadError" /></ template ></ LoadErrorState >
< div v-else v-loading = "loading" class="dispatch-detail" :aria-busy="loading" >
2026-10-20 10:17:17 +08:00
< section v-if = "detail" class="detail-section" aria-label="排车操作"><div class="density-actions"><el-button v-for="value in visibleActions" :key="value" class="density-action" @click="openAction(value)">{{ dispatchActionLabels[value] }}</el-button><el-button v-if="canDispatchAction(detail?.actions, 'EDIT', access.can)" @click="openAction('EDIT', 'ROUTE')" > 调整卸货顺序 </ el -button ></ div ></ section >
2026-10-19 17:56:31 +08:00
< el-tabs v-model = "activeTab" class="density-element-tabs density-contained-tabs" >
< el-tab-pane name = "vehicle" >< template # label >< span class = "density-tab-icon" aria -hidden =" true " > 🚚 </ span >< span > 车辆信息 </ span ></ template >< dl class = "detail-fields density-fields" >< div v-for = "field in vehicleFields" :key="field[0]"><dt>{{ field[0] }}</dt><dd>{{ field[1] }}</dd></div></dl><DispatchEvidenceFiles :model-value="detail?.vehicleInfo.vehicle?.evidenceFileIds || []" :dispatch-no="dispatchNo" kind="VEHICLE" readonly /><DispatchEvidenceFiles :model-value="detail?.vehicleInfo.driver?.evidence?.fileIds || []" :dispatch-no="dispatchNo" kind="DRIVER" readonly /></ el-tab-pane >
< el-tab-pane name = "dispatch" >< template # label >< span class = "density-tab-icon" aria -hidden =" true " > 📋 </ span >< span > 排车信息 </ span ></ template >< dl class = "detail-fields density-fields" >< div v-for = "field in dispatchFields" :key="field[0]" >< dt >{{ field [ 0 ] }}</ dt >< dd >{{ field [ 1 ] }}</ dd ></ div ></ dl ></ el-tab-pane >
< el-tab-pane name = "documents" >< template # label >< span class = "density-tab-icon" aria -hidden =" true " > 📝 </ span >< span > 单据信息 </ span ></ template >
< dl class = "detail-fields density-fields" >< div v-for = "field in documentFields" :key="field[0]"><dt>{{ field[0] }}</dt><dd>{{ field[1] }}</dd></div><div class="linked-documents"><dt>关联派送单</dt><dd><template v-for="delivery in detail?.documents.deliveries || []" :key="delivery.deliveryNo"><el-button v-if="access.can(permission.deliveryList)" link type="primary" @click="router.push({ name: 'overseas-delivery-order-detail', params: { deliveryNo: delivery.deliveryNo } })">{{ delivery.deliveryNo }}</el-button><span v-else>{{ text(delivery.deliveryNo) }}</span></template><span v-if="!detail?.documents.deliveries.length" > — </ span ></ dd ></ div ></ dl >
< div class = "density-actions" >< el-button v-for = "outbound in detail?.documents.outbounds || []" v-show="access.can(permission.outboundList)" :key="outbound.outboundNo" link @click="router.push({ name: 'overseas-outbounds', query: { documentNo: outbound.outboundNo } })" > 查看出库 {{ outbound.outboundNo }} </ el -button ></ div >
</ el-tab-pane >
< el-tab-pane name = "orders" >< template # label >< span class = "density-tab-icon" aria -hidden =" true " > 📦 </ span >< span > 关联订单明细 </ span ></ template >< DataTable :data = "relatedOrders" :columns = "orderColumns" column -settings-mode =" hide " row -key =" sourceKey " empty -text =" 暂无关联订单明细 " /></ el-tab-pane >
</ el-tabs >
2026-08-21 18:16:42 +08:00
</ div >
2026-10-20 11:36:15 +08:00
< DispatchWorkflowDialog v-model = "actionOpen" :dispatch-no="dispatchNo" :action="action" :operation="operation" @saved="handleSaved" @rejected="handleRejection" />
2026-08-21 18:16:42 +08:00
</ OverseasDetailShell >
</ template >
< style scoped >
2026-09-14 20:03:27 +08:00
. detail - section { min - width : 0 ; padding : var ( -- admin - space - 5 ); }
. dispatch - detail { min - width : 0 ; min - height : 0 ; }
2026-10-19 17:56:31 +08:00
. linked - documents { grid - column : 1 / - 1 ; }
. linked - documents dd { display : flex ; flex - wrap : wrap ; gap : var ( -- admin - space - 4 ); overflow - wrap : anywhere ; }
@ media ( max - width : 960 px ) { . detail - fields { grid - template - columns : 1 fr ; } . detail - fields > div { grid - template - columns : 132 px minmax ( 0 , 1 fr ); } }
2026-08-21 18:16:42 +08:00
</ style >