2026-08-21 18:16:42 +08:00
< script setup lang = "ts" >
2026-10-11 15:33:09 +08:00
import { inquiryReturnTarget } from '@/modules/overseas-master-data'
2026-10-03 04:35:55 +08:00
import { physicalWeightKg } from '../dispatchPhysicalWeight'
2026-09-14 20:03:27 +08:00
import '@/styles/adminDensity.css'
2026-08-21 18:16:42 +08:00
import { computed , onMounted , ref } from 'vue'
import { useRoute , useRouter } from 'vue-router'
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'
import { getDispatch , type DispatchItemDto , type DispatchSummaryDto } from '../api/dispatches'
import OverseasDetailShell from '../components/OverseasDetailShell.vue'
import { formatOverseasDateTime } from '../lastMilePresentation'
2026-10-18 16:37:37 +08:00
import { overseasWarehousePermissions as permission } from '../permissions'
2026-08-21 18:16:42 +08:00
interface DetailField { label : string ; value : string | number }
interface RelatedOrderRow {
orderNo : string
2026-10-03 04:35:55 +08:00
weight : number | null
2026-08-21 18:16:42 +08:00
palletCount : number
fbaWarehouse : string
volume : number
containerNo : string
}
const route = useRoute ()
const router = useRouter ()
2026-10-18 16:37:37 +08:00
const access = useAccessPreviewStore ()
2026-08-21 18:16:42 +08:00
const dispatchNo = computed (() => String ( route . params . dispatchNo || '' ))
const detail = ref < DispatchSummaryDto | null >( null )
const loading = ref ( false )
const loadError = ref < ApiErrorDetails | null >( null )
2026-10-18 16:37:37 +08:00
const actionBlockReasons = computed (() => {
const actions = detail . value ? . actions
if ( ! actions ) return detail . value ? [ '操作资格暂未返回,请刷新详情' ] : []
return [... new Set ([ actions . editBlockReason , actions . cancelBlockReason , actions . removePalletBlockReason , actions . transferBlockReason ]. filter ( Boolean ))]
})
function openTrip () {
if ( ! detail . value ? . workflow || ! access . can ( 'platform:fleet:trip:list' )) return
void router . push ({ name : 'overseas-master-data-trip-detail' , params : { tripNo : detail . value . workflow . tripNo }, query : { tab : 'overview' } })
}
function openOutbound () {
if ( ! detail . value ? . actions ? . outboundNo || ! access . can ( permission . outboundList )) return
void router . push ({ name : 'overseas-outbounds' , query : { documentNo : detail . value . actions . outboundNo } })
}
2026-08-21 18:16:42 +08:00
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 [] >(() => {
2026-10-03 04:35:55 +08:00
const grouped = new Map < string , { pallets : Set < string >; destinations : Set < string >; weight : number | null ; volume : number } > ()
2026-08-21 18:16:42 +08:00
for ( const item of detail . value ? . items || []) {
for ( const orderNo of orderReferences ( item )) {
2026-09-18 14:58:40 +08:00
const current = grouped . get ( orderNo ) || { pallets : new Set < string >(), destinations : new Set < string >(), weight : 0 , volume : 0 }
2026-08-21 18:16:42 +08:00
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 )
2026-10-03 04:35:55 +08:00
const itemWeight = physicalWeightKg ( item . grossWeightKg )
current . weight = current . weight === null || itemWeight === null ? null : current . weight + itemWeight
2026-08-21 18:16:42 +08:00
current . volume += Number ( item . volumeCbm || 0 )
grouped . set ( orderNo , current )
}
}
return [... grouped . entries ()]. map (([ orderNo , value ]) => ({
orderNo ,
2026-10-03 04:35:55 +08:00
weight : value . weight === null ? null : Number ( value . weight . toFixed ( 3 )),
2026-08-21 18:16:42 +08:00
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-09-18 14:58:40 +08:00
{ key : 'weight' , prop : 'weight' , label : '重量(kg)' , 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 }
}
2026-10-11 15:33:09 +08:00
function back () { void router . push ( inquiryReturnTarget ( route . query ) || { name : 'overseas-dispatches' }) }
2026-08-21 18:16:42 +08:00
onMounted ( load )
</ script >
< template >
2026-10-11 16:11:58 +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 ? dispatchStatus ( detail.status ) : ' 加载中 '" @back ="back" >
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-18 16:37:37 +08:00
< section v-if = "detail" class="detail-section" aria-label="排车操作提示"><p v-for="reason in actionBlockReasons" :key="String(reason)">{{ reason }}</p><div class="density-actions"><el-button v-if="detail.workflow && access.can('platform:fleet:trip:list')" class="density-action" @click="openTrip">处理原车次 {{ detail.workflow.tripNo }}</el-button><el-button v-if="detail.actions?.outboundNo && access.can(permission.outboundList)" class="density-action" @click="openOutbound" > 查看原出库 {{ detail.actions.outboundNo }} </ el -button ></ div ></ section >
2026-08-21 18:16:42 +08:00
< div class = "summary-grid" >
2026-09-14 20:03:27 +08:00
< section class = "detail-section" aria -labelledby =" dispatch -vehicle-heading " >< h2 id = "dispatch-vehicle-heading" > 车辆信息 </ h2 >< dl class = "detail-fields density-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 density-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 density-fields" >< div v-for = "field in documentFields" :key="field.label" >< dt >{{ field . label }}</ dt >< dd >{{ field . value }}</ dd ></ div ></ dl ></ section >
2026-08-21 18:16:42 +08:00
</ 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 >
2026-09-14 20:03:27 +08:00
. detail - section { min - width : 0 ; padding : var ( -- admin - space - 5 ); }
2026-08-21 18:16:42 +08:00
. 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-09-14 20:03:27 +08:00
. order - section h2 { margin - right : var ( -- admin - space - 1 ); margin - left : var ( -- admin - space - 1 ); }
2026-08-21 18:16:42 +08:00
@ 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 ); }
}
2026-09-14 20:03:27 +08:00
. dispatch - detail { min - width : 0 ; min - height : 0 ; }
. summary - grid { display : grid ; grid - template - columns : minmax ( 0 , 1 fr ); border : 0 ; border - bottom : 0 ; background : # fff ; }
. summary - grid > . detail - section { border - right : 0 ; border - bottom : 1 px solid var ( -- cc - border ); }
. detail - section h2 { margin : 0 0 var ( -- admin - space - 5 ); color : var ( -- cc - text ); font - size : var ( -- admin - font - section ); }
. order - section { overflow : hidden ; border : 0 ; border - top : 0 ; background : # fff ; }
2026-08-21 18:16:42 +08:00
</ style >