2026-08-21 18:16:42 +08:00
< script setup lang = "ts" >
import { computed , onMounted , ref } from 'vue'
import { useRoute , useRouter } from 'vue-router'
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'
import { getDispatch , type DispatchItemDto , type DispatchSummaryDto } from '../api/dispatches'
import OverseasDetailShell from '../components/OverseasDetailShell.vue'
import { formatOverseasDateTime } from '../lastMilePresentation'
interface DetailField { label : string ; value : string | number }
interface RelatedOrderRow {
orderNo : string
weight : string
palletCount : number
fbaWarehouse : string
volume : number
containerNo : string
}
const route = useRoute ()
const router = useRouter ()
const dispatchNo = computed (() => String ( route . params . dispatchNo || '' ))
const detail = ref < DispatchSummaryDto | null >( null )
const loading = ref ( false )
const loadError = ref < ApiErrorDetails | null >( null )
const dispatchStatusLabels : Record < string , string > = {
PENDING_DEPARTURE : '待发车' ,
PICKING : '拣货中' ,
READY_TO_DEPART : '待发车(拣货完成)' ,
PARTIAL_READY_TO_DEPART : '待发车(部分完成)' ,
DEPARTED : '已发车' ,
PARTIAL_DEPARTED : '已发车(部分完成)' ,
CANCELLED : '已取消' ,
}
const appointmentStatusLabels : Record < string , string > = {
PENDING : '待预约' ,
BOOKED : '已预约' ,
RESERVED : '已预约' ,
CONFIRMED : '已确认' ,
CANCELLED : '已取消' ,
}
function text ( value : unknown ) { return String ( value ?? '' ). trim () || '—' }
function dispatchStatus ( value : unknown ) {
const code = String ( value ?? '' ). trim ()
return dispatchStatusLabels [ code ] || text ( code )
}
function appointmentStatus ( value : unknown ) {
const code = String ( value ?? '' ). trim ()
return appointmentStatusLabels [ code ] || text ( code )
}
function orderReferences ( item : DispatchItemDto ) {
const references = String ( item . orderRefs || '' ). split ( /[,, ;; \s]+/ ). map (( value ) => value . trim ()). filter ( Boolean )
return references . length ? references : [ text ( item . orderNo )]
}
const vehicleFields = computed < DetailField [] >(() => [
{ label : '车牌号' , value : text ( detail . value ? . vehiclePlate ) },
{ label : '司机' , value : text ( detail . value ? . driverName ) },
{ label : '联系电话' , value : text ( detail . value ? . driverPhone ) },
{ label : '拖车公司名称' , value : text ( detail . value ? . carrierName ) },
{ label : '车辆状态' , value : dispatchStatus ( detail . value ? . status ) },
])
const dispatchFields = computed < DetailField [] >(() => [
{ label : '路线' , value : text ( detail . value ? . route ) },
{ label : '发车时间' , value : formatOverseasDateTime ( detail . value ? . departAt ) },
{ label : '预计到达时间' , value : formatOverseasDateTime ( detail . value ? . estimatedArrivalAt ) },
{ label : 'FBA预约' , value : appointmentStatus ( detail . value ? . fbaAppointmentStatus ) },
{ label : '车辆预约' , value : appointmentStatus ( detail . value ? . vehicleAppointmentStatus ) },
])
const documentFields = computed < DetailField [] >(() => [
{ label : '排车单号' , value : text ( detail . value ? . dispatchNo || dispatchNo . value ) },
{ label : '关联订单' , value : text ( detail . value ? . orderNos ) },
{ label : '创建时间' , value : formatOverseasDateTime ( detail . value ? . createdAt ) },
{ label : '更新时间' , value : '—' },
{ label : '备注' , value : '—' },
...( detail . value ? . partialCompletionReason
? [{ label : '部分完成原因' , value : text ( detail . value . partialCompletionReason ) }]
: []),
])
const relatedOrders = computed < RelatedOrderRow [] >(() => {
const grouped = new Map < string , { pallets : Set < string >; destinations : Set < string >; volume : number } > ()
for ( const item of detail . value ? . items || []) {
for ( const orderNo of orderReferences ( item )) {
const current = grouped . get ( orderNo ) || { pallets : new Set < string >(), destinations : new Set < string >(), volume : 0 }
current . pallets . add ( String ( item . warehousePalletId || item . planPalletId || item . id ))
const destination = text ( item . fbaWarehouseCode || item . destinationWarehouseCode || item . destinationWarehouseName )
if ( destination !== '—' ) current . destinations . add ( destination )
current . volume += Number ( item . volumeCbm || 0 )
grouped . set ( orderNo , current )
}
}
return [... grouped . entries ()]. map (([ orderNo , value ]) => ({
orderNo ,
weight : '—' ,
palletCount : value . pallets . size ,
fbaWarehouse : [... value . destinations ]. join ( '、' ) || '—' ,
volume : Number ( value . volume . toFixed ( 3 )),
containerNo : '—' ,
}))
})
const orderColumns : DataTableColumn [] = [
{ key : 'orderNo' , prop : 'orderNo' , label : '订单号' , minWidth : 210 },
2026-08-25 10:34:27 +08:00
{ key : 'weight' , prop : 'weight' , label : '重量(lb)' , minWidth : 130 , align : 'right' },
2026-08-21 18:16:42 +08:00
{ key : 'palletCount' , prop : 'palletCount' , label : '托数' , minWidth : 100 , align : 'right' },
{ key : 'fbaWarehouse' , prop : 'fbaWarehouse' , label : '目的FBA仓' , minWidth : 150 },
{ key : 'volume' , prop : 'volume' , label : '体积(m³)' , minWidth : 130 , align : 'right' },
{ key : 'containerNo' , prop : 'containerNo' , label : '集装箱号' , minWidth : 170 },
]
async function load () {
if ( ! dispatchNo . value ) {
loadError . value = { message : '详情缺少排车单号,请从排车列表重新进入' }
return
}
loading . value = true
loadError . value = null
try { detail . value = await getDispatch ( dispatchNo . value ) }
catch ( error ) { loadError . value = getApiErrorDetails ( error , '排车详情加载失败,请重试' ) }
finally { loading . value = false }
}
function back () { void router . push ({ name : 'overseas-dispatches' }) }
onMounted ( load )
</ script >
< template >
< OverseasDetailShell title = "排车详情" reference -label =" 排车单号 " : reference -value =" detail ? .dispatchNo || dispatchNo " back -label =" 返回排车列表 " domain = "排车详情" :pending = "false" : status -text =" detail ? dispatchStatus ( detail.status ) : ' 加载中 '" @back ="back" >
< 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" >
< div class = "summary-grid" >
< section class = "detail-section" aria -labelledby =" dispatch -vehicle-heading " >< h2 id = "dispatch-vehicle-heading" > 车辆信息 </ h2 >< dl class = "detail-fields" >< div v-for = "field in vehicleFields" :key="field.label" >< dt >{{ field . label }}</ dt >< dd >{{ field . value }}</ dd ></ div ></ dl ></ section >
< section class = "detail-section" aria -labelledby =" dispatch -info-heading " >< h2 id = "dispatch-info-heading" > 排车信息 </ h2 >< dl class = "detail-fields" >< div v-for = "field in dispatchFields" :key="field.label" >< dt >{{ field . label }}</ dt >< dd >{{ field . value }}</ dd ></ div ></ dl ></ section >
< section class = "detail-section document-section" aria -labelledby =" dispatch -document-heading " >< h2 id = "dispatch-document-heading" > 单据信息 </ h2 >< dl class = "detail-fields" >< div v-for = "field in documentFields" :key="field.label" >< dt >{{ field . label }}</ dt >< dd >{{ field . value }}</ dd ></ div ></ dl ></ section >
</ div >
< section class = "detail-section order-section" aria -labelledby =" dispatch -orders-heading " >< h2 id = "dispatch-orders-heading" > 关联订单明细 </ h2 >< DataTable :data = "relatedOrders" :columns = "orderColumns" column -settings-mode =" hide " row -key =" orderNo " empty -text =" 暂无关联订单明细 " /></ section >
</ div >
</ OverseasDetailShell >
</ template >
< style scoped >
. dispatch - detail { min - width : 0 ; min - height : 260 px ; }
. summary - grid { display : grid ; grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr )); border : 1 px solid var ( -- cc - border ); border - bottom : 0 ; background : # fff ; }
. detail - section { min - width : 0 ; padding : 18 px ; }
. summary - grid > . detail - section { border - right : 1 px solid var ( -- cc - border ); border - bottom : 1 px solid var ( -- cc - border ); }
. summary - grid > . detail - section : nth - child ( 2 ) { border - right : 0 ; }
2026-09-02 14:39:34 +08:00
. document - section { grid - column : 1 / - 1 ; }
2026-08-21 18:16:42 +08:00
. detail - section h2 { margin : 0 0 14 px ; color : var ( -- el - color - primary ); font - size : 16 px ; }
2026-09-02 14:39:34 +08:00
. detail - fields { display : grid ; grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr )); margin : 0 ; border - top : 1 px solid var ( -- cc - border ); }
. document - section . detail - fields { grid - template - columns : repeat ( 3 , minmax ( 0 , 1 fr )); }
2026-08-21 18:16:42 +08:00
. detail - fields > div { display : grid ; min - height : 48 px ; grid - template - columns : 180 px minmax ( 0 , 1 fr ); border - bottom : 1 px solid var ( -- cc - border ); }
. detail - fields dt , . detail - fields dd { display : flex ; min - width : 0 ; align - items : center ; padding : 9 px 12 px ; }
. detail - fields dt { color : var ( -- cc - muted ); }
. detail - fields dd { margin : 0 ; color : var ( -- cc - text ); font - weight : 600 ; overflow - wrap : anywhere ; }
. order - section { overflow : hidden ; border : 1 px solid var ( -- cc - border ); border - top : 0 ; background : # fff ; }
. order - section h2 { margin - right : 4 px ; margin - left : 4 px ; }
@ media ( max - width : 960 px ) {
. summary - grid { grid - template - columns : 1 fr ; }
. summary - grid > . detail - section , . summary - grid > . detail - section : nth - child ( 2 ) { border - right : 0 ; }
. document - section { grid - column : auto ; }
2026-09-02 14:39:34 +08:00
. detail - fields , . document - section . detail - fields { grid - template - columns : 1 fr ; }
2026-08-21 18:16:42 +08:00
. detail - fields > div { grid - template - columns : 132 px minmax ( 0 , 1 fr ); }
}
</ style >