2026-09-04 12:41:58 +08:00
< script setup lang = "ts" >
2026-09-18 16:48:35 +08:00
import { computed , onMounted , reactive , ref , watch } from 'vue'
import { useRoute , useRouter } from 'vue-router'
2026-09-04 12:41:58 +08:00
import { ElMessage , type FormInstance , type FormRules } from 'element-plus'
2026-09-18 16:48:35 +08:00
import DataTable , { type DataTableColumn } from '@/components/common/DataTable.vue'
import QueryToolbar , { type QueryField , type QueryModel } from '@/components/common/QueryToolbar.vue'
import type { TableAction } from '@/components/common/tableActions'
2026-09-05 22:26:37 +08:00
import { useAuthStore } from '@/modules/auth/public'
2026-09-04 12:41:58 +08:00
import {
createProcurement ,
createTransport ,
getBusinessOrder ,
listBusinessOrders ,
type CrossBorderBusinessKind ,
type ProcurementDetail ,
type ProcurementSummary ,
type TransportDetail ,
type TransportSummary ,
} from '../api/businessOrders'
2026-09-15 10:38:53 +08:00
import { CONTAINER_SPECIFICATIONS , DEFAULT_CONTAINER_TYPE } from '../containerSpecifications'
2026-10-07 16:46:58 +08:00
import { poolOrderStatusLabels } from '../fleetArrangement'
2026-09-04 12:41:58 +08:00
2026-09-18 16:48:35 +08:00
const props = withDefaults ( defineProps < { kind : CrossBorderBusinessKind ; createPage ?: boolean ; detailPage ?: boolean } > (), { createPage : false , detailPage : false })
2026-09-05 22:26:37 +08:00
const auth = useAuthStore ()
2026-09-18 16:48:35 +08:00
const router = useRouter ()
const route = useRoute ()
const detailOrderNo = computed (() => String ( route . params . orderNo || '' ))
2026-09-17 18:25:08 +08:00
const canCreate = computed (() => auth . hasPermission (({
procurement : 'merchant:cross-border:procurement:create' ,
trucking : 'merchant:cross-border:domestic-trucking:create' ,
looseboard : 'merchant:cross-border:pallet-shipping:create' ,
} as Record < CrossBorderBusinessKind , string >)[ props . kind ]))
2026-09-04 12:41:58 +08:00
type BusinessRow = ProcurementSummary | TransportSummary
type BusinessDetail = ProcurementDetail | TransportDetail
type PageMode = 'list' | 'create' | 'detail'
interface ColumnDefinition { key : string ; label : string ; width ?: number }
interface BusinessForm {
purchaseCategory : string
productName : string
productSpecification : string
productUrl : string
purchaseQuantity : number
transportMode : string
palletRequired : string
transportType : string
origin : string
originAddress : string
destination : string
destinationAddress : string
containerType : string
palletCount : number
packageCount : number
totalWeightKg : number
totalVolumeCbm : number
budgetAmount ?: number
currency : string
contactName : string
contactInfo : string
requestedDate : string
receivingAddress : string
remark : string
}
const configuration = computed (() => ({
procurement : {
title : '海外采购' ,
description : '查询并管理海外采购订单' ,
columns : [
{ key : 'orderNo' , label : '订单号' , width : 180 },
{ key : 'purchaseCategory' , label : '采购品类' , width : 130 },
{ key : 'productName' , label : '商品名称' , width : 160 },
{ key : 'productSpecification' , label : '商品规格' , width : 160 },
{ key : 'purchaseQuantity' , label : '采购数量' , width : 100 },
{ key : 'budgetUnitPrice' , label : '预算单价' , width : 110 },
2026-09-14 10:02:37 +08:00
{ key : 'inboundQuantity' , label : '入库量' , width : 90 },
{ key : 'outboundQuantity' , label : '出库量' , width : 90 },
{ key : 'inventoryQuantity' , label : '库存量' , width : 90 },
2026-09-04 12:41:58 +08:00
{ key : 'status' , label : '状态' , width : 120 },
{ key : 'createdAt' , label : '创建时间' , width : 170 },
] satisfies ColumnDefinition [],
},
trucking : {
title : '美国本土卡' ,
description : '查询并管理美国本土卡订单' ,
columns : transportColumns ( false ),
},
looseboard : {
title : '美本土散板' ,
description : '查询并管理美本土散板订单' ,
columns : transportColumns ( true ),
},
})[ props . kind ])
function transportColumns ( looseboard : boolean ) : ColumnDefinition [] {
return [
{ key : 'orderNo' , label : '订单编号' , width : 180 },
...( looseboard ? [{ key : 'transportMode' , label : '运输方式' , width : 120 }, { key : 'palletRequired' , label : '是否打盘' , width : 100 }] : []),
{ key : 'transportType' , label : '运输类型' , width : 130 },
{ key : 'origin' , label : '起运地' , width : 150 },
{ key : 'destination' , label : '目的地' , width : 150 },
{ key : 'containerType' , label : '柜型' , width : 100 },
{ key : 'totalWeightKg' , label : '总重量(kg)' , width : 120 },
{ key : 'budgetAmount' , label : '预算总价' , width : 110 },
{ key : 'requestedDate' , label : '要求时间' , width : 120 },
{ key : 'status' , label : '状态' , width : 130 },
2026-10-07 16:46:58 +08:00
{ key : 'fleetArrangement' , label : '承运安排' , width : 190 },
2026-09-04 12:41:58 +08:00
]
}
2026-09-18 16:48:35 +08:00
const mode = ref < PageMode >( props . detailPage ? 'detail' : props . createPage ? 'create' : 'list' )
2026-09-04 12:41:58 +08:00
const loading = ref ( false )
const submitting = ref ( false )
const submitKey = ref ( '' )
const error = ref ( '' )
const rows = ref < BusinessRow [] >([])
const detail = ref < BusinessDetail | null >( null )
2026-10-07 16:46:58 +08:00
const fleetArrangement = computed (() => detail . value && props . kind !== 'procurement' ? ( detail . value as TransportDetail ). summary . fleetArrangement : null )
2026-09-04 12:41:58 +08:00
const formRef = ref < FormInstance >()
const query = reactive ({ page : 1 , size : 20 , orderNo : '' , routeKeyword : '' , status : '' , productName : '' , purchaseCategory : '' })
const total = ref ( 0 )
2026-09-18 16:48:35 +08:00
const queryModel = computed < QueryModel >({
get : () => ({
orderNo : query . orderNo ,
purchaseCategory : query . purchaseCategory ,
productName : query . productName ,
routeKeyword : query . routeKeyword ,
status : query . status ,
}),
set : ( value ) => {
query . orderNo = String ( value . orderNo || '' )
query . purchaseCategory = String ( value . purchaseCategory || '' )
query . productName = String ( value . productName || '' )
query . routeKeyword = String ( value . routeKeyword || '' )
query . status = String ( value . status || '' )
},
})
const queryFields = computed < QueryField [] >(() => [
{ key : 'orderNo' , label : '订单号' , type : 'input' , placeholder : '请输入订单号' },
...( props . kind === 'procurement'
? [
{ key : 'purchaseCategory' , label : '采购品类' , type : 'input' , placeholder : '请输入采购品类' } as QueryField ,
{ key : 'productName' , label : '商品名称' , type : 'input' , placeholder : '请输入商品名称' } as QueryField ,
]
: [{ key : 'routeKeyword' , label : '运输路线' , type : 'input' , placeholder : '请输入起运地或目的地' } as QueryField ]),
{
key : 'status' ,
label : '状态' ,
type : 'select' ,
placeholder : '全部' ,
options : [
{ label : '待审核' , value : 'PENDING_REVIEW' },
{ label : '已通过' , value : 'APPROVED' },
{ label : '已拒绝' , value : 'REJECTED' },
{ label : '待出库' , value : 'PENDING_OUTBOUND' },
{ label : '部分出库' , value : 'PARTIALLY_OUTBOUND' },
{ label : '全部出库' , value : 'OUTBOUNDED' },
{ label : '待接单' , value : 'PENDING_ACCEPTANCE' },
2026-10-07 16:46:58 +08:00
...( props . kind === 'procurement' ? [] : Object . entries ( poolOrderStatusLabels ). filter (([ value ]) => ! [ 'IN_TRANSIT' , 'COMPLETED' , 'CANCELLED' ]. includes ( value )). map (([ value , label ]) => ({ value , label }))),
2026-09-18 16:48:35 +08:00
{ label : '处理中' , value : 'PROCESSING' },
{ label : '运输中' , value : 'IN_TRANSIT' },
{ label : '已完成' , value : 'COMPLETED' },
{ label : '已取消' , value : 'CANCELLED' },
],
},
])
2026-09-04 12:41:58 +08:00
const form = reactive < BusinessForm >( emptyForm ())
const rules : FormRules < BusinessForm > = {
purchaseCategory : [{ required : true , message : '请输入采购品类' , trigger : 'blur' }],
productName : [{ required : true , message : '请输入商品名称' , trigger : 'blur' }],
purchaseQuantity : [{ required : true , type : 'number' , min : 1 , message : '采购数量至少为 1' , trigger : 'change' }],
origin : [{ required : true , message : '请输入起运地' , trigger : 'blur' }],
destination : [{ required : true , message : '请输入目的地' , trigger : 'blur' }],
palletCount : [{ required : true , type : 'number' , min : 1 , message : '托盘数至少为 1' , trigger : 'change' }],
contactName : [{ required : true , message : '请输入联系人' , trigger : 'blur' }],
contactInfo : [{ required : true , message : '请输入联系电话或联系方式' , trigger : 'blur' }],
requestedDate : [{ required : true , message : '请选择要求时间' , trigger : 'change' }],
}
const detailFields = computed (() => {
if ( ! detail . value ) return []
const summary = detail . value . summary
if ( props . kind === 'procurement' ) {
const current = detail . value as ProcurementDetail
return [
2026-09-18 16:48:35 +08:00
[ '采购品类' , read ( summary , 'purchaseCategory' )],
2026-09-04 12:41:58 +08:00
[ '商品名称' , read ( summary , 'productName' )], [ '商品规格' , read ( summary , 'productSpecification' )],
[ '采购数量' , read ( summary , 'purchaseQuantity' )], [ '预算单价' , money ( read ( summary , 'budgetUnitPrice' ), read ( summary , 'currency' ))],
2026-09-14 10:02:37 +08:00
[ '入库量' , read ( summary , 'inboundQuantity' )], [ '出库量' , read ( summary , 'outboundQuantity' )],
[ '库存量' , read ( summary , 'inventoryQuantity' )], [ '预计到达时间' , dateTime ( current . estimatedArrivalAt )],
2026-09-04 12:41:58 +08:00
[ '联系人' , current . contactName ], [ '联系电话' , current . contactPhone ],
[ '收货地址' , current . receivingAddress ], [ '商品链接' , current . productUrl ],
[ '国内运单' , joinText ( current . domesticCarrier , current . domesticTrackingNo )],
[ '海外运单' , joinText ( current . overseasCarrier , current . overseasTrackingNo )],
2026-09-18 16:48:35 +08:00
[ '审核备注' , current . reviewRemark ], [ '备注' , current . remark ],
2026-09-04 12:41:58 +08:00
]
}
const current = detail . value as TransportDetail
return [
2026-10-07 20:43:30 +08:00
[ '运输方式' , transportModeLabel ( read ( summary , 'transportMode' ))],
2026-09-04 12:41:58 +08:00
[ '是否打盘' , yesNo ( read ( summary , 'palletRequired' ))], [ '运输类型' , transportTypeLabel ( read ( summary , 'transportType' ))],
[ '起运地' , read ( summary , 'origin' )], [ '起运地址' , current . originAddress ],
[ '目的地' , read ( summary , 'destination' )], [ '目的地址' , current . destinationAddress ],
[ '柜型' , read ( summary , 'containerType' )], [ '托盘数' , read ( summary , 'palletCount' )],
[ '件数' , read ( summary , 'packageCount' )], [ '总重量(kg)' , read ( summary , 'totalWeightKg' )],
[ '总体积(m³)' , read ( summary , 'totalVolumeCbm' )], [ '预算总价' , money ( read ( summary , 'budgetAmount' ), read ( summary , 'currency' ))],
[ '联系人' , read ( summary , 'contactName' )], [ '联系方式' , read ( summary , 'contactInfo' )],
2026-09-18 16:48:35 +08:00
[ '要求时间' , read ( summary , 'requestedDate' )], [ '备注' , current . remark ],
2026-09-04 12:41:58 +08:00
]
})
2026-09-18 16:48:35 +08:00
const tableColumns = computed < DataTableColumn [] >(() => [
{ key : 'actions' , label : '操作' , width : 90 , fixed : 'left' , slotName : 'actions' },
... configuration . value . columns . map (( column ) => ({
... column ,
prop : column . key ,
2026-10-07 16:46:58 +08:00
slotName : [ 'orderNo' , 'status' , 'fleetArrangement' ]. includes ( column . key ) ? column . key : undefined ,
2026-09-18 16:48:35 +08:00
formatter : ( row : object ) => displayCell ( row , column . key ),
})),
])
const detailStatus = computed (() => detail . value ? statusLabel ( detail . value . summary . status ) : '—' )
2026-09-04 12:41:58 +08:00
function emptyForm () : BusinessForm {
return {
purchaseCategory : '' , productName : '' , productSpecification : '' , productUrl : '' , purchaseQuantity : 1 ,
transportMode : 'SHARED_TRUCK' , palletRequired : 'NO' , transportType : 'PORT_PICKUP' , origin : '' , originAddress : '' ,
2026-09-15 10:38:53 +08:00
destination : '' , destinationAddress : '' , containerType : DEFAULT_CONTAINER_TYPE , palletCount : 1 , packageCount : 0 ,
2026-09-04 12:41:58 +08:00
totalWeightKg : 0 , totalVolumeCbm : 0 , currency : 'USD' , contactName : '' , contactInfo : '' , requestedDate : '' ,
receivingAddress : '' , remark : '' ,
}
}
function resetForm () { Object . assign ( form , emptyForm ()); submitKey . value = '' ; formRef . value ? . clearValidate () }
function read ( source : object , key : string ) { return Reflect . get ( source , key ) as unknown }
function text ( value : unknown ) { return value === null || value === undefined || value === '' ? '—' : String ( value ) }
function joinText (... values : unknown []) { const result = values . filter ( Boolean ). join ( ' / ' ); return result || '—' }
function money ( amount : unknown , currency : unknown ) { return amount === null || amount === undefined ? '—' : ` ${ text ( currency ) } ${ Number ( amount ). toFixed ( 2 ) } ` }
function dateTime ( value : unknown ) { return value ? new Date ( String ( value )). toLocaleString ( 'zh-CN' , { hour12 : false }) : '—' }
function yesNo ( value : unknown ) { return value === 'YES' ? '是' : value === 'NO' ? '否' : text ( value ) }
function transportTypeLabel ( value : unknown ) { return ({ PORT_PICKUP : '港口提柜' , WAREHOUSE_TO_WAREHOUSE : '仓到仓' , WAREHOUSE_TO_DOOR : '仓到门' } as Record < string , string >)[ String ( value )] || text ( value ) }
function transportModeLabel ( value : unknown ) { return ({ SHARED_TRUCK : '拼车' , PRIORITY_CONSOLIDATION : '优先拼载' } as Record < string , string >)[ String ( value )] || text ( value ) }
function statusLabel ( value : unknown ) {
2026-10-07 16:46:58 +08:00
if ( poolOrderStatusLabels [ String ( value )]) return poolOrderStatusLabels [ String ( value )]
2026-09-14 10:02:37 +08:00
return ({ PENDING_REVIEW : '待审核' , APPROVED : '已通过' , REJECTED : '已拒绝' , PENDING_OUTBOUND : '待出库' , PARTIALLY_OUTBOUND : '部分出库' , OUTBOUNDED : '全部出库' , PENDING_ACCEPTANCE : '待接单' , PROCESSING : '处理中' , IN_TRANSIT : '运输中' , COMPLETED : '已完成' , CANCELLED : '已取消' } as Record < string , string >)[ String ( value )] || text ( value )
2026-09-04 12:41:58 +08:00
}
2026-09-18 16:48:35 +08:00
function statusTone ( value : unknown ) {
return ({ APPROVED : 'success' , COMPLETED : 'success' , OUTBOUNDED : 'success' , REJECTED : 'danger' , CANCELLED : 'info' , PENDING_REVIEW : 'warning' , PENDING_ACCEPTANCE : 'warning' , PROCESSING : 'primary' , IN_TRANSIT : 'primary' } as Record < string , ' success ' | ' warning ' | ' danger ' | ' info ' | ' primary ' >)[ String ( value )] || 'info'
}
2026-09-04 12:41:58 +08:00
function displayCell ( candidate : unknown , key : string ) {
const row = candidate as BusinessRow
const value = read ( row , key )
if ( key === 'status' ) return statusLabel ( value )
if ( key === 'transportType' ) return transportTypeLabel ( value )
if ( key === 'transportMode' ) return transportModeLabel ( value )
if ( key === 'palletRequired' ) return yesNo ( value )
if ( key === 'createdAt' ) return dateTime ( value )
if ( key === 'budgetUnitPrice' || key === 'budgetAmount' ) return money ( value , read ( row , 'currency' ))
return text ( value )
}
function idempotencyKey () { return globalThis . crypto ? . randomUUID ? .() || `client- ${ Date . now () } - ${ Math . random (). toString ( 16 ). slice ( 2 ) } ` }
function errorMessage ( cause : unknown ) { return cause instanceof Error ? cause . message : '请求失败,请稍后重试' }
async function load () {
loading . value = true
error . value = ''
try {
const params = {
page : query . page , size : query . size , orderNo : query . orderNo || undefined , status : query . status || undefined ,
routeKeyword : query . routeKeyword || undefined , productName : query . productName || undefined ,
purchaseCategory : query . purchaseCategory || undefined ,
}
const result = props . kind === 'procurement'
? await listBusinessOrders < ProcurementSummary >( 'procurement' , params )
: await listBusinessOrders < TransportSummary >( props . kind , params )
rows . value = result . records
total . value = result . total
} catch ( cause ) {
error . value = errorMessage ( cause )
rows . value = []
total . value = 0
} finally { loading . value = false }
}
function resetQuery () {
2026-10-07 16:46:58 +08:00
const reloadDirectly = query . page === 1
2026-09-04 12:41:58 +08:00
Object . assign ( query , { page : 1 , orderNo : '' , routeKeyword : '' , status : '' , productName : '' , purchaseCategory : '' })
2026-10-07 16:46:58 +08:00
if ( reloadDirectly ) void load ()
2026-09-04 12:41:58 +08:00
}
2026-09-18 16:48:35 +08:00
function searchOrders () {
2026-10-07 16:46:58 +08:00
if ( query . page !== 1 ) query . page = 1
else void load ()
2026-09-18 16:48:35 +08:00
}
function getRowActions ( candidate : object ) : TableAction [] {
return [{ key : 'detail' , label : '详情' , run : () => openDetail ( candidate ) }]
}
function detailFieldClass ( label : unknown ) {
return [ '收货地址' , '商品链接' , '国内运单' , '海外运单' , '审核备注' , '备注' , '起运地址' , '目的地址' ]. includes ( String ( label )) ? 'is-wide' : ''
}
const listRouteName = computed (() => ({
procurement : 'cross-border-procurement' ,
trucking : 'cross-border-domestic-trucking' ,
looseboard : 'cross-border-pallet-shipping' ,
} as Record < CrossBorderBusinessKind , string >)[ props . kind ])
const detailRouteName = computed (() => ({
procurement : 'cross-border-procurement-detail' ,
trucking : 'cross-border-domestic-trucking-detail' ,
looseboard : 'cross-border-pallet-shipping-detail' ,
} as Record < CrossBorderBusinessKind , string >)[ props . kind ])
function returnToList () {
if ( props . createPage || props . detailPage ) {
void router . push ({ name : listRouteName . value })
return
}
mode . value = 'list'
}
function openDetail ( candidate : unknown ) {
2026-09-04 12:41:58 +08:00
const row = candidate as BusinessRow
2026-09-18 16:48:35 +08:00
void router . push ({ name : detailRouteName . value , params : { orderNo : row . orderNo } })
}
async function loadDetail () {
if ( ! detailOrderNo . value ) return
2026-09-04 12:41:58 +08:00
loading . value = true
error . value = ''
try {
detail . value = props . kind === 'procurement'
2026-09-18 16:48:35 +08:00
? await getBusinessOrder < ProcurementDetail >( 'procurement' , detailOrderNo . value )
: await getBusinessOrder < TransportDetail >( props . kind , detailOrderNo . value )
2026-09-04 12:41:58 +08:00
mode . value = 'detail'
2026-09-18 16:48:35 +08:00
} catch ( cause ) { detail . value = null ; error . value = errorMessage ( cause ); ElMessage . error ( error . value ) }
2026-09-04 12:41:58 +08:00
finally { loading . value = false }
}
async function submit () {
if ( ! await formRef . value ? . validate (). catch (() => false )) return
submitting . value = true
const stableSubmitKey = submitKey . value || idempotencyKey ()
submitKey . value = stableSubmitKey
try {
if ( props . kind === 'procurement' ) {
await createProcurement ({
2026-09-05 22:26:37 +08:00
customerName : auth . user ? . tenantName || '' ,
2026-09-04 12:41:58 +08:00
purchaseCategory : form . purchaseCategory , productName : form . productName ,
productSpecification : form . productSpecification || undefined , productUrl : form . productUrl || undefined ,
purchaseQuantity : form . purchaseQuantity , budgetUnitPrice : form . budgetAmount ,
currency : form . currency || undefined , receivingAddress : form . receivingAddress || undefined ,
contactName : form . contactName , contactPhone : form . contactInfo , remark : form . remark || undefined ,
idempotencyKey : stableSubmitKey ,
})
} else {
const base = {
transportType : form . transportType , origin : form . origin , originAddress : form . originAddress || undefined ,
destination : form . destination , destinationAddress : form . destinationAddress || undefined ,
containerType : form . containerType || undefined , packageCount : form . packageCount ,
totalWeightKg : form . totalWeightKg , totalVolumeCbm : form . totalVolumeCbm ,
budgetAmount : form . budgetAmount , currency : form . currency || undefined ,
contactName : form . contactName || undefined , contactInfo : form . contactInfo || undefined ,
requestedDate : form . requestedDate || undefined , remark : form . remark || undefined ,
idempotencyKey : stableSubmitKey ,
}
await createTransport ( props . kind , props . kind === 'trucking'
? { ... base , palletCount : form . palletCount }
: { ... base , transportMode : form . transportMode , palletRequired : form . palletRequired })
}
ElMessage . success ( '订单提交成功' )
resetForm ()
2026-09-18 16:48:35 +08:00
if ( props . createPage ) await router . push ({ name : listRouteName . value })
else { mode . value = 'list' ; await load () }
2026-09-04 12:41:58 +08:00
} catch ( cause ) { ElMessage . error ( errorMessage ( cause )) }
finally { submitting . value = false }
}
2026-09-18 16:48:35 +08:00
watch (() => [ query . page , query . size ], () => { if ( ! props . createPage && mode . value === 'list' ) void load () })
onMounted (() => { if ( props . detailPage ) void loadDetail (); else if ( ! props . createPage ) void load () })
2026-09-04 12:41:58 +08:00
</ script >
< template >
2026-09-18 16:48:35 +08:00
< section v-loading = "loading" class="business-orders client-ui" :aria-busy="loading" >
< header v-if = "mode === 'create'" class="page-head" >
2026-09-04 12:41:58 +08:00
< div >
2026-09-18 16:48:35 +08:00
< h2 >{{ configuration . title }} 下单 </ h2 >
< p > 请填写订单信息 , 带 * 项为必填项 </ p >
2026-09-04 12:41:58 +08:00
</ div >
2026-09-18 16:48:35 +08:00
< el-button @click ="returnToList" > 返回列表 </ el -button >
2026-09-04 12:41:58 +08:00
</ header >
< template v-if = "mode === 'list'" >
2026-09-18 16:48:35 +08:00
< QueryToolbar v-model = "queryModel" layout="grid" ariaLabel="订单查询条件" :fields="queryFields" :search-on-reset="false" @search="searchOrders" @reset="resetQuery" />
< div class = "business-table-card" >
< DataTable v -model:page = "query.page" v -model:page-size = "query.size" :data = "rows" :columns = "tableColumns" :loading = "loading" :total = "total" : page -sizes ="[ 10 , 20 , 50 , 100 ]" :row-actions = "getRowActions" row -key =" orderNo " empty -text =" 暂无订单 " >
< template # toolbar -actions >< el-button v-if = "canCreate" type="primary" @click="resetForm(); mode = 'create'" > 新增下单 </ el -button ></ template >
< template v-if = "error" #error><div class="business-load-error" role="alert"><span>{{ error }}</span><el-button size="small" @click="load" > 重新加载 </ el -button ></ div ></ template >
< template # orderNo = "{ row }" >< button class = "business-order-link" type = "button" @click ="openDetail(row)" >{{ ( row as BusinessRow ) .orderNo }}</ button ></ template >
< template # status = "{ row }" >< el-tag : type = "statusTone((row as BusinessRow).status)" effect = "light" size = "small" >{{ statusLabel (( row as BusinessRow ). status ) }}</ el-tag ></ template >
2026-10-07 16:46:58 +08:00
< template # fleetArrangement = "{ row }" >< template v-if = "(row as TransportSummary).fleetArrangement" >< div >{{ poolOrderStatusLabels [( row as TransportSummary ). fleetArrangement ! . fulfillmentStage ] || '待核对履约进度' }}</ div >< small >{{ dateTime (( row as TransportSummary ). fleetArrangement ! . plannedDepartureAt ) }}</ small ></ template >< span v-else > — </ span ></ template >
2026-09-18 16:48:35 +08:00
</ DataTable >
</ div >
2026-09-04 12:41:58 +08:00
</ template >
2026-09-18 16:48:35 +08:00
< section v-else-if = "mode === 'detail'" class="business-detail-page" aria-label="订单详情" >
< header class = "business-detail-heading" >
< div class = "business-detail-heading-main" >< el-button link type = "primary" @click ="returnToList" > ← 返回列表 </ el -button >< div >< h2 >{{ detail ? . summary . orderNo || '订单详情' }}</ h2 >< p >{{ configuration . title }} 订单 </ p ></ div ></ div >
< el-tag :type = "statusTone(detail?.summary.status)" effect = "light" >{{ detailStatus }}</ el-tag >
</ header >
< div v-if = "error" class="business-detail-error" role="alert"><strong>{{ error }}</strong><el-button size="small" @click="loadDetail" > 重新加载 </ el -button ></ div >
< section v-else class = "business-detail-section" >
< h3 > 订单信息 </ h3 >
< dl class = "detail-grid" >
< div v-for = "field in detailFields" :key="String(field[0])" :class="detailFieldClass(field[0])" >< dt >{{ field [ 0 ] }}</ dt >< dd >{{ text ( field [ 1 ]) }}</ dd ></ div >
</ dl >
</ section >
2026-10-07 16:46:58 +08:00
< section v-if = "!error && fleetArrangement" class="business-detail-section" aria-label="承运安排" >
< h3 > 承运安排 </ h3 >< p >{{ fleetArrangement . readOnlyReason }}</ p >
2026-10-07 20:59:34 +08:00
< dl class = "detail-grid arrangement-grid" >
2026-10-07 16:46:58 +08:00
< div >< dt > 预订号 </ dt >< dd >{{ fleetArrangement . bookingNo }}</ dd ></ div >
< div >< dt > 计划发车 </ dt >< dd >{{ dateTime ( fleetArrangement . plannedDepartureAt ) }}</ dd ></ div >
< div >< dt > 起运仓 </ dt >< dd >{{ text ( fleetArrangement . originWarehouseName ) }}</ dd ></ div >
< div >< dt > 目的地 </ dt >< dd >{{ text ( fleetArrangement . destinationLabel ) }}</ dd ></ div >
< div >< dt > 交货方式 </ dt >< dd >{{ fleetArrangement . deliveryMode === 'SELF_DELIVERY' ? '自行送至起运仓' : '线下安排提货至起运仓' }}</ dd ></ div >
< div >< dt > 履约进度 </ dt >< dd >{{ poolOrderStatusLabels [ fleetArrangement . fulfillmentStage ] || '待核对履约进度' }}</ dd ></ div >
< div >< dt > 更新时间 </ dt >< dd >{{ dateTime ( fleetArrangement . stageOccurredAt ) }}</ dd ></ div >
< div >< dt > 约定运费 ( 非实收 ) </ dt >< dd >{{ money ( fleetArrangement . agreedFreightAmount , fleetArrangement . currency ) }}</ dd ></ div >
< div >< dt > 来货单号 </ dt >< dd >{{ text ( fleetArrangement . incomingNo ) }}</ dd ></ div >
< div >< dt > 派送单号 </ dt >< dd >{{ text ( fleetArrangement . deliveryNo ) }}</ dd ></ div >
< div >< dt > 已签 / 未签 / 退回箱数 </ dt >< dd >{{ fleetArrangement . signedPackages ?? '—' }} / {{ fleetArrangement . unsignedPackages ?? '—' }} / {{ fleetArrangement . returnedPackages ?? '—' }}</ dd ></ div >
< div class = "is-wide" >< dt > 交货说明 </ dt >< dd >{{ text ( fleetArrangement . deliveryInstructions ) }}</ dd ></ div >
< div v-if = "fleetArrangement.exceptionSummary" class="is-wide" >< dt > 履约说明 </ dt >< dd >{{ fleetArrangement . exceptionSummary }}</ dd ></ div >
</ dl >
</ section >
2026-09-18 16:48:35 +08:00
</ section >
2026-09-04 12:41:58 +08:00
< el-card v-else shadow = "never" class = "form-card" >
< el-form ref = "formRef" :model = "form" :rules = "rules" label -width =" 120px " >
< template v-if = "kind === 'procurement'" >
< el-form-item label = "采购品类" prop = "purchaseCategory" >< el-input v -model .trim =" form.purchaseCategory " maxlength = "128" placeholder = "请输入采购品类" /></ el-form-item >
< el-form-item label = "商品名称" prop = "productName" >< el-input v -model .trim =" form.productName " maxlength = "255" placeholder = "请输入商品名称" /></ el-form-item >
< el-form-item label = "商品规格" >< el-input v -model .trim =" form.productSpecification " maxlength = "500" placeholder = "请输入规格、型号等信息" /></ el-form-item >
< el-form-item label = "商品链接" >< el-input v -model .trim =" form.productUrl " maxlength = "2000" placeholder = "请输入商品链接" /></ el-form-item >
< el-form-item label = "采购数量" prop = "purchaseQuantity" >< el-input-number v-model = "form.purchaseQuantity" :min="1" placeholder="请输入数量" /></ el-form-item >
< el-form-item label = "收货地址" >< el-input v -model .trim =" form.receivingAddress " maxlength = "1000" placeholder = "请输入收货地址" /></ el-form-item >
</ template >
< template v-else >
< el-form-item v-if = "kind === 'looseboard'" label="运输方式"><el-radio-group v-model="form.transportMode"><el-radio-button value="SHARED_TRUCK">拼车</el-radio-button><el-radio-button value="PRIORITY_CONSOLIDATION" > 优先拼载 </ el -radio-button ></ el-radio-group ></ el-form-item >
< el-form-item v-if = "kind === 'looseboard'" label="是否打盘"><el-radio-group v-model="form.palletRequired"><el-radio-button value="NO">否</el-radio-button><el-radio-button value="YES" > 是 </ el -radio-button ></ el-radio-group ></ el-form-item >
< el-form-item label = "运输类型" >< el-select v-model = "form.transportType"><el-option label="港口提柜" value="PORT_PICKUP" /><el-option label="仓到仓" value="WAREHOUSE_TO_WAREHOUSE" /><el-option label="仓到门" value="WAREHOUSE_TO_DOOR" /></ el-select ></ el-form-item >
< el-form-item label = "起运地" prop = "origin" >< el-input v -model .trim =" form.origin " maxlength = "255" placeholder = "请输入起运地" /></ el-form-item >
< el-form-item label = "起运地址" >< el-input v -model .trim =" form.originAddress " maxlength = "500" placeholder = "请输入详细起运地址" /></ el-form-item >
< el-form-item label = "目的地" prop = "destination" >< el-input v -model .trim =" form.destination " maxlength = "255" placeholder = "请输入目的地" /></ el-form-item >
< el-form-item label = "目的地址" >< el-input v -model .trim =" form.destinationAddress " maxlength = "500" placeholder = "请输入详细目的地址" /></ el-form-item >
2026-09-15 10:38:53 +08:00
< el-form-item label = "柜型" >< el-select v-model = "form.containerType"><el-option v-for="item in CONTAINER_SPECIFICATIONS" :key="item.type" :label="item.type" :value="item.type" /></ el-select ></ el-form-item >
2026-09-04 12:41:58 +08:00
< el-form-item v-if = "kind === 'trucking'" label="托盘数" prop="palletCount"><el-input-number v-model="form.palletCount" :min="1" placeholder="请输入托盘数" /></ el-form-item >
< el-form-item label = "件数" >< el-input-number v-model = "form.packageCount" :min="0" placeholder="请输入件数" /></ el-form-item >
< el-form-item label = "总重量(kg)" >< el-input-number v-model = "form.totalWeightKg" :min="0" :precision="2" placeholder="请输入总重量" /></ el-form-item >
< el-form-item label = "总体积(m³)" >< el-input-number v-model = "form.totalVolumeCbm" :min="0" :precision="3" placeholder="请输入总体积" /></ el-form-item >
< el-form-item label = "要求时间" : prop = "kind === 'trucking' ? 'requestedDate' : undefined" >< el-date-picker v-model = "form.requestedDate" type="date" value-format="YYYY-MM-DD" /></ el-form-item >
</ template >
2026-09-14 10:02:37 +08:00
< el-form-item class = "budget-form-item" : label = "kind === 'procurement' ? '预算单价' : '预算金额'" >< el-input-number v-model = "form.budgetAmount" :min="0" :precision="2" placeholder="请输入预算金额" /><el-select v-model="form.currency" class="currency"><el-option label="USD" value="USD" /><el-option label="CNY" value="CNY" /></ el-select ></ el-form-item >
2026-09-04 12:41:58 +08:00
< el-form-item label = "联系人" prop = "contactName" >< el-input v -model .trim =" form.contactName " maxlength = "128" placeholder = "请输入联系人" /></ el-form-item >
< el-form-item label = "联系方式" prop = "contactInfo" >< el-input v -model .trim =" form.contactInfo " maxlength = "128" placeholder = "请输入电话、微信或邮箱" /></ el-form-item >
< el-form-item label = "备注" >< el-input v-model = "form.remark" type="textarea" :rows="4" maxlength="1000" show-word-limit placeholder="请输入其他要求" /></ el-form-item >
2026-09-18 16:48:35 +08:00
< el-form-item >< el-button @click ="returnToList" > 取消 </ el -button >< el-button v-if = "canCreate" type="primary" :loading="submitting" @click="submit" > 提交订单 </ el -button ></ el-form-item >
2026-09-04 12:41:58 +08:00
</ el-form >
</ el-card >
</ section >
</ template >
< style scoped >
2026-09-18 16:48:35 +08:00
. business - orders { display : grid ; gap : var ( -- client - section - gap ); }
2026-09-04 12:41:58 +08:00
. page - head { display : flex ; align - items : center ; justify - content : space - between ; gap : 24 px ; }
. page - head h2 { margin : 0 ; color : # 111827 ; font - size : 26 px ; }
. page - head p { margin : 7 px 0 0 ; color : # 64748 b ; font - size : 14 px ; }
2026-09-18 16:48:35 +08:00
. business - table - card { overflow : hidden ; border : 1 px solid var ( -- client - border ); border - radius : var ( -- client - radius ); background : # fff ; }
. business - order - link { max - width : 100 % ; overflow : hidden ; padding : 0 ; border : 0 ; background : transparent ; color : var ( -- client - action ); cursor : pointer ; font : inherit ; font - weight : 650 ; text - align : left ; text - overflow : ellipsis ; white - space : nowrap ; }
. business - order - link : hover { text - decoration : underline ; text - underline - offset : 3 px ; }
. business - load - error { display : flex ; min - height : 84 px ; align - items : center ; justify - content : center ; gap : 12 px ; padding : 14 px ; color : # b42318 ; }
. business - detail - error { display : flex ; min - height : 180 px ; align - items : center ; justify - content : center ; gap : 12 px ; color : # b42318 ; }
. business - detail - page { min - width : 0 ; padding : 10 px 14 px 20 px ; overflow : hidden ; border : 1 px solid var ( -- client - border ); border - radius : var ( -- client - radius ); background : # fff ; }
. business - detail - heading { display : flex ; min - height : 50 px ; align - items : center ; justify - content : space - between ; gap : 16 px ; padding : 5 px 0 ; border - bottom : 1 px solid var ( -- border ); }
. business - detail - heading - main { display : flex ; min - width : 0 ; align - items : center ; gap : 12 px ; }
. business - detail - heading - main > div { min - width : 0 ; }
. business - detail - heading h2 { margin : 0 ; overflow : hidden ; color : var ( -- navy ); font - size : 18 px ; text - overflow : ellipsis ; white - space : nowrap ; }
. business - detail - heading p { margin : 3 px 0 0 ; color : var ( -- muted ); font - size : 11 px ; }
. business - detail - section { margin - top : 10 px ; }
. business - detail - section h3 { margin : 0 ; padding : 0 0 5 px ; border - bottom : 1 px solid # eaf0f7 ; color : var ( -- navy ); font - size : 13 px ; }
. detail - grid { display : grid ; grid - template - columns : repeat ( 4 , minmax ( 0 , 1 fr )); margin : 0 ; border - top : 1 px solid var ( -- border ); border - left : 1 px solid var ( -- border ); }
. detail - grid div { display : grid ; min - width : 0 ; min - height : 34 px ; grid - template - columns : 76 px minmax ( 0 , 1 fr ); align - items : center ; gap : 7 px ; padding : 3 px 8 px ; border - right : 1 px solid var ( -- border ); border - bottom : 1 px solid var ( -- border ); }
. detail - grid div . is - wide { grid - column : span 2 ; }
. detail - grid dt { color : var ( -- muted ); font - size : 10 px ; white - space : nowrap ; }
2026-10-07 20:59:34 +08:00
. arrangement - grid div { grid - template - columns : 124 px minmax ( 0 , 1 fr ); }
. arrangement - grid dt { white - space : normal ; overflow - wrap : anywhere ; }
2026-09-18 16:48:35 +08:00
. detail - grid dd { min - width : 0 ; margin : 0 ; overflow - wrap : anywhere ; color : var ( -- navy ); font - size : 12 px ; font - weight : 600 ; line - height : 1.4 ; }
2026-09-04 12:41:58 +08:00
. form - card : deep (. el - form ) { max - width : 860 px ; }
. form - card : deep (. el - input ), . form - card : deep (. el - textarea ), . form - card : deep (. el - select ) { width : 100 % ; }
. form - card . currency { width : 110 px ; margin - left : 12 px ; }
2026-09-14 10:02:37 +08:00
@ media ( max - width : 767 px ) {
. business - orders { gap : 14 px ; }
. page - head { align - items : flex - start ; gap : 12 px ; }
. page - head h2 { font - size : 24 px ; }
. page - head > div { min - width : 0 ; }
. detail - grid { grid - template - columns : 1 fr ; }
2026-09-18 16:48:35 +08:00
. detail - grid div , . detail - grid div . is - wide { grid - column : auto ; }
2026-09-14 10:02:37 +08:00
. detail - grid dd { overflow - wrap : break - word ; word - break : normal ; }
2026-09-18 16:48:35 +08:00
. business - detail - heading { align - items : flex - start ; flex - direction : column ; }
2026-09-14 10:02:37 +08:00
. form - card : deep (. el - form - item ) { display : block ; }
. form - card : deep (. el - form - item__label ) {
width : auto ! important ;
height : auto ;
margin - bottom : 7 px ;
line - height : 1.4 ;
justify - content : flex - start ;
}
. form - card : deep (. el - form - item__content ) { width : 100 % ; margin - left : 0 ! important ; }
. form - card : deep (. el - input - number ) { width : 100 % ; }
. form - card . budget - form - item : deep (. el - form - item__content ) { display : flex ; flex - wrap : nowrap ; }
. form - card . budget - form - item : deep (. el - input - number ) { width : auto ; min - width : 0 ; flex : 1 1 auto ; }
. form - card . currency { width : 96 px ; flex : 0 0 96 px ; margin - left : 8 px ; }
}
2026-09-04 12:41:58 +08:00
</ style >