优化
This commit is contained in:
@@ -85,6 +85,9 @@ export interface FirstLegOrderCargoPayload {
|
||||
packageCount: number
|
||||
grossWeightKg: number
|
||||
volumeCbm: number
|
||||
lengthCm?: number
|
||||
widthCm?: number
|
||||
heightCm?: number
|
||||
declaredValue?: number
|
||||
currency?: string
|
||||
dangerousGoods?: boolean
|
||||
|
||||
@@ -88,6 +88,8 @@ 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 dimension of ['lengthCm', 'widthCm', 'heightCm']) assert.match(storeSource, new RegExp(`${dimension}: item\\.${dimension}`))
|
||||
assert.match(storeSource, /inferCargoBoxSizeType\(item\.lengthCm, item\.widthCm, item\.heightCm\)/)
|
||||
})
|
||||
|
||||
test('separates domestic receiving warehouses from public destinations', () => {
|
||||
|
||||
@@ -27,7 +27,7 @@ import type { MerchantTenantStatus } from '@/modules/tenant-management/public'
|
||||
import type { PlatformReceivingWarehouse, PlatformReceivingWarehouseSnapshot } from '@chongchong/shared'
|
||||
import type { FirstLegPartySnapshot } from '@/modules/first-leg/data/firstLegParties'
|
||||
import type { FirstLegOrder, FirstLegOrderDraft, FirstLegOrderTimelineItem } from '@/modules/first-leg/types/firstLegOrder'
|
||||
import { canClientEditFirstLegOrder, getFirstLegStatusLabel } from '@/modules/first-leg/utils/firstLegOrder'
|
||||
import { canClientEditFirstLegOrder, getFirstLegStatusLabel, inferCargoBoxSizeType } from '@/modules/first-leg/utils/firstLegOrder'
|
||||
import { loadAllWarehouseRecords } from '@/modules/first-leg/utils/warehousePagination'
|
||||
|
||||
export type ClientWarehouseType = 'DOMESTIC' | 'FBA' | 'OVERSEAS'
|
||||
@@ -111,6 +111,9 @@ function savePayload(draft: FirstLegOrderDraft, warehouse: ClientWarehouse, cust
|
||||
packageCount: item.packageCount,
|
||||
grossWeightKg: item.grossWeightKg,
|
||||
volumeCbm: item.volumeCbm,
|
||||
lengthCm: item.lengthCm || undefined,
|
||||
widthCm: item.widthCm || undefined,
|
||||
heightCm: item.heightCm || undefined,
|
||||
declaredValue: item.declaredValue || undefined,
|
||||
currency: item.currency || undefined,
|
||||
dangerousGoods: item.dangerousGoods,
|
||||
@@ -273,7 +276,10 @@ function mapOrder(order: MerchantFirstLegOrder, warehouses: PlatformReceivingWar
|
||||
items: (detail?.cargoes ?? []).map((item) => ({
|
||||
id: String(item.id), sku: item.skuNo ?? '', upc: item.upcNo ?? '', cargoNameCn: item.cargoName, cargoNameEn: item.cargoNameEn ?? '',
|
||||
pieces: item.totalQuantity ?? item.packageCount, packageCount: item.packageCount, grossWeightKg: item.grossWeightKg,
|
||||
volumeCbm: item.volumeCbm, containerType: '', containerCount: 0, remark: item.cargoRemark ?? '',
|
||||
volumeCbm: item.volumeCbm,
|
||||
boxSizeType: inferCargoBoxSizeType(item.lengthCm, item.widthCm, item.heightCm),
|
||||
lengthCm: item.lengthCm ?? 0, widthCm: item.widthCm ?? 0, heightCm: item.heightCm ?? 0,
|
||||
containerType: '', containerCount: 0, remark: item.cargoRemark ?? '',
|
||||
packageType: item.packageType, hsCode: item.hsCode ?? '', declaredValue: item.declaredValue ?? 0,
|
||||
currency: item.currency ?? 'USD', dangerousGoods: item.dangerousGoods ?? false,
|
||||
})),
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { FirstLegPartySnapshot } from '../data/firstLegParties.ts'
|
||||
|
||||
export type FirstLegBusinessType = 'EXPORT' | 'IMPORT'
|
||||
export type FirstLegPriority = 'NORMAL' | 'URGENT' | 'EXPEDITED'
|
||||
export type CargoBoxSizeType = '' | 'SMALL' | 'MEDIUM' | 'LARGE' | 'EXTRA_LARGE'
|
||||
|
||||
export interface FirstLegCargoItem {
|
||||
id: string
|
||||
@@ -18,6 +19,10 @@ export interface FirstLegCargoItem {
|
||||
packageCount: number
|
||||
grossWeightKg: number
|
||||
volumeCbm: number
|
||||
boxSizeType: CargoBoxSizeType
|
||||
lengthCm: number
|
||||
widthCm: number
|
||||
heightCm: number
|
||||
containerType: string
|
||||
containerCount: number
|
||||
remark: string
|
||||
|
||||
@@ -2,12 +2,14 @@ import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
import { createFirstLegPartySnapshot } from '../data/firstLegParties.ts'
|
||||
import {
|
||||
applyCargoBoxSize,
|
||||
canClientCancelFirstLegOrder,
|
||||
canClientEditFirstLegOrder,
|
||||
createEmptyCargoItem,
|
||||
getFirstLegStatusLabel,
|
||||
getFirstLegTransportModeLabel,
|
||||
summarizeFirstLegCargo,
|
||||
updateCargoBoxVolume,
|
||||
validateFirstLegOrderDraft,
|
||||
} from './firstLegOrder.ts'
|
||||
|
||||
@@ -52,10 +54,24 @@ 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, boxSizeType: 'SMALL', lengthCm: 34, widthCm: 24, heightCm: 24, containerType: '40HQ', containerCount: 1 }],
|
||||
}
|
||||
}
|
||||
|
||||
test('applies the four fixed box sizes and recalculates total CBM from package count', () => {
|
||||
const item = createEmptyCargoItem('cargo-box')
|
||||
item.packageCount = 2
|
||||
applyCargoBoxSize(item, 'SMALL')
|
||||
assert.deepEqual([item.lengthCm, item.widthCm, item.heightCm, item.volumeCbm], [34, 24, 24, 0.039])
|
||||
|
||||
item.packageCount = 3
|
||||
updateCargoBoxVolume(item)
|
||||
assert.equal(item.volumeCbm, 0.059)
|
||||
|
||||
applyCargoBoxSize(item, '')
|
||||
assert.deepEqual([item.lengthCm, item.widthCm, item.heightCm, item.volumeCbm], [0, 0, 0, 0])
|
||||
})
|
||||
|
||||
test('uses the confirmed status and transport labels', () => {
|
||||
assert.equal(getFirstLegStatusLabel('WAIT_MEASURE'), '待测量数据')
|
||||
assert.equal(getFirstLegStatusLabel('INBOUNDED'), '已入库')
|
||||
|
||||
@@ -4,7 +4,46 @@ import {
|
||||
type FirstLegOrderStatus,
|
||||
type FirstLegTransportMode,
|
||||
} from '@chongchong/shared'
|
||||
import type { FirstLegCargoItem, FirstLegOrderDraft } from '../types/firstLegOrder.ts'
|
||||
import type { CargoBoxSizeType, FirstLegCargoItem, FirstLegOrderDraft } from '../types/firstLegOrder.ts'
|
||||
|
||||
export const CARGO_BOX_SIZES = [
|
||||
{ code: 'SMALL', label: '小箱', inner: '30 × 20 × 20', lengthCm: 34, widthCm: 24, heightCm: 24 },
|
||||
{ code: 'MEDIUM', label: '中箱', inner: '40 × 30 × 30', lengthCm: 44, widthCm: 34, heightCm: 34 },
|
||||
{ code: 'LARGE', label: '大箱', inner: '50 × 40 × 40', lengthCm: 54, widthCm: 44, heightCm: 44 },
|
||||
{ code: 'EXTRA_LARGE', label: '特大箱', inner: '60 × 50 × 50', lengthCm: 64, widthCm: 54, heightCm: 54 },
|
||||
] as const satisfies ReadonlyArray<{
|
||||
code: Exclude<CargoBoxSizeType, ''>
|
||||
label: string
|
||||
inner: string
|
||||
lengthCm: number
|
||||
widthCm: number
|
||||
heightCm: number
|
||||
}>
|
||||
|
||||
export function inferCargoBoxSizeType(lengthCm?: number, widthCm?: number, heightCm?: number): CargoBoxSizeType {
|
||||
return CARGO_BOX_SIZES.find((box) => box.lengthCm === lengthCm && box.widthCm === widthCm && box.heightCm === heightCm)?.code ?? ''
|
||||
}
|
||||
|
||||
export function applyCargoBoxSize(item: FirstLegCargoItem, type: CargoBoxSizeType) {
|
||||
item.boxSizeType = type
|
||||
const box = CARGO_BOX_SIZES.find((option) => option.code === type)
|
||||
if (!box) {
|
||||
item.lengthCm = 0
|
||||
item.widthCm = 0
|
||||
item.heightCm = 0
|
||||
item.volumeCbm = 0
|
||||
return
|
||||
}
|
||||
item.lengthCm = box.lengthCm
|
||||
item.widthCm = box.widthCm
|
||||
item.heightCm = box.heightCm
|
||||
updateCargoBoxVolume(item)
|
||||
}
|
||||
|
||||
export function updateCargoBoxVolume(item: FirstLegCargoItem) {
|
||||
if (!item.boxSizeType || item.packageCount <= 0) return
|
||||
item.volumeCbm = Number(((item.lengthCm * item.widthCm * item.heightCm * item.packageCount) / 1_000_000).toFixed(3))
|
||||
}
|
||||
|
||||
const statusLabels = new Map<FirstLegOrderStatus, string>(
|
||||
FIRST_LEG_ORDER_STATUSES.map((item) => [item.code, item.label]),
|
||||
@@ -57,6 +96,10 @@ export function createEmptyCargoItem(id = `cargo-${Date.now()}`): FirstLegCargoI
|
||||
packageCount: 1,
|
||||
grossWeightKg: 0,
|
||||
volumeCbm: 0,
|
||||
boxSizeType: '',
|
||||
lengthCm: 0,
|
||||
widthCm: 0,
|
||||
heightCm: 0,
|
||||
containerType: '',
|
||||
containerCount: 0,
|
||||
remark: '',
|
||||
@@ -104,6 +147,7 @@ export function validateFirstLegOrderDraft(draft: FirstLegOrderDraft) {
|
||||
const line = `第 ${index + 1} 条货物`
|
||||
if (!item.cargoNameCn.trim()) errors.push(`${line}缺少中文品名`)
|
||||
if (!item.packageType.trim()) errors.push(`${line}缺少包装类型`)
|
||||
if (!item.boxSizeType) errors.push(`${line}请选择箱规`)
|
||||
if (draft.destinationType === 'FBA' && !item.sku.trim()) errors.push(`${line}缺少客户 SKU`)
|
||||
if (draft.destinationType === 'FBA' && !item.upc.trim()) errors.push(`${line}缺少 UPC`)
|
||||
if (!Number.isInteger(item.pieces) || item.pieces <= 0) errors.push(`${line}件数必须为正整数`)
|
||||
|
||||
@@ -23,15 +23,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 { FirstLegOrder, FirstLegOrderDraft } from '@/modules/first-leg/types/firstLegOrder'
|
||||
import type { CargoBoxSizeType, 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'
|
||||
|
||||
@@ -870,6 +873,10 @@ function addCargoItem() {
|
||||
resetEditorValidation()
|
||||
}
|
||||
|
||||
function changeCargoBoxSize(item: FirstLegCargoItem, type: CargoBoxSizeType) {
|
||||
applyCargoBoxSize(item, type)
|
||||
}
|
||||
|
||||
function removeCargoItem(index: number) {
|
||||
if (form.items.length === 1) {
|
||||
ElMessage.warning('订单至少需要保留一条货物明细')
|
||||
@@ -1125,16 +1132,17 @@ async function searchOrders(model: QueryModel) {
|
||||
<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="箱数"><el-input-number v-model="item.packageCount" :min="0" 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" :min="0" :precision="3" controls-position="right" /></el-form-item>
|
||||
<el-form-item label="总体积 m³" :prop="`items.${index}.volumeCbm`" required :rules="positiveNumberRules('体积必须大于 0')"><el-input-number v-model="item.volumeCbm" :min="0" :precision="3" controls-position="right" 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']" :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>
|
||||
<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>
|
||||
</article>
|
||||
|
||||
Reference in New Issue
Block a user