2026-08-19 16:45:51 +08:00
< script setup lang = "ts" >
2026-08-20 09:10:36 +08:00
import { computed , onMounted , ref } from 'vue'
2026-08-19 16:45:51 +08:00
import { useRoute , useRouter } from 'vue-router'
2026-08-20 09:10:36 +08:00
import { getApiErrorDetails , type ApiErrorDetails } from '@/shared/api/http'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
2026-08-19 16:45:51 +08:00
import DataTable , { type DataTableColumn } from '@/shared/components/DataTable.vue'
2026-08-20 09:10:36 +08:00
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import { getLastMileOrder , type LastMileOrderDetailDto } from '../api/lastMileOrders'
import { formatOverseasDateTime , lastMileOrderTypeLabel , lastMileStatusLabel } from '../lastMilePresentation'
2026-08-19 16:45:51 +08:00
import OverseasDetailShell from '../components/OverseasDetailShell.vue'
2026-08-20 09:10:36 +08:00
interface DetailField { label : string ; value : string | number }
interface DocumentRow { type : string ; number : string ; createdAt : string ; quote : string }
interface CargoRow { id : string ; sku : string ; productName : string ; pallets : number ; cartons : number ; quantity : number ; weight : number ; volume : number }
interface LogRow { id : string ; occurredAt : string ; node : string ; operator : string ; result : string }
2026-08-19 16:45:51 +08:00
const route = useRoute ()
const router = useRouter ()
2026-08-20 09:10:36 +08:00
const orderId = computed (() => String ( route . params . orderId || '' ))
const customerId = computed (() => String ( route . query . customerId || '' ))
const detail = ref < LastMileOrderDetailDto | null >( null )
const loading = ref ( false )
const loadError = ref < ApiErrorDetails | null >( null )
function text ( value : unknown ) { return String ( value ?? '' ). trim () || '—' }
function yesNo ( value : boolean | null | undefined ) { return value == null ? '—' : value ? '是' : '否' }
const referenceValue = computed (() => text ( detail . value ? . order . orderNo || orderId . value ))
const orderFields = computed < DetailField [] >(() => {
const order = detail . value ? . order
return [
{ label : '订单类型' , value : lastMileOrderTypeLabel ( order ? . orderType ) },
{ label : '拒收时间' , value : '—' },
{ label : '实际入库时间' , value : '—' },
{ label : '头程单号' , value : text ( order ? . sourceOrderNo ) },
{ label : '货主' , value : text ( order ? . customerName ) },
{ label : '海外仓' , value : text ( order ? . overseasWarehouseName ) },
{ label : '转仓目的仓' , value : text ( order ? . transferWarehouseName ) },
{ label : 'FBA仓库' , value : text ( order ? . fbaWarehouseCode ) },
{ label : '集装箱号' , value : text ( order ? . containerNo ) },
{ label : '货柜异常' , value : yesNo ( order ? . abnormal ) },
{ label : '异常原因' , value : order ? . abnormal ? text ( order . subOrderStatus ) : '—' },
{ label : '运输方式' , value : '—' },
{ label : '预计到仓' , value : formatOverseasDateTime ( order ? . estimatedWarehouseAt ) },
{ label : '清关状态' , value : text ( order ? . containerStatus ) },
]
})
const detailFields = computed < DetailField [] >(() => {
const order = detail . value ? . order
return [
{ label : '订单类型' , value : lastMileOrderTypeLabel ( order ? . orderType ) },
{ label : '入仓码' , value : text ( order ? . inboundCode ) },
{ label : '目的仓' , value : text ( order ? . transferWarehouseName ) },
{ label : '托数' , value : Number ( order ? . palletCount || 0 ) },
{ label : '箱数' , value : Number ( order ? . packageCount || 0 ) },
{ label : '数量' , value : Number ( order ? . productQuantity || 0 ) },
{ label : '体积重量' , value : Number ( order ? . volumeWeightKg || 0 ) },
{ label : '预计到港时间' , value : formatOverseasDateTime ( order ? . estimatedArrivalAt ) },
{ label : '实际到港时间' , value : formatOverseasDateTime ( order ? . actualArrivalAt ) },
{ label : '预计到仓' , value : formatOverseasDateTime ( order ? . estimatedWarehouseAt ) },
{ label : '是否暂扣' , value : yesNo ( order ? . detained ) },
{ label : '备注(仅自己)' , value : text ( order ? . privateRemark ) },
{ label : '备注(客户)' , value : text ( order ? . customerRemark ) },
{ label : '备注(仓库)' , value : text ( order ? . warehouseRemark ) },
]
})
const planFields = computed < DetailField [] >(() => {
const order = detail . value ? . order
return [
{ label : '入仓码' , value : text ( order ? . inboundCode ) },
{ label : '计划入库仓' , value : text ( order ? . transferWarehouseName || order ? . overseasWarehouseName ) },
{ label : '合托批次' , value : text ( order ? . palletMergeFlag ) },
{ label : '合托后托数' , value : Number ( order ? . palletCount || 0 ) },
{ label : 'FBA预约号' , value : '—' },
{ label : '预约状态' , value : '—' },
{ label : '换标状态' , value : lastMileStatusLabel ( order ? . relabelStatus ) },
]
})
const documents = computed < DocumentRow [] >(() => {
const order = detail . value ? . order
if ( ! order ) return []
return [
[ '入库单' , order . inboundOrderNo ],
[ '出库单(只读关联)' , order . outboundOrderNo ],
[ '派送单(只读关联)' , order . deliveryOrderNo ],
[ '换标单' , order . relabelOrderNo ],
]. filter (( item ) => item [ 1 ]). map (([ type , number ]) => ({ type : String ( type ), number : String ( number ), createdAt : '—' , quote : '—' }))
})
const cargo = computed < CargoRow [] >(() => ( detail . value ? . cargo || []). map (( item ) => ({
id : String ( item . id ), sku : text ( item . skuCode ), productName : text ( item . productNameZh ), pallets : Number ( item . palletCount || 0 ),
cartons : Number ( item . packageCount || 0 ), quantity : Number ( item . productQuantity || 0 ), weight : Number ( item . grossWeightKg || 0 ), volume : Number ( item . volumeCbm || 0 ),
})))
const logs = computed < LogRow [] >(() => ( detail . value ? . statusHistory || []). map (( item ) => ({
id : String ( item . id ), occurredAt : formatOverseasDateTime ( item . createdAt ), node : lastMileStatusLabel ( item . toStatus ), operator : '—' ,
result : text ( item . remark || ` ${ lastMileStatusLabel ( item . fromStatus ) } → ${ lastMileStatusLabel ( item . toStatus ) } ` ),
})))
2026-08-19 16:45:51 +08:00
const documentColumns : DataTableColumn [] = [
2026-08-20 09:10:36 +08:00
{ key : 'type' , prop : 'type' , label : '单据类型' , minWidth : 120 }, { key : 'number' , prop : 'number' , label : '单据编号' , minWidth : 180 },
{ key : 'createdAt' , prop : 'createdAt' , label : '生成时间' , minWidth : 160 }, { key : 'quote' , prop : 'quote' , label : '报价' , minWidth : 140 },
2026-08-19 16:45:51 +08:00
]
const cargoColumns : DataTableColumn [] = [
2026-08-20 09:10:36 +08:00
{ key : 'sku' , prop : 'sku' , label : 'SKU' , minWidth : 150 }, { key : 'productName' , prop : 'productName' , label : '中文品名' , minWidth : 180 },
{ key : 'pallets' , prop : 'pallets' , label : '托数' , width : 90 , align : 'right' }, { key : 'cartons' , prop : 'cartons' , label : '箱数' , width : 90 , align : 'right' },
{ key : 'quantity' , prop : 'quantity' , label : '件数' , width : 90 , align : 'right' }, { key : 'weight' , prop : 'weight' , label : '重量(kg)' , width : 110 , align : 'right' },
{ key : 'volume' , prop : 'volume' , label : '体积(m³)' , width : 110 , align : 'right' },
2026-08-19 16:45:51 +08:00
]
const logColumns : DataTableColumn [] = [
2026-08-20 09:10:36 +08:00
{ key : 'occurredAt' , prop : 'occurredAt' , label : '时间' , minWidth : 160 }, { key : 'node' , prop : 'node' , label : '操作节点' , minWidth : 140 },
{ key : 'operator' , prop : 'operator' , label : '操作人' , minWidth : 130 }, { key : 'result' , prop : 'result' , label : '处理结果' , minWidth : 220 },
2026-08-19 16:45:51 +08:00
]
2026-08-20 09:10:36 +08:00
async function load () {
if ( ! orderId . value || ! customerId . value ) {
loadError . value = { message : '详情缺少客户范围,请从尾程订单列表重新进入' }
return
}
loading . value = true
loadError . value = null
try { detail . value = await getLastMileOrder ( orderId . value , customerId . value ) }
catch ( error ) { loadError . value = getApiErrorDetails ( error , '尾程订单详情加载失败,请重试' ) }
finally { loading . value = false }
2026-08-19 16:45:51 +08:00
}
2026-08-20 09:10:36 +08:00
function back () { void router . push ({ name : 'overseas-last-mile-orders' }) }
onMounted ( load )
2026-08-19 16:45:51 +08:00
</ script >
< template >
2026-08-20 09:10:36 +08:00
< OverseasDetailShell title = "尾程订单详情" reference -label =" 尾程单号 " :reference-value = "referenceValue" back -label =" 返回尾程订单 " domain = "尾程订单详情" :pending = "false" : status -text =" detail ? lastMileStatusLabel ( detail.order.currentStatus ) : ' 加载中 '" @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="detail-grid" :aria-busy="loading" >
< section class = "detail-section" aria -labelledby =" last -mile-order-heading " >< h2 id = "last-mile-order-heading" > 订单信息 </ h2 >< dl class = "detail-fields" >< div v-for = "field in orderFields" :key="field.label" >< dt >{{ field . label }}</ dt >< dd >{{ field . value }}</ dd ></ div ></ dl ></ section >
< section class = "detail-section" aria -labelledby =" last -mile-lines-heading " >< h2 id = "last-mile-lines-heading" > 订单明细 </ h2 >< dl class = "detail-fields" >< div v-for = "field in detailFields" :key="field.label" >< dt >{{ field . label }}</ dt >< dd >{{ field . value }}</ dd ></ div ></ dl ></ section >
< section class = "detail-section" aria -labelledby =" last -mile-plan-heading " >< h2 id = "last-mile-plan-heading" > 计划信息 </ h2 >< dl class = "detail-fields" >< div v-for = "field in planFields" :key="field.label" >< dt >{{ field . label }}</ dt >< dd >{{ field . value }}</ dd ></ div ></ dl ></ section >
< section class = "detail-section table-section" aria -labelledby =" last -mile-documents-heading " >< h2 id = "last-mile-documents-heading" > 关联单据 </ h2 >< DataTable :data = "documents" :columns = "documentColumns" column -settings-mode =" hide " empty -text =" 暂无关联单据 " /></ section >
< section class = "detail-section table-section is-wide" aria -labelledby =" last -mile-cargo-heading " >< h2 id = "last-mile-cargo-heading" > 货物信息 </ h2 >< DataTable :data = "cargo" :columns = "cargoColumns" column -settings-mode =" hide " row -key =" id " empty -text =" 暂无货物明细 " /></ section >
< section class = "detail-section table-section" aria -labelledby =" last -mile-logs-heading " >< h2 id = "last-mile-logs-heading" > 操作记录 </ h2 >< DataTable :data = "logs" :columns = "logColumns" column -settings-mode =" hide " row -key =" id " empty -text =" 暂无操作记录 " /></ section >
2026-08-19 16:45:51 +08:00
</ div >
</ OverseasDetailShell >
</ template >
< style scoped >
2026-08-20 09:10:36 +08:00
. detail - grid { display : grid ; min - width : 0 ; min - height : 240 px ; grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr )); border : 1 px solid var ( -- cc - border ); background : # fff ; }
2026-08-19 16:45:51 +08:00
. detail - section { min - width : 0 ; padding : 16 px 18 px 18 px ; border - right : 1 px solid var ( -- cc - border ); border - bottom : 1 px solid var ( -- cc - border ); }
. detail - section : nth - child ( even ) { border - right : 0 ; }
. detail - section . is - wide { grid - column : 1 / - 1 ; border - right : 0 ; }
. detail - section h2 { margin : 0 0 12 px ; color : var ( -- cc - text ); font - size : 15 px ; }
. detail - fields { display : grid ; grid - template - columns : repeat ( 3 , minmax ( 0 , 1 fr )); margin : 0 ; border - top : 1 px solid var ( -- cc - border ); border - left : 1 px solid var ( -- cc - border ); }
. detail - fields > div { min - width : 0 ; min - height : 58 px ; padding : 9 px 11 px ; border - right : 1 px solid var ( -- cc - border ); border - bottom : 1 px solid var ( -- cc - border ); }
. detail - fields dt { color : var ( -- cc - muted ); font - size : 12 px ; }
. detail - fields dd { margin : 4 px 0 0 ; color : var ( -- cc - text ); overflow - wrap : anywhere ; }
. table - section { padding - right : 0 ; padding - bottom : 0 ; padding - left : 0 ; overflow : hidden ; }
. table - section h2 { margin - right : 18 px ; margin - left : 18 px ; }
</ style >