@@ -2,7 +2,7 @@
import { computed , nextTick , onBeforeUnmount , onMounted , reactive , ref } from 'vue'
import { onBeforeRouteLeave , useRouter } from 'vue-router'
import { ElMessage , ElMessageBox , type FormInstance , type FormItemRule , type FormRules } from 'element-plus'
import { AlertTriangle , Box , Building2 , CheckCircle2 , Eye , FilePlus2 , MapPin , PackageCheck , Pencil , Plus , Send , Trash2 } from '@lucide/vue'
import { AlertTriangle , Box , CheckCircle2 , Eye , FilePlus2 , MapPin , PackageCheck , Pencil , Plus , Send , Trash2 } from '@lucide/vue'
import { FIRST _LEG _ORDER _STATUSES , FIRST _LEG _TRANSPORT _MODES , type FirstLegOrderStatus } from '@chongchong/shared'
import QueryToolbar , { type QueryField , type QueryModel } from '@/components/common/QueryToolbar.vue'
import DataTable , { type DataTableColumn } from '@/components/common/DataTable.vue'
@@ -40,6 +40,12 @@ const queryFields: QueryField[] = [
{ key : 'transportMode' , type : 'select' , placeholder : '全部运输方式' , options : FIRST _LEG _TRANSPORT _MODES . map ( ( item ) => ( { label : item . label , value : item . code } ) ) , props : { 'aria-label' : '运输方式' } } ,
{ key : 'createdRange' , type : 'daterange' , placeholder : '订单创建日期' , props : { 'aria-label' : '订单创建日期' } } ,
]
const channelOptions = [
{ code : 'SEA-US-GENERAL' , name : '美线普货专线' } ,
{ code : 'SEA-US-SENSITIVE' , name : '美线敏感货专线' } ,
{ code : 'SEA-US-EXPRESS' , name : '美线快线' } ,
{ code : 'SEA-US-FBA' , name : '美线 FBA 直送' } ,
]
const orderColumns : DataTableColumn [ ] = [
{ key : 'actions' , label : '操作' , width : 180 , fixed : 'left' , slotName : 'actions' } ,
{ key : 'order' , label : '订单' , minWidth : 190 , slotName : 'order' } ,
@@ -133,7 +139,7 @@ function emptyDraft(): FirstLegOrderDraft {
destinationWarehouseName : '' ,
pickupMethod : 'SELF_EXPRESS' ,
channelCode : 'SEA-US-FBA' ,
channelName : '美国头程渠道 ' ,
channelName : '美线 FBA 直送 ' ,
taxMode : 'BUY_DECLARATION' ,
customsInspection : false ,
destinationType : 'FBA' ,
@@ -297,10 +303,13 @@ function selectParty(role: FirstLegPartyRole, id: string) {
form . shipperPartyId = party . id
form . shipperPartySnapshot = snapshot
form . shipper = party . name
form . origin = [ snapshot . province , snapshot . city ] . filter ( Boolean ) . join ( ' ' ) || snapshot . countryRegion || snapshot . name
} else {
form . consigneePartyId = party . id
form . consigneePartySnapshot = snapshot
form . consignee = party . name
form . destination = [ snapshot . province , snapshot . city ] . filter ( Boolean ) . join ( ' ' ) || snapshot . countryRegion || snapshot . name
if ( /^[A-Za-z]{2}$/ . test ( snapshot . countryRegion ) ) form . destinationCountryCode = snapshot . countryRegion . toUpperCase ( )
}
}
@@ -309,13 +318,24 @@ function clearParty(role: FirstLegPartyRole) {
form . shipperPartyId = ''
form . shipperPartySnapshot = null
form . shipper = ''
form . origin = ''
} else {
form . consigneePartyId = ''
form . consigneePartySnapshot = null
form . consignee = ''
form . destination = ''
}
}
function selectChannel ( code : string ) {
const channel = channelOptions . find ( ( item ) => item . code === code )
form . channelName = channel ? . name ? ? ''
}
function syncAmazonWarehouse ( value : string ) {
if ( form . destinationType === 'FBA' ) form . destinationWarehouseName = value . trim ( )
}
function selectShipper ( id : string ) {
selectParty ( 'SHIPPER' , id )
}
@@ -332,10 +352,11 @@ function clearConsignee() {
clearParty ( 'CONSIGNEE' )
}
async function loadSelectedWarehouse ( id : string ) {
if ( ! id ) return
async function loadSelectedWarehouse ( id : string | number | boolean | undefined ) {
const warehouseId = typeof id === 'string' ? id : typeof id === 'number' ? String ( id ) : ''
if ( ! warehouseId ) return
try {
await store . loadWarehouseDetail ( id )
await store . loadWarehouseDetail ( warehouseId )
} catch ( error ) {
ElMessage . error ( error instanceof Error ? error . message : '仓库地址与收货时间加载失败' )
}
@@ -587,107 +608,85 @@ async function searchOrders(model: QueryModel) {
< / template >
< / div >
< el-drawer v-model = "editorOpen" class="first-leg-editor" :title="editingId ? '编辑订单草稿' : '创建头程订单'" size="76 0px" destroy-on-close :before-close="requestCloseEditor" >
< el -form ref = "orderFormRef" :model = "form" :rules = "orderFormRules" label -position = " top " class = "order-form" scroll -to -error >
< section class = "order-form-section" >
< h3 > 运输 信息< / h3 >
< el-drawer v-model = "editorOpen" class="first-leg-editor" :title="editingId ? '编辑订单草稿' : '创建头程订单'" size="108 0px" destroy-on-close :before-close="requestCloseEditor" >
< el -form ref = "orderFormRef" :model = "form" :rules = "orderFormRules" label -position = " top " class = "order-form prototype-order-form " scroll -to -error >
< section class = "order-form-section prototype-section " >
< div class = "order-section-heading" > < div > < h3 > 下单 信息< / h3 > < p > 选择送货方式 、 收货仓和运输渠道 。 < / p > < / div > < / div >
< div class = "order-form-grid" >
< el-form-item label = "运输 方式" prop = "transportM ode " required > < el-select v-model = "form.transportMode"><el-option v-for="item in FIRST_LEG_TRANSPORT_MODES" :key="item.code" :label="item.label" :value="item.code" / > < / el-select > < / el-form-item >
< el-form-item label = "业务类型 " prop = "businessType" required > < el-radio-group v-model = "form.businessType"><el-radio-button value="EXPORT">出口</el-radio-button><el-radio-button value="IMPORT" > 进口 < / el -radio -button > < / el-radio-group > < / el-form-item >
< el-form-item label = "起运地 " prop = "origin " required > < el-input v-model = "form.origin" placeholder="例如:宁波" / > < / el-form-item >
< el-form-item label = "目的地 " prop = "destination " required > < el-input v-model = "form.destination" placeholder="例如:洛杉矶" / > < / el-form-item >
< el-form-item label = "平台收货仓 " prop = "receivingWarehouseId " required > < el-select v-model = "form.receivingWarehouseId" filterable placeholder="请选择送货仓库" @change="loadSelectedWarehous e"><el-opt ion v-for="warehouse in availableWarehouses" :key="warehouse.id" :label="warehouse.city ? `${warehouse.name} · ${warehouse.city}` : warehouse.name" :value="warehouse.id" / > < / el-select > < / el-form-item >
< el-form-item label = "期望出发日期" > < el-date-picker v-model = "form.expectedDepartureDate" type="date" value-format="YYYY-MM-DD" placeholder="请选择日期" / > < / el-form-item >
< el-form-item label = "客户参考号" prop = "referenceNo" : required = "form.destinationType === 'FBA'" : rules = "form.destinationType === 'FBA' ? requiredTextRules('FBA 订单请输入客户参考号') : undefined" > < el-input v-model = "form.referenceNo" maxlength="100 " : placeholder="form.destinationType === 'FBA' ? '请输入 FBA Reference ID' : '贵司内部单号(选填)' " / > < / el-form-item >
< el-form-item label = "优先级 " > < el-select v-model = "form.priority"><el-option label="普通" value="NORMAL" /><el-option label="紧急" value="URGENT" /><el-option label="加急" value="EXPEDITED" / > < / el-select > < / el-form-item >
< el-form-item label = "目的国代码" prop = "destinationCountryCode" required > < el-input v-model = "form.destinationCountryCode" maxlength="2" placeholder="例如:US" / > < / el-form-item >
< el-form-item label = "目的仓名称" prop = "destinationWarehouseName" required > < el-input v-model = "form.destinationWarehouseName" maxlength="128" placeholder="例如:ONT8 或洛杉矶海外仓" / > < / el-form-item >
< el-form-item label = "渠道编码" prop = "channelCode" required > < el-input v-model = "form.channelCode" maxlength="64" placeholder="例如:SEA-US-FBA" / > < / el-form-item >
< el-form-item label = "渠道名称" prop = "channelName" required > < el-input v-model = "form.channelName" maxlength="128" placeholder="例如:美国海运 FBA 渠道" / > < / el-form-item >
< el-form-item label = "提货方式" prop = "pickupMethod" required > < el-select v-model = "form.pickupMethod"><el-option label="自送快递" value="SELF_EXPRESS" /><el-option label="上门提货" value="PICKUP" /><el-option label="自送卡车" value="SELF_TRUCK" / > < / el-select > < / el-form-item >
< el-form-item label = "报关模式" prop = "taxMode" required > < el-select v-model = "form.taxMode"><el-option label="买单报关" value="BUY_DECLARATION" /><el-option label="退税报关" value="TAX_REFUND" / > < / el-select > < / el-form-item >
< el-form-item label = "目的仓类型" prop = "destinationType" required > < el-select v-model = "form.destinationType"><el-option label="FBA" value="FBA" /><el-option label="海外仓" value="OVERSEAS" /><el-option label="私人地址" value="PRIVATE" / > < / el-select > < / el-form-item >
< el-form-item label = "贸易术语" > < el-input v-model = "form.tradeTerm" maxlength="16" placeholder="例如:FOB" / > < / el-form-item >
< el-form-item v-if = "form.destinationType === 'FBA'" label="Amazon 仓库编码" prop="amazonWarehouseCode" :rules="requiredTextRules('请输入 Amazon 仓库编码')" required><el-input v-model="form.amazonWarehouseCode" maxlength="32" placeholder="例如:ONT8" / > < / el-form-item >
< el-form-item v-if = "form.destinationType === 'FBA'" label="Shipment ID" prop="shipmentId" :rules="requiredTextRules('请输入 Shipment ID')" required><el-input v-model="form.shipmentId" maxlength="64" placeholder="例如:FBA15ABC123" / > < / el-form-item >
< el-form-item label = "取货 方式" prop = "pickupMeth od" required class = "form-span-full" > < el-radio-group v-model = "form.pickupMethod" class="segmented-options"><el-radio-button value="SELF_EXPRESS">自送货 · 快递</el-radio-button><el-radio-button value="PICKUP">我司提货</el-radio-button><el-radio-button value="SELF_TRUCK" > 自送货 · 卡车 < / el -radio -button > < / el-radio-group > < / el-form-item >
< el-form-item label = "平台收货仓 " prop = "receivingWarehouseId" required class = "form-span-full" > < el-radio-group v-model = "form.receivingWarehouseId" class="warehouse-options" @change="loadSelectedWarehouse"><el-radio-button v-for="warehouse in availableWarehouses" :key="warehouse.id" :value="warehouse.id" > {{ warehouse.city ? ` $ { warehouse.name } · $ { warehouse.city } ` : warehouse.name }} < / el -radio -button > < / el-radio-group > < / el-form-item >
< el-form-item label = "渠道 " prop = "channelCode " required > < el-select v-model = "form.channelCode" placeholder="请选择运输渠道" @change="selectChannel"><el-option v-if="form.channelCode && !channelOptions.some((channel) => channel.code === form.channelCode)" :label="form.channelName || form.channelCode" :value="form.channelCode" /><el-option v-for="channel in channelOptions" :key="channel.code" :label="channel.name" :value="channel.code" / > < / el-select > < / el-form-item >
< el-form-item label = "买单 / 退税 " prop = "taxMode " required > < el-radio-group v-model = "form.taxMode"><el-radio value="BUY_DECLARATION">买单报关</el-radio><el-radio value="TAX_REFUND" > 退税报关 < / el -radio > < / el-radio-group > < / el-form-item >
< el-form-item label = "运输方式 " prop = "transportMode " required > < el-radio-group v-model = "form.transportMod e"><el-rad io v-for="item in FIRST_LEG_TRANSPORT_MODES" :key="item.code" :value="item.code" > {{ item.label }} < / el -radio > < / el-radio-group > < / el-form-item >
< el-form-item label = "是否商检" prop = "customsInspection" required > < el-radio-group v-model = "form.customsInspection"><el-radio :value="true">是</el-radio><el-radio :value="false" > 否 < / el -radio > < / el-radio-group > < / el-form-item >
< el-form-item label = "期望出货日期" > < el-date-picker v-model = "form.expectedDepartureDate" type="date" value-format="YYYY-MM-DD " placeholder="请选择期望出货日期 " / > < / el-form-item >
< el-form-item label = "订单备注 " > < el-input v-model = "form.customerRemark" maxlength="1000" placeholder="选填,例如:入仓前请电话联系" / > < / el-form-item >
< / div >
< div v-if = "selectedWarehouse" class="selected-warehouse"><MapPin :size="17" / > < div > < strong > { { selectedWarehouse . name } } < / strong > < span > { { selectedWarehouse . address ? ` ${ selectedWarehouse . province } ${ selectedWarehouse . city } · ${ selectedWarehouse . address } ` : '仓库地址详情暂不可用' } } < / span > < small > { { selectedWarehouse . businessHours ? ` 收货时间: ${ selectedWarehouse . businessHours } ` : '收货时间暂不可用' } } < / small > < / div > < / div >
< / section >
< section class = "order-form-section" >
< div class = "order-section-heading" > < div > < h3 > 收 发货单位 < / h3 > < p > 仅可选择平台已启用的往来单位 ; 下方联系人和地址调整只影响本订单快照 。 < / p > < / div > < / div >
< div class = "party-editor-grid" >
< article class = "party-editor " >
< header > < span > < Building2 :size = "17" / > < strong > 发货单位 < / strong > < / span > < small > 必填 < / small > < / header >
< el-form-item label = "选择发货单位" prop = "shipperPartyId" required >
< el-select v-model = "form.shipperPartyId" filterable clearable placeholder="请选择已 启用的 发货单位" @change="selectShipper " @ clear="clearShipper" >
< el -option v-if = "form.shipperPartySnapshot && !availableShippers.some((party) => party.id === form.shipperPartyId)" :label="`${form.shipperPartySnapshot.name} · 历史快照`" :value="form.shipperPartyId" disabled / >
< el-option v-for = "party in availableShippers" :key="party.id " : label="`${party.name} · ${party.code}`" :value="party.id" / >
< / el-select >
< / el-form-item >
< div v-if = "form.shipperPartySnapshot" class="p arty-s napshot-fields" >
< div class = "party-name" > < strong > { { form . shipperPartySnapshot . name } } < / strong > < span > { { form . shipperPartySnapshot . code } } < / span > < / div >
< el-form-item label = "联系人" prop = "shipperPartySnapshot.contactName" required :rules = "requiredTextRules('请输入发货联系人姓名')" > < el-input v-model = "form.shipperPartySnapshot.contactName" maxlength="50" placeholder="请输入发货联系人姓名" / > < / el-form-item >
< el-form-item label = "联系电话" prop = "shipperPartySnapshot.phone" required :rules = "requiredTextRules('请输入发货联系电话')" > < el-input v-model = "form.shipperPartySnapshot.phone" maxlength="30" placeholder="请输入发货联系电话" / > < / el-form-item >
< el-form-item label = "联系邮箱" > < el-input v-model = "form.shipperPartySnapshot.email" maxlength="100" placeholder="例如:name@example.com" / > < / el-form-item >
< el-form-item label = "本单发货地址" prop = "shipperPartySnapshot.address" required :rules = "requiredTextRules('请输入本单实际发货地址')" > < el-input v-model = "form.shipperPartySnapshot.address" maxlength="300" placeholder="请输入本单实际发货地址" / > < / el-form-item >
< section class = "order-form-section prototype-section destination-section " >
< div class = "order-section-heading" > < div > < h3 > 发货与目的地 < / h3 > < p > 单位信息会保存为本订单快照 , 后续主数据变更不会影响本单 。 < / p > < / div > < / div >
< el-form-item label = "目的仓类型" prop = "destinationType" required > < el-radio-group v-model = "form.destinationType" class="segmented-options"><el-radio-button value="FBA">FBA 仓</el-radio-button><el-radio-button value="OVERSEAS">海外仓</el-radio-button><el-radio-button value="PRIVATE" > 私人仓 < / el -radio -button > < / el-radio-group > < / el-form-item >
< div class = "party-select-grid " >
< div class = "party-panel" >
< h4 > 发货信息 < / h4 >
< el-alert v-if = "availableShippers.length === 0" title="暂无可用发货单位" description="请先在平台往来单位中 启用“ 发货单位”类型。" type="warning " : closable="false" show -icon / >
< el-form-item label = "发货单位" prop = "shipperPartyId" required > < el-select v-model = "form.shipperPartyId" filterable clearable placeholder="请选择发货单位" @change="selectShipper" @clear="clearShipper"><el-option v-if= "form.shipperPartySnapshot && !availableShippers.some((party) => party.id === form.shipperPartyId)" :label="`${form.shipperPartySnapshot.name} · 历史快照`" :value="form.shipperPartyId" disabled /><el-option v-for="party in availableShippers" :key="party.id" :label="`${party.name} · ${party.code}`" :value="party.id" / > < / el-select > < / el-form-item >
< div v-if = "form.shipperPartySnapshot " c lass="party-contact-grid" >
< el -form -item label = "联系人" prop = "shipperPartySnapshot.contactName" required :rules = "requiredTextRules('请输入发货联系人姓名')" > < el-input v-model = "form.shipperPartySnapshot.contactName" maxlength="50" placeholder="请输入联系人" / > < / el-form-item >
< el-form-item label = "联系电话" prop = "shipperPartySnapshot.phone" required :rules = "requiredTextRules('请输入发货联系电话')" > < el-input v-model = "form.shipperPartySnapshot.phone" maxlength="30" placeholder="请输入联系电话" / > < / el-form-item >
< el-form-item label = "发货地址" prop = "shipperPartySnapshot.address" required :rules = "requiredTextRules('请输入本单实际发货地址')" class = "form-span-full" > < el-input v-model = "form.shipperP artyS napshot.address" maxlength="300" placeholder="请输入本单实际发货地址" / > < / el-form-item >
< / div >
< div v-else class = "party-empty" > 选择后可核对本单联系人和发货地址 < / div >
< / article >
< article class = "party-editor" >
< header > < span > < Building2 :size = "17" / > < strong > 收货单位 < / strong > < / span > < small > 必填 < / small > < / header >
< el-form-item label = "选择收货单位" prop = "consigneePartyId" required >
< el-select v-model = "form.consigneePartyId" filterable clearable placeholder="请选择已启用的收货单位" @change="selectConsignee" @clear="clearConsignee" >
< el -option v-if = "form.consigneePartySnapshot && !availableConsignees.some((party) => party.id === form.consigneePartyId)" :label="`${form.consigneePartySnapshot.name} · 历史快照`" :value="form.consigneePartyId" disabled / >
< el-option v-for = "party in availableConsignees" :key="party.id" :label="`${party.name} · ${party.code}`" :value="party.id" / >
< / el-select >
< / el-form-item >
< div v-if = "form.consigneePartySnapshot" class="party-snapshot-fields" >
< div class = "party-name" > < strong > { { form . consigneePartySnapshot . name } } < / strong > < span > { { form . consigneePartySnapshot . code } } < / span > < / div >
< el-form-item label = "联系人" prop = "consigneePartySnapshot.contactName" required :rules = "requiredTextRules('请输入收货联系人姓名')" > < el-input v-model = "form.consigneePartySnapshot.contactName" maxlength="50" placeholder="请输入收货联系人姓名" / > < / el-form-item >
< el-form-item label = "联系电话" prop = "consigneePartySnapshot.phone" required :rules = "requiredTextRules('请输入收货联系电话')" > < el-input v-model = "form.consigneePartySnapshot.phone" maxlength="30" placeholder="请输入收货联系电话" / > < / el-form-item >
< el-form-item label = "联系邮箱" > < el-input v-model = "form.consigneePartySnapshot.email" maxlength="100" placeholder="例如:name@example.com" / > < / el-form-item >
< el-form-item label = "本单收货地址" prop = "consigneePartySnapshot.address" required :rules = "requiredTextRules('请输入本单实际收货地址')" > < el-input v-model = "form.consigneePartySnapshot.address" maxlength="300" placeholder="请输入本单实际收货地址" / > < / el-form-item >
< / div >
< div v-else class = "party-empty" > 选择后可核对本单联系人和收货地址 < / div >
< / article >
< div class = "party-panel" >
< h4 > 目的地信息 < / h4 >
< el-alert v-if = "availableConsignees.length === 0" title="暂无可用收货单位" description="请先在平台往来单位中启用“收货单位”类型。" type="warning" :closable="false" show -icon / >
< el-form-item label = "收货单位" prop = "consigneePartyId" required > < el-select v-model = "form.consigneePartyId" filterable clearable placeholder="请选择收货单位" @change="selectConsignee" @clear="clearConsignee"><el-option v-if="form.consigneePartySnapshot && !availableConsignees.some((party) => party.id === form.consigneePartyId)" :label="`${form.consigneePartySnapshot.name} · 历史快照`" :value="form.consigneePartyId" disabled /><el-option v-for="party in availableConsignees" :key="party.id" :label="`${party.name} · ${party.code}`" :value="party.id" / > < / el-select > < / el-form-item >
< div v-if = "form.consigneePartySnapshot" class="party-contact-grid" >
< el -form -item label = "收件人" prop = "consigneePartySnapshot.contactName" required :rules = "requiredTextRules('请输入收货联系人姓名')" > < el-input v-model = "form.consigneePartySnapshot.contactName" maxlength="50" placeholder="请输入收件人姓名" / > < / el-form-item >
< el-form-item label = "联系电话" prop = "consigneePartySnapshot.phone" required :rules = "requiredTextRules('请输入收货联系电话')" > < el-input v-model = "form.consigneePartySnapshot.phone" maxlength="30" placeholder="请输入收件人电话" / > < / el-form-item >
< el-form-item label = "收件地址" prop = "consigneePartySnapshot.address" required :rules = "requiredTextRules('请输入本单实际收货地址')" class = "form-span-full" > < el-input v-model = "form.consigneePartySnapshot.address" maxlength="300" placeholder="请输入收件详细地址" / > < / el-form-item >
< / div >
< / div >
< / div >
< div class = "destination-fields" >
< el-form-item label = "目的国代码" prop = "destinationCountryCode" required > < el-input v-model = "form.destinationCountryCode" maxlength="2" placeholder="例如:US" / > < / el-form-item >
< el-form-item v-if = "form.destinationType === 'FBA'" label="亚马逊仓代码" prop="amazonWarehouseCode" required :rules="requiredTextRules('请输入 Amazon 仓库编码')"><el-input v-model="form.amazonWarehouseCode" maxlength="32" placeholder="例如:ONT8" @input="syncAmazonWarehouse" / > < / el-form-item >
< el-form-item v-else label = "目的仓名称" prop = "destinationWarehouseName" required > < el-input v-model = "form.destinationWarehouseName" maxlength="128" placeholder="请输入海外仓或私人仓名称" / > < / el-form-item >
< el-form-item v-if = "form.destinationType === 'FBA'" label="货件编号" prop="shipmentId" required :rules="requiredTextRules('请输入 Shipment ID')"><el-input v-model="form.shipmentId" maxlength="64" placeholder="Shipment ID" / > < / el-form-item >
< el-form-item label = "跟踪编号" prop = "referenceNo" : required = "form.destinationType === 'FBA'" : rules = "form.destinationType === 'FBA' ? requiredTextRules('FBA 订单请输入 Reference ID') : undefined" > < el-input v-model = "form.referenceNo" maxlength="100" :placeholder="form.destinationType === 'FBA' ? 'Reference ID' : '选填,贵司内部跟踪编号'" / > < / el-form-item >
< / div >
< / section >
< section class = "order-form-section" >
< div class = "order-section-heading" > < div > < h3 > 货物明细 < / h3 > < p > 总件数 、 重量和体积将在提交时由服务端汇总 。 < / p > < / div > < el-button @click ="addCargoItem" > < Plus :size = "16" / > 添加货物 < / el-button > < / div >
< article v-for = "(item, index) in form.items" :key="item.id" class="cargo-editor-card " >
< section class = "order-form-section prototype-section cargo-section " >
< div class = "order-section-heading" > < div > < h3 > 货物明细 < / h3 > < p > 先填写常用字段 , 申报和整柜信息可按需展开 。 < / p > < / div > < el-button @click ="addCargoItem" > < Plus :size = "16" / > 添加货物 < / el-button > < / div >
< article v-for = "(item, index) in form.items" :key="item.id" class="cargo-entry " >
< header > < strong > 货物 { { index + 1 } } < / strong > < el-button link type = "danger" : aria -label = " ` 删除货物 $ { index + 1 } ` " @click ="removeCargoItem(index)" > < Trash2 :size = "16" / > 删除 < / el-button > < / header >
< div class = "cargo-form -grid" >
< div class = "cargo-primary -grid" >
< el-form-item label = "SKU" :prop = "`items.${index}.sku`" : required = "form.destinationType === 'FBA'" : rules = "form.destinationType === 'FBA' ? requiredTextRules('FBA 货物请输入客户 SKU') : undefined" > < el-input v-model = "item.sku" placeholder="SKU NO." / > < / el-form-item >
< el-form-item label = "中文品名" :prop = "`items.${index}.cargoNameCn`" required :rules = "requiredTextRules('请输入中文品名')" > < el-input v-model = "item.cargoNameCn" placeholder="例如:蓝牙音箱" / > < / el-form-item >
< el-form-item label = "英文品名" > < el-input v-model = "item.cargoNameEn" placeholder="例如: Bluetooth speakers" / > < / el-form-item >
< el-form-item label = "客户 SKU" :prop = "`items.${index}.sku`" : required = "form.destinationType === 'FBA'" : rules = "form.destinationType === 'FBA' ? requiredTextRules('FBA 货物请输入客户 SKU') : undefined" > < el-input v-model = "item.sku " placeholder="例如:SKU-001 " / > < / el-form-item >
< el-form-item v-if = "form.destinationType === 'FBA'" label="UPC" :prop="`items.${index}.upc`" required :rules="requiredTextRules('FBA 货物请输入 UPC')"><el-input v-model="item.upc" maxlength="32" placeholder="例如:012345678905" / > < / el-form-item >
< el-form-item label = "英文品名" > < el-input v-model = "item.cargoNameEn" placeholder="Bluetooth speakers" / > < / el-form-item >
< el-form-item v-if = "form.destinationType === 'FBA'" label="UPC" :prop="`items.${index}.upc`" required :rules="requiredTextRules('FBA 货物请输入 UPC')"><el-input v-model="item.upc" maxlength="32 " placeholder="UPC NO. " / > < / el-form-item >
< el-form-item label = "包装类型" :prop = "`items.${index}.packageType`" required :rules = "requiredSelectRules('请选择包装类型')" > < el-select v-model = "item.packageType"><el-option label="纸箱" value="CARTON" /><el-option label="托盘" value="PALLET" /><el-option label="木箱" value="WOODEN_CASE" / > < / el-select > < / el-form-item >
< el-form-item label = "HS Code " > < el-input v-model = "item.hsCode" maxlength="20" placeholder="例如:8518210000 " / > < / el-form-item >
< el-form-item label = "件数" :prop = "`items.${index}.pieces`" required : rules = "positiveNumberRules('件数必须为大于 0 的整数', true)" > < el-input-number v-model = "item.pieces" :min="1" controls-position="right" / > < / el-form-item >
< el-form-item label = "总数量" :prop = "`items.${index}.pieces`" required : rules = "positiveNumberRules('件数必须为大于 0 的整数', true) "> < el-input-number v-model = "item.pieces" :min="1" controls-position="right " / > < / el-form-item >
< el-form-item label = "箱数" > < el-input-number v-model = "item.packageCount" :min="0" controls-position="right" / > < / el-form-item >
< el-form-item label = "毛重 ( kg) " :prop = "`items.${index}.grossWeightKg`" required : rules = "positiveNumberRules('毛重必须大于 0')" > < el-input-number v-model = "item.grossWeightKg" :min="0" :precision="2" controls-position="right" / > < / el-form-item >
< el-form-item label = "体积 ( m³) " :prop = "`items.${index}.volumeCbm`" required : rules = "positiveNumberRules('体积必须大于 0')" > < el-input-number v-model = "item.volumeCbm" :min="0" :precision="3" controls-position="right" / > < / el-form-item >
< el-form-item label = "总 毛重 kg" :prop = "`items.${index}.grossWeightKg`" required : rules = "positiveNumberRules('毛重必须大于 0')" > < el-input-number v-model = "item.grossWeightKg" :min="0" :precision="2" controls-position="right" / > < / el-form-item >
< el-form-item label = "总 体积 m³" :prop = "`items.${index}.volumeCbm`" required : rules = "positiveNumberRules('体积必须大于 0')" > < el-input-number v-model = "item.volumeCbm" :min="0" :precision="3" controls-position="right" / > < / el-form-item >
< / div >
< el-collapse class = "cargo-more" > < el-collapse-item title = "更多申报信息" > < div class = "cargo-secondary-grid" >
< el-form-item label = "HS Code" > < el-input v-model = "item.hsCode" maxlength="20" placeholder="例如:8518210000" / > < / el-form-item >
< el-form-item label = "申报货值" > < el-input-number v-model = "item.declaredValue" :min="0" :precision="2" controls-position="right" / > < / el-form-item >
< el-form-item label = "币种" > < el-input v-model = "item.currency" maxlength="3" placeholder="例如:USD" / > < / el-form-item >
< el-form-item label = "危险品" > < el-switch v-model = "item.dangerousGoods" / > < / el-form-item >
< template v-if = "form.transportMode === 'FCL'" >
< el -form -item label = "箱型" :prop = "`items.${index}.containerType`" required :rules = "requiredSelectRules('请选择箱型') "> < el-select v-model = "item.containerType"><el-option v-for="type in ['20GP', '40GP', '40HQ']" :key="type" :label="type" :value="type" / > < / el-select > < / el-form-item >
< el-form-item label = "柜量" :prop = "`items.${index}.containerCount`" required : rules = "positiveNumberRules('柜量必须为大于 0 的整数', true)" > < el-input-number v-model = "item.containerCount" :min="0" controls-position="right" / > < / el-form-item >
< / template >
< el-form-item label = "货物备注" class = "cargo-remark" > < el-input v-model = "item.remark" maxlength="300" placeholder="选填,例如:注意防潮" / > < / el-form-item >
< / div >
< template v-if = "form.transportMode === 'FCL'"><el-form-item label="箱型" :prop="`items.${index}.containerType`" required :rules="requiredSelectRules('请选择箱型')"><el-select v-model="item.containerType"><el-option v-for="type in ['20GP', '40GP', '40HQ']" :key="type" :label="type" :value="type" /></el-select></el-form-item><el-form-item label="柜量" :prop="`items.${index}.containerCount`" required :rules="positiveNumberRules('柜量必须为大于 0 的整数', true)"><el-input-number v-model="item.containerCount" :min="0" controls-position="right" / > < / el-form-item > < / template >
< el-form-item label = "货物备注" class = "form-span-full "> < el-input v-model = "item.remark" maxlength="300" placeholder="选填,例如:注意防潮" / > < / el-form-item >
< / div > < / el-collapse-item > < / el-collapse >
< / article >
< / section >
< section class = "order-form-section" >
< h3 > 订单备注 < / h3 >
< el-form-item label = "客户备注" > < el-input v-model = "form.customerRemark" type="textarea" :rows="3" maxlength="1000" show-word-limit placeholder="平台工作人员可以看到该备注" / > < / el-form-item >
< p class = "cargo-tip" > FBA 货物必须填写 SKU 、 UPC 、 货件编号和跟踪编号 ; 提交前请核对重量与体积 。 < / p >
< / section >
< / el-form >
< template # footer > < div class = "editor-actions" > < el-button :disabled = "saving" @click ="closeEditor" > 返回 < / el -button > < el-button v-if = "editingId ? canEditOrder : canCreateOrder" :loading="saving" @click="saveDraft">保存草稿</el-button><el-button v-if="auth.hasPermission('order:first-leg:submit')" type="primary" :loading="saving" @click="submitOrder"><Send :size="16" / > { { companyApproved ? '提交审核 ' : '提交前需完成资料审核' } } < / el-button > < / div > < / template >
< template # footer > < div class = "editor-actions" > < el-button :disabled = "saving" @click ="closeEditor" > 返回 < / el -button > < el-button v-if = "editingId ? canEditOrder : canCreateOrder" :loading="saving" @click="saveDraft">保存草稿</el-button><el-button v-if="auth.hasPermission('order:first-leg:submit')" type="primary" :loading="saving" @click="submitOrder"><Send :size="16" / > { { companyApproved ? '提交订单 ' : '提交前需完成资料审核' } } < / el-button > < / div > < / template >
< / el-drawer >
< el-drawer v-model = "detailOpen" class="first-leg-detail" title="订单详情" size="680px" >
@@ -761,33 +760,38 @@ async function searchOrders(model: QueryModel) {
. order - form - section + . order - form - section { margin - top : 24 px ; padding - top : 22 px ; border - top : 1 px solid var ( -- border ) ; }
. order - form - section h3 { margin : 0 0 16 px ; color : var ( -- navy ) ; font - size : 16 px ; }
. order - form - grid { display : grid ; grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr ) ) ; column - gap : 18 px ; }
. order - form - grid > * , . cargo - form - grid > * { min - width : 0 ; }
. order - form - grid : deep ( . el - select ) , . order - form - grid : deep ( . el - date - editor ) , . cargo - form - grid : deep ( . el - select ) , . cargo - form - grid : deep ( . el - input - number ) { width : 100 % ; }
. order - form - grid > * , . destination - fields > * , . party - contact - grid > * , . cargo - primary - grid > * , . cargo - secondary - grid > * { min - width : 0 ; }
. order - form : deep ( . el - select ) , . order - form : deep ( . el - date - editor ) , . order - form : deep ( . el - input - number ) { width : 100 % ; }
. form - span - full { grid - column : 1 / - 1 ; }
. prototype - section { padding : 2 px 2 px 0 ; }
. segmented - options { display : flex ; width : min ( 100 % , 620 px ) ; }
. segmented - options : deep ( . el - radio - button ) { flex : 1 ; }
. segmented - options : deep ( . el - radio - button _ _inner ) { width : 100 % ; min - height : 42 px ; padding : 11 px 16 px ; }
. warehouse - options { display : flex ; flex - wrap : wrap ; gap : 8 px ; }
. warehouse - options : deep ( . el - radio - button _ _inner ) { min - height : 40 px ; border : 1 px solid var ( -- border ) ; border - radius : 6 px ! important ; box - shadow : none ! important ; }
. warehouse - options : deep ( . el - radio - button . is - active . el - radio - button _ _inner ) { border - color : var ( -- primary ) ; }
. selected - warehouse { display : flex ; gap : 10 px ; margin - top : - 2 px ; padding : 12 px 14 px ; border : 1 px solid # cfe1dd ; background : # f3f8f7 ; color : var ( -- primary ) ; }
. selected - warehouse svg { flex : 0 0 17 px ; margin - top : 2 px ; }
. selected - warehouse div { display : flex ; min - width : 0 ; flex - direction : column ; gap : 4 px ; }
. selected - warehouse span , . selected - warehouse small { color : var ( -- muted ) ; font - size : 11 px ; line - height : 1.5 ; }
. party - editor - grid { display : grid ; grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr ) ) ; gap : 14 px ; }
. party - editor { min - width : 0 ; padding : 16 px ; border : 1 px solid var ( -- border ) ; background : # f9fbfb ; }
. party - editor > header { display : flex ; align - items : center ; justify - content : space - between ; gap : 12 px ; margin - bottom : 14 px ; }
. party - editor > header span { display : flex ; min - width : 0 ; align - items : center ; gap : 8 px ; color : var ( -- navy ) ; }
. party - editor > header svg { flex : 0 0 17 px ; color : var ( -- primary ) ; }
. party - editor > header small { color : var ( -- muted ) ; font - size : 11 px ; }
. party - editor : deep ( . el - select ) { width : 100 % ; }
. party - snapshot - fields { display : grid ; grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr ) ) ; column - gap : 12 px ; padding - top : 12 px ; border - top : 1 px solid var ( -- border ) ; }
. party - name { display : flex ; min - width : 0 ; grid - column : 1 / - 1 ; align - items : baseline ; justify - content : space - between ; gap : 12 px ; margin - bottom : 12 px ; }
. party - name strong { min - width : 0 ; overflow - wrap : anywhere ; color : var ( -- navy ) ; font - size : 13 px ; }
. party - name span { flex : 0 0 auto ; color : var ( -- muted ) ; font - size : 10 px ; }
. party - snapshot - fields . el - form - item : last - child { grid - column : 1 / - 1 ; }
. party - empty { display : grid ; min - height : 150 px ; place - items : center ; border - top : 1 px solid var ( -- border ) ; color : var ( -- muted ) ; font - size : 12 px ; text - align : center ; }
. order - section - heading { display : flex ; align - items : flex - start ; justify - content : space - between ; gap : 12 px ; margin - bottom : 12 px ; }
. order - section - heading h3 { margin - bottom : 5 px ; }
. order - section - heading p { margin : 0 ; color : var ( -- muted ) ; font - size : 11 px ; }
. cargo - editor - card { padding : 16 px ; border : 1 px solid var ( -- border ) ; background : # f9fbfb ; }
. cargo - editor - card + . cargo - editor - card { margin - top : 12 px ; }
. cargo - editor - card header { display : flex ; align - items : center ; justify - content : space - between ; margin - bottom : 12 px ; }
. cargo - form - grid { display : grid ; grid - template - columns : repeat ( 3 , minmax ( 0 , 1 fr ) ) ; column - gap : 14 px ; }
. cargo - remark { grid - column : 1 / - 1 ; }
. party - select - grid { display : grid ; grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr ) ) ; gap : 14 px ; }
. party - panel { min - width : 0 ; padding : 16 px ; border : 1 px solid var ( -- border ) ; background : # fbfcfc ; }
. party - panel h4 { margin : 0 0 14 px ; color : var ( -- navy ) ; font - size : 14 px ; }
. party - panel : deep ( . el - alert ) { margin - bottom : 14 px ; }
. party - contact - grid , . destination - fields { display : grid ; grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr ) ) ; column - gap : 12 px ; }
. destination - fields { margin - top : 16 px ; }
. cargo - entry { padding : 16 px ; border : 1 px solid var ( -- border ) ; background : # fbfcfc ; }
. cargo - entry + . cargo - entry { margin - top : 12 px ; }
. cargo - entry > header { display : flex ; align - items : center ; justify - content : space - between ; gap : 12 px ; margin - bottom : 12 px ; }
. cargo - primary - grid { display : grid ; grid - template - columns : repeat ( 5 , minmax ( 130 px , 1 fr ) ) ; column - gap : 12 px ; }
. cargo - secondary - grid { display : grid ; grid - template - columns : repeat ( 4 , minmax ( 130 px , 1 fr ) ) ; column - gap : 12 px ; padding - top : 6 px ; }
. cargo - more { border - top : 1 px solid var ( -- border ) ; border - bottom : 0 ; }
. cargo - more : deep ( . el - collapse - item _ _header ) , . cargo - more : deep ( . el - collapse - item _ _wrap ) { background : transparent ; }
. cargo - more : deep ( . el - collapse - item _ _content ) { padding - bottom : 0 ; }
. cargo - tip { margin : 12 px 0 0 ; color : var ( -- muted ) ; font - size : 11 px ; line - height : 1.55 ; }
. order - detail - heading { display : flex ; align - items : flex - start ; justify - content : space - between ; gap : 16 px ; padding - bottom : 20 px ; border - bottom : 1 px solid var ( -- border ) ; }
. order - detail - heading span { color : var ( -- muted ) ; font - size : 11 px ; }
. order - detail - heading h2 { margin : 6 px 0 ; font - size : 21 px ; }
@@ -841,7 +845,12 @@ async function searchOrders(model: QueryModel) {
. order - table - toolbar . el - button { width : 100 % ; min - height : 40 px ; }
. order - eligibility { flex - wrap : wrap ; }
. order - eligibility . el - button { width : 100 % ; min - height : 44 px ; }
. order - form - grid , . cargo - form - grid , . order - detail - grid , . party - editor - grid , . party - snapshot - fields { grid - template - columns : 1 fr ; }
. order - form - grid , . order - detail - grid , . party - select - grid , . party - contact - grid , . destination - fields , . cargo - primary - grid , . cargo - secondary - grid { grid - template - columns : 1 fr ; }
. segmented - options { width : 100 % ; align - items : stretch ; flex - direction : column ; }
. segmented - options : deep ( . el - radio - button _ _inner ) { border : 1 px solid var ( -- border ) ; border - radius : 6 px ! important ; box - shadow : none ! important ; }
. warehouse - options { display : grid ; grid - template - columns : 1 fr ; }
. warehouse - options : deep ( . el - radio - button ) , . warehouse - options : deep ( . el - radio - button _ _inner ) { width : 100 % ; }
. form - span - full { grid - column : auto ; }
. editor - actions : deep ( . el - button ) { min - height : 44 px ; }
. desktop - order - table { display : none ; }
. order - load - state { min - height : 240 px ; grid - template - columns : 1 fr ; justify - items : start ; }
@@ -861,7 +870,6 @@ async function searchOrders(model: QueryModel) {
. mobile - order - card footer { align - items : flex - end ; padding - top : 10 px ; border - top : 1 px solid var ( -- border ) ; }
. mobile - order - card footer > div { display : flex ; }
. order - detail - grid div : nth - child ( odd ) { border - right : 0 ; }
. party - name , . party - snapshot - fields . el - form - item : last - child { grid - column : auto ; }
. express - information { grid - template - columns : auto 1 fr ; }
. express - information > div : last - child { grid - column : 2 ; }
. cargo - summary { grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr ) ) ; }