2026-08-06 13:56:04 +08:00
< script setup lang = "ts" >
2026-08-10 17:42:17 +08:00
import { computed , nextTick , onMounted , reactive , ref , watch , type Component } from 'vue'
2026-08-07 10:45:19 +08:00
import { ElMessage } from 'element-plus'
2026-08-10 16:24:54 +08:00
import { ArrowLeft , Box , ChevronDown , ChevronUp , CircleCheck , Eye , History , ListChecks , MoreHorizontal , PackageOpen , Ruler , SlidersHorizontal , StickyNote , WalletCards } from '@lucide/vue'
2026-08-06 13:56:04 +08:00
import { useRoute , useRouter } from 'vue-router'
import {
getPlatformUnifiedOrder ,
2026-08-06 18:02:04 +08:00
approvePlatformUnifiedOrder ,
2026-08-06 19:03:13 +08:00
addPlatformUnifiedOrderNote ,
2026-08-07 14:47:11 +08:00
listPlatformUnifiedOrderFeeDetails ,
2026-08-06 13:56:04 +08:00
listPlatformUnifiedOrders ,
unifiedOrderStatusOptions ,
type UnifiedOrderDetailResource ,
type UnifiedOrderMainStatus ,
type UnifiedOrderResource ,
2026-08-07 14:47:11 +08:00
type UnifiedOrderFeeDetailResource ,
2026-08-06 13:56:04 +08:00
} from '@/modules/unified-orders/api/unifiedOrders'
2026-08-11 09:16:58 +08:00
import OrderFulfillmentTreePanel from '@/modules/unified-orders/components/OrderFulfillmentTreePanel.vue'
2026-08-06 13:56:04 +08:00
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { listPlatformTenants , type PlatformTenantResource } from '@/modules/iam'
2026-08-06 15:32:02 +08:00
import { listAllPlatformWarehouses , type PlatformWarehouseSummaryResource } from '@/modules/warehouses'
2026-08-06 13:56:04 +08:00
import { getApiErrorDetails , type ApiErrorDetails } from '@/shared/api/http'
2026-08-10 15:36:53 +08:00
import AdminPage from '@/shared/components/AdminPage.vue'
2026-08-06 13:56:04 +08:00
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
2026-08-06 18:02:04 +08:00
import AppDialog from '@/shared/components/AppDialog.vue'
2026-08-06 13:56:04 +08:00
import DataTable , { type DataTableColumn } from '@/shared/components/DataTable.vue'
2026-08-10 15:36:53 +08:00
import DetailPageLayout from '@/shared/components/DetailPageLayout.vue'
import ListPageLayout from '@/shared/components/ListPageLayout.vue'
2026-08-06 13:56:04 +08:00
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
2026-08-07 18:44:55 +08:00
import LineClampText from '@/shared/components/LineClampText.vue'
2026-08-06 13:56:04 +08:00
import QueryToolbar , { type QueryField , type QueryModel } from '@/shared/components/QueryToolbar.vue'
import StatusBadge from '@/shared/components/StatusBadge.vue'
2026-08-10 09:12:06 +08:00
import { UnifiedOrderWorkflowPanel } from '@/modules/unified-orders/workflows'
2026-08-07 14:47:11 +08:00
import {
downloadWeightImage ,
getInboundMeasurement ,
type InboundMeasurementDetail ,
} from '@/modules/inbounds'
2026-08-06 13:56:04 +08:00
type FilterModel = QueryModel & {
tenantId : string
2026-08-06 15:32:02 +08:00
receivingWarehouseId : string
destinationWarehouseId : string
inboundCode : string
expectedArrivalDate : string
feeReviewStatus : string
channelName : string
containerNo : string
cabinetNo : string
sailingDate : string
loaded : string
fullContainer : string
measurementReviewStatus : string
fbaCode : string
2026-08-06 13:56:04 +08:00
createdRange : string []
}
const route = useRoute ()
const router = useRouter ()
const access = useAccessPreviewStore ()
const records = ref < UnifiedOrderResource [] >([])
2026-08-06 19:03:13 +08:00
const selectedOrders = ref < UnifiedOrderResource [] >([])
2026-08-06 13:56:04 +08:00
const customers = ref < PlatformTenantResource [] >([])
2026-08-06 15:32:02 +08:00
const warehouses = ref < PlatformWarehouseSummaryResource [] >([])
2026-08-06 13:56:04 +08:00
const page = ref ( 1 )
const pageSize = ref ( 20 )
const total = ref ( 0 )
const loading = ref ( false )
const loadError = ref < ApiErrorDetails | null >( null )
const customerLoading = ref ( false )
const customerLoadError = ref < ApiErrorDetails | null >( null )
2026-08-06 15:32:02 +08:00
const warehouseLoading = ref ( false )
const warehouseLoadError = ref < ApiErrorDetails | null >( null )
2026-08-06 13:56:04 +08:00
const detail = ref < UnifiedOrderDetailResource | null >( null )
const detailLoading = ref ( false )
const detailError = ref < ApiErrorDetails | null >( null )
2026-08-06 19:03:13 +08:00
const activeDetailTab = ref ( 'basic' )
2026-08-06 18:02:04 +08:00
const approvalOpen = ref ( false )
const approvalLoadingId = ref ( '' )
const approvalSaving = ref ( false )
const approvalOrder = ref < UnifiedOrderResource | null >( null )
2026-08-06 19:03:13 +08:00
const noteOpen = ref ( false )
const noteSaving = ref ( false )
const noteContent = ref ( '' )
2026-08-06 20:14:56 +08:00
const noteOrder = ref < UnifiedOrderResource | null >( null )
2026-08-07 14:47:11 +08:00
const feeDetails = ref < UnifiedOrderFeeDetailResource [] >([])
const feeLoading = ref ( false )
const feeLoadedKey = ref ( '' )
const measurementDetail = ref < InboundMeasurementDetail | null >( null )
const measurementLoading = ref ( false )
const measurementLoadedKey = ref ( '' )
const measurementImageUrls = ref < string [] >([])
const measurementCollapsed = reactive ({ customer : false , outer : false , actual : false })
let detailLoadingKey = ''
2026-08-07 20:07:37 +08:00
let detailRequestSerial = 0
2026-08-06 15:32:02 +08:00
const showAdvancedFilters = ref ( false )
const filters = ref < FilterModel >( emptyFilters ())
2026-08-11 18:06:35 +08:00
const appliedFilters = ref < FilterModel >( emptyFilters ())
const selectedStatus = ref < UnifiedOrderMainStatus | '' >( '' )
2026-08-06 13:56:04 +08:00
const orderId = computed (() => typeof route . params . orderId === 'string' ? route . params . orderId : '' )
2026-08-07 20:07:37 +08:00
const detailTenantId = computed (() => typeof route . query . tenantId === 'string' ? route . query . tenantId : '' )
const detailRouteKey = computed (() => orderId . value ? ` ${ detailTenantId . value } : ${ orderId . value } ` : '' )
2026-08-06 13:56:04 +08:00
2026-08-06 19:03:13 +08:00
const detailTabs = [
{ key : 'basic' , label : '基本信息' , icon : Box },
{ key : 'cargo' , label : '货品信息' , icon : PackageOpen },
{ key : 'fees' , label : '费用信息' , icon : WalletCards },
{ key : 'history' , label : '状态历史' , icon : History },
{ key : 'measurement' , label : '测量数据' , icon : Ruler },
{ key : 'other' , label : '其他' , icon : StickyNote },
2026-08-11 09:16:58 +08:00
{ key : 'fulfillment' , label : '拆分履约' , icon : ListChecks },
2026-08-06 19:03:13 +08:00
]
2026-08-10 09:12:06 +08:00
const detailTabOptions = computed (() => detail . value ? . summary . inboundId
? [... detailTabs , { key : 'workflow' , label : '后半程作业' , icon : ListChecks }]
: detailTabs )
2026-08-07 14:47:11 +08:00
const customerMeasurementColumns = [
{ key : 'itemNo' , label : '#' }, { key : 'lengthCm' , label : '箱规长(cm)' }, { key : 'widthCm' , label : '箱规宽(cm)' },
{ key : 'heightCm' , label : '箱规高(cm)' }, { key : 'packageCount' , label : '箱数' }, { key : 'unitWeightKg' , label : '每箱毛重' },
{ key : 'unitVolumeCbm' , label : '每箱体积' }, { key : 'remark' , label : '备注' },
]
const measuredColumns = [
... customerMeasurementColumns . slice ( 0 , 7 ), { key : 'girthCm' , label : '箱规周长(cm)' }, { key : 'totalWeightKg' , label : '总毛重(T.K.GW)' },
{ key : 'totalVolumeCbm' , label : '总体积(T.CBM)' }, { key : 'volumeWeightKg' , label : '总材积' }, { key : 'remark' , label : '备注' },
]
2026-08-06 15:32:02 +08:00
const statusTabs = computed (() => [
{ value : '' as const , label : '全部' },
... unifiedOrderStatusOptions ,
])
const warehouseOptions = computed (() => warehouses . value . map (( warehouse ) => ({
value : String ( warehouse . id ),
label : ` ${ warehouse . warehouseName } ( ${ warehouse . warehouseCode } ) `,
})))
const receivingWarehouseOptions = computed (() => warehouses . value
. filter (( warehouse ) => warehouse . warehouseType === 'DOMESTIC' )
. map (( warehouse ) => ({ value : String ( warehouse . id ), label : ` ${ warehouse . warehouseName } ( ${ warehouse . warehouseCode } ) ` })))
const primaryQueryFields = computed < QueryField [] >(() => [
{ key : 'receivingWarehouseId' , type : 'select' , width : 220 , label : '定位仓库' , placeholder : warehouseLoadError . value ? '仓库加载失败' : '请选择定位仓库' , options : receivingWarehouseOptions . value , props : { 'aria-label' : '定位仓库' , filterable : true , loading : warehouseLoading . value , disabled : Boolean ( warehouseLoadError . value ) } },
{ key : 'destinationWarehouseId' , type : 'select' , width : 220 , label : '交货仓' , placeholder : warehouseLoadError . value ? '仓库加载失败' : '请选择交货仓' , options : warehouseOptions . value , props : { 'aria-label' : '交货仓' , filterable : true , loading : warehouseLoading . value , disabled : Boolean ( warehouseLoadError . value ) } },
{ key : 'inboundCode' , type : 'input' , width : 220 , label : '入仓码' , placeholder : '请输入入仓码' , props : { 'aria-label' : '入仓码' } },
2026-08-07 18:44:55 +08:00
])
const advancedQueryFields = computed < QueryField [] >(() => [
2026-08-06 15:32:02 +08:00
{ key : 'expectedArrivalDate' , type : 'date' , width : 190 , label : '预计到达时间' , placeholder : '请选择预计到达日期' , props : { 'aria-label' : '预计到达时间' } },
{ key : 'feeReviewStatus' , type : 'select' , width : 190 , label : '费用审核状态' , placeholder : '请选择费用审核状态' , options : [
2026-08-06 15:36:28 +08:00
{ value : 'UNRECORDED' , label : '未录入' }, { value : 'PENDING' , label : '待审核' },
{ value : 'APPROVED' , label : '审核通过' }, { value : 'REJECTED' , label : '审核驳回' },
2026-08-06 15:32:02 +08:00
], props : { 'aria-label' : '费用审核状态' } },
2026-08-07 18:44:55 +08:00
{
key : 'tenantId' ,
type : 'select' ,
width : 220 ,
label : '公司' ,
placeholder : customerLoadError . value ? '客户列表加载失败' : customerLoading . value ? '正在加载客户' : '请选择公司' ,
options : customers . value . map (( customer ) => ({
value : String ( customer . id ),
label : ` ${ customer . legalName ? . trim () || customer . tenantName } ( ${ customer . tenantCode || '无客户编码' } ) `,
})),
props : { 'aria-label' : '公司' , filterable : true , loading : customerLoading . value , disabled : Boolean ( customerLoadError . value ) },
},
2026-08-06 15:32:02 +08:00
{ key : 'containerNo' , type : 'input' , width : 210 , label : '集装箱号' , placeholder : '请输入集装箱号' , props : { 'aria-label' : '集装箱号' } },
{ key : 'cabinetNo' , type : 'input' , width : 190 , label : '柜号' , placeholder : '请输入柜号' , props : { 'aria-label' : '柜号' } },
{ key : 'channelName' , type : 'input' , width : 210 , label : '渠道' , placeholder : '请输入渠道名称' , props : { 'aria-label' : '渠道' } },
{ key : 'sailingDate' , type : 'date' , width : 190 , label : '船期' , placeholder : '请选择船期' , props : { 'aria-label' : '船期' } },
{ key : 'loaded' , type : 'select' , width : 180 , label : '是否已配载' , placeholder : '请选择配载状态' , options : [{ value : 'true' , label : '已配载' }, { value : 'false' , label : '未配载' }], props : { 'aria-label' : '是否已配载' } },
{ key : 'fullContainer' , type : 'select' , width : 180 , label : '是否整柜' , placeholder : '请选择装柜方式' , options : [{ value : 'true' , label : '整柜' }, { value : 'false' , label : '拼箱' }], props : { 'aria-label' : '是否整柜' } },
{ key : 'measurementReviewStatus' , type : 'select' , width : 220 , label : '测量数据审核状态' , placeholder : '请选择测量审核状态' , options : [
2026-08-06 15:36:28 +08:00
{ value : 'UNRECORDED' , label : '未录入' }, { value : 'PENDING' , label : '待审核' },
{ value : 'APPROVED' , label : '审核通过' }, { value : 'REJECTED' , label : '审核驳回' },
2026-08-06 15:32:02 +08:00
], props : { 'aria-label' : '测量数据审核状态' } },
{ key : 'fbaCode' , type : 'input' , width : 220 , label : '商品 FBA 编码' , placeholder : '请输入商品 FBA 编码' , props : { 'aria-label' : '商品 FBA 编码' } },
2026-08-07 18:44:55 +08:00
{ key : 'createdRange' , type : 'daterange' , width : 300 , label : '创建日期' , placeholder : '创建日期范围' , props : { 'aria-label' : '创建日期范围' } },
2026-08-06 15:32:02 +08:00
])
2026-08-06 19:03:13 +08:00
const allQueryFields = computed (() => [... primaryQueryFields . value , ... advancedQueryFields . value ])
2026-08-06 15:32:02 +08:00
const queryFields = computed (() => showAdvancedFilters . value
2026-08-06 19:03:13 +08:00
? allQueryFields . value
2026-08-06 15:32:02 +08:00
: primaryQueryFields . value )
2026-08-06 13:56:04 +08:00
const columns : DataTableColumn [] = [
2026-08-10 17:42:17 +08:00
{ key : 'actions' , label : '操作' , width : 300 , fixed : 'left' , slotName : 'actions' },
2026-08-06 19:03:13 +08:00
{ key : 'selection' , type : 'selection' , width : 52 , fixed : 'left' , align : 'center' , selectable : canBulkReview },
2026-08-11 09:16:58 +08:00
{ key : 'fulfillment' , type : 'expand' , width : 52 , fixed : 'left' , slotName : 'fulfillment' , hideable : false , movable : false },
2026-08-06 19:03:13 +08:00
{ key : 'inboundCode' , prop : 'inboundCode' , label : '入仓码' , minWidth : 140 , slotName : 'field' },
2026-08-07 09:06:22 +08:00
{ key : 'internalOrderStatus' , prop : 'internalOrderStatus' , label : '订单状态' , minWidth : 130 , slotName : 'orderStatus' },
{ key : 'measurementReviewStatus' , prop : 'measurementReviewStatus' , label : '测量数据审核状态' , minWidth : 170 , slotName : 'measurementReviewStatus' },
2026-08-06 19:03:13 +08:00
{ key : 'expectedArrivalAt' , prop : 'expectedArrivalAt' , label : '预计到达时间' , minWidth : 150 , slotName : 'field' },
{ key : 'feeReviewStatus' , prop : 'feeReviewStatus' , label : '费用审核状态' , minWidth : 140 , slotName : 'field' },
{ key : 'customerName' , prop : 'customerName' , label : '客户' , minWidth : 150 , slotName : 'field' },
{ key : 'receivingWarehouseName' , prop : 'receivingWarehouseName' , label : '仓库' , minWidth : 150 , slotName : 'field' },
{ key : 'destinationWarehouseName' , prop : 'destinationWarehouseName' , label : '交货仓' , minWidth : 150 , slotName : 'field' },
{ key : 'loadingStatus' , prop : 'loadingStatus' , label : '配载状态' , minWidth : 130 , slotName : 'field' },
{ key : 'holdCargo' , prop : 'holdCargo' , label : '是否扣货' , minWidth : 110 , slotName : 'field' },
{ key : 'holdReason' , prop : 'holdReason' , label : '扣货理由' , minWidth : 160 , slotName : 'field' },
{ key : 'containerNo' , prop : 'containerNo' , label : '集装箱号' , minWidth : 150 , slotName : 'field' },
{ key : 'cabinetNo' , prop : 'cabinetNo' , label : '柜号' , minWidth : 130 , slotName : 'field' },
{ key : 'companyName' , prop : 'companyName' , label : '公司' , minWidth : 150 , slotName : 'field' },
{ key : 'businessType' , prop : 'businessType' , label : '业务类型' , minWidth : 130 , slotName : 'field' },
{ key : 'pickupMethod' , prop : 'pickupMethod' , label : '提货方式' , minWidth : 130 , slotName : 'field' },
{ key : 'pickupAt' , prop : 'pickupAt' , label : '提货时间' , minWidth : 150 , slotName : 'field' },
{ key : 'pickupContactPhone' , prop : 'pickupContactPhone' , label : '提货联系人电话' , minWidth : 150 , slotName : 'field' },
{ key : 'pickupAddress' , prop : 'pickupAddress' , label : '提货地址' , minWidth : 200 , slotName : 'field' },
{ key : 'pickupPostalCode' , prop : 'pickupPostalCode' , label : '提货邮编' , minWidth : 110 , slotName : 'field' },
{ key : 'pickupContactEmail' , prop : 'pickupContactEmail' , label : '提货联系人邮箱' , minWidth : 190 , slotName : 'field' },
{ key : 'channelName' , prop : 'channelName' , label : '渠道' , minWidth : 130 , slotName : 'field' },
{ key : 'taxMode' , prop : 'taxMode' , label : '买单/退税' , minWidth : 130 , slotName : 'field' },
{ key : 'fullContainer' , prop : 'fullContainer' , label : '是否整柜' , minWidth : 110 , slotName : 'field' },
{ key : 'customsInspection' , prop : 'customsInspection' , label : '商检验' , minWidth : 110 , slotName : 'field' },
{ key : 'sailingAt' , prop : 'sailingAt' , label : '开船时间' , minWidth : 150 , slotName : 'field' },
{ key : 'portArrivalAt' , prop : 'portArrivalAt' , label : '到港时间' , minWidth : 150 , slotName : 'field' },
{ key : 'totalPackageCount' , prop : 'totalPackageCount' , label : '总箱数' , minWidth : 100 , slotName : 'field' },
{ key : 'totalGrossWeightKg' , prop : 'totalGrossWeightKg' , label : '总毛重(T.K.GW)' , minWidth : 140 , slotName : 'field' },
{ key : 'chargeableWeightKg' , prop : 'chargeableWeightKg' , label : '计费毛重' , minWidth : 120 , slotName : 'field' },
{ key : 'totalVolumeCbm' , prop : 'totalVolumeCbm' , label : '总体积(m³)' , minWidth : 120 , slotName : 'field' },
{ key : 'chargeableVolumeCbm' , prop : 'chargeableVolumeCbm' , label : '计费体积(m³)' , minWidth : 130 , slotName : 'field' },
{ key : 'totalPalletCount' , prop : 'totalPalletCount' , label : '总托盘数' , minWidth : 110 , slotName : 'field' },
{ key : 'location' , prop : 'location' , label : '库位位置' , minWidth : 130 , slotName : 'field' },
{ key : 'volumetricWeightKg' , prop : 'volumetricWeightKg' , label : '总材积重' , minWidth : 120 , slotName : 'field' },
{ key : 'totalNetWeightKg' , prop : 'totalNetWeightKg' , label : '总净重(T.K.NW)' , minWidth : 140 , slotName : 'field' },
{ key : 'destinationType' , prop : 'destinationType' , label : '目的仓类型' , minWidth : 130 , slotName : 'field' },
{ key : 'destinationAddress' , prop : 'destinationAddress' , label : '地址' , minWidth : 200 , slotName : 'field' },
{ key : 'destinationPostalCode' , prop : 'destinationPostalCode' , label : '邮编' , minWidth : 100 , slotName : 'field' },
{ key : 'destinationContact' , prop : 'destinationContact' , label : '联系人' , minWidth : 120 , slotName : 'field' },
{ key : 'destinationContactPhone' , prop : 'destinationContactPhone' , label : '联系人电话' , minWidth : 140 , slotName : 'field' },
{ key : 'measurementCompletedAt' , prop : 'measurementCompletedAt' , label : '测量完成时间' , minWidth : 150 , slotName : 'field' },
{ key : 'inboundCompletedAt' , prop : 'inboundCompletedAt' , label : '入库完成时间' , minWidth : 150 , slotName : 'field' },
{ key : 'associatedFiles' , prop : 'associatedFiles' , label : '关联文件' , minWidth : 120 , slotName : 'field' },
{ key : 'markFiles' , prop : 'markFiles' , label : '唛头文件' , minWidth : 120 , slotName : 'field' },
{ key : 'pcOrderFiles' , prop : 'pcOrderFiles' , label : 'PC端下单文件' , minWidth : 140 , slotName : 'field' },
{ key : 'expressCompany' , prop : 'expressCompany' , label : '快递公司' , minWidth : 130 , slotName : 'field' },
{ key : 'expressTrackingNo' , prop : 'expressTrackingNo' , label : '快递单号' , minWidth : 150 , slotName : 'field' },
2026-08-07 14:47:11 +08:00
{ key : 'customerRemark' , prop : 'customerRemark' , label : '备注' , minWidth : 180 , slotName : 'field' },
{ key : 'latestInternalNote' , prop : 'latestInternalNote' , label : '客服内部备注' , minWidth : 180 , slotName : 'field' },
2026-08-06 19:03:13 +08:00
{ key : 'createdBy' , prop : 'createdBy' , label : '创建人' , minWidth : 120 , slotName : 'field' },
{ key : 'createdAt' , prop : 'createdAt' , label : '创建日期' , minWidth : 150 , slotName : 'field' },
2026-08-06 13:56:04 +08:00
]
2026-08-06 15:32:02 +08:00
function emptyFilters () : FilterModel {
return {
2026-08-11 18:06:35 +08:00
tenantId : '' , receivingWarehouseId : '' , destinationWarehouseId : '' ,
2026-08-10 17:42:17 +08:00
inboundCode : '' , expectedArrivalDate : '' , feeReviewStatus : '' , channelName : '' ,
2026-08-06 15:32:02 +08:00
containerNo : '' , cabinetNo : '' , sailingDate : '' , loaded : '' , fullContainer : '' , measurementReviewStatus : '' ,
fbaCode : '' , createdRange : [],
}
}
2026-08-11 18:06:35 +08:00
function cloneFilters ( source : FilterModel ) : FilterModel {
return { ... source , createdRange : [... source . createdRange ] }
}
2026-08-06 15:32:02 +08:00
function trimmed ( value : string ) {
return value . trim () || undefined
}
function optionalBoolean ( value : string ) {
return value === '' ? undefined : value === 'true'
}
function selectStatus ( status : UnifiedOrderMainStatus | '' ) {
2026-08-11 18:06:35 +08:00
if ( selectedStatus . value === status ) return
selectedStatus . value = status
if ( page . value === 1 ) void loadOrders ()
else page . value = 1
2026-08-06 15:32:02 +08:00
}
function handleTabKeydown ( event : KeyboardEvent , index : number ) {
let nextIndex = index
if ( event . key === 'ArrowRight' ) nextIndex = ( index + 1 ) % statusTabs . value . length
else if ( event . key === 'ArrowLeft' ) nextIndex = ( index - 1 + statusTabs . value . length ) % statusTabs . value . length
else if ( event . key === 'Home' ) nextIndex = 0
else if ( event . key === 'End' ) nextIndex = statusTabs . value . length - 1
else return
event . preventDefault ()
const tab = statusTabs . value [ nextIndex ]
if ( ! tab ) return
selectStatus ( tab . value )
const buttons = ( event . currentTarget as HTMLElement ). parentElement ? . querySelectorAll < HTMLElement >( '[role="tab"]' )
buttons ? .[ nextIndex ] ? . focus ()
}
2026-08-06 13:56:04 +08:00
function can ( permission : string ) {
return access . can ( permission )
}
function customerName ( row : UnifiedOrderResource ) {
return row . customerName ? . trim () || customers . value . find (( item ) => String ( item . id ) === String ( row . tenantId )) ? . tenantName || `客户 ${ row . tenantId } `
}
function formatDate ( value : string | null | undefined ) {
return value ? value . replace ( 'T' , ' ' ). slice ( 0 , 16 ) : '—'
}
function formatNumber ( value : number | null | undefined , digits = 2 ) {
return value === null || value === undefined ? '—' : value . toLocaleString ( 'zh-CN' , { maximumFractionDigits : digits })
}
2026-08-07 14:47:11 +08:00
function partyValue ( parties : Array < Record < string , unknown > > , type : 'SENDER' | 'RECEIVER' , key : string ) {
2026-08-07 10:45:19 +08:00
const party = parties . find (( item ) => String ( item . partyType || '' ). toUpperCase () === type )
const value = party ? .[ key ]
return value === null || value === undefined || value === '' ? '—' : String ( value )
}
2026-08-07 14:47:11 +08:00
function partyAddress ( parties : Array < Record < string , unknown > > , type : 'SENDER' | 'RECEIVER' ) {
2026-08-07 10:45:19 +08:00
const party = parties . find (( item ) => String ( item . partyType || '' ). toUpperCase () === type )
if ( ! party ) return '—'
return [ 'province' , 'city' , 'district' , 'addressLine' ]. map (( key ) => party [ key ]). filter (( value ) => typeof value === 'string' && value . trim ()). join ( ' ' ) || '—'
}
function cargoTotal ( cargoes : Array < Record < string , unknown > > , key : string ) {
const total = cargoes . reduce (( sum , cargo ) => {
const value = Number ( cargo [ key ])
return Number . isFinite ( value ) ? sum + value : sum
}, 0 )
return total > 0 ? total : null
}
function displayBoolean ( value : boolean | null | undefined ) {
return value === null || value === undefined ? '—' : value ? '是' : '否'
}
2026-08-06 19:03:13 +08:00
function displayField ( row : UnifiedOrderResource , property : string | undefined ) {
if ( ! property ) return '—'
const source = row as unknown as Record < string , unknown >
const nested : Record < string , string > = {
measurementReviewStatus : 'measurement.reviewStatus' , expectedArrivalAt : 'expectedArrivalAt' ,
loadingStatus : 'transport.loadingStatus' , containerNo : 'transport.containerNo' , cabinetNo : 'transport.cabinetNo' ,
sailingAt : 'transport.sailingAt' , portArrivalAt : 'transport.portArrivalAt' ,
chargeableWeightKg : 'measurement.chargeableWeightKg' , measurementCompletedAt : 'measurement.completedAt' ,
totalGrossWeightKg : 'measurement.actualGrossWeightKg' , totalVolumeCbm : 'measurement.actualVolumeCbm' ,
expressTrackingNo : 'delivery.trackingNo' ,
}
const path = nested [ property ] || property
let value = path . split ( '.' ). reduce < unknown >(( current , key ) => (
current && typeof current === 'object' ? ( current as Record < string , unknown >)[ key ] : undefined
), source )
if (( value === null || value === undefined || value === '' ) && property === 'internalOrderStatus' ) value = source . mainStatus
if ( value === null || value === undefined || value === '' ) return '—'
if ( typeof value === 'boolean' ) return value ? '是' : '否'
if ( Array . isArray ( value )) return value . length ? ` ${ value . length } 个文件` : '—'
if ( typeof value === 'number' ) return formatNumber ( value )
if ( property . endsWith ( 'At' ) || property . endsWith ( 'Date' )) return formatDate ( String ( value ))
if ( property === 'measurementReviewStatus' ) return measurementStatusLabel ( String ( value ))
2026-08-07 09:06:22 +08:00
if ( property === 'internalOrderStatus' ) return orderStatusLabel ( String ( value ))
2026-08-06 19:03:13 +08:00
return String ( value )
}
2026-08-06 13:56:04 +08:00
function statusLabel ( value : UnifiedOrderMainStatus | string | null ) {
2026-08-07 10:45:19 +08:00
return unifiedOrderStatusOptions . find (( item ) => item . value === value ) ? . label || orderStatusLabel ( value ) || '状态更新'
}
function historyActionLabel ( value : string | null | undefined ) {
return ({
CREATE : '创建订单' , CREATED : '创建订单' , SUBMITTED : '提交订单' , APPROVED : '审核通过' ,
ORDER_APPROVED : '订单审核通过' , REJECTED : '审核驳回' , MEASURE_APPROVE : '测量审核通过' ,
MEASURE_REJECT : '测量审核驳回' , INBOUND_BEGIN : '开始入库' , INBOUND_COMPLETE : '入库完成' ,
UI_FIXTURE_MEASURE_APPROVE : '测量数据审核通过' , UI_FIXTURE_INBOUND_BEGIN : '开始入库' ,
STATUS_UPDATE : '状态更新' ,
} as Record < string , string >)[ value || '' ] || ( value ? value . replaceAll ( '_' , ' ' ) : '状态更新' )
2026-08-06 13:56:04 +08:00
}
2026-08-06 19:03:13 +08:00
function measurementStatusLabel ( value : string | null | undefined ) {
2026-08-07 09:06:22 +08:00
return ({ NOT_SUBMITTED : '未提交' , UNRECORDED : '未录入' , PENDING : '待审核' , APPROVED : '审核通过' , REJECTED : '审核驳回' } as Record < string , string >)[ value || '' ] || value || '未录入'
}
function orderStatusLabel ( value : string | null | undefined ) {
return ({
DRAFT : '草稿' , SUBMITTED : '待审核' , APPROVED : '已审核' , ACCEPTED : '已受理' , REJECTED : '已驳回' ,
PROCESSING : '处理中' , COMPLETED : '已完成' , CANCELLED : '已取消' , PENDING_REVIEW : '待审核' ,
WAIT_MEASURE : '待测量数据' , INBOUNDED : '已入库' , WAIT_SPLIT : '待分柜' , LOADING : '装柜中' ,
LOADED : '装柜完成' , DELIVERED : '派送完成' ,
} as Record < string , string >)[ value || '' ] || value || '—'
2026-08-06 19:03:13 +08:00
}
2026-08-07 14:47:11 +08:00
function cargoValue ( cargo : Record < string , unknown >, key : string ) {
const value = cargo [ key ]
if ( key === 'circumferenceCm' ) {
const width = Number ( cargo . widthCm )
const height = Number ( cargo . heightCm )
return Number . isFinite ( width ) && Number . isFinite ( height ) ? formatNumber ( 2 * ( width + height )) : '—'
}
if ( value === null || value === undefined || value === '' ) return '—'
if ( typeof value === 'number' ) return formatNumber ( value )
return String ( value )
}
function cargoImage ( cargo : Record < string , unknown >) {
const value = cargo . imageUrl || cargo . cargoImageUrl || cargo . image
return typeof value === 'string' && value ? value : ''
}
function feeDate ( value : string | null | undefined ) {
return value ? formatDate ( value ) : '—'
}
function feeText ( value : string | null | undefined ) {
return value || '—'
}
function feeNumber ( value : number | null | undefined ) {
return value === null || value === undefined ? '—' : formatNumber ( value , 4 )
}
function feeReviewLabel ( value : string | null | undefined ) {
return ({ PENDING : '待审核' , APPROVED : '审核通过' , REJECTED : '审核驳回' , NOT_SUBMITTED : '未提交' } as Record < string , string >)[ value || '' ] || value || '—'
}
function feeDirectionLabel ( value : string | null | undefined ) {
return ({ RECEIVABLE : '应收' , PAYABLE : '应付' } as Record < string , string >)[ value || '' ] || value || '—'
}
async function loadFeeDetails () {
const summary = detail . value ? . summary
if ( ! summary ? . inboundId || ! can ( 'firstleg:inbound:fee-detail:view' )) return
const key = ` ${ summary . tenantId } : ${ summary . inboundId } `
if ( feeLoadedKey . value === key || feeLoading . value ) return
feeLoading . value = true
try {
feeDetails . value = await listPlatformUnifiedOrderFeeDetails ( String ( summary . tenantId ), String ( summary . inboundId ))
feeLoadedKey . value = key
} catch {
feeDetails . value = []
} finally {
feeLoading . value = false
}
}
function measurementValue ( row : Record < string , unknown >, key : string , customerSource = false ) {
if ( customerSource && key === 'unitWeightKg' ) {
const total = Number ( row . grossWeightKg )
const count = Number ( row . packageCount )
return Number . isFinite ( total ) && Number . isFinite ( count ) && count > 0 ? formatNumber ( total / count , 4 ) : '—'
}
if ( customerSource && key === 'unitVolumeCbm' ) {
const total = Number ( row . volumeCbm )
const count = Number ( row . packageCount )
return Number . isFinite ( total ) && Number . isFinite ( count ) && count > 0 ? formatNumber ( total / count , 6 ) : '—'
}
if ( customerSource && key === 'remark' ) return String ( row . cargoRemark || '—' )
const value = row [ key ]
if ( value === null || value === undefined || value === '' ) return '—'
return typeof value === 'number' ? formatNumber ( value , 6 ) : String ( value )
}
interface MeasurementTotals {
count : number
gross : number
volume : number
volumetric : number
}
function measurementTotals ( rows : Record < string , unknown >[], customerSource = false ) {
const values = rows . reduce < MeasurementTotals >(( result , row ) => {
const count = Number ( row . packageCount )
const gross = Number ( customerSource ? row . grossWeightKg : row . totalWeightKg )
const volume = Number ( customerSource ? row . volumeCbm : row . totalVolumeCbm )
const volumetric = Number ( row . volumeWeightKg )
if ( Number . isFinite ( count )) result . count += count
if ( Number . isFinite ( gross )) result . gross += gross
if ( Number . isFinite ( volume )) result . volume += volume
if ( Number . isFinite ( volumetric )) result . volumetric += volumetric
return result
}, { count : 0 , gross : 0 , volume : 0 , volumetric : 0 })
return {
count : values . count || null , gross : values . gross || null , volume : values . volume || null ,
volumetric : values . volumetric || null , weightVolume : values . volume || null ,
}
}
function revokeMeasurementImages () {
measurementImageUrls . value . forEach (( url ) => URL . revokeObjectURL ( url ))
measurementImageUrls . value = []
}
async function loadMeasurementDetails () {
const summary = detail . value ? . summary
if ( ! summary ? . inboundId || ! can ( 'platform:firstleg:inbound:view' )) return
const key = ` ${ summary . tenantId } : ${ summary . inboundId } `
if ( measurementLoadedKey . value === key || measurementLoading . value ) return
measurementLoading . value = true
try {
measurementDetail . value = await getInboundMeasurement ( String ( summary . tenantId ), String ( summary . inboundId ))
measurementLoadedKey . value = key
revokeMeasurementImages ()
const fileIds = [... measurementDetail . value . items . map (( item ) => item . intelligentImageFileId ), ... measurementDetail . value . weightResults . map (( item ) => item . fileId )]
. filter (( value ) : value is string => Boolean ( value ))
const blobs = await Promise . all ( fileIds . map (( fileId ) => downloadWeightImage ( String ( summary . tenantId ), String ( summary . inboundId ), fileId ). catch (() => null )))
measurementImageUrls . value = blobs . filter (( blob ) : blob is Blob => Boolean ( blob )). map (( blob ) => URL . createObjectURL ( blob ))
} catch {
measurementDetail . value = null
revokeMeasurementImages ()
} finally {
measurementLoading . value = false
}
}
function selectDetailTab ( key : string ) {
activeDetailTab . value = key
if ( key === 'fees' ) void loadFeeDetails ()
if ( key === 'measurement' ) void loadMeasurementDetails ()
}
function toggleMeasurementSection ( section : keyof typeof measurementCollapsed ) {
measurementCollapsed [ section ] = ! measurementCollapsed [ section ]
}
2026-08-06 13:56:04 +08:00
function nextActionBoundary ( row : UnifiedOrderResource ) {
2026-08-06 20:14:56 +08:00
return row . nextAction === 'CONFIRM_MEASUREMENT' ? '待完成测量' : ''
2026-08-06 13:56:04 +08:00
}
2026-08-06 18:02:04 +08:00
function canApprove ( row : UnifiedOrderResource ) {
return row . mainStatus === 'PENDING_REVIEW' && row . nextAction === 'APPROVE_ORDER' && can ( 'platform:order:approve' )
}
2026-08-06 19:03:13 +08:00
function canBulkReview ( row : UnifiedOrderResource ) {
return canApprove ( row )
}
function handleSelectionChange ( rows : UnifiedOrderResource []) {
selectedOrders . value = rows
}
function openBulkReviewPlaceholder () {
if ( selectedOrders . value . length === 0 ) return
ElMessage . info ( '批量审核接口待接入,请先逐笔审核' )
}
2026-08-07 10:45:19 +08:00
function openApproval ( row : UnifiedOrderResource ) {
2026-08-06 18:02:04 +08:00
if ( ! canApprove ( row )) return
approvalOrder . value = row
approvalOpen . value = true
}
function closeApproval () {
if ( approvalSaving . value ) return
approvalOpen . value = false
approvalOrder . value = null
}
2026-08-07 10:45:19 +08:00
async function approveOrder () {
2026-08-06 18:02:04 +08:00
if ( ! approvalOrder . value ) return
const order = approvalOrder . value
const orderId = String ( order . orderId )
const tenantId = String ( order . tenantId )
approvalSaving . value = true
approvalLoadingId . value = orderId
try {
await approvePlatformUnifiedOrder ( tenantId , orderId )
approvalOpen . value = false
await loadOrders ()
if ( detail . value ? . summary . orderId === order . orderId ) await loadDetail ()
approvalOrder . value = null
2026-08-07 10:45:19 +08:00
ElMessage . success ( '订单审核通过' )
2026-08-06 18:02:04 +08:00
} catch ( error ) {
2026-08-07 10:45:19 +08:00
ElMessage . error ( getApiErrorDetails ( error , '订单审核失败,请稍后重试' ). message )
2026-08-06 18:02:04 +08:00
} finally {
approvalSaving . value = false
approvalLoadingId . value = ''
}
}
2026-08-06 20:14:56 +08:00
function openNoteDialog ( order ?: UnifiedOrderResource ) {
if ( ! can ( 'platform:order:note' )) return
const target = order || detail . value ? . summary || null
if ( ! target ) return
noteOrder . value = target
2026-08-06 19:03:13 +08:00
noteContent . value = ''
noteOpen . value = true
}
async function saveNote () {
const content = noteContent . value . trim ()
2026-08-06 20:14:56 +08:00
if ( ! noteOrder . value || ! content ) {
2026-08-06 19:03:13 +08:00
ElMessage . warning ( '请填写内部备注' )
return
}
noteSaving . value = true
try {
2026-08-06 20:14:56 +08:00
const target = noteOrder . value
await addPlatformUnifiedOrderNote ( String ( target . tenantId ), String ( target . orderId ), content )
2026-08-06 19:03:13 +08:00
noteOpen . value = false
2026-08-07 14:47:11 +08:00
await loadOrders ()
if ( detail . value ? . summary . orderId === target . orderId ) await loadDetail ( true )
2026-08-06 20:14:56 +08:00
noteOrder . value = null
2026-08-06 19:03:13 +08:00
ElMessage . success ( '内部备注已保存' )
} catch ( error ) {
ElMessage . error ( getApiErrorDetails ( error , '内部备注保存失败,请稍后重试' ). message )
} finally {
noteSaving . value = false
}
}
2026-08-06 13:56:04 +08:00
async function loadCustomers () {
customerLoading . value = true
customerLoadError . value = null
try {
const records : PlatformTenantResource [] = []
let currentPage = 1
let pages = 1
do {
const result = await listPlatformTenants ({ page : currentPage , size : 200 })
records . push (... result . records )
pages = Math . max ( 1 , result . pages )
currentPage += 1
} while ( currentPage <= pages )
customers . value = records
} catch ( error ) {
customers . value = []
customerLoadError . value = getApiErrorDetails ( error , '客户列表加载失败,暂时无法按客户筛选' )
} finally {
customerLoading . value = false
}
}
2026-08-06 15:32:02 +08:00
async function loadWarehouses () {
warehouseLoading . value = true
warehouseLoadError . value = null
try {
warehouses . value = await listAllPlatformWarehouses ({ status : 'ACTIVE' })
} catch ( error ) {
warehouses . value = []
warehouseLoadError . value = getApiErrorDetails ( error , '平台仓库加载失败,暂时无法按仓库筛选' )
} finally {
warehouseLoading . value = false
}
}
2026-08-06 13:56:04 +08:00
async function loadOrders () {
loading . value = true
loadError . value = null
try {
2026-08-11 18:06:35 +08:00
const activeFilters = appliedFilters . value
const range = activeFilters . createdRange
2026-08-06 13:56:04 +08:00
const result = await listPlatformUnifiedOrders ({
2026-08-11 18:06:35 +08:00
tenantId : activeFilters . tenantId || undefined ,
mainStatus : selectedStatus . value || undefined ,
receivingWarehouseId : activeFilters . receivingWarehouseId || undefined ,
destinationWarehouseId : activeFilters . destinationWarehouseId || undefined ,
inboundCode : trimmed ( activeFilters . inboundCode ),
expectedArrivalDate : activeFilters . expectedArrivalDate || undefined ,
feeReviewStatus : activeFilters . feeReviewStatus || undefined ,
channelName : trimmed ( activeFilters . channelName ),
containerNo : trimmed ( activeFilters . containerNo ),
cabinetNo : trimmed ( activeFilters . cabinetNo ),
sailingDate : activeFilters . sailingDate || undefined ,
loaded : optionalBoolean ( activeFilters . loaded ),
fullContainer : optionalBoolean ( activeFilters . fullContainer ),
measurementReviewStatus : activeFilters . measurementReviewStatus || undefined ,
fbaCode : trimmed ( activeFilters . fbaCode ),
2026-08-06 13:56:04 +08:00
createdFrom : range . length === 2 ? ` ${ range [ 0 ] } T00:00:00` : undefined ,
createdTo : range . length === 2 ? ` ${ range [ 1 ] } T23:59:59` : undefined ,
page : page . value ,
size : pageSize . value ,
})
records . value = result . records
2026-08-06 19:03:13 +08:00
selectedOrders . value = []
2026-08-06 13:56:04 +08:00
total . value = result . total
} catch ( error ) {
records . value = []
total . value = 0
loadError . value = getApiErrorDetails ( error , '统一订单列表加载失败,请检查 C02 统一查询接口和订单权限' )
} finally {
loading . value = false
}
}
function search () {
2026-08-11 18:06:35 +08:00
appliedFilters . value = cloneFilters ( filters . value )
2026-08-06 13:56:04 +08:00
if ( page . value !== 1 ) {
page . value = 1
return
}
void loadOrders ()
}
function reset () {
2026-08-06 15:32:02 +08:00
filters . value = emptyFilters ()
2026-08-11 18:06:35 +08:00
appliedFilters . value = emptyFilters ()
2026-08-10 18:12:23 +08:00
if ( page . value === 1 ) void loadOrders ()
else page . value = 1
2026-08-06 13:56:04 +08:00
}
2026-08-06 19:03:13 +08:00
async function openDetail ( row : UnifiedOrderResource ) {
await router . push ({ name : 'unified-order-detail' , params : { orderId : String ( row . orderId ) }, query : { tenantId : String ( row . tenantId ) } })
await nextTick ()
window . scrollTo ({ top : 0 })
2026-08-06 13:56:04 +08:00
}
function openMeasurement ( row : UnifiedOrderResource ) {
if ( ! row . inboundId || ! can ( 'platform:firstleg:inbound:view' )) return
void router . push ({ name : 'inbound-order-detail' , params : { inboundId : String ( row . inboundId ) }, query : { tenantId : String ( row . tenantId ) } })
}
2026-08-06 20:14:56 +08:00
function openInboundFromDetail () {
const summary = detail . value ? . summary
if ( ! summary ? . inboundId || ! can ( 'platform:firstleg:inbound:view' )) return
void router . push ({
name : 'inbound-order-detail' ,
params : { inboundId : String ( summary . inboundId ) },
query : { tenantId : String ( summary . tenantId ) },
})
2026-08-06 13:56:04 +08:00
}
2026-08-10 17:42:17 +08:00
type UnifiedOrderRowAction = 'note' | 'approve' | 'measurement'
const unifiedOrderRowActionIcons : Record < UnifiedOrderRowAction , Component > = {
note : StickyNote ,
approve : CircleCheck ,
measurement : Ruler ,
}
function unifiedOrderSecondaryActions ( row : UnifiedOrderResource ) : UnifiedOrderRowAction [] {
return [
...( can ( 'platform:order:note' ) ? [ 'note' as const ] : []),
...( canApprove ( row ) ? [ 'approve' as const ] : []),
...( row . mainStatus === 'WAIT_MEASURE' && row . inboundId && can ( 'platform:firstleg:inbound:view' ) ? [ 'measurement' as const ] : []),
]
}
function directUnifiedOrderActions ( row : UnifiedOrderResource ) {
return unifiedOrderSecondaryActions ( row ). slice ( 0 , 2 )
}
function moreUnifiedOrderActions ( row : UnifiedOrderResource ) {
return unifiedOrderSecondaryActions ( row ). slice ( 2 )
}
function moreUnifiedOrderActionsLabel ( row : UnifiedOrderResource ) {
const compatibleOrder = row as UnifiedOrderResource & { id ?: string | number | null }
const identifier = [ compatibleOrder . inboundCode , compatibleOrder . orderNo , compatibleOrder . orderId , compatibleOrder . id ]
. map (( value ) => String ( value ?? '' ). trim ())
. find ( Boolean )
return `更多订单操作: ${ identifier || '当前订单' } `
}
function unifiedOrderActionLabel ( action : UnifiedOrderRowAction ) {
if ( action === 'note' ) return '添加备注'
if ( action === 'approve' ) return '审核通过'
return '测量作业'
}
function unifiedOrderActionType ( action : UnifiedOrderRowAction ) {
return action === 'approve' ? 'success' : 'primary'
}
2026-08-06 20:14:56 +08:00
function runMoreAction ( command : string | number | object , row : UnifiedOrderResource ) {
if ( command === 'note' ) openNoteDialog ( row )
if ( command === 'approve' ) openApproval ( row )
if ( command === 'measurement' ) openMeasurement ( row )
2026-08-06 13:56:04 +08:00
}
function closeDetail () {
detail . value = null
detailError . value = null
void router . push ({ name : 'unified-orders' })
}
2026-08-07 14:47:11 +08:00
async function loadDetail ( force = false ) {
2026-08-06 13:56:04 +08:00
if ( ! orderId . value ) return
2026-08-07 20:07:37 +08:00
const tenantId = detailTenantId . value
2026-08-06 13:56:04 +08:00
if ( ! tenantId ) {
detailError . value = { message : '订单详情缺少客户标识,请从订单列表重新打开' }
return
}
2026-08-07 14:47:11 +08:00
const requestKey = ` ${ tenantId } : ${ orderId . value } `
if ( ! force && ( detailLoadingKey === requestKey || detail . value ? . summary . orderId === orderId . value && String ( detail . value . summary . tenantId ) === tenantId )) return
2026-08-07 20:07:37 +08:00
const serial = ++ detailRequestSerial
2026-08-07 14:47:11 +08:00
detailLoadingKey = requestKey
feeDetails . value = []
feeLoadedKey . value = ''
measurementDetail . value = null
measurementLoadedKey . value = ''
revokeMeasurementImages ()
2026-08-06 13:56:04 +08:00
detailLoading . value = true
detailError . value = null
try {
2026-08-07 20:07:37 +08:00
const response = await getPlatformUnifiedOrder ( tenantId , orderId . value )
if ( serial !== detailRequestSerial || requestKey !== detailRouteKey . value ) return
detail . value = response
2026-08-06 13:56:04 +08:00
} catch ( error ) {
2026-08-07 20:07:37 +08:00
if ( serial !== detailRequestSerial || requestKey !== detailRouteKey . value ) return
2026-08-06 13:56:04 +08:00
detail . value = null
detailError . value = getApiErrorDetails ( error , '统一订单详情加载失败,请检查 C02 统一详情接口和订单权限' )
} finally {
2026-08-07 20:07:37 +08:00
if ( serial === detailRequestSerial ) detailLoading . value = false
if ( serial === detailRequestSerial && detailLoadingKey === requestKey ) detailLoadingKey = ''
2026-08-06 13:56:04 +08:00
}
}
watch ([ page , pageSize ], () => { void loadOrders () })
2026-08-07 20:07:37 +08:00
watch ( detailRouteKey , ( value ) => {
2026-08-06 19:03:13 +08:00
if ( value ) {
activeDetailTab . value = 'basic'
2026-08-07 20:07:37 +08:00
detail . value = null
detailError . value = null
2026-08-06 19:03:13 +08:00
void loadDetail ()
2026-08-07 20:07:37 +08:00
} else {
detailRequestSerial += 1
detail . value = null
detailError . value = null
detailLoading . value = false
2026-08-06 19:03:13 +08:00
}
2026-08-06 13:56:04 +08:00
}, { immediate : true })
onMounted (() => {
void loadCustomers ()
2026-08-06 15:32:02 +08:00
void loadWarehouses ()
2026-08-07 14:47:11 +08:00
if ( ! orderId . value ) void loadOrders ()
2026-08-06 13:56:04 +08:00
})
</ script >
< template >
2026-08-10 15:36:53 +08:00
< AdminPage >
< section class = "business-page unified-orders-page" >
2026-08-06 19:03:13 +08:00
< template v-if = "orderId" >
2026-08-10 15:36:53 +08:00
< DetailPageLayout ariaLabel = "统一订单详情" >
2026-08-06 19:03:13 +08:00
< header class = "order-detail-header" >
< div class = "order-detail-heading" >
2026-08-06 20:14:56 +08:00
< el-button link class = "back-button" @click ="closeDetail" >< ArrowLeft :size = "16" /> 返回列表 </ el-button >
< span class = "order-mark" aria -hidden =" true " >< Box :size = "19" /></ span >
2026-08-06 19:03:13 +08:00
< div class = "order-title" >
2026-08-06 20:14:56 +08:00
< h1 >
< el-button v-if = "detail?.summary.inboundId && can('platform:firstleg:inbound:view')" link type="primary" class="inbound-code-link" @click="openInboundFromDetail" >{{ detail.summary.inboundCode || ` 入库单 $ { detail.summary.inboundId } ` }} </ el -button >
< span v-else >{{ detail ? .summary.inboundCode || ' 暂未生成 ' }}</ span >
</ h1 >
2026-08-06 19:03:13 +08:00
< p >{{ detail ? . summary . orderNo || `订单 ${ orderId } ` }}</ p >
</ div >
< StatusBadge v-if = "detail" :status="statusLabel(detail.summary.mainStatus)" />
</ div >
2026-08-06 20:14:56 +08:00
< div v-if = "detail && canApprove(detail.summary)" class="detail-header-actions" >
2026-08-06 19:03:13 +08:00
< el-button v-if = "canApprove(detail.summary)" type="success" plain :loading="approvalLoadingId === String(detail.summary.orderId)" @click="openApproval(detail.summary)"><CircleCheck :size="15" /> 审核通过 </ el-button >
</ div >
</ header >
2026-08-06 15:32:02 +08:00
2026-08-06 19:03:13 +08:00
< LoadErrorState v-if = "detailError" class="detail-load-error" title="订单详情暂时无法加载" :message="detailError.message" :loading="detailLoading" @retry="loadDetail" >
< template # details >< ApiErrorMeta :details = "detailError" /></ template >
</ LoadErrorState >
< div v-else v-loading = "detailLoading" class="order-detail-content" >
< template v-if = "detail" >
< section class = "detail-overview" aria -label =" 订单概览 " >
< article >< span > 客户 </ span >< strong >{{ customerName ( detail . summary ) }}</ strong >< small >{{ detail . summary . channelName || '未配置渠道' }}</ small ></ article >
< article >< span > 总箱数 </ span >< strong >{{ detail . summary . totalPackageCount ?? '—' }}< small > 箱 </ small ></ strong >< small >{{ detail . order . cargoes . length }} 条货品明细 </ small ></ article >
< article >< span > 实测毛重 </ span >< strong >{{ formatNumber ( detail . summary . measurement ? . actualGrossWeightKg ) }}< small > kg </ small ></ strong >< small >{{ measurementStatusLabel ( detail . summary . measurement ? . reviewStatus ) }}</ small ></ article >
< article >< span > 实测体积 </ span >< strong >{{ formatNumber ( detail . summary . measurement ? . actualVolumeCbm , 3 ) }}< small > m³ </ small ></ strong >< small > 最近更新 {{ formatDate ( detail . summary . updatedAt ) }}</ small ></ article >
</ section >
< div class = "detail-tabs" role = "tablist" aria -label =" 订单详情模块 " >
2026-08-10 09:12:06 +08:00
< button v-for = "tab in detailTabOptions" :id="`order-detail-tab-${tab.key}`" :key="tab.key" type="button" role="tab" :class="{ active: activeDetailTab === tab.key }" :aria-selected="activeDetailTab === tab.key" :aria-controls="`order-detail-panel-${tab.key}`" :tabindex="activeDetailTab === tab.key ? 0 : -1" @click="selectDetailTab(tab.key)"><component :is="tab.icon" :size="17" />< span >{{ tab . label }}</ span ></ button >
2026-08-06 19:03:13 +08:00
</ div >
< section :id = "`order-detail-panel-${activeDetailTab}`" class = "detail-panel" role = "tabpanel" :aria-labelledby = "`order-detail-tab-${activeDetailTab}`" tabindex = "0" >
< template v-if = "activeDetailTab === 'basic'" >
< div class = "panel-title" >< div >< h2 > 订单基本信息 </ h2 >< p >{{ detail . summary . orderNo || `订单 ${ detail . summary . orderId } ` }}</ p ></ div >< Box :size = "19" /></ div >
< dl class = "order-info-grid" >
< div >< dt > 公司 </ dt >< dd >{{ customerName ( detail . summary ) }}</ dd ></ div >
< div >< dt > 运输方式 </ dt >< dd >{{ detail . order . order ? . transportMode || '—' }}</ dd ></ div >
< div >< dt > 当前状态 </ dt >< dd >< StatusBadge :status = "statusLabel(detail.summary.mainStatus)" /></ dd ></ div >
2026-08-07 10:45:19 +08:00
< div >< dt > 内部订单状态 </ dt >< dd >{{ detail . summary . internalOrderStatus || '—' }}</ dd ></ div >
2026-08-06 19:03:13 +08:00
< div >< dt > 平台收货仓 </ dt >< dd >{{ detail . summary . receivingWarehouseName || '—' }}</ dd ></ div >
2026-08-07 10:45:19 +08:00
< div >< dt > 收货仓地址 </ dt >< dd >{{ detail . order . order ? . receivingWarehouseAddress || '—' }}</ dd ></ div >
2026-08-06 19:03:13 +08:00
< div >< dt > 目的仓 </ dt >< dd >{{ detail . summary . destinationWarehouseName || '—' }}</ dd ></ div >
< div >< dt > 目的国家 </ dt >< dd >{{ detail . order . order ? . destinationCountryCode || '—' }}</ dd ></ div >
2026-08-07 10:45:19 +08:00
< div >< dt > 目的仓地址 </ dt >< dd >{{ detail . summary . destinationAddress || '—' }}</ dd ></ div >
< div >< dt > 目的仓邮编 </ dt >< dd >{{ detail . summary . destinationPostalCode || '—' }}</ dd ></ div >
< div >< dt > 目的仓联系人 </ dt >< dd >{{ detail . summary . destinationContact || '—' }}</ dd ></ div >
< div >< dt > 目的仓联系人电话 </ dt >< dd >{{ detail . summary . destinationContactPhone || '—' }}</ dd ></ div >
2026-08-06 19:03:13 +08:00
< div >< dt > 渠道 </ dt >< dd >{{ detail . summary . channelName || '—' }}</ dd ></ div >
2026-08-07 14:47:11 +08:00
< div >< dt > 业务类型 </ dt >< dd >{{ detail . summary . businessType || '—' }}</ dd ></ div >
< div >< dt > 贸易条款 </ dt >< dd >{{ detail . order . order ? . tradeTerm || '—' }}</ dd ></ div >
2026-08-07 10:45:19 +08:00
< div >< dt > 买单 / 退税 </ dt >< dd >{{ detail . order . order ? . taxMode || '—' }}</ dd ></ div >
< div >< dt > 费用审核状态 </ dt >< dd >{{ detail . summary . feeReviewStatus || '—' }}</ dd ></ div >
< div >< dt > 是否扣货 </ dt >< dd >{{ displayBoolean ( detail . summary . holdCargo ) }}</ dd ></ div >
< div >< dt > 扣货理由 </ dt >< dd >{{ detail . summary . holdReason || '—' }}</ dd ></ div >
< div >< dt > 提货方式 </ dt >< dd >{{ detail . order . order ? . pickupMethod || '—' }}</ dd ></ div >
< div >< dt > 提货时间 </ dt >< dd >{{ formatDate ( detail . summary . pickupAt ) }}</ dd ></ div >
< div >< dt > 到仓时间 </ dt >< dd >{{ detail . order . order ? . receivingTime || '—' }}</ dd ></ div >
< div >< dt > 货物到仓时间 </ dt >< dd >{{ formatDate ( detail . summary . expectedArrivalAt || detail . summary . inboundCompletedAt ) }}</ dd ></ div >
2026-08-06 19:03:13 +08:00
< div >< dt > 预计出运 </ dt >< dd >{{ formatDate ( detail . summary . expectedShipDate ) }}</ dd ></ div >
2026-08-07 10:45:19 +08:00
< div >< dt > 预计到达时间 </ dt >< dd >{{ formatDate ( detail . summary . expectedArrivalAt ) }}</ dd ></ div >
2026-08-06 19:03:13 +08:00
< div >< dt > 集装箱号 </ dt >< dd >{{ detail . summary . transport ? . containerNo || '—' }}</ dd ></ div >
2026-08-07 10:45:19 +08:00
< div >< dt > 柜号 </ dt >< dd >{{ detail . summary . transport ? . cabinetNo || '—' }}</ dd ></ div >
2026-08-06 19:03:13 +08:00
< div >< dt > 船名 / 航次 </ dt >< dd >{{ detail . summary . transport ? . vesselName || '—' }} / {{ detail . summary . transport ? . voyageNo || '—' }}</ dd ></ div >
< div >< dt > 开船时间 </ dt >< dd >{{ formatDate ( detail . summary . transport ? . sailingAt ) }}</ dd ></ div >
< div >< dt > 到港时间 </ dt >< dd >{{ formatDate ( detail . summary . transport ? . portArrivalAt ) }}</ dd ></ div >
2026-08-07 10:45:19 +08:00
< div >< dt > 配载状态 </ dt >< dd >{{ detail . summary . transport ? . loadingStatus || '—' }}</ dd ></ div >
< div >< dt > 目的仓类型 </ dt >< dd >{{ detail . order . order ? . destinationType || '—' }}</ dd ></ div >
< div >< dt > 是否整柜 </ dt >< dd >{{ detail . order . order ? . containerMode || '—' }}</ dd ></ div >
< div >< dt > 商检验 </ dt >< dd >{{ displayBoolean ( detail . order . order ? . customsInspection ) }}</ dd ></ div >
< div >< dt > 总箱数 </ dt >< dd >{{ detail . summary . totalPackageCount ?? '—' }} 箱 </ dd ></ div >
< div >< dt > 总托盘数 </ dt >< dd >{{ detail . summary . totalPalletCount ?? '—' }}</ dd ></ div >
< div >< dt > 总毛重 </ dt >< dd >{{ formatNumber ( cargoTotal ( detail . order . cargoes , 'grossWeightKg' ) ?? detail . summary . measurement ? . actualGrossWeightKg ) }} kg </ dd ></ div >
< div >< dt > 总净重 </ dt >< dd >{{ formatNumber ( cargoTotal ( detail . order . cargoes , 'netWeightKg' )) }} kg </ dd ></ div >
< div >< dt > 总体积 </ dt >< dd >{{ formatNumber ( cargoTotal ( detail . order . cargoes , 'volumeCbm' ) ?? detail . summary . measurement ? . actualVolumeCbm , 3 ) }} m³ </ dd ></ div >
< div >< dt > 计费毛重 </ dt >< dd >{{ formatNumber ( detail . summary . measurement ? . chargeableWeightKg ) }} kg </ dd ></ div >
< div >< dt > 计费体积 </ dt >< dd >{{ formatNumber ( detail . summary . chargeableVolumeCbm , 3 ) }} m³ </ dd ></ div >
< div >< dt > 总材积重 </ dt >< dd >{{ formatNumber ( detail . summary . volumetricWeightKg ) }} kg </ dd ></ div >
< div >< dt > 总材积量 </ dt >< dd >{{ formatNumber ( detail . summary . volumetricWeightKg ) }}</ dd ></ div >
< div >< dt > 库位位置 </ dt >< dd >{{ detail . summary . location || '—' }}</ dd ></ div >
2026-08-06 19:03:13 +08:00
< div >< dt > 创建时间 </ dt >< dd >{{ formatDate ( detail . summary . createdAt ) }}</ dd ></ div >
2026-08-07 10:45:19 +08:00
< div >< dt > 入库完成时间 </ dt >< dd >{{ formatDate ( detail . summary . inboundCompletedAt ) }}</ dd ></ div >
< div >< dt > 测量完成时间 </ dt >< dd >{{ formatDate ( detail . summary . measurementCompletedAt || detail . summary . measurement ? . completedAt ) }}</ dd ></ div >
< div class = "wide" >< dt > 提货联系人 </ dt >< dd >{{ partyValue ( detail . order . parties , 'SENDER' , 'contactName' ) }} · {{ detail . summary . pickupContactPhone || partyValue ( detail . order . parties , 'SENDER' , 'phone' ) }} · {{ detail . summary . pickupContactEmail || partyValue ( detail . order . parties , 'SENDER' , 'email' ) }}</ dd ></ div >
< div class = "wide" >< dt > 提货地址 </ dt >< dd >{{ detail . summary . pickupAddress || partyAddress ( detail . order . parties , 'SENDER' ) }} · {{ detail . summary . pickupPostalCode || partyValue ( detail . order . parties , 'SENDER' , 'postalCode' ) }}</ dd ></ div >
2026-08-07 14:47:11 +08:00
< div class = "wide" >< dt > 目的地联系人 </ dt >< dd >{{ partyValue ( detail . order . parties , 'RECEIVER' , 'contactName' ) }} · {{ partyValue ( detail . order . parties , 'RECEIVER' , 'phone' ) }} · {{ partyValue ( detail . order . parties , 'RECEIVER' , 'email' ) }}</ dd ></ div >
< div >< dt > 目的仓联系人邮箱 </ dt >< dd >{{ partyValue ( detail . order . parties , 'RECEIVER' , 'email' ) }}</ dd ></ div >
< div class = "wide" >< dt > 目的地地址 </ dt >< dd >{{ partyAddress ( detail . order . parties , 'RECEIVER' ) }} · {{ partyValue ( detail . order . parties , 'RECEIVER' , 'postalCode' ) }}</ dd ></ div >
2026-08-07 10:45:19 +08:00
< div class = "wide" >< dt > 备注 </ dt >< dd >{{ detail . order . order ? . customerRemark || '—' }}</ dd ></ div >
< div class = "wide" >< dt > 关联文件 </ dt >< dd >{{ detail . summary . associatedFiles ? . join ( '、' ) || '—' }}</ dd ></ div >
< div class = "wide" >< dt > 唛头文件 </ dt >< dd >{{ detail . summary . markFiles ? . join ( '、' ) || '—' }}</ dd ></ div >
< div class = "wide" >< dt > PC 端下单文件 </ dt >< dd >{{ detail . summary . pcOrderFiles ? . join ( '、' ) || '—' }}</ dd ></ div >
< div >< dt > 快递公司 </ dt >< dd >{{ detail . summary . expressCompany || detail . order . order ? . expressCompany || '—' }}</ dd ></ div >
< div >< dt > 快递单号 </ dt >< dd >{{ detail . summary . expressTrackingNo || detail . order . order ? . expressTrackingNo || '—' }}</ dd ></ div >
< div class = "wide" >< dt > 客服内部备注 </ dt >< dd >{{ detail . summary . internalRemark || '—' }}</ dd ></ div >
< div >< dt > 创建人 </ dt >< dd >{{ detail . summary . createdBy || '—' }}</ dd ></ div >
< div >< dt > 下一步 </ dt >< dd >{{ canApprove ( detail . summary ) ? '审核订单' : nextActionBoundary ( detail . summary ) || '—' }}</ dd ></ div >
2026-08-06 19:03:13 +08:00
</ dl >
</ template >
< template v-else-if = "activeDetailTab === 'cargo'" >
< div class = "panel-title" >< div >< h2 > 货品信息 </ h2 >< p > 共 {{ detail . order . cargoes . length }} 条 </ p ></ div >< PackageOpen :size = "19" /></ div >
< div class = "detail-table-wrap" >
< el-table :data = "detail.order.cargoes" empty -text =" 暂无货品信息 " >
2026-08-07 14:47:11 +08:00
< el-table-column prop = "itemNo" label = "#" width = "64" />
< el-table-column label = "报关HS_CODE" min -width =" 130 " >< template # default = "{ row }" >{{ cargoValue ( row as Record < string , unknown >, 'hsCode' ) }}</ template ></ el-table-column >
< el-table-column label = "货品图片" width = "110" >< template # default = "{ row }" >< el-image v-if = "cargoImage(row as Record<string, unknown>)" :src="cargoImage(row as Record<string, unknown>)" fit="contain" class="cargo-image" />< span v-else > — </ span ></ template > </ el -table-column >
< el-table-column label = "SKU NO." min -width =" 150 " >< template # default = "{ row }" >{{ cargoValue ( row as Record < string , unknown >, 'skuNo' ) }}</ template ></ el-table-column >
< el-table-column label = "中文品名" min -width =" 170 " >< template # default = "{ row }" >{{ cargoValue ( row as Record < string , unknown >, 'cargoName' ) }}</ template ></ el-table-column >
< el-table-column label = "英文品名" min -width =" 170 " >< template # default = "{ row }" >{{ cargoValue ( row as Record < string , unknown >, 'cargoNameEn' ) }}</ template ></ el-table-column >
< el-table-column label = "清关HS_CODE" min -width =" 130 " >< template # default = "{ row }" >{{ cargoValue ( row as Record < string , unknown >, 'customsHsCode' ) }}</ template ></ el-table-column >
< el-table-column label = "箱规长(cm)" min -width =" 110 " >< template # default = "{ row }" >{{ cargoValue ( row as Record < string , unknown >, 'lengthCm' ) }}</ template ></ el-table-column >
< el-table-column label = "箱规宽(cm)" min -width =" 110 " >< template # default = "{ row }" >{{ cargoValue ( row as Record < string , unknown >, 'widthCm' ) }}</ template ></ el-table-column >
< el-table-column label = "箱规高(cm)" min -width =" 110 " >< template # default = "{ row }" >{{ cargoValue ( row as Record < string , unknown >, 'heightCm' ) }}</ template ></ el-table-column >
< el-table-column label = "箱规周长(cm)" min -width =" 120 " >< template # default = "{ row }" >{{ cargoValue ( row as Record < string , unknown >, 'circumferenceCm' ) }}</ template ></ el-table-column >
< el-table-column label = "每箱产品数量" min -width =" 130 " >< template # default = "{ row }" >{{ cargoValue ( row as Record < string , unknown >, 'unitsPerPackage' ) }}</ template ></ el-table-column >
< el-table-column label = "箱数" width = "90" >< template # default = "{ row }" >{{ cargoValue ( row as Record < string , unknown >, 'packageCount' ) }}</ template ></ el-table-column >
< el-table-column label = "总毛重(T.KGW)" min -width =" 140 " >< template # default = "{ row }" >{{ cargoValue ( row as Record < string , unknown >, 'grossWeightKg' ) }}</ template ></ el-table-column >
< el-table-column label = "总净重(T.KNW)" min -width =" 140 " >< template # default = "{ row }" >{{ cargoValue ( row as Record < string , unknown >, 'netWeightKg' ) }}</ template ></ el-table-column >
2026-08-06 19:03:13 +08:00
</ el-table >
</ div >
</ template >
< template v-else-if = "activeDetailTab === 'fees'" >
2026-08-07 14:47:11 +08:00
< div class = "panel-title" >< div >< h2 > 费用信息 </ h2 >< p > 共 {{ feeDetails . length }} 条应收应付费用明细 </ p ></ div >< WalletCards :size = "19" /></ div >
< el-alert v-if = "!can('firstleg:inbound:fee-detail:view')" title="当前账号没有查看费用明细的权限" type="warning" :closable="false" show -icon />
< div v-else v-loading = "feeLoading" class="detail-table-wrap" >
< el-table :data = "feeDetails" empty -text =" 暂无费用数据 " row -key =" id " >
< el-table-column label = "审核状态" min -width =" 120 " >< template # default = "{ row }" >< StatusBadge : status = "feeReviewLabel((row as UnifiedOrderFeeDetailResource).reviewStatus)" /></ template ></ el-table-column >
< el-table-column label = "应收/应付" min -width =" 110 " >< template # default = "{ row }" >{{ feeDirectionLabel (( row as UnifiedOrderFeeDetailResource ). direction ) }}</ template ></ el-table-column >
< el-table-column label = "客户公司名" min -width =" 170 " >< template # default = "{ row }" >{{ feeText (( row as UnifiedOrderFeeDetailResource ). customerName ) }}</ template ></ el-table-column >
< el-table-column label = "裹箱头" min -width =" 150 " >< template # default = "{ row }" >{{ feeText (( row as UnifiedOrderFeeDetailResource ). invoiceTitle ) }}</ template ></ el-table-column >
< el-table-column label = "入仓码" min -width =" 150 " >< template # default = "{ row }" >{{ feeText (( row as UnifiedOrderFeeDetailResource ). inboundCode ) }}</ template ></ el-table-column >
< el-table-column label = "业务发生日" min -width =" 130 " >< template # default = "{ row }" >{{ feeDate (( row as UnifiedOrderFeeDetailResource ). businessDate ) }}</ template ></ el-table-column >
< el-table-column label = "渠道" min -width =" 130 " >< template # default = "{ row }" >{{ feeText (( row as UnifiedOrderFeeDetailResource ). channelName ) }}</ template ></ el-table-column >
< el-table-column label = "目的仓" min -width =" 150 " >< template # default = "{ row }" >{{ feeText (( row as UnifiedOrderFeeDetailResource ). destinationName ) }}</ template ></ el-table-column >
< el-table-column label = "FBA编号" min -width =" 130 " >< template # default = "{ row }" >{{ feeText (( row as UnifiedOrderFeeDetailResource ). fbaNo ) }}</ template ></ el-table-column >
< el-table-column label = "箱数" width = "90" >< template # default = "{ row }" >{{ feeNumber (( row as UnifiedOrderFeeDetailResource ). packageCount ) }}</ template ></ el-table-column >
< el-table-column label = "毛重" min -width =" 110 " >< template # default = "{ row }" >{{ feeNumber (( row as UnifiedOrderFeeDetailResource ). actualGrossWeightKg ) }}</ template ></ el-table-column >
< el-table-column label = "体积" min -width =" 110 " >< template # default = "{ row }" >{{ feeNumber (( row as UnifiedOrderFeeDetailResource ). actualVolumeCbm ) }}</ template ></ el-table-column >
< el-table-column label = "费用类型" min -width =" 150 " >< template # default = "{ row }" >{{ feeText (( row as UnifiedOrderFeeDetailResource ). feeTypeNameSnapshot ) }}</ template ></ el-table-column >
< el-table-column label = "货币" width = "90" >< template # default = "{ row }" >{{ feeText (( row as UnifiedOrderFeeDetailResource ). currency ) }}</ template ></ el-table-column >
< el-table-column label = "应收金额" min -width =" 120 " >< template # default = "{ row }" >{{ feeNumber (( row as UnifiedOrderFeeDetailResource ). amount ) }}</ template ></ el-table-column >
< el-table-column label = "计费体称/重量" min -width =" 150 " >< template # default = "{ row }" >{{ feeNumber (( row as UnifiedOrderFeeDetailResource ). chargeableQuantity ) }} {{ ( row as UnifiedOrderFeeDetailResource ). chargeUnit || '' }}</ template ></ el-table-column >
< el-table-column label = "单价" min -width =" 110 " >< template # default = "{ row }" >{{ feeNumber (( row as UnifiedOrderFeeDetailResource ). unitPrice ) }}</ template ></ el-table-column >
< el-table-column label = "入仓时间" min -width =" 150 " >< template # default = "{ row }" >{{ feeDate (( row as UnifiedOrderFeeDetailResource ). inboundedAt ) }}</ template ></ el-table-column >
< el-table-column label = "出仓时间" min -width =" 150 " >< template # default = "{ row }" >{{ feeDate (( row as UnifiedOrderFeeDetailResource ). outboundAt ) }}</ template ></ el-table-column >
< el-table-column label = "仓储天数" min -width =" 110 " >< template # default = "{ row }" >{{ feeNumber (( row as UnifiedOrderFeeDetailResource ). storageDays ) }}</ template ></ el-table-column >
< el-table-column label = "参与结算" min -width =" 110 " >< template # default = "{ row }" >{{ ( row as UnifiedOrderFeeDetailResource ). settlementIncluded == null ? '—' : ( row as UnifiedOrderFeeDetailResource ). settlementIncluded ? '是' : '否' }}</ template ></ el-table-column >
< el-table-column label = "账单编号" min -width =" 140 " >< template # default = "{ row }" >{{ feeText (( row as UnifiedOrderFeeDetailResource ). billNo ) }}</ template ></ el-table-column >
< el-table-column label = "结算起始日" min -width =" 130 " >< template # default = "{ row }" >{{ feeDate (( row as UnifiedOrderFeeDetailResource ). settlementStartDate ) }}</ template ></ el-table-column >
< el-table-column label = "结算结束日" min -width =" 130 " >< template # default = "{ row }" >{{ feeDate (( row as UnifiedOrderFeeDetailResource ). settlementEndDate ) }}</ template ></ el-table-column >
</ el-table >
</ div >
2026-08-06 19:03:13 +08:00
</ template >
< template v-else-if = "activeDetailTab === 'history'" >
< div class = "panel-title" >< div >< h2 > 状态历史 </ h2 >< p > 共 {{ detail . timeline . length }} 条 </ p ></ div >< History :size = "19" /></ div >
< el-empty v-if = "detail.timeline.length === 0" description="暂无状态历史" :image-size="64" />
2026-08-07 10:45:19 +08:00
< ol v-else class = "history-list" aria -label =" 状态历史 " >< li v-for = "(item, index) in detail.timeline" :key="`${item.source || 'event'}-${item.occurredAt || index}`"><span class="history-marker" aria-hidden="true" /><div><div class="history-line"><strong>{{ historyActionLabel(item.action || item.eventType) }}</strong><StatusBadge :status="statusLabel(item.mainStatus)" /></div><p>{{ item.operatorName || '系统' }} · {{ formatDate(item.occurredAt) }}</p><p v-if="item.remark" class="history-remark" >{{ item.remark }}</ p ></ div ></ li ></ ol >
2026-08-06 19:03:13 +08:00
</ template >
2026-08-11 09:16:58 +08:00
< template v-else-if = "activeDetailTab === 'fulfillment'" >
< div class = "panel-title" >< div >< h2 > 拆分履约 </ h2 >< p > 根单 、 已完成分片与当前余量 </ p ></ div >< ListChecks :size = "19" /></ div >
< OrderFulfillmentTreePanel :tenant-id = "String(detail.summary.tenantId)" :order-id = "String(detail.summary.orderId)" />
</ template >
2026-08-10 09:12:06 +08:00
< template v-else-if = "activeDetailTab === 'workflow'" >
< UnifiedOrderWorkflowPanel
:tenant-id = "String(detail.summary.tenantId)"
:inbound-id = "String(detail.summary.inboundId)"
2026-08-10 12:05:20 +08:00
:order-id = "String(detail.summary.orderId)"
2026-08-10 09:12:06 +08:00
:main-status = "detail.summary.mainStatus"
:inbound-version = "detail.summary.inboundVersion"
:picking-version = "detail.summary.loading?.pickingVersion"
@updated ="loadDetail(true)"
/>
</ template >
< template v-else-if = "activeDetailTab === 'measurement'" >
2026-08-06 19:03:13 +08:00
< div class = "panel-title" >< div >< h2 > 测量数据 </ h2 >< p >{{ detail . summary . measurement ? . completedAt ? `完成于 ${ formatDate ( detail . summary . measurement . completedAt ) } ` : '尚未完成测量' }}</ p ></ div >< Ruler :size = "19" /></ div >
2026-08-07 14:47:11 +08:00
< section class = "measurement-section" >
< button type = "button" class = "measurement-section-title" :aria-expanded = "!measurementCollapsed.customer" @click ="toggleMeasurementSection('customer')" >< h3 > 一 、 客户下单数据 </ h3 >< span class = "measurement-section-summary" >< span > 总件数 : {{ measurementTotals ( detail . order . cargoes , true ). count ?? '—' }}</ span >< span > 总毛重 : {{ formatNumber ( measurementTotals ( detail . order . cargoes , true ). gross ) }}</ span >< span > 总体积 : {{ formatNumber ( measurementTotals ( detail . order . cargoes , true ). volume , 6 ) }}</ span >< span > 总材积重 : {{ formatNumber ( measurementTotals ( detail . order . cargoes , true ). volumetric ) }}</ span >< span > 重量体积 : {{ formatNumber ( measurementTotals ( detail . order . cargoes , true ). weightVolume , 6 ) }}</ span ></ span >< ChevronUp v-if = "!measurementCollapsed.customer" :size="18" /><ChevronDown v-else :size="18" /></ button >
< div v-if = "!measurementCollapsed.customer" >
2026-08-07 18:44:55 +08:00
< div class = "detail-table-wrap" >< el-table :data = "detail.order.cargoes" empty -text =" 暂无客户下单数据 " >< el-table-column v-for = "column in customerMeasurementColumns" :key="column.key" :label="column.label" :prop="column.key" min-width="120"><template #default="{ row }"><LineClampText v-if="column.key === 'remark'" :content="measurementValue(row as Record<string, unknown>, column.key, true)" />< template v-else >{{ measurementValue ( row as Record < string , unknown >, column . key , true ) }}</ template ></ template ></ el-table-column ></ el-table ></ div >
2026-08-07 14:47:11 +08:00
< div class = "measurement-total" >< strong > 合计 </ strong >< span > 箱数 : {{ measurementTotals ( detail . order . cargoes , true ). count ?? '—' }}</ span >< span > 总毛重 : {{ formatNumber ( measurementTotals ( detail . order . cargoes , true ). gross ) }}</ span >< span > 总体积 : {{ formatNumber ( measurementTotals ( detail . order . cargoes , true ). volume , 6 ) }}</ span >< span > 总材积 : {{ formatNumber ( measurementTotals ( detail . order . cargoes , true ). volumetric ) }}</ span ></ div >
</ div >
</ section >
< section class = "measurement-section" >
< button type = "button" class = "measurement-section-title" :aria-expanded = "!measurementCollapsed.outer" @click ="toggleMeasurementSection('outer')" >< h3 > 二 、 外箱规数据 </ h3 >< span class = "measurement-section-summary" >< span > 总件数 : {{ measurementTotals (( measurementDetail ? . items || []) as unknown as Record < string , unknown >[]). count ?? '—' }}</ span >< span > 总毛重 : {{ formatNumber ( measurementTotals (( measurementDetail ? . items || []) as unknown as Record < string , unknown >[]). gross ) }}</ span >< span > 总体积 : {{ formatNumber ( measurementTotals (( measurementDetail ? . items || []) as unknown as Record < string , unknown >[]). volume , 6 ) }}</ span >< span > 总材积重 : {{ formatNumber ( measurementTotals (( measurementDetail ? . items || []) as unknown as Record < string , unknown >[]). volumetric ) }}</ span >< span > 重量体积 : {{ formatNumber ( measurementTotals (( measurementDetail ? . items || []) as unknown as Record < string , unknown >[]). weightVolume , 6 ) }}</ span ></ span >< ChevronUp v-if = "!measurementCollapsed.outer" :size="18" /><ChevronDown v-else :size="18" /></ button >
< div v-if = "!measurementCollapsed.outer" >
2026-08-07 18:44:55 +08:00
< div v-loading = "measurementLoading" class="detail-table-wrap"><el-table :data="measurementDetail?.items || []" empty-text="暂无外箱规数据"><el-table-column v-for="column in measuredColumns" :key="column.key" :label="column.label" :prop="column.key" min-width="120"><template #default="{ row }"><LineClampText v-if="column.key === 'remark'" :content="measurementValue(row as Record<string, unknown>, column.key)" />< template v-else >{{ measurementValue ( row as Record < string , unknown >, column . key ) }}</ template ></ template ></ el-table-column ></ el-table ></ div >
2026-08-07 14:47:11 +08:00
< div class = "measurement-total" >< strong > 合计 </ strong >< span > 箱数 : {{ measurementTotals (( measurementDetail ? . items || []) as unknown as Record < string , unknown >[]). count ?? '—' }}</ span >< span > 总毛重 : {{ formatNumber ( measurementTotals (( measurementDetail ? . items || []) as unknown as Record < string , unknown >[]). gross ) }}</ span >< span > 总体积 : {{ formatNumber ( measurementTotals (( measurementDetail ? . items || []) as unknown as Record < string , unknown >[]). volume , 6 ) }}</ span >< span > 总材积 : {{ formatNumber ( measurementTotals (( measurementDetail ? . items || []) as unknown as Record < string , unknown >[]). volumetric ) }}</ span ></ div >
</ div >
</ section >
< section class = "measurement-section" >
< button type = "button" class = "measurement-section-title" :aria-expanded = "!measurementCollapsed.actual" @click ="toggleMeasurementSection('actual')" >< h3 > 三 、 实际测量数据 </ h3 >< span class = "measurement-section-summary" >< span > 总件数 : {{ measurementDetail ? . actualPackageCount ?? '—' }}</ span >< span > 总毛重 : {{ formatNumber ( measurementDetail ? . actualGrossWeightKg ) }}</ span >< span > 总体积 : {{ formatNumber ( measurementDetail ? . actualVolumeCbm , 6 ) }}</ span >< span > 总材积重 : {{ formatNumber ( measurementDetail ? . volumeWeightKg ) }}</ span >< span > 重量体积 : {{ formatNumber ( measurementDetail ? . actualVolumeCbm , 6 ) }}</ span ></ span >< ChevronUp v-if = "!measurementCollapsed.actual" :size="18" /><ChevronDown v-else :size="18" /></ button >
< div v-if = "!measurementCollapsed.actual" >
2026-08-07 18:44:55 +08:00
< div v-loading = "measurementLoading" class="detail-table-wrap"><el-table :data="measurementDetail?.items || []" empty-text="暂无实际测量数据"><el-table-column v-for="column in measuredColumns" :key="column.key" :label="column.label" :prop="column.key" min-width="120"><template #default="{ row }"><LineClampText v-if="column.key === 'remark'" :content="measurementValue(row as Record<string, unknown>, column.key)" />< template v-else >{{ measurementValue ( row as Record < string , unknown >, column . key ) }}</ template ></ template ></ el-table-column ></ el-table ></ div >
2026-08-07 14:47:11 +08:00
< div class = "measurement-total" >< strong > 合计 </ strong >< span > 箱数 : {{ measurementDetail ? . actualPackageCount ?? '—' }}</ span >< span > 总毛重 : {{ formatNumber ( measurementDetail ? . actualGrossWeightKg ) }}</ span >< span > 总体积 : {{ formatNumber ( measurementDetail ? . actualVolumeCbm , 6 ) }}</ span >< span > 总材积 : {{ formatNumber ( measurementDetail ? . volumeWeightKg ) }}</ span ></ div >
< div class = "measurement-media" >< h4 > 测量图片 </ h4 >< div v-if = "measurementImageUrls.length" class="measurement-media-grid"><el-image v-for="url in measurementImageUrls" :key="url" :src="url" fit="contain" :preview-src-list="measurementImageUrls" class="measurement-media-image" /></div><el-empty v-else description="暂无测量图片" :image-size="48" /><h4>测量视频</h4><el-empty description="暂无测量视频" :image-size="48" /></ div >
</ div >
</ section >
2026-08-06 19:03:13 +08:00
</ template >
< template v-else >
2026-08-06 20:14:56 +08:00
< div class = "panel-title" >< div >< h2 > 内部备注 </ h2 >< p > 仅平台内部可见 </ p ></ div >< el-button v-if = "can('platform:order:note')" type="primary" size="small" @click="openNoteDialog()"><StickyNote :size="15" /> 添加备注 </ el-button ></ div >
2026-08-06 19:03:13 +08:00
< el-empty v-if = "detail.order.internalNotes.length === 0" description="暂无内部备注" :image-size="64" />
< ul v-else class = "notes-list" aria -label =" 内部备注 " >< li v-for = "item in detail.order.internalNotes" :key="String(item.id)" >< p >{{ item . content }}</ p >< span > 操作人 {{ item . createdBy || '系统' }} · {{ formatDate ( item . createdAt ) }}</ span ></ li ></ ul >
</ template >
</ section >
</ template >
</ div >
2026-08-10 15:36:53 +08:00
</ DetailPageLayout >
2026-08-06 19:03:13 +08:00
</ template >
< template v-else >
2026-08-10 15:36:53 +08:00
< ListPageLayout ariaLabel = "统一订单列表" >
2026-08-10 17:42:17 +08:00
< template # toolbar >
2026-08-06 19:03:13 +08:00
< div class = "orders-query-panel" >
< div class = "status-tabs" role = "tablist" aria -label =" 订单主状态 " >
< button
v-for = "(tab, index) in statusTabs"
: key = "tab.value || 'ALL'"
type = "button"
role = "tab"
2026-08-11 18:06:35 +08:00
: aria -selected =" selectedStatus = == tab.value "
2026-08-06 19:03:13 +08:00
aria -controls =" unified -orders-results "
2026-08-11 18:06:35 +08:00
: tabindex = "selectedStatus === tab.value ? 0 : -1"
: class = "['status-tab', { 'is-active': selectedStatus === tab.value }]"
2026-08-06 19:03:13 +08:00
@click ="selectStatus(tab.value)"
@keydown ="handleTabKeydown($event, index)"
>{{ tab . label }}</ button >
</ div >
2026-08-10 17:42:17 +08:00
< QueryToolbar id = "unified-orders-advanced-filters" v-model = "filters" class="orders-query-toolbar" ariaLabel="订单列表筛选" :fields="queryFields" :available-fields="allQueryFields" preference-key="unified-orders-query-v2" :search-on-reset="false" @search="search" @reset="reset" >
< template # actions >
< el-button :aria-expanded = "showAdvancedFilters" aria -controls =" unified -orders-advanced-filters " @click ="showAdvancedFilters = !showAdvancedFilters" >
< SlidersHorizontal :size = "16" />{{ showAdvancedFilters ? '收起筛选' : '高级筛选' }}
< ChevronUp v-if = "showAdvancedFilters" :size="15" /><ChevronDown v-else :size="15" />
</ el-button >
</ template >
</ QueryToolbar >
2026-08-06 19:03:13 +08:00
</ div >
2026-08-10 17:42:17 +08:00
</ template >
< template v-if = "loadError" # state >
< LoadErrorState title = "订单列表暂时无法加载" :message = "loadError.message" :loading = "loading" @retry ="loadOrders" >
2026-08-06 13:56:04 +08:00
< template # details >< ApiErrorMeta :details = "loadError" /></ template >
</ LoadErrorState >
2026-08-10 17:42:17 +08:00
</ template >
2026-08-06 13:56:04 +08:00
2026-08-10 17:42:17 +08:00
< template # default >
< div v-if = "!loadError" id="unified-orders-results" class="surface-panel table-panel table-card" role="tabpanel" >
2026-08-06 19:03:13 +08:00
< DataTable v -model:page = "page" v -model:page-size = "pageSize" :data = "records" :columns = "columns" preference -key =" unified -orders-table " :loading = "loading" :total = "total" : page -sizes ="[ 20 , 50 , 100 ]" row -key =" orderId " empty -text =" 暂无符合条件的订单 " aria -label =" 统一订单列表 " @ selection -change =" handleSelectionChange " >
2026-08-06 20:14:56 +08:00
< template # toolbar -actions >
2026-08-06 19:03:13 +08:00
< el-button type = "primary" plain : disabled = "selectedOrders.length === 0" title = "批量审核接口待接入" @click ="openBulkReviewPlaceholder" >< ListChecks :size = "16" /> 批量审核 < span v-if = "selectedOrders.length" > ( {{ selectedOrders.length }} ) </ span > </ el -button >
</ template >
2026-08-06 13:56:04 +08:00
< template # actions = "{ row }" >
< div class = "row-actions" >
< el-button link type = "primary" @click ="openDetail(row as UnifiedOrderResource)" >< Eye :size = "15" /> 详情 </ el-button >
2026-08-10 17:42:17 +08:00
< el-button v-for = "action in directUnifiedOrderActions(row as UnifiedOrderResource)" :key="action" link :type="unifiedOrderActionType(action)" :loading="action === 'approve' && approvalLoadingId === String((row as UnifiedOrderResource).orderId)" @click="runMoreAction(action, row as UnifiedOrderResource)"><component :is="unifiedOrderRowActionIcons[action]" :size="15" />{{ unifiedOrderActionLabel ( action ) }}</ el-button >
< el-dropdown v-if = "moreUnifiedOrderActions(row as UnifiedOrderResource).length" trigger="click" @command="runMoreAction($event, row as UnifiedOrderResource)" >
< el-button link type = "primary" : aria -label =" moreUnifiedOrderActionsLabel ( row as UnifiedOrderResource )" : title = "moreUnifiedOrderActionsLabel(row as UnifiedOrderResource)" >< MoreHorizontal :size = "17" /></ el-button >
2026-08-06 20:14:56 +08:00
< template # dropdown >
< el-dropdown-menu >
2026-08-10 17:42:17 +08:00
< el-dropdown-item v-for = "action in moreUnifiedOrderActions(row as UnifiedOrderResource)" :key="action" :command="action"><component :is="unifiedOrderRowActionIcons[action]" :size="15" />{{ unifiedOrderActionLabel ( action ) }}</ el-dropdown-item >
2026-08-06 20:14:56 +08:00
</ el-dropdown-menu >
</ template >
</ el-dropdown >
2026-08-06 13:56:04 +08:00
</ div >
</ template >
2026-08-11 09:16:58 +08:00
< template # fulfillment = "{ row }" >
< OrderFulfillmentTreePanel
: tenant -id =" String (( row as UnifiedOrderResource ) .tenantId )"
: order -id =" String (( row as UnifiedOrderResource ) .orderId )"
compact
/>
</ template >
2026-08-07 09:06:22 +08:00
< template # orderStatus = "{ row }" >< StatusBadge : status = "orderStatusLabel((row as UnifiedOrderResource).internalOrderStatus || (row as UnifiedOrderResource).mainStatus)" /></ template >
< template # measurementReviewStatus = "{ row }" >< StatusBadge : status = "measurementStatusLabel((row as UnifiedOrderResource).measurement?.reviewStatus)" /></ template >
2026-08-07 18:44:55 +08:00
< template # field = "{ row, column }" >< LineClampText v-if = "['customerRemark', 'latestInternalNote'].includes(String(column.property))" :content="displayField(row as UnifiedOrderResource, column.property)" :lines="2" />< template v-else >{{ displayField ( row as UnifiedOrderResource , column.property ) }}</ template ></ template >
2026-08-06 13:56:04 +08:00
< template # order = "{ row }" >< div class = "primary-cell" >< strong >{{ ( row as UnifiedOrderResource ). orderNo || `订单 ${ ( row as UnifiedOrderResource ). orderId } ` }}</ strong >< span v-if = "(row as UnifiedOrderResource).inboundCode" > 入仓码 {{ ( row as UnifiedOrderResource ) .inboundCode }}</ span ></ div ></ template >
< template # mainStatus = "{ row }" >< StatusBadge : status = "statusLabel((row as UnifiedOrderResource).mainStatus)" /></ template >
< template # customer = "{ row }" >< div class = "primary-cell" >< strong >{{ customerName ( row as UnifiedOrderResource ) }}</ strong >< span > 客户 ID {{ ( row as UnifiedOrderResource ). tenantId }}</ span ></ div ></ template >
< template # receivingWarehouseName = "{ row }" >{{ ( row as UnifiedOrderResource ). receivingWarehouseName || '—' }}</ template >
< template # destinationWarehouseName = "{ row }" >{{ ( row as UnifiedOrderResource ). destinationWarehouseName || '—' }}</ template >
< template # transport = "{ row }" >< div class = "primary-cell" >< strong >{{ ( row as UnifiedOrderResource ). transport ? . containerNo || ( row as UnifiedOrderResource ). transport ? . loadingStatus || '—' }}</ strong >< span v-if = "(row as UnifiedOrderResource).transport?.vesselName" >{{ ( row as UnifiedOrderResource ) .transport ? .vesselName }} · {{ ( row as UnifiedOrderResource ) .transport ? .voyageNo || '—' }}</ span ></ div ></ template >
< template # cargo = "{ row }" >< span >{{ ( row as UnifiedOrderResource ). totalPackageCount ?? '—' }} 箱 </ span >< span v-if = "(row as UnifiedOrderResource).measurement?.actualGrossWeightKg !== null && (row as UnifiedOrderResource).measurement?.actualGrossWeightKg !== undefined"> · {{ formatNumber((row as UnifiedOrderResource).measurement?.actualGrossWeightKg) }} kg</span><span v-if="(row as UnifiedOrderResource).measurement?.actualVolumeCbm !== null && (row as UnifiedOrderResource).measurement?.actualVolumeCbm !== undefined" > · {{ formatNumber (( row as UnifiedOrderResource ) .measurement ? .actualVolumeCbm , 3 ) }} m ³ </ span ></ template >
< template # expectedShipDate = "{ row }" >{{ formatDate (( row as UnifiedOrderResource ). expectedShipDate ) }}</ template >
< template # updatedAt = "{ row }" >{{ formatDate (( row as UnifiedOrderResource ). updatedAt ) }}</ template >
2026-08-10 16:24:54 +08:00
< template # pagination -left =" { total : tableTotal } " >< span > 共 {{ tableTotal }} 条 </ span ></ template >
2026-08-06 13:56:04 +08:00
</ DataTable >
</ div >
2026-08-10 17:42:17 +08:00
</ template >
2026-08-10 15:36:53 +08:00
</ ListPageLayout >
2026-08-06 19:03:13 +08:00
</ template >
< AppDialog v-model = "noteOpen" title="添加内部备注" confirm-text="保存备注" :confirm-loading="noteSaving" @confirm="saveNote" >
< el-input v-model = "noteContent" type="textarea" :rows="5" maxlength="2000" show-word-limit placeholder="请输入仅平台内部可见的备注" aria-label="内部备注内容" />
</ AppDialog >
2026-08-06 18:02:04 +08:00
2026-08-07 10:45:19 +08:00
< AppDialog v-model = "approvalOpen" title="审核订单" confirm-text="审核通过" confirm-type="success" :confirm-loading="approvalSaving" destroy-on-close @confirm="approveOrder" @cancel="closeApproval" >
< el-alert title = "确认订单资料无误后审核通过,订单将进入待测量阶段。" type = "info" :closable = "false" show -icon />
2026-08-06 18:02:04 +08:00
</ AppDialog >
2026-08-07 14:47:11 +08:00
2026-08-06 13:56:04 +08:00
</ section >
2026-08-10 15:36:53 +08:00
</ AdminPage >
2026-08-06 13:56:04 +08:00
</ template >
< style scoped >
2026-08-10 15:36:53 +08:00
. unified - orders - page { min - width : 0 ; gap : 16 px ; }
2026-08-06 13:56:04 +08:00
. page - intro { display : flex ; align - items : flex - start ; justify - content : space - between ; gap : 16 px ; margin - bottom : 16 px ; }
. page - intro h1 { margin : 0 ; color : var ( -- cc - text ); font - size : 22 px ; line - height : 1.35 ; }
. page - intro p { margin : 5 px 0 0 ; color : var ( -- cc - muted ); font - size : 13 px ; line - height : 1.6 ; }
. page - intro : deep (. el - button ) { flex : 0 0 auto ; }
2026-08-06 19:03:13 +08:00
. orders - query - panel { overflow : hidden ; border : 1 px solid var ( -- cc - border ); border - radius : 8 px ; background : # fff ; }
2026-08-06 15:32:02 +08:00
. status - tabs { display : flex ; max - width : 100 % ; overflow - x : auto ; margin - bottom : 0 ; border - bottom : 1 px solid var ( -- cc - border ); scrollbar - width : thin ; }
. status - tab { position : relative ; flex : 0 0 auto ; min - height : 44 px ; padding : 0 18 px ; border : 0 ; background : transparent ; color : var ( -- cc - muted ); font : inherit ; font - size : 14 px ; cursor : pointer ; }
. status - tab :: after { position : absolute ; right : 12 px ; bottom : 0 ; left : 12 px ; height : 3 px ; background : transparent ; content : '' ; }
. status - tab : hover { color : var ( -- cc - primary ); background : # f6f9fd ; }
. status - tab : focus - visible { outline : 2 px solid var ( -- cc - primary ); outline - offset : - 2 px ; }
. status - tab . is - active { color : var ( -- cc - primary ); font - weight : 600 ; }
. status - tab . is - active :: after { background : var ( -- cc - primary ); }
2026-08-10 15:36:53 +08:00
. orders - query - toolbar { border : 0 ; }
2026-08-10 16:24:54 +08:00
. table - card { overflow : hidden ; }
2026-08-06 13:56:04 +08:00
. row - actions { display : flex ; align - items : center ; gap : 4 px ; white - space : nowrap ; }
. row - actions : deep (. el - button ) { margin - left : 0 ; }
. primary - cell { display : grid ; min - width : 0 ; gap : 4 px ; }
. primary - cell strong { overflow - wrap : anywhere ; }
. primary - cell span { color : var ( -- cc - muted ); font - size : 12 px ; overflow - wrap : anywhere ; }
2026-08-06 20:14:56 +08:00
. order - detail - header { display : flex ; min - width : 0 ; min - height : 42 px ; align - items : center ; justify - content : space - between ; gap : 16 px ; margin - bottom : 12 px ; }
. order - detail - heading { display : flex ; min - width : 0 ; align - items : center ; gap : 10 px ; }
. back - button { flex : 0 0 auto ; padding : 0 ; }
. order - mark { display : grid ; width : 36 px ; height : 36 px ; flex : 0 0 36 px ; place - items : center ; border - radius : 7 px ; background : # 1 d2939 ; color : # fff ; }
2026-08-06 19:03:13 +08:00
. order - title { min - width : 0 ; }
2026-08-06 20:14:56 +08:00
. order - title h1 { margin : 0 ; overflow : hidden ; color : # 172033 ; font - size : 18 px ; line - height : 1.2 ; text - overflow : ellipsis ; white - space : nowrap ; }
. inbound - code - link { width : 100 % ; max - width : 100 % ; overflow : hidden ; justify - content : flex - start ; padding : 0 ; color : var ( -- cc - primary ); font : inherit ; font - weight : 700 ; vertical - align : baseline ; }
. inbound - code - link : deep ( span ) { display : block ; min - width : 0 ; overflow : hidden ; text - overflow : ellipsis ; white - space : nowrap ; }
. order - title p { margin : 2 px 0 0 ; color : var ( -- cc - muted ); font - size : 11 px ; }
2026-08-06 19:03:13 +08:00
. detail - header - actions { display : flex ; flex : 0 0 auto ; gap : 8 px ; }
. detail - header - actions : deep (. el - button + . el - button ) { margin - left : 0 ; }
. detail - load - error { min - height : 280 px ; }
. order - detail - content { min - height : 520 px ; }
. detail - overview { display : grid ; grid - template - columns : repeat ( 4 , minmax ( 0 , 1 fr )); overflow : hidden ; margin - bottom : 16 px ; border : 1 px solid var ( -- cc - border ); border - radius : 8 px ; background : # fff ; }
. detail - overview article { min - width : 0 ; padding : 18 px 20 px ; border - right : 1 px solid var ( -- cc - border ); }
. detail - overview article : last - child { border - right : 0 ; }
. detail - overview article > span , . detail - overview article > small { display : block ; overflow : hidden ; color : var ( -- cc - muted ); font - size : 11 px ; text - overflow : ellipsis ; white - space : nowrap ; }
. detail - overview strong { display : block ; margin : 7 px 0 5 px ; overflow : hidden ; color : # 172033 ; font - size : 20 px ; line - height : 1.2 ; text - overflow : ellipsis ; white - space : nowrap ; }
. detail - overview strong small { margin - left : 5 px ; color : # 475467 ; font - size : 11 px ; }
. detail - tabs { display : grid ; grid - template - columns : repeat ( 6 , minmax ( 124 px , 1 fr )); overflow - x : auto ; border : 1 px solid var ( -- cc - border ); border - bottom : 0 ; border - radius : 8 px 8 px 0 0 ; background : # fff ; }
. detail - tabs button { position : relative ; display : flex ; min - height : 56 px ; align - items : center ; justify - content : center ; gap : 7 px ; padding : 0 14 px ; border : 0 ; border - right : 1 px solid # edf0f4 ; background : transparent ; color : # 667085 ; font - weight : 650 ; cursor : pointer ; }
. detail - tabs button : last - child { border - right : 0 ; }
. detail - tabs button :: after { position : absolute ; right : 14 px ; bottom : 0 ; left : 14 px ; height : 3 px ; background : transparent ; content : '' ; }
. detail - tabs button : hover { background : # fafbff ; color : # 344054 ; }
. detail - tabs button . active { background : # f7f8ff ; color : var ( -- cc - primary ); }
. detail - tabs button . active :: after { background : var ( -- cc - primary ); }
2026-08-07 10:45:19 +08:00
. detail - panel { min - width : 0 ; min - height : 390 px ; padding : 16 px ; border : 1 px solid var ( -- cc - border ); border - radius : 0 0 8 px 8 px ; background : # fff ; }
. panel - title { display : flex ; align - items : flex - start ; justify - content : space - between ; gap : 16 px ; margin - bottom : 10 px ; padding - bottom : 8 px ; border - bottom : 1 px solid var ( -- cc - border ); }
2026-08-06 19:03:13 +08:00
. panel - title h2 { margin : 0 ; color : # 344054 ; font - size : 16 px ; }
. panel - title p { margin : 5 px 0 0 ; color : var ( -- cc - muted ); font - size : 11 px ; }
. panel - title > svg { color : var ( -- cc - primary ); }
2026-08-07 10:45:19 +08:00
. order - info - grid { display : grid ; grid - template - columns : repeat ( 3 , minmax ( 0 , 1 fr )); gap : 0 ; margin : 0 ; overflow : hidden ; border : 1 px solid # edf0f4 ; background : # fff ; }
. order - info - grid > div , . measurement - grid > div { display : flex ; min - width : 0 ; min - height : 38 px ; align - items : baseline ; gap : 8 px ; padding : 8 px 12 px ; border - right : 1 px solid # edf0f4 ; border - bottom : 1 px solid # edf0f4 ; background : # fff ; }
. order - info - grid > div . wide { grid - column : span 2 ; }
. order - info - grid dt , . measurement - grid dt { flex : 0 0 auto ; margin : 0 ; color : var ( -- cc - muted ); font - size : 11 px ; white - space : nowrap ; }
. order - info - grid dd , . measurement - grid dd { min - width : 0 ; margin : 0 ; color : # 25324 a ; font - size : 12 px ; font - weight : 650 ; line - height : 1.35 ; overflow - wrap : anywhere ; }
2026-08-06 19:03:13 +08:00
. detail - table - wrap { overflow - x : auto ; }
. detail - table - wrap : deep (. el - table ) { min - width : 720 px ; -- el - table - header - bg - color : # f8fafc ; }
2026-08-07 14:47:11 +08:00
. cargo - image { width : 72 px ; height : 56 px ; border : 1 px solid var ( -- cc - border ); border - radius : 6 px ; background : # f8fafc ; }
2026-08-06 19:03:13 +08:00
. measurement - grid { display : grid ; grid - template - columns : repeat ( 4 , minmax ( 0 , 1 fr )); gap : 1 px ; margin : 0 ; border : 1 px solid var ( -- cc - border ); background : var ( -- cc - border ); }
2026-08-07 14:47:11 +08:00
. measurement - section { margin - top : 18 px ; }
. measurement - section - title { display : flex ; width : 100 % ; min - height : 48 px ; align - items : center ; gap : 16 px ; padding : 9 px 14 px ; border : 1 px solid var ( -- cc - border ); border - radius : 7 px 7 px 0 0 ; background : # f8fafc ; color : var ( -- cc - text ); font : inherit ; text - align : left ; cursor : pointer ; }
. measurement - section - title : hover { background : # f2f6fb ; }
. measurement - section - title : focus - visible { outline : 2 px solid var ( -- cc - primary ); outline - offset : 2 px ; }
2026-08-07 15:19:26 +08:00
. measurement - section - title [ aria - expanded = 'false' ] { border - radius : 7 px ; }
2026-08-07 14:47:11 +08:00
. measurement - section - title h3 { flex : 0 0 auto ; margin : 0 ; color : var ( -- cc - text ); font - size : 14 px ; }
. measurement - section - summary { display : flex ; min - width : 0 ; flex : 1 ; align - items : center ; justify - content : flex - end ; gap : 18 px ; color : var ( -- cc - muted ); font - size : 12 px ; white - space : nowrap ; }
. measurement - section - title > svg { flex : 0 0 auto ; color : var ( -- cc - muted ); }
. measurement - total { display : flex ; min - height : 42 px ; align - items : center ; justify - content : flex - end ; gap : 24 px ; padding : 9 px 14 px ; border : 1 px solid var ( -- cc - border ); background : # f8fafc ; color : var ( -- cc - muted ); font - size : 12 px ; }
. measurement - total { border - top : 0 ; border - radius : 0 0 7 px 7 px ; }
. measurement - total strong { margin - right : auto ; color : var ( -- cc - text ); font - size : 13 px ; }
. measurement - media { margin - top : 20 px ; }
. measurement - media h4 { margin : 18 px 0 10 px ; color : var ( -- cc - text ); font - size : 14 px ; }
. measurement - media - grid { display : flex ; flex - wrap : wrap ; gap : 12 px ; }
. measurement - media - image { width : 110 px ; height : 90 px ; border : 1 px solid var ( -- cc - border ); border - radius : 6 px ; background : # f8fafc ; }
2026-08-06 19:03:13 +08:00
. history - list , . notes - list { display : grid ; gap : 0 ; margin : 0 ; padding : 0 ; list - style : none ; }
. history - list li { position : relative ; display : grid ; grid - template - columns : 16 px minmax ( 0 , 1 fr ); gap : 12 px ; padding - bottom : 20 px ; }
. history - list li : not ( : last - child ) :: after { position : absolute ; top : 15 px ; bottom : 0 ; left : 7 px ; width : 1 px ; background : var ( -- cc - border ); content : '' ; }
. history - marker { z - index : 1 ; width : 14 px ; height : 14 px ; margin - top : 3 px ; border : 3 px solid var ( -- cc - primary ); border - radius : 50 % ; background : # fff ; }
. history - line { display : flex ; align - items : center ; justify - content : space - between ; gap : 12 px ; }
. history - list strong { color : # 344054 ; font - size : 13 px ; }
. history - list p , . notes - list p { margin : 4 px 0 0 ; color : var ( -- cc - muted ); font - size : 12 px ; line - height : 1.55 ; overflow - wrap : anywhere ; }
. history - list . history - remark { margin - top : 8 px ; padding : 10 px 12 px ; background : # f8fafc ; color : # 475467 ; }
. notes - list li { padding : 14 px 0 ; border - bottom : 1 px solid # edf0f4 ; }
. notes - list li : first - child { padding - top : 0 ; }
. notes - list li : last - child { padding - bottom : 0 ; border - bottom : 0 ; }
2026-08-06 13:56:04 +08:00
. notes - list li > p { margin : 0 ; color : var ( -- cc - text ); font - size : 13 px ; }
. notes - list span { display : block ; margin - top : 5 px ; color : var ( -- cc - muted ); font - size : 12 px ; }
2026-08-06 19:03:13 +08:00
@ media ( max - width : 960 px ) { . detail - overview { grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr )); } . detail - overview article : nth - child ( 2 ) { border - right : 0 ; } . detail - overview article : nth - child ( - n + 2 ) { border - bottom : 1 px solid var ( -- cc - border ); } . order - info - grid { grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr )); } . measurement - grid { grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr )); } }
2026-08-07 14:47:11 +08:00
@ media ( max - width : 720 px ) { . page - intro { align - items : stretch ; flex - direction : column ; } . page - intro : deep (. el - button ) { width : 100 % ; min - height : 44 px ; } . status - tab { min - height : 46 px ; padding : 0 14 px ; } . table - actions : deep (. el - button ) { width : 100 % ; min - height : 40 px ; } . row - actions { flex - wrap : wrap ; } . order - detail - header { align - items : stretch ; flex - direction : column ; } . order - detail - heading { display : grid ; grid - template - columns : auto 36 px minmax ( 0 , 1 fr ) auto ; } . detail - header - actions { display : grid ; grid - template - columns : 1 fr ; } . detail - header - actions : deep (. el - button ) { min - height : 40 px ; } . detail - overview , . order - info - grid , . measurement - grid { grid - template - columns : 1 fr ; } . detail - overview article { border - right : 0 ; border - bottom : 1 px solid var ( -- cc - border ); } . detail - overview article : last - child { border - bottom : 0 ; } . order - info - grid > div . wide { grid - column : auto ; } . detail - tabs { grid - template - columns : repeat ( 6 , 132 px ); } . detail - panel { padding : 16 px ; } . measurement - section - title { align - items : flex - start ; flex - wrap : wrap ; padding : 12 px ; } . measurement - section - summary { width : 100 % ; flex - basis : calc ( 100 % - 34 px ); flex - wrap : wrap ; justify - content : flex - start ; gap : 6 px 16 px ; white - space : normal ; } . measurement - section - title > svg { margin - left : auto ; } . measurement - total { flex - wrap : wrap ; justify - content : flex - start ; gap : 6 px 16 px ; } . measurement - total strong { width : 100 % ; } }
2026-08-06 20:14:56 +08:00
@ media ( max - width : 420 px ) { . order - detail - heading { grid - template - columns : auto minmax ( 0 , 1 fr ) auto ; } . order - mark { display : none ; } }
2026-08-06 13:56:04 +08:00
</ style >