This commit is contained in:
2026-08-07 19:23:46 +08:00
parent aa6c349fce
commit 0b2380689c
10 changed files with 77 additions and 52 deletions
+4 -2
View File
@@ -150,12 +150,13 @@ export interface FirstLegOrderSaveRequest {
}
export interface MerchantFirstLegOrder {
id: number | string
id?: number | string
orderId?: number | string
orderNo: string | null
customerId: number | string | null
customerName: string
status: ApiFirstLegStatus
transportMode: ApiTransportMode
transportMode?: ApiTransportMode
tradeTerm: string | null
destinationCountryCode: string
destinationWarehouseId: number | string | null
@@ -191,6 +192,7 @@ export interface MerchantFirstLegOrder {
export interface MerchantFirstLegOrderCargo extends FirstLegOrderCargoPayload {
id: number | string
itemNo: number
productUsage?: string
}
export interface MerchantFirstLegOrderParty extends FirstLegOrderPartyPayload {
@@ -19,6 +19,8 @@ test('declares the documented merchant warehouse and first-leg endpoints', () =>
test('maps transport and status fields instead of treating container mode as transport mode', () => {
assert.match(storeSource, /order\.transportMode === 'SEA'.*order\.containerMode === 'FULL_CONTAINER' \? 'FCL' : 'LCL'/s)
assert.match(storeSource, /order\.id \?\? order\.orderId \?\? ''/)
assert.match(storeSource, /order\.transportMode \? clientTransport\(order\) : null/)
assert.match(storeSource, /draft\.transportMode === 'ROAD'\) return 'TRUCK'/)
assert.match(storeSource, /status === 'SUBMITTED'\) return 'PENDING_REVIEW'/)
assert.match(storeSource, /status === 'REJECTED'\) return 'PENDING_REVIEW'/)
@@ -88,6 +90,16 @@ test('sends backend-required channel, pricing, customs, destination and cargo fi
assert.match(storeSource, /destinationWarehouseId: draft\.destinationType === 'PRIVATE' \? undefined : draft\.destinationWarehouseId/)
assert.match(storeSource, /draft\.pickupMethod === 'PICKUP'.*draft\.shipperPartySnapshot/s)
assert.match(storeSource, /draft\.destinationType === 'PRIVATE'.*draft\.consigneePartySnapshot/s)
for (const field of [
'customsHsCode', 'lengthCm', 'widthCm', 'heightCm', 'unitsPerPackage', 'totalQuantity',
'netWeightKg', 'unitDeclaredValue', 'material', 'usage', 'model', 'brand',
'productLengthCm', 'productWidthCm', 'productHeightCm', 'shipmentId', 'referenceId',
'productImageFileId',
]) {
assert.match(storeSource, new RegExp(`${field}: item\\.`))
}
assert.match(apiSource, /POST[^\n]*product-images|method: 'POST', url: '\/merchant\/v1\/files\/product-images'/)
assert.match(apiSource, /GET[^\n]*product-images|method: 'GET', url: `\/merchant\/v1\/files\/product-images\/\$\{fileId\}`/)
})
test('separates domestic receiving warehouses from public destinations', () => {
@@ -279,8 +279,9 @@ function mapOrder(order: MerchantFirstLegOrder, warehouses: PlatformReceivingWar
const shipper = detail ? partySnapshot(detail, 'SENDER') : null
const consignee = detail ? partySnapshot(detail, 'RECEIVER') : null
return {
id: String(order.id), orderNo: order.orderNo, referenceNo: order.referenceId ?? '', transportMode: clientTransport(order),
businessType: 'EXPORT', origin: shipper?.city ?? '', destination: order.destinationWarehouseName || order.destinationCountryCode,
id: String(order.id ?? order.orderId ?? ''), orderNo: order.orderNo, referenceNo: order.referenceId ?? '',
transportMode: order.transportMode ? clientTransport(order) : null,
businessType: 'EXPORT', origin: shipper?.city ?? '', destination: order.destinationWarehouseName || order.destinationCountryCode || '',
receivingWarehouseId: String(order.receivingWarehouseId), receivingWarehouseName: order.receivingWarehouseName,
receivingWarehouseSnapshot: orderWarehouseSnapshot(order, warehouse), expectedDepartureDate: order.expectedShipDate ?? '', etd: '', eta: '',
shipperPartyId: shipper?.id ?? '', shipperPartySnapshot: shipper, shipper: shipper?.name ?? '',
@@ -294,11 +295,11 @@ function mapOrder(order: MerchantFirstLegOrder, warehouses: PlatformReceivingWar
customsHsCode: item.customsHsCode ?? '', lengthCm: item.lengthCm ?? 0, widthCm: item.widthCm ?? 0,
heightCm: item.heightCm ?? 0, unitsPerPackage: item.unitsPerPackage ?? 1,
pieces: item.totalQuantity ?? item.packageCount, packageCount: item.packageCount, grossWeightKg: item.grossWeightKg,
volumeCbm: item.volumeCbm, containerType: '', containerCount: 0, remark: item.cargoRemark ?? '',
volumeCbm: item.volumeCbm, remark: item.cargoRemark ?? '',
packageType: item.packageType, hsCode: item.hsCode ?? '', declaredValue: item.declaredValue ?? 0,
currency: item.currency ?? 'USD', dangerousGoods: item.dangerousGoods ?? false,
netWeightKg: item.netWeightKg ?? 0, unitDeclaredValue: item.unitDeclaredValue ?? 0,
material: item.material ?? '', usage: item.usage ?? '', model: item.model ?? '', brand: item.brand ?? '',
material: item.material ?? '', usage: item.usage ?? item.productUsage ?? '', model: item.model ?? '', brand: item.brand ?? '',
productLengthCm: item.productLengthCm ?? 0, productWidthCm: item.productWidthCm ?? 0,
productHeightCm: item.productHeightCm ?? 0, shipmentId: item.shipmentId ?? order.shipmentId ?? '',
referenceId: item.referenceId ?? order.referenceId ?? '',
@@ -39,8 +39,6 @@ export interface FirstLegCargoItem {
productHeightCm: number
shipmentId: string
referenceId: string
containerType: string
containerCount: number
remark: string
packageType: string
hsCode: string
@@ -52,7 +52,7 @@ function validDraft() {
destinationType: 'FBA',
amazonWarehouseCode: 'ONT8',
shipmentId: 'FBA-001',
items: [{ ...createEmptyCargoItem('cargo-1'), sku: 'SKU-001', upc: '012345678905', cargoNameCn: '家居用品', pieces: 100, grossWeightKg: 1200, volumeCbm: 3.5, containerType: '40HQ', containerCount: 1 }],
items: [{ ...createEmptyCargoItem('cargo-1'), sku: 'SKU-001', upc: '012345678905', cargoNameCn: '家居用品', pieces: 100, grossWeightKg: 1200, volumeCbm: 3.5, shipmentId: 'FBA-001', referenceId: 'REF-001' }],
}
}
@@ -81,13 +81,10 @@ test('validates the confirmed minimum submission fields', () => {
const invalid = validDraft()
invalid.receivingWarehouseId = ''
invalid.items[0].grossWeightKg = 0
invalid.items[0].containerType = ''
invalid.items[0].containerCount = 0
const errors = validateFirstLegOrderDraft(invalid)
assert.ok(errors.includes('请选择平台收货仓'))
assert.ok(errors.includes('第 1 条货物毛重必须大于 0'))
assert.ok(errors.includes('第 1 条货物需要填写箱型和柜量'))
})
test('only requires sender for pickup and receiver for a private destination', () => {
@@ -111,20 +108,22 @@ test('only requires sender for pickup and receiver for a private destination', (
assert.ok(validateFirstLegOrderDraft(privateDraft).includes('请填写收件人姓名'))
})
test('requires FBA reference, warehouse, shipment, SKU and UPC fields', () => {
test('requires FBA warehouse and cargo-level shipment, reference, SKU and UPC fields', () => {
const draft = validDraft()
draft.referenceNo = ''
draft.amazonWarehouseCode = ''
draft.shipmentId = ''
draft.items[0].sku = ''
draft.items[0].upc = ''
draft.items[0].shipmentId = ''
draft.items[0].referenceId = ''
const errors = validateFirstLegOrderDraft(draft)
assert.ok(errors.includes('FBA 订单请填写客户参考号'))
assert.ok(errors.includes('FBA 订单请填写 Amazon 仓库编码'))
assert.ok(errors.includes('FBA 订单请填写 Shipment ID'))
assert.ok(errors.includes('第 1 条货物缺少客户 SKU'))
assert.ok(errors.includes('第 1 条货物缺少 UPC'))
assert.ok(errors.includes('第 1 条货物缺少 Shipment ID'))
assert.ok(errors.includes('第 1 条货物缺少 Reference ID'))
})
test('requires public destination ids while keeping private addresses detached from public warehouses', () => {
@@ -141,15 +140,6 @@ test('requires public destination ids while keeping private addresses detached f
assert.ok(validateFirstLegOrderDraft(privateDraft).includes('私人地址不能引用平台公共仓库'))
})
test('does not require container details for non-FCL cargo', () => {
const draft = validDraft()
draft.transportMode = 'AIR'
draft.items[0].containerType = ''
draft.items[0].containerCount = 0
assert.deepEqual(validateFirstLegOrderDraft(draft), [])
})
test('summarizes cargo values from line items', () => {
const first = validDraft().items[0]
const second = { ...first, id: 'cargo-2', pieces: 40, packageCount: 4, grossWeightKg: 300, volumeCbm: 1.25 }
@@ -78,8 +78,6 @@ export function createEmptyCargoItem(id = `cargo-${Date.now()}`): FirstLegCargoI
productHeightCm: 0,
shipmentId: '',
referenceId: '',
containerType: '',
containerCount: 0,
remark: '',
packageType: 'CARTON',
hsCode: '',
@@ -132,9 +130,6 @@ export function validateFirstLegOrderDraft(draft: FirstLegOrderDraft) {
if (!Number.isInteger(item.unitsPerPackage) || item.unitsPerPackage <= 0) errors.push(`${line}每箱数量必须为正整数`)
if (item.grossWeightKg <= 0) errors.push(`${line}毛重必须大于 0`)
if (item.volumeCbm <= 0) errors.push(`${line}体积必须大于 0`)
if (draft.transportMode === 'FCL' && (!item.containerType || item.containerCount <= 0)) {
errors.push(`${line}需要填写箱型和柜量`)
}
})
return errors
+6 -3
View File
@@ -1,11 +1,14 @@
const allowedTypes = new Set(['image/jpeg', 'image/png'])
const allowedExtensions = new Set(['jpg', 'jpeg', 'png'])
const allowedImageTypes: Readonly<Record<string, ReadonlySet<string>>> = {
'image/jpeg': new Set(['jpg', 'jpeg']),
'image/png': new Set(['png']),
}
export const PRODUCT_IMAGE_MAX_SIZE = 5 * 1024 * 1024
export function validateProductImageFile(file: Pick<File, 'name' | 'size' | 'type'>) {
const extension = file.name.split('.').pop()?.toLowerCase() ?? ''
if (!allowedTypes.has(file.type.toLowerCase()) || !allowedExtensions.has(extension)) {
const allowedExtensions = allowedImageTypes[file.type.toLowerCase()]
if (!allowedExtensions?.has(extension)) {
return '产品图片仅支持 JPG、JPEG、PNG 格式'
}
if (file.size <= 0) return '产品图片内容为空'
@@ -215,8 +215,9 @@ test('distills order creation into the confirmed prototype sections with server
assert.ok(source.indexOf('<h3>目的地信息</h3>') < source.indexOf('<h3>货物明细</h3>'))
assert.match(source, /class="segmented-options"/)
assert.match(source, /class="prototype-form-row prototype-form-row--tabs"/)
assert.match(source, /class="cargo-primary-grid"/)
assert.match(source, /title="更多申报信息"/)
for (const heading of ['商品信息', '箱规、数量与重量', '报关与产品资料', '货件与备注']) {
assert.match(source, new RegExp(`<h4>${heading}</h4>`))
}
assert.match(source, /参考运费/)
assert.match(source, /仅供参考,最终以实际结算为准/)
assert.doesNotMatch(source, /v-model="form\.(?:businessType|origin|destination|priority|tradeTerm)"/)
@@ -268,7 +269,7 @@ test('shows field-level validation and concise guidance for required order input
assert.match(source, /prop="consigneePartySnapshot\.address" required/)
assert.match(source, /:prop="`items\.\$\{index\}\.cargoNameCn`" required/)
assert.match(source, /:prop="`items\.\$\{index\}\.grossWeightKg`" required/)
assert.match(source, /label="UPC".*required.*v-model="item\.upc"/)
assert.match(source, /label="UPC NO\."[^>]*:required="form\.destinationType === 'FBA'"[^>]*>\s*<el-input v-model="item\.upc"/)
assert.match(source, /placeholder="例如:蓝牙音箱"/)
assert.match(source, /\.order-form :deep\(\.el-form-item__error\).*position: static/)
})
@@ -297,7 +298,12 @@ test('shows the full customer-safe order detail without platform-only controls',
for (const heading of ['订单与运输', '收发货信息', '关键业务摘要', '履约快递信息', '货物明细']) {
assert.match(source, new RegExp(`<h3>${heading}<\\/h3>`))
}
for (const field of ['贸易条款', '发货方式', '报关方式', '运输渠道', '收货要求', '目的仓类型', '海关查验', '提交时间', '完成时间', 'SKU / UPC', 'HS 编码', '申报价值', '危险品']) {
for (const field of [
'贸易条款', '发货方式', '报关方式', '运输渠道', '收货要求', '目的仓类型', '海关查验',
'提交时间', '完成时间', 'SKU NO.', 'UPC NO.', '报关 HS CODE', '清关 HS CODE', '每箱数量',
'总净重', '清关申报单价', '中英文材质', '中英文用途', '产品规格', 'Shipment ID',
'Reference ID', '危险品',
]) {
assert.match(source, new RegExp(field))
}
assert.doesNotMatch(detailSource, /承运商|待平台指派/)
@@ -394,7 +394,10 @@ watch(isCreatePage, (isCreate, wasCreate) => {
}, { immediate: true })
watch(detailOpen, (open) => {
if (!open) resetInboundBillingState()
if (!open) {
resetInboundBillingState()
if (detailOrder.value) releaseProductImagePreviews(detailOrder.value.items)
}
})
onBeforeUnmount(() => {
@@ -942,7 +945,8 @@ function removeCargoItem(index: number) {
ElMessage.warning('订单至少需要保留一条货物明细')
return
}
form.items.splice(index, 1)
const [removed] = form.items.splice(index, 1)
if (removed) releaseProductImagePreviews([removed])
resetEditorValidation()
}
@@ -1083,7 +1087,7 @@ async function searchOrders(model: QueryModel) {
<template #order="{ row }">
<button class="order-number-link" type="button" @click="openDetail(row as FirstLegOrder)">{{ (row as FirstLegOrder).orderNo || '未提交草稿' }}</button>
<span v-if="(row as FirstLegOrder).businessSummary.inboundCode" class="order-reference">入仓码 {{ (row as FirstLegOrder).businessSummary.inboundCode }}</span>
<span class="order-reference">{{ (row as FirstLegOrder).referenceNo || '无客户参考号' }}</span>
<span class="order-reference">{{ (row as FirstLegOrder).referenceNo || '无 Reference ID' }}</span>
</template>
<template #route="{ row }"><strong>{{ (row as FirstLegOrder).destination || '目的地待更新' }}</strong><span class="order-reference">{{ (row as FirstLegOrder).transportMode ? getFirstLegTransportModeLabel((row as FirstLegOrder).transportMode) : (row as FirstLegOrder).channelName || '渠道待更新' }}</span></template>
<template #cargo="{ row }">{{ getOrderCargoSummary(row as FirstLegOrder) }}</template>
@@ -1101,7 +1105,7 @@ async function searchOrders(model: QueryModel) {
<article v-for="order in filteredOrders" :key="order.id" class="mobile-order-card">
<header><button type="button" @click="openDetail(order)">{{ order.orderNo || '未提交草稿' }}</button><el-tag :type="getFirstLegStatusTone(order.status)" effect="light">{{ getFirstLegStatusLabel(order.status) }}</el-tag></header>
<span v-if="order.businessSummary.inboundCode" class="order-reference">入仓码 {{ order.businessSummary.inboundCode }}</span>
<span class="order-reference">{{ order.referenceNo || '无客户参考号' }}</span>
<span class="order-reference">{{ order.referenceNo || '无 Reference ID' }}</span>
<dl><div><dt>目的地</dt><dd>{{ order.destination || '待更新' }}</dd></div><div><dt>收货仓</dt><dd>{{ order.receivingWarehouseName || '待更新' }}</dd></div><div><dt>货物摘要</dt><dd>{{ getOrderCargoSummary(order) }}</dd></div><div><dt>运输渠道</dt><dd>{{ order.transportMode ? getFirstLegTransportModeLabel(order.transportMode) : order.channelName || '待更新' }}</dd></div></dl>
<footer><span>更新于 {{ formatDateTime(order.updatedAt) }}</span><div><el-button link type="primary" @click="openDetail(order)"><Eye :size="15" />详情</el-button><el-button v-if="canEditOrder && canClientEditFirstLegOrder(order.status)" link type="primary" :loading="openingEditor" @click="openEdit(order)"><Pencil :size="15" />编辑</el-button></div></footer>
</article>
@@ -1243,13 +1247,6 @@ async function searchOrders(model: QueryModel) {
</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>
<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>
@@ -1279,7 +1276,6 @@ async function searchOrders(model: QueryModel) {
<h3>订单与运输</h3>
<dl class="order-detail-grid">
<div v-if="detailOrder.businessSummary.inboundCode"><dt>入仓码</dt><dd>{{ detailOrder.businessSummary.inboundCode }}</dd></div>
<div><dt>客户参考号</dt><dd>{{ displayValue(detailOrder.referenceNo) }}</dd></div>
<div><dt>创建时间</dt><dd>{{ formatDateTime(detailOrder.createdAt) }}</dd></div>
<div><dt>运输方式</dt><dd>{{ getFirstLegTransportModeLabel(detailOrder.transportMode) }}</dd></div>
<div><dt>贸易条款</dt><dd>{{ displayValue(detailOrder.tradeTerm) }}</dd></div>
@@ -1291,7 +1287,6 @@ async function searchOrders(model: QueryModel) {
<div><dt>目的仓类型</dt><dd>{{ destinationTypeLabels[detailOrder.destinationType] }}</dd></div>
<div><dt>目的国家 / 仓库</dt><dd>{{ detailOrder.destinationCountryCode || '-' }} · {{ detailOrder.destinationWarehouseName || '-' }}</dd></div>
<div v-if="detailOrder.destinationType === 'FBA'"><dt>Amazon 仓库代码</dt><dd>{{ displayValue(detailOrder.amazonWarehouseCode) }}</dd></div>
<div v-if="detailOrder.destinationType === 'FBA'"><dt>Shipment ID</dt><dd>{{ displayValue(detailOrder.shipmentId) }}</dd></div>
<div><dt>期望出运</dt><dd>{{ displayValue(detailOrder.expectedDepartureDate) }}</dd></div>
<div><dt>海关查验</dt><dd>{{ detailOrder.customsInspection ? '需要' : '不需要' }}</dd></div>
<div><dt>提交时间</dt><dd>{{ detailOrder.submittedAt ? formatDateTime(detailOrder.submittedAt) : '-' }}</dd></div>
@@ -1317,7 +1312,30 @@ 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><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>
<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 NO.</dt><dd>{{ item.sku || '-' }}</dd></div><div><dt>UPC NO.</dt><dd>{{ item.upc || '-' }}</dd></div>
<div><dt>报关 HS CODE</dt><dd>{{ item.hsCode || '-' }}</dd></div><div><dt>清关 HS CODE</dt><dd>{{ item.customsHsCode || '-' }}</dd></div>
<div><dt>箱规(长×宽×高)</dt><dd>{{ item.lengthCm || '-' }} × {{ item.widthCm || '-' }} × {{ item.heightCm || '-' }} cm</dd></div>
<div><dt>每箱数量</dt><dd>{{ item.unitsPerPackage }} 件</dd></div><div><dt>箱数</dt><dd>{{ 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.netWeightKg ? `${item.netWeightKg} kg` : '-' }}</dd></div><div><dt>总体积</dt><dd>{{ item.volumeCbm }} m³</dd></div>
<div><dt>清关申报单价</dt><dd>{{ item.unitDeclaredValue ? `USD ${item.unitDeclaredValue.toLocaleString()}` : '-' }}</dd></div><div><dt>清关申报金额</dt><dd>{{ item.declaredValue ? `USD ${item.declaredValue.toLocaleString()}` : '-' }}</dd></div>
<div><dt>中英文材质</dt><dd>{{ item.material || '-' }}</dd></div><div><dt>中英文用途</dt><dd>{{ item.usage || '-' }}</dd></div><div><dt>型号</dt><dd>{{ item.model || '-' }}</dd></div><div><dt>品牌</dt><dd>{{ item.brand || '-' }}</dd></div>
<div><dt>产品规格(长×宽×高)</dt><dd>{{ item.productLengthCm || '-' }} × {{ item.productWidthCm || '-' }} × {{ item.productHeightCm || '-' }} cm</dd></div>
<div><dt>Shipment ID</dt><dd>{{ item.shipmentId || '-' }}</dd></div><div><dt>Reference ID</dt><dd>{{ item.referenceId || '-' }}</dd></div>
<div><dt>包装类型</dt><dd>{{ packageTypeLabels[item.packageType] || item.packageType || '-' }}</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"
@@ -1514,6 +1532,8 @@ async function searchOrders(model: QueryModel) {
}
@media (min-width: 721px) and (max-width: 1050px) {
.first-leg-query-toolbar :deep(.query-toolbar__filters) { grid-template-columns: repeat(2, minmax(140px, 1fr)); }
.cargo-form-grid, .cargo-form-grid--basic { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.product-image-field { grid-row: auto; }
}
@media (max-width: 720px) {
.first-leg-query-toolbar { flex-wrap: wrap; }