@@ -1,8 +1,8 @@
< script setup lang = "ts" >
import { computed , nextTick , onBeforeUnmount , onMounted , reactive , ref , watch } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage , ElMessageBox , type FormInstance , type FormItemRule , type FormRules } from 'element-plus'
import { AlertTriangle , Box , Eye , FilePlus2 , MapPin , PackageCheck , Pencil , Plus , Send , Trash2 } from '@lucide/vue'
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 QueryToolbar , { type QueryField , type QueryModel } from '@/components/common/QueryToolbar.vue'
import DataTable , { type DataTableColumn } from '@/components/common/DataTable.vue'
@@ -13,7 +13,9 @@ import OrderProgressPanel from '@/modules/first-leg/components/OrderProgressPane
import { getMerchantInboundBilling } from '@/modules/first-leg/api/inboundBilling'
import {
estimateMerchantFirstLegPrice ,
downloadMerchantProductImage ,
getMerchantPricingChannels ,
uploadMerchantProductImage ,
type MerchantPriceEstimate ,
type MerchantPriceEstimateRequest ,
type MerchantPricingChannel ,
@@ -23,20 +25,18 @@ import { useTenantAdministrationStore } from '@/modules/tenant-management/public
import { useAuthStore } from '@/modules/auth/public'
import { useUnsavedWorkRegistration } from '@/navigation/workspace-tabs/unsavedWork'
import type { FirstLegPartyRole , FirstLegPartySnapshot } from '@/modules/first-leg/data/firstLegParties'
import type { CargoBoxSizeType , FirstLegCargoItem , FirstLegOrder , FirstLegOrderDraft } from '@/modules/first-leg/types/firstLegOrder'
import type { FirstLegCargoItem , FirstLegOrder , FirstLegOrderDraft } from '@/modules/first-leg/types/firstLegOrder'
import type { MerchantInboundBilling } from '@/modules/first-leg/types/inboundBilling'
import {
canClientEditFirstLegOrder ,
applyCargoBoxSize ,
CARGO _BOX _SIZES ,
createEmptyCargoItem ,
getFirstLegStatusLabel ,
getFirstLegStatusTone ,
getFirstLegTransportModeLabel ,
summarizeFirstLegCargo ,
updateCargoBoxVolume ,
validateFirstLegOrderDraft ,
} from '@/modules/first-leg/utils/firstLegOrder'
import { validateProductImageFile } from '@/modules/first-leg/utils/productImage'
const store = useFirstLegOrdersStore ( )
const tenantStore = useTenantAdministrationStore ( )
@@ -402,6 +402,8 @@ onBeforeUnmount(() => {
channelRequestSequence += 1
estimateRequestSequence += 1
if ( estimateTimer ) clearTimeout ( estimateTimer )
releaseProductImagePreviews ( form . items )
if ( detailOrder . value ) releaseProductImagePreviews ( detailOrder . value . items )
} )
function markEditorClean ( ) {
@@ -422,10 +424,65 @@ function cloneDraft(draft: FirstLegOrderDraft): FirstLegOrderDraft {
}
function replaceForm ( draft : FirstLegOrderDraft ) {
releaseProductImagePreviews ( form . items )
Object . assign ( form , cloneDraft ( draft ) )
markEditorClean ( )
}
function releaseProductImagePreviews ( items : FirstLegCargoItem [ ] ) {
items . forEach ( ( item ) => {
if ( item . productImagePreviewUrl . startsWith ( 'blob:' ) ) URL . revokeObjectURL ( item . productImagePreviewUrl )
item . productImagePreviewUrl = ''
} )
}
async function ensureProductImagePreview ( item : FirstLegCargoItem ) {
if ( ! item . productImageFileId || item . productImagePreviewUrl || item . productImageUploading ) return
item . productImageUploading = true
item . productImageError = ''
try {
const blob = await downloadMerchantProductImage ( item . productImageFileId )
item . productImagePreviewUrl = URL . createObjectURL ( blob )
} catch ( error ) {
item . productImageError = error instanceof Error ? error . message : '产品图片加载失败'
} finally {
item . productImageUploading = false
}
}
async function uploadCargoProductImage ( item : FirstLegCargoItem , options : UploadRequestOptions ) {
const file = options . file
const validationError = validateProductImageFile ( file )
if ( validationError ) {
item . productImageError = validationError
ElMessage . warning ( validationError )
throw new Error ( validationError )
}
item . productImageUploading = true
item . productImageError = ''
try {
const result = await uploadMerchantProductImage ( file )
if ( item . productImagePreviewUrl . startsWith ( 'blob:' ) ) URL . revokeObjectURL ( item . productImagePreviewUrl )
item . productImageFileId = String ( result . fileId )
item . productImageName = result . fileName
item . productImagePreviewUrl = URL . createObjectURL ( file )
} catch ( error ) {
item . productImageError = error instanceof Error ? error . message : '产品图片上传失败'
ElMessage . error ( item . productImageError )
throw error
} finally {
item . productImageUploading = false
}
}
function removeCargoProductImage ( item : FirstLegCargoItem ) {
if ( item . productImagePreviewUrl . startsWith ( 'blob:' ) ) URL . revokeObjectURL ( item . productImagePreviewUrl )
item . productImageFileId = ''
item . productImageName = ''
item . productImagePreviewUrl = ''
item . productImageError = ''
}
function draftFromOrder ( order : FirstLegOrder ) : FirstLegOrderDraft {
return {
referenceNo : order . referenceNo ,
@@ -456,7 +513,12 @@ function draftFromOrder(order: FirstLegOrder): FirstLegOrderDraft {
destinationType : order . destinationType ,
amazonWarehouseCode : order . amazonWarehouseCode ,
shipmentId : order . shipmentId ,
items : order . items . map ( ( item ) => ( { ... item } ) ) ,
items : order . items . map ( ( item ) => ( {
... item ,
productImagePreviewUrl : '' ,
productImageUploading : false ,
productImageError : '' ,
} ) ) ,
}
}
@@ -755,6 +817,7 @@ async function openEdit(order: FirstLegOrder) {
}
editingId . value = detailedOrder . id
replaceForm ( draft )
await Promise . all ( form . items . map ( ensureProductImagePreview ) )
if ( warehouseAvailable ) void loadSelectedWarehouse ( detailedOrder . receivingWarehouseId )
detailOpen . value = false
editorOpen . value = true
@@ -773,7 +836,8 @@ async function openDetail(order: FirstLegOrder) {
detailError . value = ''
void loadInboundBilling ( order )
try {
await store . loadDetail ( order . id )
const detailedOrder = await store . loadDetail ( order . id )
await Promise . all ( detailedOrder . items . map ( ensureProductImagePreview ) )
} catch ( error ) {
detailError . value = error instanceof Error ? error . message : '订单详情加载失败'
ElMessage . error ( detailError . value )
@@ -873,10 +937,6 @@ function addCargoItem() {
resetEditorValidation ( )
}
function changeCargoBoxSize ( item : FirstLegCargoItem , type : CargoBoxSizeType ) {
applyCargoBoxSize ( item , type )
}
function removeCargoItem ( index : number ) {
if ( form . items . length === 1 ) {
ElMessage . warning ( '订单至少需要保留一条货物明细' )
@@ -1116,37 +1176,81 @@ async function searchOrders(model: QueryModel) {
< el-form-item label = "收件地址" prop = "consigneePartySnapshot.address" required :rules = "requiredTextRules('请输入本单实际收货地址')" class = "prototype-form-row prototype-form-row--wide" > < el-input v-model = "form.consigneePartySnapshot.address" maxlength="300" placeholder="请输入收件详细地址" / > < / el-form-item >
< el-form-item label = "邮编" class = "prototype-form-row prototype-form-row--short" > < el-input v-model = "form.consigneePartySnapshot.postalCode" maxlength="20" placeholder="请输入邮编" / > < / el-form-item >
< / template >
< el-form-item v-if = "form.destinationType === 'FBA'" label="货件编号" prop="shipmentId" required :rules="requiredTextRules('请输入 Shipment ID')" class="prototype-form-row prototype-form-row--compact"><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" class = "prototype-form-row prototype-form-row--compact" > < el-input v-model = "form.referenceNo" maxlength="100" :placeholder="form.destinationType === 'FBA' ? 'Reference ID' : '选填,贵司内部跟踪编号'" / > < / el-form-item >
< / div >
< / section >
< 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 >
< 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 >
< 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 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 = "总数量" :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}.boxSizeType`" required :rules = "requiredSelectRules('请选择箱规')" > < el-select v-model = "item.boxSizeType" clearable placeholder="请选择固定箱规" @change="changeCargoBoxSize(item, $event as CargoBoxSizeType)"><el-option v-for="box in CARGO_BOX_SIZES" :key="box.code" :value="box.code" :label="`${box.label} · ${box.lengthCm} × ${box.widthCm} × ${box.heightCm} cm`" / > < / el-select > < / el-form-item >
< el-form-item label = "箱数" > < el-input-number v-model = "item.packageCount" :min="1" controls-position="right" @change="updateCargoBoxVolume(item) " / > < / 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" :m in="0" :precision="3" controls-position="right" disabled / > < / 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', '45HQ']" :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 >
< 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 :load ing ="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>
<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>
<section v-if=" form . transportMode === 'FCL' " class=" cargo - field - section ">
<h4>整柜信息</h4>
<div class=" cargo - form - grid ">
<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=" 1 " controls-position=" right " /></el-form-item>
</div>
</section>
</article>
< p class = "cargo-tip" > FBA 货物必须填写 SKU 、 UPC 、 货件编号和跟踪编号 ; 提交前请核对重量与体积 。 < / p >
<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>
@@ -1213,7 +1317,7 @@ async function searchOrders(model: QueryModel) {
<div v-if=" detailOrder . expressCompanyName || detailOrder . trackingNumber " class=" express - information "><PackageCheck :size=" 20 " /><div><span>快递公司</span><strong>{{ detailOrder.expressCompanyName || '待平台补充' }}</strong></div><div><span>快递单号</span><strong>{{ detailOrder.trackingNumber || '待平台补充' }}</strong></div></div>
<div v-else class=" express - empty "><PackageCheck :size=" 20 " /><div><strong>平台尚未维护快递信息</strong><span>快递公司和快递单号由平台后台维护,客户端仅只读展示。</span></div></div>
</section>
< section class = "detail-section" > < h3 > 货物明细 < / h3 > < div v-if = "detailSummary" class="cargo-summary "><div><Box :size="18 " /><span>明细</span><strong>{{ detailOrder.items.length }} 项</strong></div><div><span>件数</span><strong>{{ detailSummary.pieces }}</strong></div><div><span>毛重</span><strong>{{ detailSummary.grossWeightKg.toLocaleString() }} kg</strong></div><div><span>体积</span><strong>{{ detailSummary.volumeCbm }} m³</strong></div></div><div v-if="detailOrder.items.length" class="cargo-detail-list"><article v-for="(item, index) in detailOrder.items " :key="item.id "><header><span>货物 {{ index + 1 }}</span><strong>{{ item.cargoNameCn || '未命名货物' }}</strong><small>{{ item.cargoNameEn || '' }}</small></header><dl><div><dt>SKU / UPC</dt><dd>{{ item.sku || '-' }} / {{ item.upc || '-' }}</dd></div><div><dt>HS 编码</dt><dd>{{ item.hsCode || '-' }}</dd></div><div><dt>包装 / 箱数</dt><dd>{{ packageTypeLabels[item.packageType] || item.packageType || '-' }} · {{ item.packageCount }} 箱</dd></div><div><dt>数量</dt><dd>{{ item.pieces }} 件</dd></div><div><dt>毛重</dt><dd>{{ item.grossWeightKg }} kg</dd></div><div><dt>体积</dt><dd>{{ item.volumeCbm }} m³</dd></div><div><dt>申报价值</dt><dd>{{ item.declaredValue ? `${item.currency} ${item.declaredValue.toLocaleString()}` : '-' }}</dd></div><div><dt>危险品</dt><dd>{{ item.dangerousGoods ? '是' : '否' }}</dd></div></dl><p v-if="item.remark ">{{ item.remark }}</p></article></div><el-empty v-else description="暂无货物明细" :image-size="56" / > < / section >
<section class=" detail - section "><h3>货物明细</h3><div v-if=" detailSummary " class=" cargo - summary "><div><Box :size=" 18 " /><span>明细</span><strong>{{ detailOrder.items.length }} 项</strong></div><div><span>件数</span><strong>{{ detailSummary.pieces }}</strong></div><div><span>毛重</span><strong>{{ detailSummary.grossWeightKg.toLocaleString() }} kg</strong></div><div><span>体积</span><strong>{{ detailSummary.volumeCbm }} m³</strong></div></div><div v-if=" detailOrder . items . length " class=" cargo - detail - list "><article v-for=" ( item , index ) in detailOrder . items " :key=" item . id "><header><span>货物 {{ index + 1 }}</span><strong>{{ item.cargoNameCn || '未命名货物' }}</strong><small>{{ item.cargoNameEn || '' }}</small></header><div v-if=" item . productImageFileId " class=" cargo - detail - image "><img v-if=" item . productImagePreviewUrl " :src=" item . productImagePreviewUrl " :alt=" ` ${ item . cargoNameCn || ` 货物 ${ index + 1 } ` } 产品图片 ` " /><span v-else-if=" item . productImageUploading ">图片加载中...</span><span v-else>{{ item.productImageError || '产品图片暂不可用' }}</span></div>< dl><div><dt>SKU / UPC</dt><dd>{{ item.sku || '-' }} / {{ item.upc || '-' }}</dd></div><div><dt>HS 编码</dt><dd>{{ item.hsCode || '-' }}</dd></div><div><dt>包装 / 箱数</dt><dd>{{ packageTypeLabels[item.packageType] || item.packageType || '-' }} · {{ item.packageCount }} 箱</dd></div><div><dt>数量</dt><dd>{{ item.pieces }} 件</dd></div><div><dt>毛重</dt><dd>{{ item.grossWeightKg }} kg</dd></div><div><dt>体积</dt><dd>{{ item.volumeCbm }} m³</dd></div><div><dt>申报价值</dt><dd>{{ item.declaredValue ? `${item.currency} ${item.declaredValue.toLocaleString()}` : '-' }}</dd></div><div><dt>危险品</dt><dd>{{ item.dangerousGoods ? '是' : '否' }}</dd></div></dl><p v-if="item . remark ">{{ item.remark }}</p></article></div><el-empty v-else description=" 暂无货物明细 " :image-size=" 56 " /></section>
<OrderProgressPanel :order=" detailOrder " />
<InboundBillingPanel
:data=" inboundBilling "
@@ -1274,7 +1378,7 @@ 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 - primary - grid > * , . cargo - secondary - grid > * { min - width : 0 ; }
. 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 ; }
@@ -1324,12 +1428,23 @@ async function searchOrders(model: QueryModel) {
. 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 ; }
. 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 - 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 ; }
. product - image - control { display : grid ; grid - template - columns : 78 px minmax ( 0 , 1 fr ) ; align - items : center ; gap : 7 px 10 px ; width : 100 % ; }
. product - image - preview , . product - image - placeholder { position : relative ; display : flex ; width : 78 px ; height : 78 px ; grid - row : 1 / span 3 ; align - items : center ; justify - content : center ; overflow : hidden ; border : 1 px dashed # cbd5da ; background : # fff ; color : var ( -- muted ) ; }
. product - image - preview img { width : 100 % ; height : 100 % ; object - fit : contain ; }
. product - image - preview : deep ( . el - button ) { position : absolute ; top : 4 px ; right : 4 px ; width : 26 px ; height : 26 px ; }
. product - image - placeholder { flex - direction : column ; gap : 5 px ; font - size : 10 px ; }
. product - image - control small { color : var ( -- muted ) ; font - size : 10 px ; line - height : 1.4 ; }
. product - image - error { grid - column : 2 ; overflow - wrap : anywhere ; color : # b42318 ; font - size : 10 px ; line - height : 1.4 ; }
. product - dimensions - field , . cargo - remark - field { grid - column : span 2 ; }
. dimension - inputs { display : grid ; width : 100 % ; grid - template - columns : minmax ( 0 , 1 fr ) auto minmax ( 0 , 1 fr ) auto minmax ( 0 , 1 fr ) ; align - items : center ; gap : 6 px ; }
. dimension - inputs > span { color : var ( -- muted ) ; }
. cargo - detail - image { display : flex ; width : 112 px ; height : 112 px ; align - items : center ; justify - content : center ; margin - top : 12 px ; overflow : hidden ; border : 1 px solid var ( -- border ) ; background : # fff ; color : var ( -- muted ) ; font - size : 10 px ; text - align : center ; }
. cargo - detail - image img { width : 100 % ; height : 100 % ; object - fit : contain ; }
. 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 ; }
@@ -1409,7 +1524,9 @@ 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 , . order - detail - grid , . cargo - primary - grid , . cargo - secondary - grid , . party - detail - grid { grid - template - columns : 1 fr ; }
. order - form - grid , . order - detail - grid , . cargo - form - grid , . cargo - form - grid -- basic , . party - detail - grid { grid - template - columns : 1 fr ; }
. product - image - field { grid - row : auto ; }
. product - dimensions - field , . cargo - remark - field { grid - column : auto ; }
. fulfillment - summary { grid - template - columns : 1 fr ; }
. fulfillment - summary > div , . fulfillment - summary > div : not ( : nth - child ( 3 n ) ) { border - right : 0 ; border - bottom : 1 px solid var ( -- border ) ; }
. fulfillment - summary > div : last - child { border - bottom : 0 ; }