@@ -2,8 +2,8 @@
import { computed , nextTick , onBeforeUnmount , onMounted , reactive , ref , watch } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage , ElMessageBox , type FormInstance , type FormItemRule , type FormRules , type UploadRequestOptions } from 'element-plus'
import { AlertTriangle , Box , Eye , FilePlus2 , ImagePlus , MapPin , PackageCheck , Pencil , Plus , Send , Trash2 } from '@lucide/vue'
import { FIRST _LEG _ORDER _STATUSES , FIRST _LEG _TRANSPORT _MODES , type FirstLegOrderStatus } from '@chongchong/shared'
import { AlertTriangle , Box , Download , Eye , FilePlus2 , ImagePlus , MapPin , PackageCheck , Pencil , Plus , Send , Trash2 , Upload } from '@lucide/vue'
import { FIRST _LEG _ORDER _STATUSES , 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'
import { ApiError } from '@/api/http'
@@ -65,6 +65,19 @@ const channelError = ref('')
const priceEstimate = ref < MerchantPriceEstimate | null > ( null )
const priceEstimateLoading = ref ( false )
const priceEstimateError = ref ( '' )
const selectedCargoIds = ref < string [ ] > ( [ ] )
const cargoImportInput = ref < HTMLInputElement > ( )
const cargoTemplateColumns = [
[ 'SKU NO.' , 'sku' ] , [ '中文品名' , 'cargoNameCn' ] , [ '英文品名' , 'cargoNameEn' ] , [ '报关 HS CODE' , 'hsCode' ] , [ '清关 HS CODE' , 'customsHsCode' ] ,
[ '包装类型' , 'packageType' ] , [ '箱规长 cm' , 'lengthCm' ] , [ '箱规宽 cm' , 'widthCm' ] , [ '箱规高 cm' , 'heightCm' ] , [ '每箱数量' , 'unitsPerPackage' ] ,
[ '箱数' , 'packageCount' ] , [ '总毛重' , 'grossWeightKg' ] , [ '总体积' , 'volumeCbm' ] , [ '总数量' , 'pieces' ] , [ '总净重' , 'netWeightKg' ] ,
[ '清关申报单价 USD' , 'unitDeclaredValue' ] , [ '清关申报金额 USD' , 'declaredValue' ] , [ '材质' , 'material' ] , [ '用途' , 'usage' ] , [ '型号' , 'model' ] ,
[ '品牌' , 'brand' ] , [ '产品长 cm' , 'productLengthCm' ] , [ '产品宽 cm' , 'productWidthCm' ] , [ '产品高 cm' , 'productHeightCm' ] , [ '危险品' , 'dangerousGoods' ] ,
[ '货件编号' , 'shipmentId' ] , [ '跟踪编号' , 'referenceId' ] , [ 'UPC NO.' , 'upc' ] , [ '备注' , 'remark' ] ,
] as const
const cargoNumberKeys = new Set < string > ( [ 'lengthCm' , 'widthCm' , 'heightCm' , 'unitsPerPackage' , 'packageCount' , 'grossWeightKg' , 'volumeCbm' , 'pieces' , 'netWeightKg' , 'unitDeclaredValue' , 'declaredValue' , 'productLengthCm' , 'productWidthCm' , 'productHeightCm' ] )
const orderColumns : DataTableColumn [ ] = [
{ key : 'actions' , label : '操作' , width : 180 , fixed : 'left' , slotName : 'actions' } ,
{ key : 'order' , label : '订单' , minWidth : 190 , slotName : 'order' } ,
@@ -979,6 +992,103 @@ function removeCargoItem(index: number) {
resetEditorValidation ( )
}
const allCargoSelected = computed ( ( ) => form . items . length > 0 && selectedCargoIds . value . length === form . items . length )
const someCargoSelected = computed ( ( ) => selectedCargoIds . value . length > 0 && ! allCargoSelected . value )
function toggleAllCargoItems ( checked : boolean | string | number ) {
selectedCargoIds . value = checked ? form . items . map ( ( item ) => item . id ) : [ ]
}
function toggleCargoItem ( itemId : string , checked : boolean | string | number ) {
if ( checked ) {
if ( ! selectedCargoIds . value . includes ( itemId ) ) selectedCargoIds . value . push ( itemId )
return
}
selectedCargoIds . value = selectedCargoIds . value . filter ( ( id ) => id !== itemId )
}
function removeSelectedCargoItems ( ) {
if ( ! selectedCargoIds . value . length ) {
ElMessage . warning ( '请先勾选要删除的货物' )
return
}
if ( selectedCargoIds . value . length >= form . items . length ) {
ElMessage . warning ( '订单至少需要保留一条货物明细' )
return
}
const selectedIds = new Set ( selectedCargoIds . value )
const removedItems = form . items . filter ( ( item ) => selectedIds . has ( item . id ) )
form . items . splice ( 0 , form . items . length , ... form . items . filter ( ( item ) => ! selectedIds . has ( item . id ) ) )
releaseProductImagePreviews ( removedItems )
selectedCargoIds . value = [ ]
resetEditorValidation ( )
}
function downloadCargoTemplate ( ) {
const headers = cargoTemplateColumns . map ( ( [ label ] ) => ` <th> ${ label } </th> ` ) . join ( '' )
const example = cargoTemplateColumns . map ( ( [ label ] ) => {
if ( label === '包装类型' ) return '<td>CARTON</td>'
if ( label === '危险品' ) return '<td>否</td>'
if ( [ '每箱数量' , '箱数' , '总数量' ] . includes ( label ) ) return '<td>1</td>'
return '<td></td>'
} ) . join ( '' )
const html = ` <!doctype html><html><head><meta charset="UTF-8"></head><body><table><thead><tr> ${ headers } </tr></thead><tbody><tr> ${ example } </tr></tbody></table></body></html> `
const url = URL . createObjectURL ( new Blob ( [ '\ufeff' , html ] , { type : 'application/vnd.ms-excel;charset=utf-8' } ) )
const anchor = document . createElement ( 'a' )
anchor . href = url
anchor . download = '货物明细导入模板.xls'
anchor . click ( )
URL . revokeObjectURL ( url )
}
function parseCargoTemplateRows ( text : string ) {
const documentNode = new DOMParser ( ) . parseFromString ( text , 'text/html' )
const tableRows = [ ... documentNode . querySelectorAll ( 'table tr' ) ]
if ( tableRows . length < 2 ) return [ ]
const headers = [ ... tableRows [ 0 ] . querySelectorAll ( 'th,td' ) ] . map ( ( cell ) => cell . textContent ? . trim ( ) || '' )
return tableRows . slice ( 1 ) . map ( ( row ) => {
const values = [ ... row . querySelectorAll ( 'td' ) ] . map ( ( cell ) => cell . textContent ? . trim ( ) || '' )
return { headers , values }
} ) . filter ( ( { values } ) => values . some ( Boolean ) )
}
async function importCargoTemplate ( event : Event ) {
const input = event . target as HTMLInputElement
const file = input . files ? . [ 0 ]
input . value = ''
if ( ! file ) return
if ( ! file . name . toLowerCase ( ) . endsWith ( '.xls' ) ) {
ElMessage . error ( '请上传由“下载模板”生成的 .xls 文件' )
return
}
try {
const parsedRows = parseCargoTemplateRows ( await file . text ( ) )
if ( ! parsedRows . length ) throw new Error ( '模板中没有可导入的货物数据' )
const imported = parsedRows . map ( ( { headers , values } ) => {
const item = createEmptyCargoItem ( ` imported-cargo- ${ ++ cargoSequence } ` )
for ( const [ columnLabel , itemKey ] of cargoTemplateColumns ) {
const value = values [ headers . indexOf ( columnLabel ) ] ? ? ''
if ( itemKey === 'dangerousGoods' ) item . dangerousGoods = [ '是' , 'true' , '1' , 'yes' ] . includes ( value . toLowerCase ( ) )
else if ( cargoNumberKeys . has ( itemKey ) ) ( item as unknown as Record < string , number > ) [ itemKey ] = Number ( value ) || 0
else ( item as unknown as Record < string , string > ) [ itemKey ] = value
}
item . packageType = item . packageType || 'CARTON'
item . unitsPerPackage = item . unitsPerPackage || 1
item . packageCount = item . packageCount || 1
item . pieces = item . pieces || 1
return item
} )
const shouldReplaceDefaultRow = form . items . length === 1 && ! form . items [ 0 ] . cargoNameCn && ! form . items [ 0 ] . sku
if ( shouldReplaceDefaultRow ) form . items . splice ( 0 , 1 , ... imported )
else form . items . push ( ... imported )
selectedCargoIds . value = [ ]
resetEditorValidation ( )
ElMessage . success ( ` 已导入 ${ imported . length } 行货物明细 ` )
} catch ( error ) {
ElMessage . error ( error instanceof Error ? error . message : '货物明细导入失败' )
}
}
async function validateEditorForm ( ) {
let fieldsValid = false
try {
@@ -1157,7 +1267,7 @@ async function searchOrders(model: QueryModel) {
< FirstLegOrderEditorShell v-model = "editorOpen" :page="isCreatePage" :title="editingId ? '编辑订单草稿' : '创建订单'" :before-close="requestCloseEditor" @back="closeEditor" >
< el -form ref = "orderFormRef" v-loading = "isCreatePage && supportingDataLoading" :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-section-heading" > < div > < h3 > 客户下单 < / h3 > < p > 先选择货物如何送到国内收货仓 , 并填写本次运输安排 。 < / p > < / div > < / div >
< div class = "prototype-form-flow shipping-form-flow" >
< el-form-item label = "取货方式" prop = "pickupMethod" required class = "prototype-form-row prototype-form-row--tabs" > < el-radio-group v-model = "form.pickupMethod" class="segmented-options" @change="changePickupMethod"><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 >
< template v-if = "form.pickupMethod === 'PICKUP' && form.shipperPartySnapshot" >
@@ -1167,9 +1277,8 @@ async function searchOrders(model: QueryModel) {
< el-form-item label = "城市" prop = "shipperPartySnapshot.city" required :rules = "requiredTextRules('请输入提货城市')" class = "prototype-form-row prototype-form-row--compact" > < el-input v-model = "form.shipperPartySnapshot.city" maxlength="100" placeholder="请输入城市" / > < / el-form-item >
< el-form-item label = "提货地址" prop = "shipperPartySnapshot.address" required :rules = "requiredTextRules('请输入提货地址')" class = "prototype-form-row prototype-form-row--wide" > < el-input v-model = "form.shipperPartySnapshot.address" maxlength="300" placeholder="请输入详细提货地址" / > < / el-form-item >
< / template >
< el-form-item label = "国内收货仓" prop = "receivingWarehouseId" required class = "prototype-form-row prototype-form-row--wide" > < 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.name }} < / el -radio -button > < / el-radio-group > < / el-form-item >
< el-form-item label = "国内收货仓" prop = "receivingWarehouseId" required class = "prototype-form-row prototype-form-row--wide" > < el-radio-group v-model = "form.receivingWarehouseId" class="warehouse-options" @change="loadSelectedWarehouse"><el-radio v-for="warehouse in availableWarehouses" :key="warehouse.id" :value="warehouse.id" > {{ warehouse.name }} < / el -radio > < / el-radio-group > < / el-form-item >
< / div >
< div v-if = "selectedWarehouse" class="selected-warehouse" aria-label="所选收货仓详情"><MapPin :size="17" /><div><span>{{ selectedWarehouse.address ? `${selectedWarehouse.province}${selectedWarehouse.city} · ${selectedWarehouse.address}` : '仓库地址详情暂不可用' }}</span><small>{{ selectedWarehouse.businessHours ? `收货时间:${selectedWarehouse.businessHours}` : '收货时间暂不可用' }}</small><small v-if="selectedWarehouse.contactName || selectedWarehouse.contactPhone" > 联系人 : {{ selectedWarehouse.contactName | | ' - ' }} {{ selectedWarehouse.contactPhone }} < / small > < / div > < / div >
< div class = "order-form-grid compact-transport-grid" >
< el-form-item label = "渠道" prop = "channelCode" required >
< el-select v-model = "form.channelCode" :loading="channelLoading" :disabled="!form.receivingWarehouseId || channelLoading" :placeholder="form.receivingWarehouseId ? '请选择运输渠道' : '请先选择国内收货仓'" @change="selectChannel"><el-option v-for="channel in channelOptions" :key="channel.channelCode" :label="channel.channelName" :value="channel.channelCode" / > < / el-select >
@@ -1180,18 +1289,18 @@ async function searchOrders(model: QueryModel) {
< el-radio-group v-model = "form.pricingTaxMode"><el-radio v-for="mode in availablePricingTaxModes" :key="mode" :value="mode" > {{ pricingTaxModeLabel ( mode ) }} < / el -radio > < / el-radio-group >
< small v-if = "form.channelCode && availablePricingTaxModes.length === 0" class="field-guidance field-guidance--error" > 当前渠道没有可用报价方式 , 请重新选择渠道 。 < / small >
< / 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.transportMode"><el-radio 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 = "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.transportMode"><el-radio value="FCL">整柜</el-radio><el-radio value="LCL" > 散货 < / 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="10 00" placeholder="选填,例如:入仓前请电话联系" / > < / el-form-item >
< el-form-item label = "订单备注" > < el-input v-model = "form.customerRemark" type="textarea" :rows="3" resize="vertical" maxlength="3 00" show-word-limit placeholder="选填,例如:入仓前请电话联系" / > < / el-form-item >
< / div >
< / section >
< section class = "order-form-section prototype-section" >
< div class = "order-section-heading" > < div > < h3 > 目的地信息 < / h3 > < p > 再选择货物最终送达的仓库或私人地址 。 < / p > < / div > < / div >
< section class = "order-form-section prototype-section destination-section " >
< div class = "order-section-heading destination-section-heading " > < div > < h3 > 目的地信息 < / h3 > < / div > < / div >
< div class = "prototype-form-flow destination-form-flow" >
< el-form-item label = "目的仓类型" prop = "destinationType" required class = "prototype-form-row prototype-form-row--tabs" > < el-radio-group v-model = "form.destinationType" class="segmented-options" @change="changeDestinationType"><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 >
< el-form-item label = "目的仓类型" prop = "destinationType" required class = "prototype-form-row prototype-form-row--tabs destination-type-row " > < el-radio-group v-model = "form.destinationType" class="segmented-options destination-type-options " @change="changeDestinationType"><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 >
< el-form-item v-if = "form.destinationType === 'PRIVATE'" label="私人地址簿" class="prototype-form-row prototype-form-row--wide address-book-row" >
< div class = "address-book-control" > < el-select v-model = "selectedPrivateAddressId" clearable filterable placeholder="选择当前账号保存的私人地址" @change="selectPrivateAddress"><el-option v-for="address in privateAddresses" :key="address.id" :label="address.label" :value="address.id" /></el-select><el-button @click="savePrivateAddress">保存当前地址</el-button><el-button v-if="selectedPrivateAddressId" link type="danger" @click="removePrivateAddress" > 移除 < / el -button > < / div >
@@ -1213,77 +1322,60 @@ async function searchOrders(model: QueryModel) {
< / section >
< section class = "order-form-section prototype-section cargo-section" >
< div class = "order-section-heading" > < div > < h3 > 货物明细 < / h3 > < / 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 >
< section class = "cargo-field-section" >
< h4 > 商品信息 < / h4 >
< div class = "cargo-form-grid cargo-form-grid--basic " >
< el-form-item label = "SKU NO." :prop = "`items.${index}.sku`" : required = "form.destinationType === 'FBA'" : rules = "form.destinationType === 'FBA' ? requiredTextRules('FBA 货物请输入 SKU NO.') : undefined" > < el-input v-model = "item.sku" maxlength="64" placeholder="SKU NO." / > < / el-form-item >
< el-form-item label = "中文品名" :prop = "`items.${index}.cargoNameCn`" required :rules = "requiredTextRules('请输入中文品名')" > < el-input v-model = "item.cargoNameCn" maxlength="128" placeholder="例如:蓝牙音箱" / > < / el-form-item >
< el-form-item label = "英文品名 DESCRIPTION OF GOODS" > < el-input v-model = "item.cargoNameEn" maxlength="128" placeholder="Bluetooth speakers" / > < / el-form-item >
< el-form-item label = "图片 PRODUCT PICTURE" class = "product-image-field" >
< div class = "product-image-control" >
< div v-if = "item.productImagePreviewUrl" class="product-image-preview"><img :src="item.productImagePreviewUrl" :alt="`${item.cargoNameCn || `货物 ${index + 1}`}产品图片`" /><el-button circle type="danger" size="small" :aria-label="`移除货物 ${index + 1} 产品图片`" @click="removeCargoProductImage(item)"><Trash2 :size="14" / > < / el-button > < / div >
< div v-else class = "product-image-placeholder" > < ImagePlus :size = "22" / > < span > { { item . productImageUploading ? '上传中...' : '暂无图片' } } < / span > < / div >
< el-upload :show-file-list = "false" accept = ".jpg,.jpeg,.png,image/jpeg,image/png" :disabled = "item.productImageUploading" : http -request = " ( options : UploadRequestOptions ) = > uploadCargoProductImage ( item , options ) "><el-button :loading=" item . productImageUploading "><ImagePlus :size=" 15 " />{{ item.productImageFileId ? '替换图片' : '上传图片' }}</el-button></el-upload>
<small>JPG、PNG,最大 5MB</small>
<span v-if=" item . productImageError " class=" product - image - error " role=" alert ">{{ item.productImageError }}</span>
</div>
</el-form-item>
<el-form-item label=" UPC NO . " :prop=" ` items. ${ index } .upc ` " :required=" form . destinationType === 'FBA' " :rules=" form . destinationType === 'FBA' ? requiredTextRules ( 'FBA 货物请输入 UPC NO.' ) : undefined " ><el-input v-model="item . upc " maxlength=" 64 " placeholder=" UPC NO . " /></el-form-item>
</div>
</section>
< div class = "order-section-heading cargo-table-heading " > < div > < h3 > 货物明细 < / h3 > < / div > < / div >
< div class = "cargo-table-toolbar " >
< div class = "cargo-import-actions" > < input ref = "cargoImportInput" class = "cargo-import-input" type = "file" accept = ".xls,application/vnd.ms-excel" @change ="importCargoTemplate" / > < el-button class = "cargo-upload-button" @click ="cargoImportInput?.click()" > < Upload :size = "16" / > 上传 Excel < / el-button > < el-button class = "cargo-download-button" @click ="downloadCargoTemplate "> < Download :size = "16" / > 下载模板 < / el-button > < / div >
< div class = "cargo-table-actions" > < el-button @click ="addCargoItem" > < Plus :size = "16" / > 添加行 < / el-button > < el-button type = "danger" plain :disabled = "!selectedCargoIds.length" @click ="removeSelectedCargoItems" > < Trash2 :size = "15" / > 删除所选行 < / el-button > < / div >
< / div >
< div class = "cargo-table-wrap " >
< table class = "cargo-edit-table" >
< thead > < tr >
< th class = "cargo-check-cell" > < el-checkbox :model-value = "allCargoSelected" :indeterminate = "someCargoSelected" aria -label = " 全选货物 " @change ="toggleAllCargoItems" / > < / th > < th class = "cargo-index-cell" > # < / th >
< th > SKU NO . < small > FBA必填 < / small > < / th > < th > 中文品名 < / th > < th class = "cargo-image-column" > 图片 < / th > < th > 英文品名 < / th > < th > 报关 HS CODE < / th > < th > 清关 HS CODE < / th > < th > 包装类型 < / th >
< th class = "cargo-number-column" > 箱规长 cm < / th > < th class = "cargo-number-column" > 箱规宽 cm < / th > < th class = "cargo-number-column" > 箱规高 cm < / th > < th class = "cargo-number-column" > 每箱数量 < / th > < th class = "cargo-number-column" > 箱数 < / th > < th class = "cargo-number-column" > 总毛重 < / th > < th class = "cargo-number-column" > 总体积 < / th > < th class = "cargo-number-column" > 总数量 < / th > < th class = "cargo-number-column" > 总净重 < / th >
< th > 清关申报单价 USD < / th > < th > 清关申报金额 USD < / th > < th > 材质 < / th > < th > 用途 < / th > < th > 型号 < / th > < th > 品牌 < / th > < th class = "cargo-dimensions-column" > 产品规格 长 × 宽 × 高 < / th > < th > 危险品 < / th > < th > 货件编号 < small > FBA必填 < / small > < / th > < th > 跟踪编号 < small > FBA必填 < / small > < / th > < th > UPC NO . < small > FBA必填 < / small > < / th > < th > 备注 < / th > < th class = "cargo-operation-column" > 操作 < / th >
< / tr > < / thead >
< tbody > < tr v-for = "(item, index) in form.items" :key="item.id" >
< td class = "cargo-check-cell" > < el-checkbox :model-value = "selectedCargoIds.includes(item.id)" : aria -label = " ` 选择货物 $ { index + 1 } ` " @change ="toggleCargoItem(item.id, $event)" / > < / td > < td class = "cargo-index-cell" > { { index + 1 } } < / td >
< td > < el-form-item :prop = "`items.${index}.sku`" : required = "form.destinationType === 'FBA'" : rules = "form.destinationType === 'FBA' ? requiredTextRules('FBA 货物请输入 SKU NO.') : undefined" > < el-input v-model = "item.sku" maxlength="64" placeholder="SKU编号" / > < / el-form-item > < / td >
< td > < el-form-item :prop = "`items.${index}.cargoNameCn`" required :rules = "requiredTextRules('请输入中文品名')" > < el-input v-model = "item.cargoNameCn" maxlength="128" placeholder="中文品名" / > < / el-form-item > < / td >
< td class = "cargo-image-column" > < div class = "cargo-table-image" > < img v-if = "item.productImagePreviewUrl" :src="item.productImagePreviewUrl" :alt="`${item.cargoNameCn || `货物 ${index + 1}`}产品图片`" /><el-upload v-else :show-file-list="false" accept=".jpg,.jpeg,.png,image/jpeg,image/png" :disabled="item.productImageUploading" :http-request="(options: UploadRequestOptions) => uploadCargoProductImage(item, options)"><el-button text :loading="item.productImageUploading" aria-label="上传图片"><ImagePlus :size="17" /></el-button></el-upload><el-button v-if="item.productImagePreviewUrl" class="cargo-image-remove" text type="danger" aria-label="移除图片" @click="removeCargoProductImage(item)"><Trash2 :size="12" / > < / el-button > < / div > < / td >
< td > < el-input v-model = "item.cargoNameEn" maxlength="128" placeholder="English Name" /></td><td><el-input v-model="item.hsCode" maxlength="20" placeholder="HS Code" /></td><td ><el-input v-model="item.customsHsCode " maxlength="20 " placeholder="HS Code" / > < / td >
< td > < el-form-item :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 > < / td >
< td > < el-input-number v-model = "item.lengthCm" :min="0" :precision="2" :controls="false" /></td><td><el-input-number v-model="item.widthCm" :min="0" :precision="2" :controls="false" /></td><td><el-input-number v-model="item.heightCm" :min="0" :precision="2" :controls="false" / > < / td >
< td > < el-form-item :prop = "`items.${index}.unitsPerPackage`" required : rules = "positiveNumberRules('每箱数量必须大于 0', true)" > < el-input-number v-model = "item.unitsPerPackage" :min="1" :controls="false" /></el-form-item></td><td><el-form-item :prop="`items.${index}.packageCount`" required :rules="positiveNumberRules('箱数必须大于 0', true)"><el-input-number v-model="item.packageCount" :min="1" :controls="false" /></el-form-item></td><td><el-form-item :prop="`items.${index}.grossWeightKg`" required :rules="positiveNumberRules('总毛重必须大于 0')"><el-input-number v-model="item.grossWeightKg" :min="0" :precision="3" :controls="false" /></el-form-item></td><td><el-form-item :prop="`items.${index}.volumeCbm`" required :rules="positiveNumberRules('总体积必须大于 0')"><el-input-number v-model="item.volumeCbm" :min="0" :precision="4" :controls="false" /></el-form-item></td><td><el-form-item :prop="`items.${index}.pieces`" required :rules="positiveNumberRules('总数量必须大于 0', true)"><el-input-number v-model="item.pieces" :min="1" :controls="false" /></el-form-item></td><td><el-input-number v-model="item.netWeightKg" :min="0" :precision="3" :controls="false" / > < / td >
< td > < el-input-number v-model = "item.unitDeclaredValue" :min="0" :precision="2" :controls="false" /></td><td><el-input-number v-model="item.declaredValue" :min="0" :precision="2" :controls="false" /></td><td><el-input v-model="item.material" maxlength="128" placeholder="材质" /></td><td><el-input v-model="item.usage" maxlength="128" placeholder="用途" /></td><td><el-input v-model="item.model" maxlength="64" placeholder="型号" /></td><td><el-input v-model="item.brand" maxlength="64" placeholder="品牌" / > < / td >
< td class = "cargo-dimensions-column" > < div class = "cargo-table-dimensions" > < el-input-number v-model = "item.productLengthCm" :min="0" :precision="2" :controls="false" /><span>× </span><el-input-number v-model="item.productWidthCm" :min="0" :precision="2" :controls="false" /><span>× </span><el-input-number v-model="item.productHeightCm" :min="0" :precision="2" :controls="false" /></div></td><td class="cargo-danger-cell"><el-switch v-model="item.dangerousGoods" / > < / td >
< td > < el-form-item :prop = "`items.${index}.shipmentId`" : required = "form.destinationType === 'FBA'" : rules = "form.destinationType === 'FBA' ? requiredTextRules('亚马逊货物请输入 Shipment ID') : undefined" > < el-input v-model = "item.shipmentId" maxlength="64" placeholder="SHIPMENT ID" /></el-form-item></td><td><el-form-item :prop="`items.${index}.referenceId`" :required="form.destinationType === 'FBA'" :rules="form.destinationType === 'FBA' ? requiredTextRules('亚马逊货物请输入 Reference ID') : undefined"><el-input v-model="item.referenceId" maxlength="64" placeholder="REFERENCE ID" /></el-form-item></td><td><el-form-item :prop="`items.${index}.upc`" :required="form.destinationType === 'FBA'" :rules="form.destinationType === 'FBA' ? requiredTextRules('FBA 货物请输入 UPC NO.') : undefined"><el-input v-model="item.upc" maxlength="64" placeholder="UPC NO." /></el-form-item></td><td><el-input v-model="item.remark" maxlength="500" placeholder="备注" /></td><td class="cargo-operation-column"><el-button link type="danger" :aria-label="`删除货物 ${index + 1}`" @click="removeCargoItem(index)"><Trash2 :size="15" / > 删除 < / el-button > < / td >
< / tr > < / tbody >
< / table >
< / div >
< div class = "cargo-table-hint" > < span > FBA 货物的 SKU NO . 、 UPC NO . 为必填项 < / span > < i > < / i > < span > 亚马逊货物的货件编号和跟踪编号为必填项 < / span > < / div >
< / section >
<section class=" cargo - field - section ">
<h4>箱规、数量与重量</h4>
<div class=" cargo - form - grid ">
<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=" 箱规长 cm "><el-input-number v-model=" item . lengthCm " :min=" 0 " :precision=" 2 " controls-position=" right " /></el-form-item>
<el-form-item label=" 箱规宽 cm "><el-input-number v-model=" item . widthCm " :min=" 0 " :precision=" 2 " controls-position=" right " /></el-form-item>
<el-form-item label=" 箱规高 cm "><el-input-number v-model=" item . heightCm " :min=" 0 " :precision=" 2 " controls-position=" right " /></el-form-item>
<el-form-item label=" 每箱数量 QTY / CTN " :prop=" ` items. ${ index } .unitsPerPackage ` " required :rules=" positiveNumberRules ( '每箱数量必须为大于 0 的整数' , true ) "><el-input-number v-model=" item . unitsPerPackage " :min=" 1 " controls-position=" right " /></el-form-item>
<el-form-item label=" 箱数 CARTONS " :prop=" ` items. ${ index } .packageCount ` " required :rules=" positiveNumberRules ( '箱数必须为大于 0 的整数' , true ) "><el-input-number v-model=" item . packageCount " :min=" 1 " controls-position=" right " /></el-form-item>
<el-form-item label=" 总毛重 T . K . GW " :prop=" ` items. ${ index } .grossWeightKg ` " required :rules=" positiveNumberRules ( '总毛重必须大于 0' ) "><el-input-number v-model=" item . grossWeightKg " :min=" 0 " :precision=" 3 " controls-position=" right " /></el-form-item>
<el-form-item label=" 总体积 T . CBM " :prop=" ` items. ${ index } .volumeCbm ` " required :rules=" positiveNumberRules ( '总体积必须大于 0' ) "><el-input-number v-model=" item . volumeCbm " :min=" 0 " :precision=" 4 " controls-position=" right " /></el-form-item>
<el-form-item label=" 总数量 T . QTY " :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=" 总净重 T . QNW "><el-input-number v-model=" item . netWeightKg " :min=" 0 " :precision=" 3 " controls-position=" right " /></el-form-item>
</div>
</section>
<section class=" cargo - field - section ">
<h4>报关与产品资料</h4>
<div class=" cargo - form - grid ">
<el-form-item label=" 报关 HS CODE "><el-input v-model=" item . hsCode " maxlength=" 20 " placeholder=" 报关 HS CODE " /></el-form-item>
<el-form-item label=" 清关 HS CODE "><el-input v-model=" item . customsHsCode " maxlength=" 20 " placeholder=" 清关 HS CODE " /></el-form-item>
<el-form-item label=" 清关申报单价 USD "><el-input-number v-model=" item . unitDeclaredValue " :min=" 0 " :precision=" 2 " controls-position=" right " /></el-form-item>
<el-form-item label=" 清关申报金额 USD "><el-input-number v-model=" item . declaredValue " :min=" 0 " :precision=" 2 " controls-position=" right " /></el-form-item>
<el-form-item label=" 中英文材质 MATERIAL "><el-input v-model=" item . material " maxlength=" 128 " placeholder=" 材质 / Material " /></el-form-item>
<el-form-item label=" 中英文用途 USE FOR "><el-input v-model=" item . usage " maxlength=" 128 " placeholder=" 用途 / Use for " /></el-form-item>
<el-form-item label=" 型号 MODEL "><el-input v-model=" item . model " maxlength=" 64 " placeholder=" 型号 " /></el-form-item>
<el-form-item label=" 品牌 LOGO "><el-input v-model=" item . brand " maxlength=" 64 " placeholder=" 品牌 " /></el-form-item>
<el-form-item label=" 产品规格 长 × 宽 × 高 " class=" product - dimensions - field "><div class=" dimension - inputs "><el-input-number v-model=" item . productLengthCm " :min=" 0 " :precision=" 2 " placeholder=" 长 cm " controls-position=" right " /><span>× </span><el-input-number v-model=" item . productWidthCm " :min=" 0 " :precision=" 2 " placeholder=" 宽 cm " controls-position=" right " /><span>× </span><el-input-number v-model=" item . productHeightCm " :min=" 0 " :precision=" 2 " placeholder=" 高 cm " controls-position=" right " /></div></el-form-item>
<el-form-item label=" 危险品 "><el-switch v-model=" item . dangerousGoods " /></el-form-item>
</div>
</section>
<section class=" cargo - field - section ">
<h4>货件与备注</h4>
<div class=" cargo - form - grid ">
<el-form-item label=" 货件编号 SHIPMENT ID " :prop=" ` items. ${ index } .shipmentId ` " :required=" form . destinationType === 'FBA' " :rules=" form . destinationType === 'FBA' ? requiredTextRules ( '亚马逊货物请输入 Shipment ID' ) : undefined "><el-input v-model=" item . shipmentId " maxlength=" 64 " placeholder=" SHIPMENT ID " /></el-form-item>
<el-form-item label=" 跟踪编号 REFERENCE ID " :prop=" ` items. ${ index } .referenceId ` " :required=" form . destinationType === 'FBA' " :rules=" form . destinationType === 'FBA' ? requiredTextRules ( '亚马逊货物请输入 Reference ID' ) : undefined "><el-input v-model=" item . referenceId " maxlength=" 64 " placeholder=" REFERENCE ID " /></el-form-item>
<el-form-item label=" 备注 " class=" cargo - remark - field "><el-input v-model=" item . remark " maxlength=" 500 " placeholder=" 选填 , 例如 : 注意防潮 " /></el-form-item>
</div>
</section>
</article>
<aside class=" price - estimate " aria-live=" polite ">
<div class=" price - estimate _ _heading "><div><strong>参考运费</strong><span>仅供参考,最终以实际结算为准</span></div><el-button v-if=" priceEstimateError " link type=" primary " @click=" loadPriceEstimate ">重新估价</el-button></div>
<div v-if=" priceEstimateLoading " class=" price - estimate _ _state ">正在匹配当前生效价格...</div>
<div v-else-if=" priceEstimate " class=" price - estimate _ _result "><strong>{{ priceEstimate.currency }} {{ Number(priceEstimate.estimatedAmount).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }) }}</strong><span>计费量 {{ priceEstimate.chargeableQuantity }} {{ priceEstimate.chargeUnit }} · 单价 {{ priceEstimate.currency }} {{ priceEstimate.unitPrice }}</span><small>{{ priceEstimate.disclaimer || '仅供参考,最终以实际结算为准' }}</small></div>
<div v-else-if=" priceEstimateError " class=" price - estimate _ _state is - error ">暂未取得参考价:{{ priceEstimateError }}。您仍可正常保存订单草稿。</div>
<div v-else class=" price - estimate _ _state ">选择收货仓、渠道和目的仓,并填写货物重量与体积后显示参考价。</div>
</aside>
< section class = "order-form-section prototype-section cost-estimate-section" aria -label = " 费用预估 " >
< div class = "order-section-heading" > < div > < h3 > 费用预估 < / h3 > < / div > < / div >
< div class = "cost-estimate-cards" >
< article class = "cost-estimate-card" >
< span class = "cost-estimate-badge is-fastest" > 最快 < / span >
< strong class = "cost-estimate-days" > 10 天 < / strong >
< div class = "cost-estimate-price" > < small > ¥ < / small > < strong > 3 , 310 < / strong > < / div >
< p > 优先配送 , 时效保障 < / p >
< / article >
< article class = "cost-estimate-card is-recommended" >
< span class = "cost-estimate-badge is-recommend" > 推荐 < / span >
< strong class = "cost-estimate-days" > 13 天 < / strong >
< div class = "cost-estimate-price" > < small > ¥ < / small > < strong > 2 , 990 < / strong > < / div >
< p > 性价比之选 < / p >
< / article >
< article class = "cost-estimate-card" >
< span class = "cost-estimate-badge is-economy" > 经济 < / span >
< strong class = "cost-estimate-days" > 16 天 < / strong >
< div class = "cost-estimate-price" > < small > ¥ < / small > < strong > 2 , 500 < / strong > < / div >
< p > 经济实惠 < / p >
< / article >
< / div >
< p class = "cost-estimate-note" > 以上费用为静态预估展示 , 实际费用以最终结算结果为准 。 < / 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" / > { { companySubmissionBlocked ? '提交前需完成资料审核' : '提交订单' } } < / el-button > < / div > < / template >
@@ -1444,63 +1536,132 @@ async function searchOrders(model: QueryModel) {
: global ( . first - leg - editor . el - drawer _ _header ) { flex : 0 0 auto ; min - height : 58 px ; margin - bottom : 0 ; padding : 17 px 22 px ; border - bottom : 1 px solid var ( -- border ) ; }
: global ( . first - leg - editor . el - drawer _ _body ) { min - height : 0 ; padding : 18 px 22 px 32 px ; overscroll - behavior : contain ; }
: global ( . first - leg - editor . el - drawer _ _footer ) { flex : 0 0 auto ; padding : 12 px 22 px ; border - top : 1 px solid var ( -- border ) ; background : # fff ; }
. order - form { width : min ( 100 % , 1000 px ) ; margin : 0 auto ; padding : 0 0 12 px ; }
. is - create - route { gap : 0 ; }
. is - create - route . first - leg - editor - page { width : 100 % ; }
. is - create - route . supporting - data - error , . is - create - route > . el - alert { margin - bottom : 14 px ; }
. is - create - route . prototype - order - form { width : 100 % ; margin : 0 ; padding : 0 0 16 px ; }
. is - create - route . prototype - section : not ( . cargo - section ) : not ( . cost - estimate - section ) : not ( . destination - section ) { width : min ( 100 % , 900 px ) ; }
. order - form { width : min ( 100 % , 1000 px ) ; margin : 0 ; padding : 0 0 12 px ; }
. order - form : deep ( . el - form - item _ _error ) { position : static ; padding - top : 6 px ; overflow - wrap : anywhere ; line - height : 1.45 ; }
. order - form - section + . order - form - section { margin - top : 24 px ; padding - top : 22 px ; border - top : 1 px solid var ( -- border ) ; }
. order - form - section + . order - form - section { margin - top : 30 px ; padding - top : 28 px ; border - top : 1 px solid # edf0f5 ; }
. 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 : 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 ) ; }
. prototype - section { padding : 4 px 0 0 ; }
. segmented - options { display : flex ; width : min ( 100 % , 560 px ) ; overflow : hidden ; border : 1 px solid # e3e8f0 ; border - radius : 5 px ; background : # fff ; }
. segmented - options : deep ( . el - radio - button ) { flex : 1 ; }
. segmented - options : deep ( . el - radio - button _ _inner ) { display : flex ; width : 100 % ; min - height : 42 px ; box - sizing : border - box ; align - items : center ; justify - content : center ; padding : 11 px 16 px ; }
. warehouse - options { display : flex ; flex - wrap : wrap ; gap : 8 px ; }
. warehouse - options : deep ( . el - radio - button _ _inner ) { display : flex ; min - height : 40 px ; box - sizing : border - box ; align - items : center ; justify - content : center ; 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 ; align - items : flex - start ; gap : 10 px ; margin - top : 0 ; padding : 11 px 13 px ; border : 1 px solid # d9def8 ; background : # f6f7fe ; 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 : 2 px ; }
. selected - warehouse span , . selected - warehouse small { color : var ( -- muted ) ; font - size : 11 px ; line - height : 1.5 ; }
. order - section - heading { display : flex ; align - items : flex - start ; justify - content : space - between ; gap : 12 px ; margin - bottom : 12 px ; }
. segmented - options : deep ( . el - radio - button _ _inner ) { display : flex ; width : 100 % ; min - height : 42 px ; box - sizing : border - box ; align - items : center ; justify - content : center ; padding : 10 px 16 px ; border : 0 ; border - right : 1 px solid # e3e8f0 ; border - radius : 0 ; box - shadow : none ; color : # 526070 ; font - size : 13 px ; }
. segmented - options : deep ( . el - radio - button : last - child . el - radio - button _ _inner ) { border - right : 0 ; }
. segmented - options : deep ( . el - radio - button _ _original - radio : checked + . el - radio - button _ _inner ) { border - color : var ( -- primary ) ; background : var ( -- primary ) ; box - shadow : none ; color : # fff ; }
. warehouse - options { display : flex ; width : 100 % ; align - items : center ; flex - wrap : wrap ; gap : 10 px 22 px ; padding - top : 9 px ; }
. warehouse - options : deep ( . el - radio ) { height : auto ; margin - right : 0 ; color : # 4 d5a6c ; }
. warehouse - options : deep ( . el - radio _ _label ) { padding - left : 7 px ; font - size : 13 px ; }
. order - section - heading { display : flex ; align - items : flex - start ; justify - content : space - between ; gap : 12 px ; margin - bottom : 18 px ; }
. order - section - heading h3 { margin - bottom : 5 px ; }
. order - section - heading p { margin : 0 ; color : var ( -- muted ) ; font - size : 11 px ; }
. prototype - form - flow { display : flex ; max - width : 900 px ; flex - direction : column ; padding - top : 4 px ; }
. prototype - form - row { display : grid ; width : 100 % ; grid - template - columns : 140 px minmax ( 0 , 560 px ) ; align - items : start ; margin - bottom : 18 px ; }
. prototype - form - row -- wide { grid - template - columns : 140 px minmax ( 0 , 720 px ) ; }
. prototype - form - row -- compact { grid - template - columns : 140 px minmax ( 0 , 420 px ) ; }
. prototype - form - row -- short { grid - template - columns : 140 px minmax ( 0 , 280 px ) ; }
. prototype - form - row -- tabs { grid - template - columns : 140 px minmax ( 0 , 520 px ) ; margin - bottom : 22 px ; }
. prototype - form - row : deep ( . el - form - item _ _label ) { width : auto ! important ; min - height : 40 px ; justify - content : flex - end ; padding : 0 20 px 0 0 ; line - height : 40 px ; }
. order - section - heading p { margin : 0 ; color : # 8 b96a6 ; font - size : 12 px ; }
. prototype - form - flow { display : flex ; max - width : 860 px ; flex - direction : column ; padding - top : 2 px ; }
. prototype - form - row { display : grid ; width : 100 % ; grid - template - columns : 114 px minmax ( 0 , 560 px ) ; align - items : start ; margin - bottom : 16 px ; }
. prototype - form - row -- wide { grid - template - columns : 114 px minmax ( 0 , 760 px ) ; }
. prototype - form - row -- compact { grid - template - columns : 114 px minmax ( 0 , 420 px ) ; }
. prototype - form - row -- short { grid - template - columns : 114 px minmax ( 0 , 280 px ) ; }
. prototype - form - row -- tabs { grid - template - columns : 114 px minmax ( 0 , 560 px ) ; margin - bottom : 18 px ; }
. prototype - form - row : deep ( . el - form - item _ _label ) { width : auto ! important ; min - height : 40 px ; justify - content : flex - end ; padding : 0 18 px 0 0 ; line - height : 40 px ; color : # 4 d5a6c ; font - size : 13 px ; }
. prototype - form - row : deep ( . el - form - item _ _content ) { display : block ; min - width : 0 ; line - height : normal ; }
. prototype - form - row : deep ( . el - input _ _wrapper ) , . prototype - form - row : deep ( . el - select _ _wrapper ) { min - height : 40 px ; }
. prototype - form - row : deep ( . el - input _ _wrapper ) , . prototype - form - row : deep ( . el - select _ _wrapper ) { min - height : 40 px ; box - shadow : 0 0 0 1 px # dfe5ed inset ; }
. address - book - control { display : grid ; grid - template - columns : minmax ( 260 px , 1 fr ) auto auto ; gap : 10 px ; align - items : center ; }
. address - book - control : deep ( . el - button ) { min - height : 40 px ; margin - left : 0 ; }
. field - guidance { display : block ; margin - top : 5 px ; color : var ( -- muted ) ; line - height : 1.45 ; }
. field - guidance -- error { color : # b54708 ; }
. price - estimate { margin - top : 16 px ; padding : 15 px 16 px ; border : 1 px solid # cbd5da ; background : # f7faf9 ; }
. price - estimate _ _heading { display : flex ; align - items : flex - start ; justify - content : space - between ; gap : 12 px ; }
. price - estimate _ _heading > div { display : flex ; min - width : 0 ; flex - direction : column ; gap : 3 px ; }
. price - estimate _ _heading strong { color : var ( -- navy ) ; font - size : 13 px ; }
. price - estimate _ _heading span , . price - estimate _ _state , . price - estimate _ _result span , . price - estimate _ _result small { color : var ( -- muted ) ; font - size : 11 px ; line - height : 1.5 ; }
. price - estimate _ _state { padding - top : 12 px ; }
. price - estimate _ _state . is - error { color : # b54708 ; }
. price - estimate _ _result { display : grid ; grid - template - columns : auto minmax ( 0 , 1 fr ) ; align - items : baseline ; gap : 5 px 14 px ; padding - top : 12 px ; }
. price - estimate _ _result > strong { color : var ( -- primary ) ; font - size : 20 px ; }
. price - estimate _ _result > small { grid - column : 1 / - 1 ; }
. cost - estimate - section { width : 100 % ; }
. cost - estimate - section . order - section - heading { margin - bottom : 12 px ; }
. cost - estimate - cards { display : grid ; grid - template - columns : repeat ( 3 , minmax ( 0 , 1 fr ) ) ; gap : 14 px ; }
. cost - estimate - card { position : relative ; display : flex ; min - height : 158 px ; box - sizing : border - box ; align - items : center ; justify - content : center ; flex - direction : column ; padding : 24 px 18 px 18 px ; border : 1 px solid # dde4f2 ; border - radius : 8 px ; background : # fff ; box - shadow : 0 2 px 8 px rgba ( 15 , 31 , 58 , .025 ) ; text - align : center ; }
. cost - estimate - card . is - recommended { border : 2 px solid # 637 fec ; background : # f8f8ff ; box - shadow : 0 8 px 22 px rgba ( 99 , 127 , 236 , .13 ) ; }
. cost - estimate - badge { position : absolute ; top : - 9 px ; right : 14 px ; padding : 3 px 11 px ; border - radius : 12 px ; color : # fff ; font - size : 10 px ; font - weight : 650 ; line - height : 1.4 ; }
. cost - estimate - badge . is - fastest , . cost - estimate - badge . is - recommend { background : # 5368 c6 ; }
. cost - estimate - badge . is - economy { background : # 71858 e ; }
. cost - estimate - days { color : # 30343 b ; font - size : 21 px ; }
. cost - estimate - price { display : flex ; align - items : baseline ; gap : 5 px ; margin - top : 13 px ; color : # 637 fec ; }
. cost - estimate - price small { font - size : 15 px ; font - weight : 700 ; }
. cost - estimate - price strong { font - size : 29 px ; line - height : 1 ; }
. cost - estimate - card p { margin : 12 px 0 0 ; color : # 98 a0ad ; font - size : 11 px ; }
. cost - estimate - note { margin : 10 px 0 0 ; color : # 98 a0ad ; font - size : 10 px ; text - align : right ; }
. selected - destination { display : flex ; width : min ( 100 % , 720 px ) ; align - items : flex - start ; gap : 9 px ; margin : - 4 px 0 18 px 140 px ; padding : 10 px 12 px ; background : # f2f7f6 ; color : var ( -- primary ) ; }
. selected - destination svg { flex : 0 0 17 px ; margin - top : 2 px ; }
. selected - destination div { display : flex ; min - width : 0 ; flex - direction : column ; gap : 2 px ; }
. selected - destination span { color : var ( -- navy ) ; font - size : 12 px ; font - weight : 600 ; }
. selected - destination small { color : var ( -- muted ) ; font - size : 11 px ; line - height : 1.5 ; }
. destination - section { width : 100 % ; }
. destination - section - heading { margin - bottom : 12 px ; padding - bottom : 12 px ; border - bottom : 1 px solid # edf0f5 ; }
. destination - section - heading h3 { margin - bottom : 0 ; }
. destination - form - flow { width : min ( 100 % , 900 px ) ; padding - left : 0 ; }
. destination - type - row { grid - template - columns : 114 px minmax ( 0 , 300 px ) ; margin - bottom : 16 px ; }
. destination - type - options { width : 300 px ; }
. destination - type - options : deep ( . el - radio - button _ _inner ) { min - height : 36 px ; padding : 8 px 14 px ; font - size : 12 px ; }
. destination - form - flow > . prototype - form - row : not ( . destination - type - row ) : not ( . prototype - form - row -- wide ) : not ( . prototype - form - row -- compact ) : not ( . prototype - form - row -- short ) { grid - template - columns : 114 px minmax ( 0 , 360 px ) ; }
. flow - divider { width : min ( 100 % , 860 px ) ; margin : 4 px 0 22 px ; border - top : 1 px solid var ( -- border ) ; }
. compact - transport - grid { margin - top : 18 px ; }
. cargo - entry { padding : 16 px ; border : 1 px solid var ( -- border ) ; background : # fbfcfc ; }
. compact - transport - grid { width : min ( 100 % , 820 px ) ; grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr ) ) ; gap : 0 28 px ; margin : 20 px 0 0 114 px ; }
. compact - transport - grid : deep ( . el - form - item _ _label ) { color : # 4 d5a6c ; font - size : 13 px ; }
. shipping - form - flow { max - width : 900 px ; }
. prototype - order - form > . prototype - section : first - child { width : 100 % ; }
. shipping - form - flow . prototype - form - row { margin - bottom : 10 px ; }
. shipping - form - flow . prototype - form - row -- tabs { margin - bottom : 12 px ; }
. compact - transport - grid { display : grid ; width : 100 % ; grid - template - columns : 1 fr ; gap : 0 ; margin : 6 px 0 0 ; }
. compact - transport - grid > : deep ( . el - form - item ) { display : grid ; width : 100 % ; grid - template - columns : 114 px minmax ( 0 , 560 px ) ; align - items : start ; margin - bottom : 10 px ; }
. compact - transport - grid > : deep ( . el - form - item : nth - child ( 2 ) ) { display : none ; }
. compact - transport - grid > : deep ( . el - form - item : nth - child ( 1 ) ) { grid - template - columns : 114 px minmax ( 0 , 360 px ) ; }
. compact - transport - grid > : deep ( . el - form - item : nth - child ( 6 ) ) { grid - template - columns : 114 px minmax ( 0 , 360 px ) ; }
. compact - transport - grid > : deep ( . el - form - item : nth - child ( 7 ) ) { grid - template - columns : 114 px minmax ( 0 , 620 px ) ; margin - top : 2 px ; margin - bottom : 0 ; }
. compact - transport - grid > : deep ( . el - form - item _ _label ) { width : auto ! important ; min - height : 40 px ; justify - content : flex - end ; padding : 0 18 px 0 0 ; line - height : 40 px ; }
. compact - transport - grid > : deep ( . el - form - item _ _content ) { display : block ; min - width : 0 ; line - height : normal ; }
. compact - transport - grid > : deep ( . el - form - item . el - input _ _wrapper ) , . compact - transport - grid > : deep ( . el - form - item . el - select _ _wrapper ) { min - height : 40 px ; }
. cargo - section { min - width : 0 ; padding - bottom : 4 px ; }
. cargo - table - heading { margin - bottom : 8 px ; }
. cargo - table - toolbar { display : flex ; align - items : center ; justify - content : space - between ; gap : 16 px ; margin - bottom : 10 px ; }
. cargo - import - actions { display : flex ; align - items : center ; gap : 10 px ; }
. cargo - import - input { position : absolute ; width : 1 px ; height : 1 px ; overflow : hidden ; opacity : 0 ; pointer - events : none ; }
. cargo - import - actions : deep ( . el - button ) { min - height : 38 px ; margin - left : 0 ; padding : 8 px 17 px ; }
. cargo - import - actions . cargo - upload - button { border - style : dashed ; border - width : 2 px ; border - color : # b8c5ff ; color : # 637 fec ; }
. cargo - import - actions . cargo - upload - button : hover , . cargo - import - actions . cargo - upload - button : focus - visible { border - color : var ( -- primary ) ; background : # f5f7ff ; color : var ( -- primary ) ; }
. cargo - import - actions . cargo - download - button { color : # 4 d5a6c ; }
. cargo - import - actions . cargo - download - button : deep ( svg ) { color : # 09 a8d8 ; }
. cargo - table - actions { display : flex ; align - items : center ; gap : 8 px ; }
. cargo - table - actions : deep ( . el - button ) { margin - left : 0 ; }
. cargo - table - wrap { width : 100 % ; overflow - x : auto ; border : 1 px solid # e6eaf0 ; border - radius : 6 px ; background : # fff ; scrollbar - color : # c8ced8 # f1f3f6 ; scrollbar - width : thin ; }
. cargo - edit - table { width : max - content ; min - width : 100 % ; border - collapse : collapse ; table - layout : fixed ; color : # 4 d5a6c ; font - size : 12 px ; }
. cargo - edit - table th { height : 44 px ; padding : 7 px 8 px ; border - bottom : 1 px solid # e6eaf0 ; background : # f4f5fb ; color : # 637 fec ; font - size : 11 px ; font - weight : 650 ; text - align : left ; white - space : nowrap ; }
. cargo - edit - table th small { margin - left : 3 px ; color : # e8a20c ; font - size : 9 px ; font - weight : 600 ; }
. cargo - edit - table td { width : 130 px ; height : 58 px ; padding : 7 px 6 px ; border - bottom : 1 px solid # edf0f5 ; background : # fff ; vertical - align : top ; }
. cargo - edit - table tbody tr : last - child td { border - bottom : 0 ; }
. cargo - edit - table : deep ( . el - form - item ) { margin - bottom : 0 ; }
. cargo - edit - table : deep ( . el - form - item _ _content ) { display : block ; line - height : normal ; }
. cargo - edit - table : deep ( . el - form - item _ _error ) { width : 100 % ; padding : 3 px 2 px 0 ; font - size : 9 px ; white - space : normal ; }
. cargo - edit - table : deep ( . el - input ) , . cargo - edit - table : deep ( . el - select ) , . cargo - edit - table : deep ( . el - input - number ) { width : 100 % ; }
. cargo - edit - table : deep ( . el - input _ _wrapper ) , . cargo - edit - table : deep ( . el - select _ _wrapper ) { min - height : 34 px ; padding : 1 px 8 px ; box - shadow : none ; }
. cargo - edit - table : deep ( . el - input _ _wrapper : hover ) , . cargo - edit - table : deep ( . el - select _ _wrapper : hover ) { box - shadow : 0 0 0 1 px # b9c8f5 inset ; }
. cargo - edit - table : deep ( . el - input _ _inner ) { font - size : 11 px ; text - align : left ; }
. cargo - check - cell , . cargo - index - cell { width : 38 px ! important ; text - align : center ! important ; vertical - align : middle ! important ; }
. cargo - image - column { width : 62 px ! important ; text - align : center ! important ; }
. cargo - number - column { width : 92 px ! important ; }
. cargo - dimensions - column { width : 240 px ! important ; }
. cargo - operation - column { width : 76 px ! important ; text - align : center ! important ; }
. cargo - danger - cell { text - align : center ; vertical - align : middle ! important ; }
. cargo - table - image { position : relative ; display : flex ; width : 34 px ; height : 34 px ; align - items : center ; justify - content : center ; margin : 0 auto ; border : 1 px dashed # d6dce5 ; border - radius : 4 px ; color : # 788493 ; }
. cargo - table - image img { width : 100 % ; height : 100 % ; border - radius : 3 px ; object - fit : contain ; }
. cargo - table - image : deep ( . el - button ) { width : 30 px ; height : 30 px ; padding : 0 ; }
. cargo - table - image . cargo - image - remove { position : absolute ; top : - 9 px ; right : - 9 px ; width : 19 px ; height : 19 px ; border : 1 px solid # ffd7d7 ; border - radius : 50 % ; background : # fff ; }
. cargo - table - dimensions { display : grid ; grid - template - columns : minmax ( 0 , 1 fr ) auto minmax ( 0 , 1 fr ) auto minmax ( 0 , 1 fr ) ; gap : 3 px ; align - items : center ; }
. cargo - table - dimensions > span { color : # 9 aa4b2 ; }
. cargo - table - hint { display : flex ; align - items : center ; flex - wrap : wrap ; gap : 9 px ; padding : 10 px 2 px 0 ; color : # 8 b96a6 ; font - size : 11 px ; }
. cargo - table - hint : : before { color : # e5a512 ; content : '●' ; font - size : 8 px ; }
. cargo - table - hint i { width : 3 px ; height : 3 px ; border - radius : 50 % ; background : # aab2bd ; }
. cargo - entry { padding : 18 px 20 px ; border : 1 px solid # e2e7ee ; background : # fff ; box - shadow : 0 2 px 8 px rgba ( 15 , 31 , 58 , .025 ) ; }
. 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 - field - section + . cargo - field - section { margin - top : 14 px ; padding - top : 14 px ; border - top : 1 px solid var ( -- border ) ; }
. cargo - field - section h4 { margin : 0 0 12 px ; color : var ( -- navy ) ; font - size : 12 px ; }
. cargo - entry > header { display : flex ; align - items : center ; justify - content : space - between ; gap : 12 px ; margin - bottom : 16 px ; }
. cargo - field - section + . cargo - field - section { margin - top : 18 px ; padding - top : 16 px ; border - top : 1 px solid # edf0f5 ; }
. cargo - field - section h4 { margin : 0 0 14 px ; color : # 516071 ; font - size : 12 px ; }
. cargo - form - grid { display : grid ; grid - template - columns : repeat ( 4 , minmax ( 0 , 1 fr ) ) ; column - gap : 12 px ; }
. cargo - form - grid -- basic { grid - template - columns : repeat ( 3 , minmax ( 0 , 1 fr ) ) ; }
. product - image - field { grid - row : span 2 ; }
@@ -1620,10 +1781,14 @@ async function searchOrders(model: QueryModel) {
. address - book - control : deep ( . el - select ) { grid - column : 1 / - 1 ; }
. 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 ; width : 100 % ; grid - template - columns : 1 fr ; }
. warehouse - options : deep ( . el - radio - button ) , . warehouse - options : deep ( . el - radio - button _ _inner ) { width : 100 % ; }
. price - estimate _ _result { grid - template - columns : 1 fr ; }
. price - estimate _ _result > small { grid - column : auto ; }
. warehouse - options { width : 100 % ; gap : 12 px 18 px ; }
. compact - transport - grid > : deep ( . el - form - item ) , . compact - transport - grid > : deep ( . el - form - item : nth - child ( 7 ) ) { display : block ; margin - bottom : 14 px ; }
. compact - transport - grid > : deep ( . el - form - item : nth - child ( 2 ) ) { display : none ; }
. compact - transport - grid > : deep ( . el - form - item _ _label ) { min - height : auto ; justify - content : flex - start ; padding : 0 0 8 px ; line - height : 1.4 ; }
. cargo - table - toolbar { align - items : stretch ; flex - direction : column ; }
. cargo - import - actions , . cargo - table - actions { flex - wrap : wrap ; }
. cost - estimate - cards { grid - template - columns : 1 fr ; }
. cost - estimate - card { min - height : 140 px ; }
: global ( . first - leg - editor . el - drawer _ _header ) { min - height : 56 px ; padding : 15 px 20 px ; }
: global ( . first - leg - editor . el - drawer _ _body ) { padding : 16 px 20 px 28 px ; }
: global ( . first - leg - editor . el - drawer _ _footer ) { padding : 10 px 20 px ; }