From 0b2380689cccfade1913b74a932df8c8620ff970 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Fri, 7 Aug 2026 19:23:46 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components.d.ts | 2 - src/modules/first-leg/api/firstLeg.ts | 6 ++- .../first-leg/store/firstLegOrders.test.mjs | 12 +++++ src/modules/first-leg/store/firstLegOrders.ts | 9 ++-- src/modules/first-leg/types/firstLegOrder.ts | 2 - .../first-leg/utils/firstLegOrder.test.mjs | 22 +++------ src/modules/first-leg/utils/firstLegOrder.ts | 5 -- src/modules/first-leg/utils/productImage.ts | 9 ++-- .../views/FirstLegOrdersView.test.mjs | 14 ++++-- .../first-leg/views/FirstLegOrdersView.vue | 48 +++++++++++++------ 10 files changed, 77 insertions(+), 52 deletions(-) diff --git a/src/components.d.ts b/src/components.d.ts index 16c6945..ace4296 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -21,8 +21,6 @@ declare module 'vue' { ElButton: typeof import('element-plus/es')['ElButton'] ElCheckbox: typeof import('element-plus/es')['ElCheckbox'] ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup'] - ElCollapse: typeof import('element-plus/es')['ElCollapse'] - ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem'] ElDatePicker: typeof import('element-plus/es')['ElDatePicker'] ElDialog: typeof import('element-plus/es')['ElDialog'] ElDrawer: typeof import('element-plus/es')['ElDrawer'] diff --git a/src/modules/first-leg/api/firstLeg.ts b/src/modules/first-leg/api/firstLeg.ts index e3486ec..fa414f4 100644 --- a/src/modules/first-leg/api/firstLeg.ts +++ b/src/modules/first-leg/api/firstLeg.ts @@ -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 { diff --git a/src/modules/first-leg/store/firstLegOrders.test.mjs b/src/modules/first-leg/store/firstLegOrders.test.mjs index 1cdb121..ab16531 100644 --- a/src/modules/first-leg/store/firstLegOrders.test.mjs +++ b/src/modules/first-leg/store/firstLegOrders.test.mjs @@ -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', () => { diff --git a/src/modules/first-leg/store/firstLegOrders.ts b/src/modules/first-leg/store/firstLegOrders.ts index fe5dd98..67cc7f3 100644 --- a/src/modules/first-leg/store/firstLegOrders.ts +++ b/src/modules/first-leg/store/firstLegOrders.ts @@ -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 ?? '', diff --git a/src/modules/first-leg/types/firstLegOrder.ts b/src/modules/first-leg/types/firstLegOrder.ts index 85f3b13..8117cab 100644 --- a/src/modules/first-leg/types/firstLegOrder.ts +++ b/src/modules/first-leg/types/firstLegOrder.ts @@ -39,8 +39,6 @@ export interface FirstLegCargoItem { productHeightCm: number shipmentId: string referenceId: string - containerType: string - containerCount: number remark: string packageType: string hsCode: string diff --git a/src/modules/first-leg/utils/firstLegOrder.test.mjs b/src/modules/first-leg/utils/firstLegOrder.test.mjs index 8187b03..221113a 100644 --- a/src/modules/first-leg/utils/firstLegOrder.test.mjs +++ b/src/modules/first-leg/utils/firstLegOrder.test.mjs @@ -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 } diff --git a/src/modules/first-leg/utils/firstLegOrder.ts b/src/modules/first-leg/utils/firstLegOrder.ts index 0bf283f..59160e5 100644 --- a/src/modules/first-leg/utils/firstLegOrder.ts +++ b/src/modules/first-leg/utils/firstLegOrder.ts @@ -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 diff --git a/src/modules/first-leg/utils/productImage.ts b/src/modules/first-leg/utils/productImage.ts index 1600dba..6307418 100644 --- a/src/modules/first-leg/utils/productImage.ts +++ b/src/modules/first-leg/utils/productImage.ts @@ -1,11 +1,14 @@ -const allowedTypes = new Set(['image/jpeg', 'image/png']) -const allowedExtensions = new Set(['jpg', 'jpeg', 'png']) +const allowedImageTypes: Readonly>> = { + '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) { 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 '产品图片内容为空' diff --git a/src/modules/first-leg/views/FirstLegOrdersView.test.mjs b/src/modules/first-leg/views/FirstLegOrdersView.test.mjs index ec14feb..d40c11a 100644 --- a/src/modules/first-leg/views/FirstLegOrdersView.test.mjs +++ b/src/modules/first-leg/views/FirstLegOrdersView.test.mjs @@ -215,8 +215,9 @@ test('distills order creation into the confirmed prototype sections with server assert.ok(source.indexOf('

目的地信息

') < source.indexOf('

货物明细

')) 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(`

${heading}

`)) + } 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*${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, /承运商|待平台指派/) diff --git a/src/modules/first-leg/views/FirstLegOrdersView.vue b/src/modules/first-leg/views/FirstLegOrdersView.vue index eb58bc8..60155c5 100644 --- a/src/modules/first-leg/views/FirstLegOrdersView.vue +++ b/src/modules/first-leg/views/FirstLegOrdersView.vue @@ -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) { @@ -1101,7 +1105,7 @@ async function searchOrders(model: QueryModel) {
{{ getFirstLegStatusLabel(order.status) }}
入仓码 {{ order.businessSummary.inboundCode }} - {{ order.referenceNo || '无客户参考号' }} + {{ order.referenceNo || '无 Reference ID' }}
目的地
{{ order.destination || '待更新' }}
收货仓
{{ order.receivingWarehouseName || '待更新' }}
货物摘要
{{ getOrderCargoSummary(order) }}
运输渠道
{{ order.transportMode ? getFirstLegTransportModeLabel(order.transportMode) : order.channelName || '待更新' }}
更新于 {{ formatDateTime(order.updatedAt) }}
详情编辑
@@ -1243,13 +1247,6 @@ async function searchOrders(model: QueryModel) { -
-

整柜信息

-
- - -
-