2026-08-26 15:48:34 +08:00
< script setup lang = "ts" >
2026-09-14 20:03:27 +08:00
import '@/styles/adminDensity.css'
2026-08-30 16:40:03 +08:00
import { computed , ref , watch } from 'vue'
2026-08-26 15:48:34 +08:00
import { ArrowLeft } from '@lucide/vue'
import { useRoute , useRouter } from 'vue-router'
import AdminPage from '@/shared/components/AdminPage.vue'
2026-08-30 16:40:03 +08:00
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
2026-08-26 15:48:34 +08:00
import DetailPageLayout from '@/shared/components/DetailPageLayout.vue'
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import StatusBadge from '@/shared/components/StatusBadge.vue'
import { getApiErrorDetails , type ApiErrorDetails } from '@/shared/api/http'
import { formatOverseasDateTime } from '../lastMilePresentation'
2026-10-03 04:35:55 +08:00
import { getDeliveryOrder , type DeliveryOrderDetail , type DeliveryFeeRow } from '../api/deliveryOrders'
import { coveredServiceLabels } from '../feeCoverageTypes'
2026-08-26 15:48:34 +08:00
2026-08-30 16:40:03 +08:00
const route = useRoute ()
const router = useRouter ()
const detail = ref < DeliveryOrderDetail | null >( null )
const loading = ref ( false )
const error = ref < ApiErrorDetails | null >( null )
const activeTab = ref ( 'order' )
const deliveryNo = computed (() => String ( route . params . deliveryNo || '' ). trim ())
const customerId = computed (() => String ( route . query . customerId || '' ). trim ())
2026-08-26 15:48:34 +08:00
const statusLabels : Record < string , string > = { WAIT_DELIVERY : '待派送' , DELIVERING : '派送中' , DELIVERED : '派送完成' , SIGNED : '已签收' , REJECTED : '已拒签' , RETURN_PENDING : '退货处理中' , CANCELLED : '已取消' }
2026-08-30 16:40:03 +08:00
const methodLabels : Record < string , string > = { FBA_PICKUP : 'FBA自提' , CUSTOMER_PICKUP : '客户自提' , TRUCK : '卡车派送' , TRUCK_PALLET : '卡派打托' , TRUCK_FLOOR : '卡派地板' , EXPRESS : '快递' , OUTSOURCED : '外配' , DEDICATED_TRUCK : '专车直送' , LTL : '零担派送' , SELF_DELIVERY : '自送' , THIRD_PARTY : '第三方配送' }
2026-09-03 17:17:11 +08:00
const pickupLabels : Record < string , string > = { ONE_PICKUP_ONE_DROP : '一提一卸' , ONE_PICKUP_MULTI_DROP : '一提多卸' , MULTI_PICKUP_ONE_DROP : '多提一卸' , MULTI_PICKUP_MULTI_DROP : '多提多卸' , WAREHOUSE_PICKUP : '仓库自提' , DOOR_PICKUP : '上门提货' , SELF_PICKUP : '自提' }
const loadingLabels : Record < string , string > = { PALLET : '托盘' , FLOOR : '地板' , MIXED : '混装' , CARTON : '散箱' }
2026-09-08 18:02:56 +08:00
const feeLabels : Record < string , string > = { DELIVERY_FEE : '派送费用' , PRIVATE_LAST_MILE_DELIVERY : '私人仓尾程派送费' , EXPRESS_FEE : '快递费用' , LOADING_FEE : '装车费' , APPOINTMENT_FEE : '买约费用' , OTHER : '其他费用' }
2026-10-03 04:35:55 +08:00
const operationLabels : Record < string , string > = { CREATE_DELIVERY : '创建派送单' , ASSIGN_VEHICLE : '车队分配' , DISPATCH : '开始派送' , SIGN : '签收' , COMPLETE_DELIVERY : '完成派送' , UPDATE_DELIVERY_STATUS : '更新派送状态' , CUSTOMER_VISIT : '客户回访' , MARK_RETURN : '标记退货' , CANCEL_OUTBOUND : '取消出库' , CANCEL_OUTBOUND_REASON : '取消原因' , CONFIRM_EXTERNAL_FREIGHT : '确认外接运费' }
2026-08-30 16:40:03 +08:00
const resultLabels : Record < string , string > = { CREATED : '已创建' }
2026-08-26 15:48:34 +08:00
function show ( value : unknown ) { const text = String ( value ?? '' ). trim (); return text || '—' }
2026-10-03 04:35:55 +08:00
function feeStatus ( fee : DeliveryFeeRow ) {
if ( fee . voidStatus === 'CONFIRMED' ) return '已作废'
if ( fee . voidStatus === 'PENDING_FINANCE' ) return '作废待财务确认'
if ( fee . voidStatus === 'FINANCE_ATTENTION' ) return '作废待财务处理'
if ( fee . sourceStatus === 'SUPERSEDED' ) return '已替代'
return ({ APPROVED : '业务已审核' , PENDING : '待审核' , DRAFT : '草稿' , REJECTED : '已驳回' } as Record < string , string >)[ fee . reviewStatus || '' ] || '—'
}
2026-09-03 17:17:11 +08:00
function displayPerson ( value : unknown ) { const text = String ( value ?? '' ). trim (); return text && ! /^\d+$/ . test ( text ) ? text : '—' }
2026-08-30 16:40:03 +08:00
function quantity ( value : number | null | undefined , unit : string ) { return value === null || value === undefined ? '—' : ` ${ value } ${ unit } ` }
function customerConfirmation () { const signedAt = detail . value ? . order . signedAt ; return signedAt ? `已签收 · ${ formatOverseasDateTime ( signedAt ) } ` : '—' }
2026-08-26 15:48:34 +08:00
function back () { void router . push ({ name : 'overseas-delivery-orders' }) }
2026-08-30 16:40:03 +08:00
async function load () {
if ( ! deliveryNo . value || ! customerId . value ) { detail . value = null ; error . value = { message : '缺少派送单客户范围,无法安全查询详情' }; return }
loading . value = true ; error . value = null
try { detail . value = await getDeliveryOrder ( deliveryNo . value , customerId . value ) }
catch ( cause ) { detail . value = null ; error . value = getApiErrorDetails ( cause , '派送单详情加载失败' ) }
finally { loading . value = false }
}
watch ([ deliveryNo , customerId ], () => { void load () }, { immediate : true })
2026-08-26 15:48:34 +08:00
</ script >
2026-08-30 16:40:03 +08:00
< template >
2026-09-14 20:03:27 +08:00
< AdminPage class = "admin-density" >
2026-08-30 16:40:03 +08:00
< DetailPageLayout aria -label =" 派送单详情 " >
< template # header >
< el-breadcrumb separator = ">" class = "detail-breadcrumb" >
< el-breadcrumb-item > 首页 </ el-breadcrumb-item >< el-breadcrumb-item > 海外派送 </ el-breadcrumb-item >
< el-breadcrumb-item : to = "{ name: 'overseas-delivery-orders' }" > 派送单列表 </ el-breadcrumb-item >< el-breadcrumb-item > 详情 </ el-breadcrumb-item >
</ el-breadcrumb >
< div class = "detail-heading" >
2026-09-14 20:03:27 +08:00
< el-button class = "density-back" link aria -label =" 返回派送单列表 " @click ="back" >< ArrowLeft :size = "16" /> 返回 </ el-button >
2026-08-30 16:40:03 +08:00
< div class = "detail-title" >< div class = "title-line" >< h1 > 派送单详情 </ h1 >< StatusBadge v-if = "detail" :status="statusLabels[detail.order.status] || detail.order.status" /></ div >< p > 派送单号 {{ deliveryNo }}</ p ></ div >
</ div >
</ template >
< LoadErrorState v-if = "error" title="派送单详情加载失败" :message="error.message" :loading="loading" @retry="load" >
< template # details >< ApiErrorMeta :details = "error" /></ template >
</ LoadErrorState >
< section v-else v-loading = "loading" class="surface-panel delivery-detail" >
2026-09-14 20:03:27 +08:00
< el-tabs v-model = "activeTab" class="detail-tabs density-element-tabs density-contained-tabs" >
< el-tab-pane name = "order" >< template # label >< span class = "density-tab-icon" aria -hidden =" true " > 📋 </ span >< span > 订单信息 </ span ></ template >
< section class = "tab-content" aria -label =" 订单信息 " >
< dl v-if = "detail" class="info-grid density-fields" >
2026-08-30 16:40:03 +08:00
< div class = "info-item" >< dt > 派送单号 </ dt >< dd >{{ show ( detail . order . deliveryNo ) }}</ dd ></ div >< div class = "info-item" >< dt > 状态 </ dt >< dd >{{ statusLabels [ detail . order . status ] || detail . order . status }}</ dd ></ div >
2026-09-15 11:54:19 +08:00
< div class = "info-item" >< dt > 仓库 </ dt >< dd >{{ show ( detail . order . overseasWarehouseName ) }}</ dd ></ div >< div class = "info-item" >< dt > 目的 FBA 仓 </ dt >< dd >{{ show ( detail . order . destinationWarehouseCode || detail . order . destinationWarehouseName ) }}</ dd ></ div >
< div class = "info-item" >< dt > 运单号 </ dt >< dd >{{ show ( detail . order . waybillNo ) }}</ dd ></ div >< div class = "info-item" >< dt > 派送人 </ dt >< dd >{{ show ( detail . order . carrierName ) }}</ dd ></ div >
< div class = "info-item" >< dt > 派送时间 </ dt >< dd >{{ formatOverseasDateTime ( detail . order . dispatchedAt || detail . order . plannedDeliveryAt ) }}</ dd ></ div >< div class = "info-item" >< dt > 提货方式 </ dt >< dd >{{ pickupLabels [ detail . order . pickupMethod || '' ] || show ( detail . order . pickupMethod ) }}</ dd ></ div >
< div class = "info-item" >< dt > 装车类型 </ dt >< dd >{{ loadingLabels [ detail . order . loadingType || '' ] || show ( detail . order . loadingType ) }}</ dd ></ div >< div class = "info-item" >< dt > 派送方式 </ dt >< dd >{{ methodLabels [ detail . order . deliveryMethod || '' ] || show ( detail . order . deliveryMethod ) }}</ dd ></ div >
< div class = "info-item" >< dt > 司机 / 车队 </ dt >< dd >{{ show ( detail . order . driverName ) }}</ dd ></ div >
2026-08-30 16:40:03 +08:00
< div class = "info-item" >< dt > 车牌号 </ dt >< dd >{{ show ( detail . order . vehiclePlate ) }}</ dd ></ div >< div class = "info-item" >< dt > 总箱数 </ dt >< dd >{{ quantity ( detail . order . packageCount , '箱' ) }}</ dd ></ div >
< div class = "info-item" >< dt > 总重量 </ dt >< dd >{{ quantity ( detail . order . totalWeightKg , 'kg' ) }}</ dd ></ div >< div class = "info-item" >< dt > 总体积 </ dt >< dd >{{ quantity ( detail . order . totalVolumeCbm , 'm³' ) }}</ dd ></ div >
< div class = "info-item" >< dt > 预计到达时间 </ dt >< dd >{{ formatOverseasDateTime ( detail . order . expectedArrivalAt ) }}</ dd ></ div >< div class = "info-item" >< dt > 客户确认 </ dt >< dd >{{ customerConfirmation () }}</ dd ></ div >
< div class = "info-item info-item--wide" >< dt > 备注 </ dt >< dd >{{ show ( detail . order . remark ) }}</ dd ></ div >
</ dl >
</ section >
</ el-tab-pane >
2026-09-14 20:03:27 +08:00
< el-tab-pane name = "sku" >< template # label >< span class = "density-tab-icon" aria -hidden =" true " > 📦 </ span >< span >{{ `SKU明细 ( ${ detail ? . skus . length || 0 } )` }}</ span ></ template >
< section class = "tab-content" aria -label =" SKU明细 " >
2026-08-30 16:40:03 +08:00
< el-table : data = "detail?.skus || []" border stripe empty -text =" 暂无SKU明细 " >
2026-09-29 20:52:21 +08:00
< el-table-column prop = "inboundCode" label = "来源单号 / 入库码" min -width =" 160 " />< el-table-column prop = "customerName" label = "客户" min -width =" 180 " />
2026-08-30 16:40:03 +08:00
< el-table-column prop = "destinationWarehouseCode" label = "目的仓编码" min -width =" 140 " />< el-table-column prop = "receivingAddress" label = "收货地址" min -width =" 240 " />
< el-table-column prop = "shipmentNo" label = "货件编号" min -width =" 160 " />< el-table-column prop = "fbaTrackingNo" label = "FBA追踪号" min -width =" 160 " />
< el-table-column prop = "skuCode" label = "SKU" min -width =" 140 " />< el-table-column prop = "productName" label = "品名" min -width =" 160 " />
2026-09-29 20:52:21 +08:00
< el-table-column prop = "packageCount" label = "箱数" width = "90" align = "right" />< el-table-column v-if = "detail?.skus.some(item => item.productQuantity != null)" prop="productQuantity" label="件数" width="90" align="right" /><el-table-column prop="weightKg" label="重量(kg)" width="110" align="right" /><el-table-column prop="volumeCbm" label="体积(m³)" width="110" align="right" />
2026-08-30 16:40:03 +08:00
</ el-table >
</ section >
</ el-tab-pane >
2026-09-14 20:03:27 +08:00
< el-tab-pane name = "fees" >< template # label >< span class = "density-tab-icon" aria -hidden =" true " > 💰 </ span >< span >{{ `费用明细 ( ${ detail ? . fees . length || 0 } )` }}</ span ></ template >
< section class = "tab-content" aria -label =" 派送单费用明细 " >
2026-10-03 04:35:55 +08:00
< template v-if = "detail?.feeCoverage?.references.length" >
< p > 本次已确认的原费用覆盖 ( {{ formatOverseasDateTime ( detail . feeCoverage . confirmedAt ) }} ): 原费用金额供核对 , 不计作本次新增 。 </ p >
< p v-for = "reference in detail.feeCoverage.references" :key="`${reference.source.financeFeeId}-${reference.services.join('-')}`" >{{ detail.feeCoverage.cargo.find ( cargo = > cargo . sourceInboundId === reference . source . sourceInboundId ) ? . inboundCode || reference . source . inboundCode || '—' }} · {{ reference . source . feeItemName || '—' }} · {{ reference . source . currency }} {{ reference . source . amount }} · 版本 {{ reference . source . sourceVersion }} · 已含 {{ reference . services . map ( service => coveredServiceLabels [ service ]). join ( '、' ) }}</ p >
</ template >
2026-08-30 16:40:03 +08:00
< el-table : data = "detail?.fees || []" border stripe empty -text =" 暂无费用明细 " >
< el-table-column type = "index" label = "序号" width = "80" />< el-table-column label = "应收/应付" width = "120" >< template # default = "scope" >{{ scope . row . feeDirection === 'PAYABLE' ? '应付' : scope . row . feeDirection === 'RECEIVABLE' ? '应收' : show ( scope . row . feeDirection ) }}</ template ></ el-table-column >
< el-table-column label = "费用类型" min -width =" 180 " >< template # default = "scope" >{{ feeLabels [ scope . row . feeType ] || scope . row . feeType }}</ template ></ el-table-column >< el-table-column prop = "currency" label = "币种" width = "100" />
2026-10-03 04:35:55 +08:00
< el-table-column prop = "amount" label = "金额" min -width =" 140 " align = "right" />< el-table-column label = "费用状态" min -width =" 165 " >< template # default = "scope" >{{ feeStatus ( scope . row as DeliveryFeeRow ) }}</ template ></ el-table-column >< el-table-column prop = "remark" label = "备注" min -width =" 240 " />
2026-08-30 16:40:03 +08:00
</ el-table >
</ section >
</ el-tab-pane >
2026-09-14 20:03:27 +08:00
< el-tab-pane name = "operations" >< template # label >< span class = "density-tab-icon" aria -hidden =" true " > 📝 </ span >< span >{{ `操作记录 ( ${ detail ? . operations . length || 0 } )` }}</ span ></ template >
< section class = "tab-content" aria -label =" 操作记录 " >
2026-08-30 16:40:03 +08:00
< el-table : data = "detail?.operations || []" border stripe empty -text =" 暂无操作记录 " >
< el-table-column label = "操作时间" min -width =" 180 " >< template # default = "scope" >{{ formatOverseasDateTime ( scope . row . operatedAt ) }}</ template ></ el-table-column >
< el-table-column label = "操作节点" min -width =" 180 " >< template # default = "scope" >{{ operationLabels [ scope . row . operationNode ] || scope . row . operationNode }}</ template ></ el-table-column >
2026-09-03 17:17:11 +08:00
< el-table-column label = "操作人" min -width =" 160 " >< template # default = "scope" >{{ displayPerson ( scope . row . operatorName ) }}</ template ></ el-table-column >< el-table-column label = "处理结果" min -width =" 280 " >< template # default = "scope" >{{ resultLabels [ scope . row . result ] || scope . row . result }}</ template ></ el-table-column >
2026-08-30 16:40:03 +08:00
</ el-table >
</ section >
</ el-tab-pane >
</ el-tabs >
</ section >
</ DetailPageLayout >
</ AdminPage >
</ template >
< style scoped >
2026-09-14 20:03:27 +08:00
. detail - breadcrumb { margin - bottom : var ( -- admin - space - 5 ); }. detail - heading { display : flex ; align - items : center ; gap : var ( -- admin - space - 5 ); }. title - line { display : flex ; align - items : center ; gap : var ( -- admin - space - 4 ); }. tab - content h2 { margin : 0 0 var ( -- admin - space - 5 ); font - size : var ( -- admin - font - section ); line - height : 22 px ; }
2026-08-30 16:40:03 +08:00
@ media ( max - width : 980 px ) { . info - grid { grid - template - columns : 1 fr ; }. info - item , . info - item : nth - child ( odd ), . info - item : nth - child ( even ) { padding : 0 ; }. info - item -- wide { grid - column : auto ; } }
2026-09-14 20:03:27 +08:00
: deep (. detail - page - layout ) { gap : 0 ; background : var ( -- admin - surface ); border : 1 px solid var ( -- cc - border ); }
: deep (. detail - page - layout__header ) { padding : var ( -- admin - space - 5 ); border - bottom : 1 px solid var ( -- cc - border ); }
. detail - title { min - width : 0 ; display : flex ; align - items : center ; flex - wrap : wrap ; gap : var ( -- admin - space - 5 ); }
. detail - title p { margin : 0 ; color : var ( -- cc - muted ); font - size : var ( -- admin - font - title ); font - weight : 600 ; overflow - wrap : anywhere ; }
. title - line h1 { margin : 0 ; font - size : var ( -- admin - font - section ); line - height : 28 px ; font - weight : 500 ; }
. delivery - detail { min - height : 0 ; overflow : hidden ; border : 0 ; border - radius : 0 ; }
. tab - content { width : 100 % ; padding : 0 ; box - sizing : border - box ; }
. info - item -- wide { grid - column : 1 / - 1 ; }
2026-08-30 16:40:03 +08:00
</ style >