2026-08-06 13:56:04 +08:00
< script setup lang = "ts" >
2026-08-06 18:02:04 +08:00
import { computed , nextTick , onMounted , reactive , ref , watch } from 'vue'
import { ElMessage , type FormInstance , type FormRules } from 'element-plus'
import { ChevronDown , ChevronUp , CircleCheck , ClipboardList , Eye , RefreshCw , SlidersHorizontal } 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 ,
assignPlatformUnifiedOrderCarrier ,
2026-08-06 13:56:04 +08:00
listPlatformUnifiedOrders ,
unifiedOrderStatusOptions ,
type UnifiedOrderDetailResource ,
type UnifiedOrderMainStatus ,
type UnifiedOrderResource ,
} from '@/modules/unified-orders/api/unifiedOrders'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { listPlatformTenants , type PlatformTenantResource } from '@/modules/iam'
2026-08-06 18:02:04 +08:00
import { listPlatformPartners , type PlatformPartnerResource } from '@/modules/master-data'
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'
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 AppDrawer from '@/shared/components/AppDrawer.vue'
import DataTable , { type DataTableColumn } from '@/shared/components/DataTable.vue'
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import QueryToolbar , { type QueryField , type QueryModel } from '@/shared/components/QueryToolbar.vue'
import StatusBadge from '@/shared/components/StatusBadge.vue'
type FilterModel = QueryModel & {
tenantId : string
keyword : string
2026-08-06 15:32:02 +08:00
orderNo : string
2026-08-06 13:56:04 +08:00
mainStatus : UnifiedOrderMainStatus | ''
2026-08-06 15:32:02 +08:00
receivingWarehouseId : string
destinationWarehouseId : string
inboundCode : string
internalOrderStatus : 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 [] >([])
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 )
const detailOpen = ref ( false )
2026-08-06 18:02:04 +08:00
const approvalOpen = ref ( false )
const approvalLoadingId = ref ( '' )
const approvalSaving = ref ( false )
const carrierLoading = ref ( false )
const carrierLoadError = ref < ApiErrorDetails | null >( null )
const carriers = ref < PlatformPartnerResource [] >([])
const approvalOrder = ref < UnifiedOrderResource | null >( null )
const carrierFormRef = ref < FormInstance >()
const carrierForm = reactive ({ carrierId : '' })
2026-08-06 15:32:02 +08:00
const showAdvancedFilters = ref ( false )
const filters = ref < FilterModel >( emptyFilters ())
2026-08-06 18:02:04 +08:00
const carrierRules : FormRules = {
carrierId : [{ required : true , message : '请选择承运商' , trigger : 'change' }],
}
2026-08-06 13:56:04 +08:00
const orderId = computed (() => typeof route . params . orderId === 'string' ? route . params . orderId : '' )
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 [] >(() => [
2026-08-06 13:56:04 +08:00
{
key : 'tenantId' ,
type : 'select' ,
width : 220 ,
2026-08-06 15:32:02 +08:00
label : '公司' ,
2026-08-06 13:56:04 +08:00
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 18:34:57 +08:00
{ key : 'keyword' , type : 'input' , width : 260 , label : '综合搜索' , placeholder : '请输入订单号' , props : { 'aria-label' : '订单号关键词' } },
2026-08-06 15:32:02 +08:00
{ key : 'orderNo' , type : 'input' , width : 220 , label : '订单号' , placeholder : '请输入订单号' , props : { 'aria-label' : '订单号' } },
{ key : 'createdRange' , type : 'daterange' , width : 300 , label : '创建日期' , placeholder : '创建日期范围' , props : { 'aria-label' : '创建日期范围' } },
2026-08-06 13:56:04 +08:00
])
2026-08-06 15:32:02 +08:00
const advancedQueryFields = 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' : '入仓码' } },
{ key : 'internalOrderStatus' , type : 'select' , width : 190 , label : '订单状态' , placeholder : '请选择订单状态' , options : [
2026-08-06 15:36:28 +08:00
{ value : 'SUBMITTED' , label : '待审核' }, { value : 'APPROVED' , label : '已审核' },
{ value : 'ACCEPTED' , label : '已受理' }, { value : 'PROCESSING' , label : '处理中' }, { value : 'COMPLETED' , label : '已完成' },
2026-08-06 15:32:02 +08:00
], props : { 'aria-label' : '订单状态' } },
{ 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' : '费用审核状态' } },
{ 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 编码' } },
])
const queryFields = computed (() => showAdvancedFilters . value
? [... primaryQueryFields . value , ... advancedQueryFields . value ]
: primaryQueryFields . value )
2026-08-06 13:56:04 +08:00
const columns : DataTableColumn [] = [
{ key : 'actions' , label : '操作' , width : 180 , fixed : 'left' , slotName : 'actions' },
{ key : 'order' , label : '订单信息' , minWidth : 220 , slotName : 'order' },
{ key : 'mainStatus' , label : '主状态' , width : 132 , slotName : 'mainStatus' },
{ key : 'customer' , label : '客户' , minWidth : 180 , slotName : 'customer' },
{ key : 'receivingWarehouseName' , label : '平台收货仓' , minWidth : 160 , slotName : 'receivingWarehouseName' },
{ key : 'destinationWarehouseName' , label : '目的仓' , minWidth : 160 , slotName : 'destinationWarehouseName' },
{ key : 'transport' , label : '运输/装柜' , minWidth : 180 , slotName : 'transport' },
{ key : 'cargo' , label : '货物数据' , minWidth : 180 , slotName : 'cargo' },
{ key : 'expectedShipDate' , label : '预计出运' , minWidth : 150 , slotName : 'expectedShipDate' },
{ key : 'updatedAt' , label : '最近更新' , minWidth : 150 , slotName : 'updatedAt' },
]
2026-08-06 15:32:02 +08:00
function emptyFilters () : FilterModel {
return {
tenantId : '' , keyword : '' , orderNo : '' , mainStatus : '' , receivingWarehouseId : '' , destinationWarehouseId : '' ,
inboundCode : '' , internalOrderStatus : '' , expectedArrivalDate : '' , feeReviewStatus : '' , channelName : '' ,
containerNo : '' , cabinetNo : '' , sailingDate : '' , loaded : '' , fullContainer : '' , measurementReviewStatus : '' ,
fbaCode : '' , createdRange : [],
}
}
function trimmed ( value : string ) {
return value . trim () || undefined
}
function optionalBoolean ( value : string ) {
return value === '' ? undefined : value === 'true'
}
function selectStatus ( status : UnifiedOrderMainStatus | '' ) {
filters . value = { ... filters . value , mainStatus : status }
search ()
}
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 })
}
function statusLabel ( value : UnifiedOrderMainStatus | string | null ) {
return unifiedOrderStatusOptions . find (( item ) => item . value === value ) ? . label || value || '状态更新'
}
const nextActionLabels : Record < string , string > = {
APPROVE_ORDER : '审核通过' ,
CONFIRM_MEASUREMENT : '完成测量' ,
SMART_SPLIT : '智能分柜' ,
CONFIRM_PRELOAD : '确认预装清单' ,
COMPLETE_LOADING : '确认装柜完成' ,
COMPLETE_DELIVERY : '确认派送完成' ,
}
function nextActionBoundary ( row : UnifiedOrderResource ) {
2026-08-06 18:02:04 +08:00
if ( row . nextAction === 'APPROVE_ORDER' ) return ''
2026-08-06 13:56:04 +08:00
const label = row . nextAction ? nextActionLabels [ row . nextAction ] || row . nextAction : ''
return label ? `待接入: ${ label } ` : ''
}
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' )
}
async function loadCarriers () {
carrierLoading . value = true
carrierLoadError . value = null
try {
const records : PlatformPartnerResource [] = []
let currentPage = 1
let pages = 1
do {
const result = await listPlatformPartners ({ page : currentPage , size : 200 , type : 'CARRIER' , status : 'ACTIVE' })
records . push (... result . records )
pages = Math . max ( 1 , result . pages )
currentPage += 1
} while ( currentPage <= pages )
carriers . value = records . filter (( item ) => item . status === 'ACTIVE' && item . types . includes ( 'CARRIER' ))
} catch ( error ) {
carriers . value = []
carrierLoadError . value = getApiErrorDetails ( error , '承运商列表加载失败,请稍后重试' )
} finally {
carrierLoading . value = false
}
}
async function openApproval ( row : UnifiedOrderResource ) {
if ( ! canApprove ( row )) return
approvalOrder . value = row
const currentSnapshot = detail . value ? . summary . orderId === row . orderId ? detail . value . order . order : null
carrierForm . carrierId = currentSnapshot ? . carrierId == null ? '' : String ( currentSnapshot . carrierId )
approvalOpen . value = true
await nextTick ()
carrierFormRef . value ? . clearValidate ()
await loadCarriers ()
}
function closeApproval () {
if ( approvalSaving . value ) return
approvalOpen . value = false
approvalOrder . value = null
carrierForm . carrierId = ''
}
async function assignCarrierAndApprove () {
if ( ! approvalOrder . value ) return
const valid = await carrierFormRef . value ? . validate (). catch (() => false )
if ( ! valid ) return
const carrier = carriers . value . find (( item ) => String ( item . id ) === carrierForm . carrierId )
if ( ! carrier ) {
ElMessage . error ( '所选承运商已停用或不存在,请重新选择' )
return
}
const order = approvalOrder . value
const orderId = String ( order . orderId )
const tenantId = String ( order . tenantId )
approvalSaving . value = true
approvalLoadingId . value = orderId
try {
await assignPlatformUnifiedOrderCarrier ( tenantId , orderId , String ( carrier . id ))
await approvePlatformUnifiedOrder ( tenantId , orderId )
approvalOpen . value = false
await loadOrders ()
if ( detail . value ? . summary . orderId === order . orderId ) await loadDetail ()
approvalOrder . value = null
ElMessage . success ( '承运商已指派,订单审核通过' )
} catch ( error ) {
ElMessage . error ( getApiErrorDetails ( error , '承运商指派或订单审核失败,请稍后重试' ). message )
} finally {
approvalSaving . value = false
approvalLoadingId . value = ''
}
}
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 {
const range = filters . value . createdRange
const result = await listPlatformUnifiedOrders ({
tenantId : filters . value . tenantId || undefined ,
2026-08-06 15:32:02 +08:00
keyword : trimmed ( filters . value . keyword ),
orderNo : trimmed ( filters . value . orderNo ),
2026-08-06 13:56:04 +08:00
mainStatus : filters . value . mainStatus || undefined ,
2026-08-06 15:32:02 +08:00
receivingWarehouseId : filters . value . receivingWarehouseId || undefined ,
destinationWarehouseId : filters . value . destinationWarehouseId || undefined ,
inboundCode : trimmed ( filters . value . inboundCode ),
internalOrderStatus : filters . value . internalOrderStatus || undefined ,
expectedArrivalDate : filters . value . expectedArrivalDate || undefined ,
feeReviewStatus : filters . value . feeReviewStatus || undefined ,
channelName : trimmed ( filters . value . channelName ),
containerNo : trimmed ( filters . value . containerNo ),
cabinetNo : trimmed ( filters . value . cabinetNo ),
sailingDate : filters . value . sailingDate || undefined ,
loaded : optionalBoolean ( filters . value . loaded ),
fullContainer : optionalBoolean ( filters . value . fullContainer ),
measurementReviewStatus : filters . value . measurementReviewStatus || undefined ,
fbaCode : trimmed ( filters . value . 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
total . value = result . total
} catch ( error ) {
records . value = []
total . value = 0
loadError . value = getApiErrorDetails ( error , '统一订单列表加载失败,请检查 C02 统一查询接口和订单权限' )
} finally {
loading . value = false
}
}
function search () {
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-06 13:56:04 +08:00
page . value = 1
void loadOrders ()
}
function openDetail ( row : UnifiedOrderResource ) {
void router . push ({ name : 'unified-order-detail' , params : { orderId : String ( row . orderId ) }, query : { tenantId : String ( row . tenantId ) } })
}
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 ) } })
}
function workflowEligible ( row : UnifiedOrderResource ) {
return Boolean ( row . inboundId && [ 'WAIT_SPLIT' , 'LOADING' , 'LOADED' ]. includes ( row . mainStatus ))
}
function openWorkflow ( row : UnifiedOrderResource ) {
if ( ! workflowEligible ( row ) || ! can ( 'platform:firstleg:inbound:view' )) return
void router . push ({
name : 'unified-order-workflow' ,
params : { orderId : String ( row . orderId ) },
query : { tenantId : String ( row . tenantId ) },
})
}
function closeDetail () {
detailOpen . value = false
detail . value = null
detailError . value = null
void router . push ({ name : 'unified-orders' })
}
async function loadDetail () {
if ( ! orderId . value ) return
const tenantId = typeof route . query . tenantId === 'string' ? route . query . tenantId : ''
if ( ! tenantId ) {
detailError . value = { message : '订单详情缺少客户标识,请从订单列表重新打开' }
detailOpen . value = true
return
}
detailLoading . value = true
detailError . value = null
detailOpen . value = true
try {
detail . value = await getPlatformUnifiedOrder ( tenantId , orderId . value )
} catch ( error ) {
detail . value = null
detailError . value = getApiErrorDetails ( error , '统一订单详情加载失败,请检查 C02 统一详情接口和订单权限' )
} finally {
detailLoading . value = false
}
}
watch ([ page , pageSize ], () => { void loadOrders () })
watch ( orderId , ( value ) => {
if ( value ) void loadDetail ()
else detailOpen . value = false
}, { immediate : true })
onMounted (() => {
void loadCustomers ()
2026-08-06 15:32:02 +08:00
void loadWarehouses ()
2026-08-06 13:56:04 +08:00
void loadOrders ()
})
</ script >
< template >
< section class = "unified-orders-page" >
2026-08-06 15:32:02 +08:00
< div class = "status-tabs" role = "tablist" aria -label =" 订单主状态 " >
< button
v-for = "(tab, index) in statusTabs"
: key = "tab.value || 'ALL'"
type = "button"
role = "tab"
: aria -selected =" filters.mainStatus === tab.value "
aria -controls =" unified -orders-results "
: tabindex = "filters.mainStatus === tab.value ? 0 : -1"
: class = "['status-tab', { 'is-active': filters.mainStatus === tab.value }]"
@click ="selectStatus(tab.value)"
@keydown ="handleTabKeydown($event, index)"
>{{ tab . label }}</ button >
</ div >
< QueryToolbar id = "unified-orders-advanced-filters" v-model = "filters" ariaLabel="订单列表筛选" :fields="queryFields" :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 13:56:04 +08:00
< LoadErrorState v-if = "loadError" class="orders-load-error" title="订单列表暂时无法加载" :message="loadError.message" :loading="loading" @retry="loadOrders" >
< template # details >< ApiErrorMeta :details = "loadError" /></ template >
</ LoadErrorState >
2026-08-06 15:32:02 +08:00
< div v-else id = "unified-orders-results" class = "table-card" role = "tabpanel" >
2026-08-06 13:56:04 +08:00
< DataTable v -model:page = "page" v -model:page-size = "pageSize" :data = "records" :columns = "columns" :loading = "loading" :total = "total" : page -sizes ="[ 20 , 50 , 100 ]" row -key =" orderId " empty -text =" 暂无符合条件的订单 " aria -label =" 统一订单列表 " >
< template # actions = "{ row }" >
< div class = "row-actions" >
< el-button link type = "primary" @click ="openDetail(row as UnifiedOrderResource)" >< Eye :size = "15" /> 详情 </ el-button >
2026-08-06 18:02:04 +08:00
< el-button v-if = "canApprove(row as UnifiedOrderResource)" link type="success" :loading="approvalLoadingId === String((row as UnifiedOrderResource).orderId)" @click="openApproval(row as UnifiedOrderResource)"><CircleCheck :size="15" /> 审核通过 </ el-button >
2026-08-06 13:56:04 +08:00
< el-button v-if = "(row as UnifiedOrderResource).mainStatus === 'WAIT_MEASURE' && (row as UnifiedOrderResource).inboundId && can('platform:firstleg:inbound:view')" link type="primary" @click="openMeasurement(row as UnifiedOrderResource)" > 测量作业 </ el -button >
< el-button v-if = "workflowEligible(row as UnifiedOrderResource) && can('platform:firstleg:inbound:view')" link type="primary" @click="openWorkflow(row as UnifiedOrderResource)"><ClipboardList :size="15" /> 后半程作业 </ el-button >
< span v-if = "nextActionBoundary(row as UnifiedOrderResource)" class="action-boundary" >{{ nextActionBoundary ( row as UnifiedOrderResource ) }}</ span >
</ div >
</ template >
< 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 >
</ DataTable >
</ div >
< AppDrawer v-model = "detailOpen" :title="detail ? `订单详情 · ${detail.summary.orderNo || detail.summary.orderId}` : '订单详情'" size="min(760px, 94vw)" :loading="detailLoading" :show-footer="false" destroy-on-close @closed="closeDetail" >
< LoadErrorState v-if = "detailError" title="订单详情暂时无法加载" :message="detailError.message" :loading="detailLoading" @retry="loadDetail"><template #details><ApiErrorMeta :details="detailError" /></ template ></ LoadErrorState >
< div v-else-if = "detail" class="detail-content" >
< section class = "detail-summary" aria -label =" 订单概览 " >
2026-08-06 18:02:04 +08:00
< div class = "detail-summary__header" >< div >< span class = "eyebrow" > 订单主状态 </ span >< h2 >{{ detail . summary . orderNo || `订单 ${ detail . summary . orderId } ` }}</ h2 ></ div >< div class = "detail-summary__actions" >< StatusBadge :status = "statusLabel(detail.summary.mainStatus)" />< 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><el-button v-if="workflowEligible(detail.summary) && can('platform:firstleg:inbound:view')" type="primary" plain @click="openWorkflow(detail.summary)"><ClipboardList :size="15" /> 后半程作业 </ el-button ></ div ></ div >
< dl class = "detail-grid" >< div >< dt > 客户 </ dt >< dd >{{ customerName ( detail . summary ) }}</ dd ></ div >< div >< dt > 入仓码 </ dt >< dd >{{ detail . summary . inboundCode || '—' }}</ dd ></ div >< div >< dt > 平台收货仓 </ dt >< dd >{{ detail . summary . receivingWarehouseName || '—' }}</ dd ></ div >< div >< dt > 目的仓 </ dt >< dd >{{ detail . summary . destinationWarehouseName || '—' }}</ dd ></ div >< div >< dt > 承运商 </ dt >< dd >{{ detail . order . order ? . carrierName || '待平台指派' }}</ dd ></ div >< div >< dt > 下一步 </ dt >< dd >{{ canApprove ( detail . summary ) ? '选择承运商并审核' : nextActionBoundary ( detail . summary ) || '—' }}</ dd ></ div >< div >< dt > 最近更新 </ dt >< dd >{{ formatDate ( detail . summary . updatedAt ) }}</ dd ></ div ></ dl >
2026-08-06 13:56:04 +08:00
</ section >
< section class = "detail-section" aria -label =" 状态历史 " >< h3 > 状态历史 </ h3 >< el-empty v-if = "detail.timeline.length === 0" description="暂无状态历史" :image-size="56" /><ol v-else class="history-list"><li v-for="(item, index) in detail.timeline" :key="`${item.source || 'event'}-${item.occurredAt || index}`"><StatusBadge :status="statusLabel(item.mainStatus)" /><div><strong>{{ item.action || item.eventType || '状态更新' }}</strong><p>{{ item.operatorName || '系统' }} · {{ formatDate(item.occurredAt) }}</p><p v-if="item.remark" >{{ item.remark }}</ p ></ div ></ li ></ ol ></ section >
< section class = "detail-section" aria -label =" 内部备注 " >< h3 > 内部备注 </ h3 >< el-empty v-if = "detail.order.internalNotes.length === 0" description="暂无内部备注" :image-size="56" /><ul v-else class="notes-list"><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 ></ section >
</ div >
</ AppDrawer >
2026-08-06 18:02:04 +08:00
< AppDialog v-model = "approvalOpen" title="选择承运商并审核" confirm-text="指派并审核通过" confirm-type="success" :loading="carrierLoading" :confirm-loading="approvalSaving" :confirm-disabled="Boolean(carrierLoadError) || carriers.length === 0" destroy-on-close @confirm="assignCarrierAndApprove" @cancel="closeApproval" >
< el-alert title = "订单审核通过后将进入履约流程;承运商名称由后端公共主数据生成快照。" type = "info" :closable = "false" show -icon />
< el-alert v-if = "carrierLoadError" class="carrier-load-error" type="error" :closable="false" show -icon >
< template # title >< div class = "api-error-alert-title" >< span >{{ carrierLoadError . message }}</ span >< ApiErrorMeta :details = "carrierLoadError" /></ div ></ template >
< el-button link type = "danger" :loading = "carrierLoading" @click ="loadCarriers" > 重新加载 </ el -button >
</ el-alert >
< el-alert v-else-if = "!carrierLoading && carriers.length === 0" class="carrier-load-error" title="暂无启用的承运商,请先到往来单位维护承运商主数据。" type="warning" :closable="false" show -icon />
< el-form ref = "carrierFormRef" :model = "carrierForm" :rules = "carrierRules" label -position =" top " class = "carrier-form" : disabled = "carrierLoading || approvalSaving || Boolean(carrierLoadError)" >
< el-form-item label = "承运商" prop = "carrierId" required >
< el-select v-model = "carrierForm.carrierId" filterable :loading="carrierLoading" placeholder="请选择启用的承运商" >
< el-option v-for = "carrier in carriers" :key="String(carrier.id)" :label="`${carrier.partnerName}( ${carrier.partnerCode}) `" :value="String(carrier.id)" />
</ el-select >
</ el-form-item >
</ el-form >
</ AppDialog >
2026-08-06 13:56:04 +08:00
</ section >
</ template >
< style scoped >
. unified - orders - page { min - width : 0 ; }
. 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 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 ); }
. status - tabs + : deep (. query - toolbar ) { border - top : 0 ; }
2026-08-06 13:56:04 +08:00
. orders - load - error { margin - top : 12 px ; }
. table - card { margin - top : 16 px ; overflow : hidden ; border : 1 px solid var ( -- cc - border ); border - radius : 8 px ; background : # fff ; }
. row - actions { display : flex ; align - items : center ; gap : 4 px ; white - space : nowrap ; }
. row - actions : deep (. el - button ) { margin - left : 0 ; }
2026-08-06 18:02:04 +08:00
. carrier - form { margin - top : 18 px ; }
. carrier - form : deep (. el - select ) { width : 100 % ; }
. carrier - load - error { margin - top : 14 px ; }
2026-08-06 13:56:04 +08:00
. 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 ; }
. detail - content { display : grid ; gap : 22 px ; min - width : 0 ; }
. detail - summary , . detail - section { padding : 16 px ; border : 1 px solid var ( -- cc - border ); border - radius : 8 px ; background : # fff ; }
. detail - summary__header { display : flex ; align - items : flex - start ; justify - content : space - between ; gap : 12 px ; }
. detail - summary__actions { display : flex ; align - items : center ; flex - wrap : wrap ; justify - content : flex - end ; gap : 8 px ; }
. eyebrow { color : var ( -- cc - muted ); font - size : 12 px ; }
. detail - summary h2 { margin : 4 px 0 0 ; color : var ( -- cc - text ); font - size : 18 px ; }
. detail - grid { display : grid ; grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr )); gap : 14 px 20 px ; margin : 20 px 0 0 ; }
. detail - grid div { min - width : 0 ; }
. detail - grid dt { color : var ( -- cc - muted ); font - size : 12 px ; }
. detail - grid dd { margin : 4 px 0 0 ; color : var ( -- cc - text ); overflow - wrap : anywhere ; }
. detail - section h3 { margin : 0 0 12 px ; color : var ( -- cc - text ); font - size : 15 px ; }
. history - list , . notes - list { display : grid ; gap : 12 px ; margin : 0 ; padding : 0 ; list - style : none ; }
. history - list li { display : flex ; align - items : flex - start ; gap : 10 px ; padding - bottom : 12 px ; border - bottom : 1 px solid # edf0f4 ; }
. history - list li : last - child , . notes - list li : last - child { padding - bottom : 0 ; border - bottom : 0 ; }
. history - list strong { font - size : 13 px ; }
. history - list p , . notes - list p { margin : 3 px 0 0 ; color : var ( -- cc - muted ); font - size : 12 px ; line - height : 1.5 ; overflow - wrap : anywhere ; }
. notes - list li { padding - bottom : 12 px ; border - bottom : 1 px solid # edf0f4 ; }
. 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 15:32:02 +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 ; } . detail - grid { grid - template - columns : 1 fr ; } . detail - summary__header { flex - direction : column ; } . detail - summary__actions { justify - content : flex - start ; } . row - actions { flex - wrap : wrap ; } }
2026-08-06 13:56:04 +08:00
</ style >