2026-07-31 13:43:16 +08:00
< script setup lang = "ts" >
2026-08-04 12:57:50 +08:00
import { computed , nextTick , onMounted , reactive , ref , watch } from 'vue'
2026-07-31 13:43:16 +08:00
import { ElMessage , ElMessageBox } from 'element-plus'
2026-08-01 12:17:33 +08:00
import type { FormInstance , FormRules } from 'element-plus'
2026-08-03 17:44:54 +08:00
import { ArrowLeft , Eye , FilePenLine , PackageCheck } from '@lucide/vue'
import { useRoute , useRouter } from 'vue-router'
2026-07-31 13:43:16 +08:00
import {
acceptPlatformFirstLegOrder ,
addPlatformFirstLegOrderNote ,
approvePlatformFirstLegOrder ,
cancelPlatformFirstLegOrder ,
completePlatformFirstLegOrder ,
getPlatformFirstLegOrder ,
listPlatformFirstLegOrders ,
startPlatformFirstLegOrder ,
updatePlatformFirstLegOrderExpress ,
type PlatformFirstLegOrderDetailResource ,
type PlatformFirstLegOrderResource ,
type PlatformFirstLegOrderStatus ,
2026-08-03 18:49:18 +08:00
} from '@/modules/first-leg-orders/api/platformFirstLegOrders'
import { ApiResponseError , getApiErrorDetails , type ApiErrorDetails } from '@/shared/api/http'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import StatusBadge from '@/shared/components/StatusBadge.vue'
import DataTable , { type DataTableColumn } from '@/shared/components/DataTable.vue'
import QueryToolbar , { type QueryField , type QueryModel } from '@/shared/components/QueryToolbar.vue'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
2026-08-04 12:57:50 +08:00
import { listPlatformTenants , type PlatformTenantResource } from '@/modules/iam'
2026-07-31 13:43:16 +08:00
2026-08-03 17:44:54 +08:00
const route = useRoute ()
const router = useRouter ()
2026-07-31 13:43:16 +08:00
const access = useAccessPreviewStore ()
const orders = ref < PlatformFirstLegOrderResource [] >([])
2026-08-04 12:57:50 +08:00
const tenants = ref < PlatformTenantResource [] >([])
const tenantLoadError = ref < ApiErrorDetails | null >( null )
2026-07-31 13:43:16 +08:00
const total = ref ( 0 )
const page = ref ( 1 )
2026-08-01 12:17:33 +08:00
const pageSize = ref ( 20 )
2026-07-31 13:43:16 +08:00
const loading = ref ( false )
const actionLoadingId = ref ( '' )
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 )
2026-08-01 12:17:33 +08:00
const noteForm = reactive ({ content : '' })
2026-07-31 13:43:16 +08:00
const noteSaving = ref ( false )
const expressSaving = ref ( false )
2026-08-01 12:17:33 +08:00
const expressFormRef = ref < FormInstance >()
const noteFormRef = ref < FormInstance >()
2026-07-31 13:43:16 +08:00
const expressForm = reactive ({ expressCompany : '' , expressTrackingNo : '' })
2026-08-01 12:17:33 +08:00
const filters = ref < QueryModel & {
2026-08-04 12:57:50 +08:00
tenantId : string
2026-08-01 12:17:33 +08:00
keyword : string
status : PlatformFirstLegOrderStatus | ''
dateRange : string []
}>({
2026-08-04 12:57:50 +08:00
tenantId : '' ,
2026-07-31 13:43:16 +08:00
keyword : '' ,
2026-08-04 12:57:50 +08:00
status : 'SUBMITTED' ,
2026-07-31 13:43:16 +08:00
dateRange : [],
})
2026-08-04 12:57:50 +08:00
const queryFields = computed < QueryField [] >(() => [
{
key : 'tenantId' ,
type : 'select' ,
placeholder : tenantLoadError . value ? '企业列表加载失败' : '全部企业' ,
props : { 'aria-label' : '企业客户' , filterable : true , disabled : Boolean ( tenantLoadError . value ) },
options : tenants . value . map (( tenant ) => ({ value : String ( tenant . id ), label : ` ${ tenant . tenantName } ( ${ tenant . tenantCode || tenant . id } ) ` })),
},
{ key : 'keyword' , type : 'input' , placeholder : '订单号或发货客户名称' , props : { 'aria-label' : '订单关键词' } },
2026-08-01 12:17:33 +08:00
{
key : 'status' ,
type : 'select' ,
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 : 'dateRange' , type : 'daterange' , props : { 'aria-label' : '订单创建日期' } },
2026-08-04 12:57:50 +08:00
])
2026-08-01 12:17:33 +08:00
const orderColumns : DataTableColumn [] = [
{ key : 'actions' , label : '操作' , minWidth : 200 , fixed : 'left' , slotName : 'actions' },
{ 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' },
{ key : 'status' , label : '状态' , width : 120 , slotName : 'status' },
]
const expressRules : FormRules = {
expressCompany : [{ required : true , message : '请填写快递公司' , trigger : 'blur' }],
2026-08-03 17:44:54 +08:00
expressTrackingNo : [
{ required : true , message : '请填写快递单号' , trigger : 'blur' },
{ pattern : /^[A-Za-z0-9-]{4,128}$/ , message : '快递单号格式不正确' , trigger : 'blur' },
],
2026-08-01 12:17:33 +08:00
}
const noteRules : FormRules = {
content : [{ required : true , message : '请填写内部备注' , trigger : 'blur' }],
}
2026-08-03 17:44:54 +08:00
const detailOrderId = computed (() => {
const orderId = route . query . orderId
return typeof orderId === 'string' && orderId ? orderId : ''
})
2026-08-04 12:57:50 +08:00
const detailTenantId = computed (() => {
const tenantId = route . query . tenantId
return typeof tenantId === 'string' && tenantId ? tenantId : ''
})
2026-08-03 17:44:54 +08:00
const isDetailPage = computed (() => Boolean ( detailOrderId . value ))
2026-07-31 13:43:16 +08:00
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 || status
}
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' , ' ' ) : '-'
}
2026-08-04 12:57:50 +08:00
function tenantName ( tenantId : number | string ) {
return tenants . value . find (( tenant ) => String ( tenant . id ) === String ( tenantId )) ? . tenantName || `企业 ${ tenantId } `
}
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 })
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 , '企业列表加载失败,暂时无法按企业筛选;订单仍可正常审核' )
}
}
2026-07-31 13:43:16 +08:00
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 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 ''
}
2026-08-01 12:17:33 +08:00
async function loadOrders () {
2026-07-31 13:43:16 +08:00
loadError . value = null
loading . value = true
try {
2026-08-04 12:57:50 +08:00
const result = await listPlatformFirstLegOrders ({
tenantId : filters . value . tenantId || undefined ,
2026-08-01 12:17:33 +08:00
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 ,
2026-07-31 13:43:16 +08:00
page : page . value ,
2026-08-01 12:17:33 +08:00
size : pageSize . value ,
2026-07-31 13:43:16 +08:00
})
orders . value = result . records
total . value = result . total
} catch ( error ) {
orders . value = []
total . value = 0
loadError . value = getApiErrorDetails ( error , '订单列表加载失败,请检查 Gateway、租户范围和订单权限' )
} finally {
loading . value = false
}
}
2026-08-01 12:17:33 +08:00
function searchOrders () {
if ( page . value !== 1 ) {
page . value = 1
return
}
2026-07-31 13:43:16 +08:00
void loadOrders ()
}
2026-08-04 12:57:50 +08:00
async function resetOrders () {
filters . value = { ... filters . value , status : 'SUBMITTED' }
await nextTick ()
searchOrders ()
}
2026-08-01 12:17:33 +08:00
watch ([ page , pageSize ], () => {
void loadOrders ()
})
2026-07-31 13:43:16 +08:00
async function openDetail ( order : PlatformFirstLegOrderResource ) {
selectedOrder . value = order
2026-08-04 12:57:50 +08:00
await router . push ({
path : route . path ,
query : { ... route . query , orderId : String ( order . id ), tenantId : String ( order . tenantId ) },
})
2026-08-03 17:44:54 +08:00
}
2026-08-04 12:57:50 +08:00
async function loadDetail ( orderId : string , tenantId = detailTenantId . value ) {
if ( ! tenantId ) {
detail . value = null
ElMessage . error ( '订单详情缺少企业租户标识,请从订单列表重新打开' )
return
}
2026-07-31 13:43:16 +08:00
detail . value = null
detailLoading . value = true
try {
2026-08-04 12:57:50 +08:00
detail . value = await getPlatformFirstLegOrder ( tenantId , orderId )
2026-08-03 17:44:54 +08:00
selectedOrder . value = detail . value . order
2026-07-31 13:43:16 +08:00
} catch ( error ) {
const details = getApiErrorDetails ( error , '订单详情加载失败' )
ElMessage . error ( details . message )
} finally {
detailLoading . value = false
}
}
async function refreshAfterAction ( orderId : string ) {
await loadOrders ()
2026-08-03 17:44:54 +08:00
if ( detailOrderId . value === orderId ) {
await loadDetail ( orderId )
2026-07-31 13:43:16 +08:00
}
}
2026-08-03 17:44:54 +08:00
function closeDetailPage () {
2026-08-04 12:57:50 +08:00
const { orderId : _orderId , tenantId : _tenantId , ... query } = route . query
2026-08-03 17:44:54 +08:00
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
}
2026-07-31 13:43:16 +08:00
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 )
2026-08-04 12:57:50 +08:00
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 ))
2026-07-31 13:43:16 +08:00
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 )
2026-08-04 12:57:50 +08:00
await cancelPlatformFirstLegOrder ( String ( order . tenantId ), String ( order . id ), result . value . trim ())
2026-07-31 13:43:16 +08:00
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
2026-08-01 12:17:33 +08:00
void nextTick (() => expressFormRef . value ? . clearValidate ())
2026-07-31 13:43:16 +08:00
}
async function saveExpress () {
2026-08-04 12:57:50 +08:00
if ( ! selectedOrder . value ) return
2026-08-01 12:17:33 +08:00
const valid = await expressFormRef . value ? . validate (). catch (() => false )
if ( ! valid ) return
2026-07-31 13:43:16 +08:00
expressSaving . value = true
try {
2026-08-04 12:57:50 +08:00
await updatePlatformFirstLegOrderExpress ( String ( selectedOrder . value . tenantId ), String ( selectedOrder . value . id ), {
2026-07-31 13:43:16 +08:00
expressCompany : expressForm . expressCompany . trim (),
expressTrackingNo : expressForm . expressTrackingNo . trim (),
})
expressDialogOpen . value = false
await refreshAfterAction ( String ( selectedOrder . value . id ))
ElMessage . success ( '履约快递信息已保存' )
} catch ( error ) {
2026-08-03 17:44:54 +08:00
ElMessage . error ( getFieldValidationMessage ( error , 'expressTrackingNo' , '履约快递信息保存失败' ))
2026-07-31 13:43:16 +08:00
} finally {
expressSaving . value = false
}
}
function openNote ( order : PlatformFirstLegOrderResource ) {
selectedOrder . value = order
2026-08-01 12:17:33 +08:00
noteForm . content = ''
2026-07-31 13:43:16 +08:00
noteDialogOpen . value = true
2026-08-01 12:17:33 +08:00
void nextTick (() => noteFormRef . value ? . clearValidate ())
2026-07-31 13:43:16 +08:00
}
async function saveNote () {
2026-08-04 12:57:50 +08:00
if ( ! selectedOrder . value ) return
2026-08-01 12:17:33 +08:00
const valid = await noteFormRef . value ? . validate (). catch (() => false )
if ( ! valid ) return
2026-07-31 13:43:16 +08:00
noteSaving . value = true
try {
2026-08-04 12:57:50 +08:00
await addPlatformFirstLegOrderNote ( String ( selectedOrder . value . tenantId ), String ( selectedOrder . value . id ), noteForm . content . trim ())
2026-07-31 13:43:16 +08:00
noteDialogOpen . value = false
await refreshAfterAction ( String ( selectedOrder . value . id ))
ElMessage . success ( '内部备注已保存' )
} catch ( error ) {
ElMessage . error ( getApiErrorDetails ( error , '内部备注保存失败' ). message )
} finally {
noteSaving . value = false
}
}
2026-08-04 12:57:50 +08:00
watch ([ detailOrderId , detailTenantId ], ([ orderId , tenantId ]) => {
2026-08-03 17:44:54 +08:00
if ( orderId ) {
2026-08-04 12:57:50 +08:00
void loadDetail ( orderId , tenantId )
2026-08-03 17:44:54 +08:00
return
}
detail . value = null
selectedOrder . value = null
2026-08-01 12:17:33 +08:00
void loadOrders ()
2026-08-03 17:44:54 +08:00
}, { immediate : true })
2026-08-04 12:57:50 +08:00
onMounted (() => void loadTenants ())
2026-07-31 13:43:16 +08:00
</ script >
< template >
< div class = "business-page" >
2026-08-03 17:44:54 +08:00
< template v-if = "isDetailPage" >
< section class = "surface-panel detail-page" v-loading = "detailLoading" >
< header class = "detail-page-header" >
< div >
< h1 > 订单详情 </ h1 >
< p > 查看订单信息 、 履约快递 、 内部备注和状态历史 。 </ p >
</ div >
< el-button @click ="closeDetailPage" >< ArrowLeft :size = "16" /> 返回订单列表 </ el-button >
</ header >
2026-07-31 13:43:16 +08:00
< template v-if = "detail" >
2026-08-03 17:44:54 +08:00
< div class = "order-detail-head" >
< div >
< span >{{ transportLabel ( detail . order ) }}</ span >
< h2 >{{ detail . order . orderNo || detail . order . id }}</ h2 >
2026-08-04 12:57:50 +08:00
< p >{{ tenantName ( detail . order . tenantId ) }} · {{ detail . order . customerName }} · {{ detail . order . referenceId || '无客户参考号' }}</ p >
2026-08-03 17:44:54 +08:00
</ 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" 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 }" >{{ row . grossWeightKg }} kg / {{ 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 || item . action || '状态已更新。' }}</ p ></ el-timeline-item ></ el-timeline >
</ div >
2026-07-31 13:43:16 +08:00
</ template >
2026-08-03 17:44:54 +08:00
< el-empty v-else-if = "!detailLoading" description="订单详情暂不可用"><el-button type="primary" @click="closeDetailPage" > 返回订单列表 </ el -button ></ el-empty >
</ section >
</ template >
< template v-else >
< QueryToolbar
v-model = "filters"
ariaLabel = "头程订单筛选"
:fields = "queryFields"
search -text =" 查询 "
2026-08-04 12:57:50 +08:00
:search-on-reset = "false"
2026-08-03 17:44:54 +08:00
@search ="searchOrders"
2026-08-04 12:57:50 +08:00
@reset ="resetOrders"
2026-08-03 17:44:54 +08:00
/>
2026-08-04 12:57:50 +08:00
< 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 >
2026-08-03 17:44:54 +08:00
< 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 >
< section class = "surface-panel table-panel" >
< DataTable
v -model:page = "page"
v -model:page-size = "pageSize"
:data = "orders"
:columns = "orderColumns"
:loading = "loading"
:total = "total"
: page -sizes ="[ 20 , 50 , 100 ]"
empty -text =" 当前没有符合条件的头程订单 "
row -key =" id "
>
< template # actions = "{ row }" >< div class = "table-actions" >< el-button link type = "primary" @click ="openDetail(row as PlatformFirstLegOrderResource)" >< Eye :size = "15" /> 详情 </ el-button >< el-button v-if = "actionLabel(row as PlatformFirstLegOrderResource)" link type="success" :loading="actionLoadingId === String((row as PlatformFirstLegOrderResource).id)" @click="runLifecycleAction(row as PlatformFirstLegOrderResource)">{{ actionLabel(row as PlatformFirstLegOrderResource) }}</el-button><el-button v-if="canCancel(row as PlatformFirstLegOrderResource)" link type="danger" :loading="actionLoadingId === String((row as PlatformFirstLegOrderResource).id)" @click="cancelOrder(row as PlatformFirstLegOrderResource)">取消</el-button><el-button v-if="can('platform:order:fulfillment') && !['DRAFT', 'REJECTED', 'CANCELLED'].includes((row as PlatformFirstLegOrderResource).status)" link type="primary" @click="openExpress(row as PlatformFirstLegOrderResource)"><PackageCheck :size="15" />快递</el-button><el-button v-if="can('platform:order:note')" link type="primary" @click="openNote(row as PlatformFirstLegOrderResource)"><FilePenLine :size="15" /> 备注 </ el-button ></ div ></ 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 >
2026-08-04 12:57:50 +08:00
< template # customer = "{ row }" >< div class = "primary-cell" >< strong >{{ tenantName (( row as PlatformFirstLegOrderResource ). tenantId ) }}</ strong >< span >{{ ( row as PlatformFirstLegOrderResource ). customerName }} · {{ ( row as PlatformFirstLegOrderResource ). tenantId }}</ span ></ div ></ template >
2026-08-03 17:44:54 +08:00
< 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 >
< template # status = "{ row }" >< StatusBadge : status = "statusLabel((row as PlatformFirstLegOrderResource).status)" /></ template >
< template # empty >< el-empty description = "当前没有符合条件的头程订单" /></ template >
</ DataTable >
</ section >
</ template >
2026-07-31 13:43:16 +08:00
< el-dialog v-model = "expressDialogOpen" title="维护履约快递" width="520px" >
< el-alert title = "当前后端契约只接收快递公司名称和快递单号;快递主数据关联待后端补充。" type = "info" :closable = "false" show -icon />
2026-08-01 12:17:33 +08:00
< 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 >
2026-07-31 13:43:16 +08:00
< 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="520px" >
2026-08-01 12:17:33 +08:00
< 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 >
2026-07-31 13:43:16 +08:00
< 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 >
</ template >
< style scoped >
2026-08-03 17:44:54 +08:00
. detail - page { overflow : hidden ; }
. 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 ; }
2026-07-31 13:43:16 +08:00
. party - grid p { margin : 4 px 0 0 ; color : var ( -- cc - muted ); font - size : 12 px ; line - height : 1.6 ; overflow - wrap : anywhere ; }
2026-08-03 17:44:54 +08:00
@ 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 ; }
}
2026-07-31 13:43:16 +08:00
</ style >