@@ -1,744 +0,0 @@
< script setup lang = "ts" >
import { computed , nextTick , onMounted , reactive , ref , watch } from 'vue'
import { ElMessage , ElMessageBox } from 'element-plus'
import type { FormInstance , FormRules } from 'element-plus'
import { ArrowLeft , CircleCheckBig , CircleX , Eye , FilePenLine , MoreHorizontal , PackageCheck } from '@lucide/vue'
import { useRoute , useRouter } from 'vue-router'
import {
acceptPlatformFirstLegOrder ,
addPlatformFirstLegOrderNote ,
approvePlatformFirstLegOrder ,
batchApprovePlatformFirstLegOrders ,
cancelPlatformFirstLegOrder ,
completePlatformFirstLegOrder ,
getPlatformFirstLegOrder ,
listPlatformFirstLegOrders ,
startPlatformFirstLegOrder ,
updatePlatformFirstLegOrderExpress ,
type PlatformFirstLegOrderDetailResource ,
type PlatformFirstLegOrderResource ,
type PlatformFirstLegOrderStatus ,
} from '@/modules/first-leg-orders/api/platformFirstLegOrders'
import { ApiResponseError , getApiErrorDetails , type ApiErrorDetails } from '@/shared/api/http'
import AdminPage from '@/shared/components/AdminPage.vue'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import DetailPageLayout from '@/shared/components/DetailPageLayout.vue'
import StatusBadge from '@/shared/components/StatusBadge.vue'
import DataTable , { type DataTableColumn } from '@/shared/components/DataTable.vue'
import ListPageLayout from '@/shared/components/ListPageLayout.vue'
import QueryToolbar , { type QueryField , type QueryModel } from '@/shared/components/QueryToolbar.vue'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { listPlatformTenants , type PlatformTenantResource } from '@/modules/iam'
const route = useRoute ( )
const router = useRouter ( )
const access = useAccessPreviewStore ( )
const orders = ref < PlatformFirstLegOrderResource [ ] > ( [ ] )
const tenants = ref < PlatformTenantResource [ ] > ( [ ] )
const tenantLoadError = ref < ApiErrorDetails | null > ( null )
const total = ref ( 0 )
const page = ref ( 1 )
const pageSize = ref ( 20 )
const loading = ref ( false )
const actionLoadingId = ref ( '' )
const batchReviewLoading = ref ( false )
const selectedReviewOrders = ref < PlatformFirstLegOrderResource [ ] > ( [ ] )
const loadError = ref < ApiErrorDetails | null > ( null )
const detailLoading = ref ( false )
const detail = ref < PlatformFirstLegOrderDetailResource | null > ( null )
const noteDialogOpen = ref ( false )
const expressDialogOpen = ref ( false )
const selectedOrder = ref < PlatformFirstLegOrderResource | null > ( null )
const noteForm = reactive ( { content : '' } )
const noteSaving = ref ( false )
const expressSaving = ref ( false )
const expressFormRef = ref < FormInstance > ( )
const noteFormRef = ref < FormInstance > ( )
const expressForm = reactive ( { expressCompany : '' , expressTrackingNo : '' } )
const filters = ref < QueryModel & {
tenantId : string
keyword : string
status : PlatformFirstLegOrderStatus | ' '
dateRange : string [ ]
} > ( {
tenantId : '' ,
keyword : '' ,
status : 'SUBMITTED' ,
dateRange : [ ] ,
} )
const queryFields = computed < QueryField [ ] > ( ( ) => [
{
key : 'tenantId' ,
type : 'select' ,
label : '企业客户' ,
width : 160 ,
placeholder : tenantLoadError . value ? '企业列表加载失败' : '全部企业' ,
props : { 'aria-label' : '企业客户' , filterable : true , disabled : Boolean ( tenantLoadError . value ) } ,
options : tenants . value . map ( ( tenant ) => ( { value : String ( tenant . id ) , label : tenant . legalName ? . trim ( ) || tenant . tenantName } ) ) ,
} ,
{
key : 'status' ,
type : 'select' ,
label : '订单状态' ,
width : 120 ,
placeholder : '全部状态' ,
props : { 'aria-label' : '订单状态' } ,
options : [
{ value : 'DRAFT' , label : '草稿' } ,
{ value : 'SUBMITTED' , label : '待审核' } ,
{ value : 'APPROVED' , label : '已审核' } ,
{ value : 'ACCEPTED' , label : '已受理' } ,
{ value : 'PROCESSING' , label : '处理中' } ,
{ value : 'COMPLETED' , label : '已完成' } ,
{ value : 'CANCELLED' , label : '已取消' } ,
{ value : 'REJECTED' , label : '已驳回(旧契约)' } ,
] ,
} ,
{ key : 'keyword' , type : 'input' , label : '订单关键词' , placeholder : '订单号或发货客户名称' , props : { 'aria-label' : '订单关键词' } } ,
{ key : 'dateRange' , type : 'daterange' , label : '订单创建日期' , props : { 'aria-label' : '订单创建日期' } } ,
] )
const orderColumns = computed < DataTableColumn [ ] > ( ( ) => [
{ key : 'actions' , label : '操作' , minWidth : 288 , fixed : 'left' , slotName : 'actions' } ,
... ( can ( 'platform:order:approve' ) ? [ {
key : 'selection' , type : 'selection' , width : 48 , fixed : 'left' ,
selectable : ( row : PlatformFirstLegOrderResource ) => row . status === 'SUBMITTED' && ! batchReviewLoading . value ,
} satisfies DataTableColumn ] : [ ] ) ,
{ key : 'status' , label : '状态' , width : 108 , slotName : 'status' } ,
{ key : 'orderInfo' , label : '订单信息' , minWidth : 210 , slotName : 'orderInfo' } ,
{ key : 'customer' , label : '客户' , minWidth : 190 , showOverflowTooltip : true , slotName : 'customer' } ,
{ key : 'transport' , label : '运输方案' , minWidth : 180 , slotName : 'transport' } ,
{ key : 'destination' , label : '目的地' , minWidth : 150 , slotName : 'destination' } ,
{ key : 'receivingWarehouse' , label : '平台收货仓' , minWidth : 180 , slotName : 'receivingWarehouse' } ,
{ key : 'express' , label : '履约快递' , minWidth : 160 , slotName : 'express' } ,
] )
const expressRules : FormRules = {
expressCompany : [ { required : true , message : '请填写快递公司' , trigger : 'blur' } ] ,
expressTrackingNo : [
{ required : true , message : '请填写快递单号' , trigger : 'blur' } ,
{ pattern : /^[A-Za-z0-9-]{4,128}$/ , message : '快递单号格式不正确' , trigger : 'blur' } ,
] ,
}
const noteRules : FormRules = {
content : [ { required : true , message : '请填写内部备注' , trigger : 'blur' } ] ,
}
const detailOrderId = computed ( ( ) => {
const orderId = route . query . orderId
return typeof orderId === 'string' && orderId ? orderId : ''
} )
const detailTenantId = computed ( ( ) => {
const tenantId = route . query . tenantId
return typeof tenantId === 'string' && tenantId ? tenantId : ''
} )
const isDetailPage = computed ( ( ) => Boolean ( detailOrderId . value ) )
const statusOptions : Array < { value : PlatformFirstLegOrderStatus ; label : string } > = [
{ value : 'DRAFT' , label : '草稿' } ,
{ value : 'SUBMITTED' , label : '待审核' } ,
{ value : 'APPROVED' , label : '已审核' } ,
{ value : 'ACCEPTED' , label : '已受理' } ,
{ value : 'PROCESSING' , label : '处理中' } ,
{ value : 'COMPLETED' , label : '已完成' } ,
{ value : 'CANCELLED' , label : '已取消' } ,
{ value : 'REJECTED' , label : '已驳回(旧契约)' } ,
]
function statusLabel ( status : PlatformFirstLegOrderStatus ) {
return statusOptions . find ( ( item ) => item . value === status ) ? . label || '未知状态'
}
function historyActionLabel ( action : string | null | undefined ) {
return ( {
CREATE : '创建订单' , CREATED : '创建订单' , SUBMIT : '提交订单' , SUBMITTED : '提交订单' ,
APPROVE : '审核通过' , APPROVED : '审核通过' , REJECT : '审核驳回' , REJECTED : '审核驳回' ,
CANCEL : '取消订单' , CANCELLED : '取消订单' , STATUS _UPDATE : '状态更新' ,
} as Record < string , string > ) [ action || '' ] || '状态已更新。'
}
function transportLabel ( order : PlatformFirstLegOrderResource ) {
if ( order . transportMode === 'SEA' ) return order . containerMode === 'FULL_CONTAINER' ? '海运整箱 FCL' : '海运拼箱 LCL'
if ( order . transportMode === 'AIR' ) return '空运'
if ( order . transportMode === 'RAIL' ) return '铁路运输'
return '陆运'
}
function formattedDate ( value : string | null | undefined ) {
return value ? value . replace ( 'T' , ' ' ) . slice ( 0 , 16 ) : '-'
}
function formatMeasurement ( value : unknown ) {
const numeric = Number ( value )
return value === null || value === undefined || value === '' || ! Number . isFinite ( numeric )
? '—'
: numeric . toLocaleString ( 'zh-CN' , { maximumFractionDigits : 3 } )
}
function tenantFor ( tenantId : number | string ) {
return tenants . value . find ( ( tenant ) => String ( tenant . id ) === String ( tenantId ) )
}
function tenantName ( tenantId : number | string ) {
return tenantFor ( tenantId ) ? . tenantName || ` 企业 ${ tenantId } `
}
function customerDisplay ( order : PlatformFirstLegOrderResource ) {
const tenant = tenantFor ( order . tenantId )
const name = tenant ? . legalName ? . trim ( ) || tenant ? . tenantName ? . trim ( ) || order . customerName ? . trim ( ) || '未命名企业'
const code = tenant ? . tenantCode ? . trim ( )
const secondary = code ? ` 客户编码 ${ code } ` : order . customerName ? . trim ( )
return { name , secondary : secondary && secondary !== name ? secondary : '' }
}
async function loadTenants ( ) {
tenantLoadError . value = null
try {
const records : PlatformTenantResource [ ] = [ ]
let currentPage = 1
let pages = 1
do {
const result = await listPlatformTenants ( { page : currentPage , size : 200 , status : 'ACTIVE' } )
records . push ( ... result . records )
pages = Math . max ( 1 , result . pages )
currentPage += 1
} while ( currentPage <= pages )
tenants . value = records
} catch ( error ) {
tenants . value = [ ]
tenantLoadError . value = getApiErrorDetails ( error , '企业列表加载失败,暂时无法按企业筛选;订单仍可正常审核' )
}
}
function can ( action : string ) {
return access . can ( action )
}
function canCancel ( order : PlatformFirstLegOrderResource ) {
return can ( 'platform:order:cancel' ) && [ 'DRAFT' , 'SUBMITTED' , 'APPROVED' , 'ACCEPTED' , 'REJECTED' ] . includes ( order . status )
}
function canMaintainExpress ( order : PlatformFirstLegOrderResource ) {
return can ( 'platform:order:fulfillment' ) && ! [ 'DRAFT' , 'REJECTED' , 'CANCELLED' ] . includes ( order . status )
}
type SecondaryAction = 'express' | 'note' | 'cancel'
function secondaryActions ( order : PlatformFirstLegOrderResource ) : SecondaryAction [ ] {
const actions : SecondaryAction [ ] = [ ]
if ( canMaintainExpress ( order ) ) actions . push ( 'express' )
if ( can ( 'platform:order:note' ) ) actions . push ( 'note' )
if ( canCancel ( order ) ) actions . push ( 'cancel' )
return actions
}
function directSecondaryActionCount ( order : PlatformFirstLegOrderResource ) {
const primaryActionCount = actionLabel ( order ) ? 2 : 1
return Math . max ( 0 , 3 - primaryActionCount )
}
function isDirectSecondaryAction ( order : PlatformFirstLegOrderResource , action : SecondaryAction ) {
const index = secondaryActions ( order ) . indexOf ( action )
return index >= 0 && index < directSecondaryActionCount ( order )
}
function isOverflowSecondaryAction ( order : PlatformFirstLegOrderResource , action : SecondaryAction ) {
const index = secondaryActions ( order ) . indexOf ( action )
return index >= directSecondaryActionCount ( order )
}
function hasOverflowActions ( order : PlatformFirstLegOrderResource ) {
return secondaryActions ( order ) . length > directSecondaryActionCount ( order )
}
function moreOrderActionsLabel ( order : PlatformFirstLegOrderResource ) {
const compatibleOrder = order as PlatformFirstLegOrderResource & { orderId ? : string | number | null }
const identifier = [ compatibleOrder . orderNo , compatibleOrder . id , compatibleOrder . orderId ]
. map ( ( value ) => String ( value ? ? '' ) . trim ( ) )
. find ( Boolean )
return ` 更多订单操作: ${ identifier || '当前订单' } `
}
function runSecondaryAction ( command : string | number | object , order : PlatformFirstLegOrderResource ) {
if ( command === 'express' ) openExpress ( order )
if ( command === 'note' ) openNote ( order )
if ( command === 'cancel' ) void cancelOrder ( order )
}
function actionLabel ( order : PlatformFirstLegOrderResource ) {
if ( order . status === 'SUBMITTED' && can ( 'platform:order:approve' ) ) return '审核通过'
if ( order . status === 'APPROVED' && can ( 'platform:order:accept' ) ) return '受理订单'
if ( order . status === 'ACCEPTED' && can ( 'platform:order:process' ) ) return '开始处理'
if ( order . status === 'PROCESSING' && can ( 'platform:order:complete' ) ) return '完成订单'
return ''
}
async function loadOrders ( ) {
loadError . value = null
selectedReviewOrders . value = [ ]
loading . value = true
try {
const result = await listPlatformFirstLegOrders ( {
tenantId : filters . value . tenantId || undefined ,
keyword : filters . value . keyword . trim ( ) || undefined ,
status : filters . value . status || undefined ,
createdFrom : filters . value . dateRange . length === 2 ? ` ${ filters . value . dateRange [ 0 ] } T00:00:00 ` : undefined ,
createdTo : filters . value . dateRange . length === 2 ? ` ${ filters . value . dateRange [ 1 ] } T23:59:59 ` : undefined ,
page : page . value ,
size : pageSize . value ,
} )
orders . value = result . records
total . value = result . total
} catch ( error ) {
orders . value = [ ]
total . value = 0
loadError . value = getApiErrorDetails ( error , '订单列表加载失败,请检查 Gateway、租户范围和订单权限' )
} finally {
loading . value = false
}
}
function handleReviewSelectionChange ( rows : PlatformFirstLegOrderResource [ ] ) {
selectedReviewOrders . value = rows . filter ( ( row ) => row . status === 'SUBMITTED' )
}
function groupOrdersByTenant ( selected : PlatformFirstLegOrderResource [ ] ) {
const groups = new Map < string , PlatformFirstLegOrderResource [ ] > ( )
for ( const order of selected ) {
const tenantId = String ( order . tenantId )
groups . set ( tenantId , [ ... ( groups . get ( tenantId ) || [ ] ) , order ] )
}
return groups
}
async function batchApproveSelectedOrders ( ) {
const selected = [ ... selectedReviewOrders . value ]
if ( ! selected . length || batchReviewLoading . value ) return
const customerCount = new Set ( selected . map ( ( order ) => String ( order . tenantId ) ) ) . size
batchReviewLoading . value = true
try {
await ElMessageBox . confirm (
` 确认批量审核通过已勾选的 ${ selected . length } 个订单吗?涉及 ${ customerCount } 个客户,审核成功后订单将进入待测量。 ` ,
'批量审核通过' ,
{ confirmButtonText : '确认批量审核' , cancelButtonText : '取消' , type : 'warning' } ,
)
let successCount = 0
let failureCount = 0
const failureMessages : string [ ] = [ ]
for ( const [ tenantId , tenantOrders ] of groupOrdersByTenant ( selected ) ) {
try {
const result = await batchApprovePlatformFirstLegOrders (
tenantId ,
tenantOrders . map ( ( order ) => String ( order . id ) ) ,
)
successCount += result . successCount
failureCount += result . failureCount
failureMessages . push ( ... result . failedOrders . map ( ( failure ) => failure . message ) )
} catch ( error ) {
failureCount += tenantOrders . length
failureMessages . push ( getApiErrorDetails ( error , '该客户订单批量审核失败' ) . message )
}
}
await loadOrders ( )
const reasons = [ ... new Set ( failureMessages . filter ( Boolean ) ) ] . slice ( 0 , 3 ) . join ( '; ' )
if ( failureCount === 0 ) {
ElMessage . success ( ` 已批量审核通过 ${ successCount } 个订单 ` )
} else if ( successCount > 0 ) {
ElMessage . warning ( ` 批量审核完成:成功 ${ successCount } 个,失败 ${ failureCount } 个 ${ reasons ? ` 。 ${ reasons } ` : '' } ` )
} else {
ElMessage . error ( ` 批量审核失败: ${ failureCount } 个订单未通过 ${ reasons ? ` 。 ${ reasons } ` : '' } ` )
}
} catch ( error ) {
if ( error === 'cancel' || error === 'close' ) return
ElMessage . error ( getApiErrorDetails ( error , '批量审核失败,请稍后重试' ) . message )
} finally {
batchReviewLoading . value = false
}
}
function searchOrders ( ) {
if ( page . value !== 1 ) {
page . value = 1
return
}
void loadOrders ( )
}
async function resetOrders ( ) {
filters . value = { ... filters . value , status : 'SUBMITTED' }
await nextTick ( )
searchOrders ( )
}
watch ( [ page , pageSize ] , ( ) => {
void loadOrders ( )
} )
async function openDetail ( order : PlatformFirstLegOrderResource ) {
selectedOrder . value = order
await router . push ( {
path : route . path ,
query : { ... route . query , orderId : String ( order . id ) , tenantId : String ( order . tenantId ) } ,
} )
}
async function loadDetail ( orderId : string , tenantId = detailTenantId . value ) {
if ( ! tenantId ) {
detail . value = null
ElMessage . error ( '订单详情缺少企业租户标识,请从订单列表重新打开' )
return
}
detail . value = null
detailLoading . value = true
try {
detail . value = await getPlatformFirstLegOrder ( tenantId , orderId )
selectedOrder . value = detail . value . order
} catch ( error ) {
const details = getApiErrorDetails ( error , '订单详情加载失败' )
ElMessage . error ( details . message )
} finally {
detailLoading . value = false
}
}
async function refreshAfterAction ( orderId : string ) {
await loadOrders ( )
if ( detailOrderId . value === orderId ) {
await loadDetail ( orderId )
}
}
function closeDetailPage ( ) {
const { orderId : _orderId , tenantId : _tenantId , ... query } = route . query
void router . replace ( { path : route . path , query } )
}
function getFieldValidationMessage ( error : unknown , field : string , fallback : string ) {
const details = getApiErrorDetails ( error , fallback )
if ( error instanceof ApiResponseError && error . details && typeof error . details === 'object' ) {
const fieldMessage = ( error . details as Record < string , unknown > ) [ field ]
if ( typeof fieldMessage === 'string' && fieldMessage ) {
return ` ${ details . message } : ${ fieldMessage } `
}
}
return details . message
}
async function runLifecycleAction ( order : PlatformFirstLegOrderResource ) {
const label = actionLabel ( order )
if ( ! label ) return
try {
await ElMessageBox . confirm ( ` 确认 ${ label } “ ${ order . orderNo || order . id } ”吗?该操作会写入订单状态历史。 ` , label , {
confirmButtonText : ` 确认 ${ label } ` ,
cancelButtonText : '取消' ,
type : 'warning' ,
} )
actionLoadingId . value = String ( order . id )
const tenantId = String ( order . tenantId )
if ( order . status === 'SUBMITTED' ) await approvePlatformFirstLegOrder ( tenantId , String ( order . id ) )
if ( order . status === 'APPROVED' ) await acceptPlatformFirstLegOrder ( tenantId , String ( order . id ) )
if ( order . status === 'ACCEPTED' ) await startPlatformFirstLegOrder ( tenantId , String ( order . id ) )
if ( order . status === 'PROCESSING' ) await completePlatformFirstLegOrder ( tenantId , String ( order . id ) )
await refreshAfterAction ( String ( order . id ) )
ElMessage . success ( ` ${ label } 已完成 ` )
} catch ( error ) {
if ( error === 'cancel' || error === 'close' ) return
ElMessage . error ( getApiErrorDetails ( error , ` ${ label } 失败,请稍后重试 ` ) . message )
} finally {
actionLoadingId . value = ''
}
}
async function cancelOrder ( order : PlatformFirstLegOrderResource ) {
try {
const result = await ElMessageBox . prompt ( '请填写取消原因。后端要求该字段必填,并将写入订单状态历史。' , '取消订单' , {
confirmButtonText : '确认取消' ,
cancelButtonText : '返回' ,
inputPattern : /\S+/ ,
inputErrorMessage : '请填写取消原因' ,
inputPlaceholder : '例如:客户取消本次运输安排' ,
} )
actionLoadingId . value = String ( order . id )
await cancelPlatformFirstLegOrder ( String ( order . tenantId ) , String ( order . id ) , result . value . trim ( ) )
await refreshAfterAction ( String ( order . id ) )
ElMessage . success ( '订单已取消' )
} catch ( error ) {
if ( error === 'cancel' || error === 'close' ) return
ElMessage . error ( getApiErrorDetails ( error , '取消订单失败,请稍后重试' ) . message )
} finally {
actionLoadingId . value = ''
}
}
function openExpress ( order : PlatformFirstLegOrderResource ) {
selectedOrder . value = order
expressForm . expressCompany = order . expressCompany || ''
expressForm . expressTrackingNo = order . expressTrackingNo || ''
expressDialogOpen . value = true
void nextTick ( ( ) => expressFormRef . value ? . clearValidate ( ) )
}
async function saveExpress ( ) {
if ( ! selectedOrder . value ) return
const valid = await expressFormRef . value ? . validate ( ) . catch ( ( ) => false )
if ( ! valid ) return
expressSaving . value = true
try {
await updatePlatformFirstLegOrderExpress ( String ( selectedOrder . value . tenantId ) , String ( selectedOrder . value . id ) , {
expressCompany : expressForm . expressCompany . trim ( ) ,
expressTrackingNo : expressForm . expressTrackingNo . trim ( ) ,
} )
expressDialogOpen . value = false
await refreshAfterAction ( String ( selectedOrder . value . id ) )
ElMessage . success ( '履约快递信息已保存' )
} catch ( error ) {
ElMessage . error ( getFieldValidationMessage ( error , 'expressTrackingNo' , '履约快递信息保存失败' ) )
} finally {
expressSaving . value = false
}
}
function openNote ( order : PlatformFirstLegOrderResource ) {
selectedOrder . value = order
noteForm . content = ''
noteDialogOpen . value = true
void nextTick ( ( ) => noteFormRef . value ? . clearValidate ( ) )
}
async function saveNote ( ) {
if ( ! selectedOrder . value ) return
const valid = await noteFormRef . value ? . validate ( ) . catch ( ( ) => false )
if ( ! valid ) return
noteSaving . value = true
try {
await addPlatformFirstLegOrderNote ( String ( selectedOrder . value . tenantId ) , String ( selectedOrder . value . id ) , noteForm . content . trim ( ) )
noteDialogOpen . value = false
await refreshAfterAction ( String ( selectedOrder . value . id ) )
ElMessage . success ( '内部备注已保存' )
} catch ( error ) {
ElMessage . error ( getApiErrorDetails ( error , '内部备注保存失败' ) . message )
} finally {
noteSaving . value = false
}
}
watch ( [ detailOrderId , detailTenantId ] , ( [ orderId , tenantId ] ) => {
if ( orderId ) {
void loadDetail ( orderId , tenantId )
return
}
detail . value = null
selectedOrder . value = null
void loadOrders ( )
} , { immediate : true } )
onMounted ( ( ) => void loadTenants ( ) )
< / script >
< template >
< AdminPage >
< div class = "business-page" >
< template v-if = "isDetailPage" >
< section class = "surface-panel detail-page" v-loading = "detailLoading" >
< DetailPageLayout ariaLabel = "头程订单详情" >
< template # header >
< header class = "detail-page-header" >
< div >
< h1 > 订单详情 < / h1 >
< p > 查看订单信息 、 履约快递 、 内部备注和状态历史 。 < / p >
< / div >
< el-button @click ="closeDetailPage" > < ArrowLeft :size = "16" / > 返回订单列表 < / el-button >
< / header >
< / template >
< template v-if = "detail" >
< div class = "order-detail-head" >
< div >
< span > { { transportLabel ( detail . order ) } } < / span >
< h2 > { { detail . order . orderNo || detail . order . id } } < / h2 >
< p > { { tenantName ( detail . order . tenantId ) } } · { { detail . order . customerName } } · { { detail . order . referenceId || '无客户参考号' } } < / p >
< / div >
< StatusBadge :status = "statusLabel(detail.order.status)" / >
< / div >
< div class = "detail-section" >
< h3 > 订单概要 < / h3 >
< el-descriptions :column = "2" border >
< el-descriptions-item label = "目标仓 / 国家" > { { detail . order . destinationWarehouseName || '-' } } · { { detail . order . destinationCountryCode || '-' } } < / el-descriptions-item >
< el-descriptions-item label = "平台收货仓" > { { detail . order . receivingWarehouseName || '-' } } < / el-descriptions-item >
< el-descriptions-item label = "收货地址" :span = "2" > { { detail . order . receivingWarehouseAddress || '-' } } < / el-descriptions-item >
< el-descriptions-item label = "渠道" > { { detail . order . channelName || '-' } } ( { { detail . order . channelCode || '-' } } ) < / el-descriptions-item >
< el-descriptions-item label = "预计出运" > { { detail . order . expectedShipDate || '-' } } < / el-descriptions-item >
< el-descriptions-item label = "客户备注" :span = "2" > { { detail . order . customerRemark || '-' } } < / el-descriptions-item >
< / el-descriptions >
< / div >
< div class = "detail-section" >
< h3 > 货物明细 < / h3 >
< div class = "detail-table-scroll" >
< el-table :data = "detail.cargoes" size = "small" border stripe empty -text = " 暂无货物明细 " >
< el-table-column prop = "itemNo" label = "#" width = "52" / >
< el-table-column prop = "cargoName" label = "品名" min -width = " 150 " / >
< el-table-column prop = "packageType" label = "包装" min -width = " 100 " / >
< el-table-column prop = "packageCount" label = "件/箱" width = "86" / >
< el-table-column label = "毛重 / 体积" min -width = " 150 " > < template # default = "{ row }" > { { formatMeasurement ( row . grossWeightKg ) } } kg / { { formatMeasurement ( row . volumeCbm ) } } m³ < / template > < / el-table-column >
< / el-table >
< / div >
< / div >
< div class = "detail-section" >
< h3 > 收发货方 < / h3 >
< div class = "party-grid" >
< div v-for = "party in detail.parties" :key="party.id" >
< span > { { party . partyType === 'SENDER' ? '发货方' : '收货方' } } < / span >
< strong > { { party . companyName } } < / strong >
< p > { { party . contactName } } · { { party . phone } } < / p >
< p > { { [ party . province , party . city , party . district , party . addressLine ] . filter ( Boolean ) . join ( ' ' ) } } < / p >
< / div >
< / div >
< / div >
< div class = "detail-section" >
< h3 > 履约快递 < / h3 >
< el-descriptions :column = "2" border >
< el-descriptions-item label = "快递公司" > { { detail . order . expressCompany || '-' } } < / el-descriptions-item >
< el-descriptions-item label = "快递单号" > { { detail . order . expressTrackingNo || '-' } } < / el-descriptions-item >
< / el-descriptions >
< / div >
< div class = "detail-section" >
< h3 > 内部备注 < / h3 >
< el-empty v-if = "!detail.internalNotes.length" description="暂无内部备注" :image-size="52" / >
< el-timeline v-else > < el -timeline -item v-for = "note in detail.internalNotes" :key="note.id" :timestamp="formattedDate(note.createdAt)" placement="top" > < p > { { note . content } } < / p > < / el-timeline-item > < / el-timeline >
< / div >
< div class = "detail-section" >
< h3 > 状态历史 < / h3 >
< el-timeline > < el-timeline-item v-for = "item in detail.statusHistory" :key="item.id" :timestamp="`${formattedDate(item.occurredAt)} · ${item.operatorName || '系统'}`" placement="top" > < strong > { { statusLabel ( item . toStatus ) } } < / strong > < p > { { item . reason || historyActionLabel ( item . action ) } } < / p > < / el-timeline-item > < / el-timeline >
< / div >
< / template >
< el-empty v-else-if = "!detailLoading" description="订单详情暂不可用"><el-button type="primary" @click="closeDetailPage"><ArrowLeft :size="16" / > 返回订单列表 < / el-button > < / el-empty >
< / DetailPageLayout >
< / section >
< / template >
< template v-else >
< ListPageLayout class = "first-leg-order-list" ariaLabel = "头程订单列表" >
< template # toolbar >
< QueryToolbar
v-model = "filters"
ariaLabel = "头程订单筛选"
:fields = "queryFields"
preference -key = " first -leg -orders -query "
search -text = " 查询 "
:search-on-reset = "false"
@search ="searchOrders"
@reset ="resetOrders"
/ >
< / template >
< template v-if = "tenantLoadError || loadError" # state >
< el-alert v-if = "tenantLoadError" type="warning" :closable="false" show-icon role="alert" >
< template # title > < div class = "api-error-alert-title" > < span > { { tenantLoadError . message } } < / span > < ApiErrorMeta :details = "tenantLoadError" / > < / div > < / template >
< el-button link type = "warning" @click ="loadTenants" > 重新加载企业列表 < / el -button >
< / el-alert >
< el-alert v-if = "loadError" type="error" :closable="false" show-icon role="alert" >
< template # title > < div class = "api-error-alert-title" > < span > { { loadError . message } } < / span > < ApiErrorMeta :details = "loadError" / > < / div > < / template >
< el-button link type = "danger" @click ="loadOrders" > 重新加载 < / el -button >
< / el-alert >
< / template >
< template # default >
< section v-if = "!loadError" class="surface-panel table-panel" >
< DataTable
v -model :page = "page"
v -model :page-size = "pageSize"
:data = "orders"
:columns = "orderColumns"
preference -key = " first -leg -orders -table "
:loading = "loading"
:total = "total"
: page -sizes = " [ 20 , 50 , 100 ] "
empty -text = " 当前没有符合条件的头程订单 "
row -key = " id "
@ selection -change = " handleReviewSelectionChange "
>
< template # toolbar -actions >
< el-button
v-if = "can('platform:order:approve')"
type = "success"
:loading = "batchReviewLoading"
: disabled = "loading || batchReviewLoading || selectedReviewOrders.length === 0"
@click ="batchApproveSelectedOrders"
> < CircleCheckBig :size = "16" / > 批量审核通过 < span v-if = "selectedReviewOrders.length" > ( {{ selectedReviewOrders.length }} ) < / span > < / el -button >
< / template >
< template # pagination -left = " { total : tableTotal } " > < span > 共 { { tableTotal } } 条订单 < / span > < / template >
< template # actions = "{ row }" >
< div class = "table-actions order-row-actions" >
< el-button link type = "primary" : disabled = "actionLoadingId === String((row as PlatformFirstLegOrderResource).id)" @click ="openDetail(row as PlatformFirstLegOrderResource)" > < Eye :size = "15" / > 详情 < / el-button >
< el-button v-if = "actionLabel(row as PlatformFirstLegOrderResource)" size="small" plain type="success" :loading="actionLoadingId === String((row as PlatformFirstLegOrderResource).id)" @click="runLifecycleAction(row as PlatformFirstLegOrderResource)"><CircleCheckBig :size="15" / > { { actionLabel ( row as PlatformFirstLegOrderResource ) } } < / el-button >
< el-button v-if = "isDirectSecondaryAction(row as PlatformFirstLegOrderResource, 'express')" size="small" plain type="primary" :disabled="actionLoadingId === String((row as PlatformFirstLegOrderResource).id)" @click="openExpress(row as PlatformFirstLegOrderResource)"><PackageCheck :size="15" / > 维护快递 < / el-button >
< el-button v-if = "isDirectSecondaryAction(row as PlatformFirstLegOrderResource, 'note')" size="small" plain type="primary" :disabled="actionLoadingId === String((row as PlatformFirstLegOrderResource).id)" @click="openNote(row as PlatformFirstLegOrderResource)"><FilePenLine :size="15" / > 内部备注 < / el-button >
< el-button v-if = "isDirectSecondaryAction(row as PlatformFirstLegOrderResource, 'cancel')" size="small" plain type="danger" :disabled="actionLoadingId === String((row as PlatformFirstLegOrderResource).id)" @click="cancelOrder(row as PlatformFirstLegOrderResource)"><CircleX :size="15" / > 取消订单 < / el-button >
< el-dropdown v-if = "hasOverflowActions(row as PlatformFirstLegOrderResource)" trigger="click" popper-class="first-leg-action-menu" :disabled="actionLoadingId === String((row as PlatformFirstLegOrderResource).id)" @command="runSecondaryAction($event, row as PlatformFirstLegOrderResource)" >
< el -button link type = "primary" : aria -label = " moreOrderActionsLabel ( row as PlatformFirstLegOrderResource ) " : title = "moreOrderActionsLabel(row as PlatformFirstLegOrderResource)" > < MoreHorizontal :size = "17" / > < / el-button >
< template # dropdown >
< el-dropdown-menu >
< el-dropdown-item v-if = "isOverflowSecondaryAction(row as PlatformFirstLegOrderResource, 'express')" command="express"><PackageCheck :size="15" / > 维护快递 < / el-dropdown-item >
< el-dropdown-item v-if = "isOverflowSecondaryAction(row as PlatformFirstLegOrderResource, 'note')" command="note"><FilePenLine :size="15" / > 内部备注 < / el-dropdown-item >
< el-dropdown-item v-if = "isOverflowSecondaryAction(row as PlatformFirstLegOrderResource, 'cancel')" command="cancel" divided><CircleX :size="15" / > 取消订单 < / el-dropdown-item >
< / el-dropdown-menu >
< / template >
< / el-dropdown >
< / div >
< / template >
< template # status = "{ row }" > < StatusBadge : status = "statusLabel((row as PlatformFirstLegOrderResource).status)" / > < / template >
< template # orderInfo = "{ row }" > < div class = "primary-cell" > < strong > { { ( row as PlatformFirstLegOrderResource ) . orderNo || '-' } } < / strong > < span > { { ( row as PlatformFirstLegOrderResource ) . referenceId || '无客户参考号' } } · { { formattedDate ( ( row as PlatformFirstLegOrderResource ) . createdAt ) } } < / span > < / div > < / template >
< template # customer = "{ row }" > < div class = "primary-cell" > < strong > { { customerDisplay ( row as PlatformFirstLegOrderResource ) . name } } < / strong > < span v-if = "customerDisplay(row as PlatformFirstLegOrderResource).secondary" > {{ customerDisplay ( row as PlatformFirstLegOrderResource ) .secondary }} < / span > < / div > < / template >
< template # transport = "{ row }" > < div class = "primary-cell" > < strong > { { transportLabel ( row as PlatformFirstLegOrderResource ) } } < / strong > < span > { { ( row as PlatformFirstLegOrderResource ) . channelName || '未填写渠道' } } < / span > < / div > < / template >
< template # destination = "{ row }" > < div class = "primary-cell" > < strong > { { ( row as PlatformFirstLegOrderResource ) . destinationWarehouseName || '-' } } < / strong > < span > { { ( row as PlatformFirstLegOrderResource ) . destinationCountryCode || '-' } } < / span > < / div > < / template >
< template # receivingWarehouse = "{ row }" > < div class = "primary-cell" > < strong > { { ( row as PlatformFirstLegOrderResource ) . receivingWarehouseName || '-' } } < / strong > < span > { { ( row as PlatformFirstLegOrderResource ) . receivingWarehouseAddress || '-' } } < / span > < / div > < / template >
< template # express = "{ row }" > < div class = "primary-cell" > < strong > { { ( row as PlatformFirstLegOrderResource ) . expressCompany || '未维护' } } < / strong > < span > { { ( row as PlatformFirstLegOrderResource ) . expressTrackingNo || '—' } } < / span > < / div > < / template >
< / DataTable >
< / section >
< / template >
< / ListPageLayout >
< / template >
< el-dialog v-model = "expressDialogOpen" title="维护履约快递" width="min(520px, calc(100vw - 28px))" >
< el -form ref = "expressFormRef" :model = "expressForm" :rules = "expressRules" label -position = " top " class = "delivery-form" > < el-form-item label = "快递公司" prop = "expressCompany" required > < el-input v -model .trim = " expressForm.expressCompany " maxlength = "64" placeholder = "例如 顺丰速运" / > < / el-form-item > < el-form-item label = "快递单号" prop = "expressTrackingNo" required > < el-input v -model .trim = " expressForm.expressTrackingNo " maxlength = "128" placeholder = "请输入完整快递单号" / > < / el-form-item > < / el-form >
< template # footer > < el-button :disabled = "expressSaving" @click ="expressDialogOpen = false" > 取消 < / el -button > < el-button type = "primary" :loading = "expressSaving" @click ="saveExpress" > 保存快递信息 < / el -button > < / template >
< / el-dialog >
< el-dialog v-model = "noteDialogOpen" title="添加内部备注" width="min(520px, calc(100vw - 28px))" >
< el -form ref = "noteFormRef" :model = "noteForm" :rules = "noteRules" label -position = " top " > < el-form-item label = "内部备注" prop = "content" required > < el-input v -model .trim = " noteForm.content " maxlength = "2000" type = "textarea" :rows = "4" show -word -limit placeholder = "仅平台内部可见" / > < / el-form-item > < / el-form >
< template # footer > < el-button :disabled = "noteSaving" @click ="noteDialogOpen = false" > 取消 < / el -button > < el-button type = "primary" :loading = "noteSaving" @click ="saveNote" > 保存备注 < / el -button > < / template >
< / el-dialog >
< / div >
< / AdminPage >
< / template >
< style scoped >
. detail - page { overflow : hidden ; }
. first - leg - order - list { gap : 16 px ; }
. detail - page - header { display : flex ; min - height : 88 px ; align - items : center ; justify - content : space - between ; gap : 18 px ; padding : 20 px 24 px ; border - bottom : 1 px solid var ( -- cc - border ) ; }
. detail - page - header h1 { margin : 0 ; font - size : 20 px ; line - height : 1.35 ; }
. detail - page - header p { margin : 5 px 0 0 ; color : var ( -- cc - muted ) ; font - size : 12 px ; line - height : 1.5 ; }
. detail - table - scroll { overflow - x : auto ; }
. party - grid p { margin : 4 px 0 0 ; color : var ( -- cc - muted ) ; font - size : 12 px ; line - height : 1.6 ; overflow - wrap : anywhere ; }
. order - row - actions { gap : 8 px ; white - space : nowrap ; }
. order - row - actions : deep ( . el - button + . el - button ) { margin - left : 0 ; }
: global ( . first - leg - action - menu . el - dropdown - menu _ _item ) { min - height : 44 px ; gap : 8 px ; }
@ media ( max - width : 720 px ) {
. detail - page - header { align - items : flex - start ; flex - direction : column ; padding : 16 px ; }
. detail - page - header . el - button { width : 100 % ; min - height : 40 px ; }
. detail - table - scroll : deep ( . el - table ) { min - width : 560 px ; }
}
@ media ( max - width : 360 px ) {
. order - row - actions { align - items : stretch ; flex - direction : column ; }
. order - row - actions : deep ( . el - button ) { width : 100 % ; min - height : 40 px ; }
}
< / style >