优化
This commit is contained in:
@@ -2,14 +2,12 @@ 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'
|
||||
|
||||
@@ -54,24 +52,10 @@ 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, boxSizeType: 'SMALL', lengthCm: 34, widthCm: 24, heightCm: 24, containerType: '40HQ', containerCount: 1 }],
|
||||
items: [{ ...createEmptyCargoItem('cargo-1'), sku: 'SKU-001', upc: '012345678905', cargoNameCn: '家居用品', pieces: 100, grossWeightKg: 1200, volumeCbm: 3.5, 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,46 +4,7 @@ import {
|
||||
type FirstLegOrderStatus,
|
||||
type FirstLegTransportMode,
|
||||
} from '@chongchong/shared'
|
||||
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))
|
||||
}
|
||||
import type { FirstLegCargoItem, FirstLegOrderDraft } from '../types/firstLegOrder.ts'
|
||||
|
||||
const statusLabels = new Map<FirstLegOrderStatus, string>(
|
||||
FIRST_LEG_ORDER_STATUSES.map((item) => [item.code, item.label]),
|
||||
@@ -92,14 +53,31 @@ export function createEmptyCargoItem(id = `cargo-${Date.now()}`): FirstLegCargoI
|
||||
upc: '',
|
||||
cargoNameCn: '',
|
||||
cargoNameEn: '',
|
||||
productImageFileId: '',
|
||||
productImageName: '',
|
||||
productImagePreviewUrl: '',
|
||||
productImageUploading: false,
|
||||
productImageError: '',
|
||||
customsHsCode: '',
|
||||
lengthCm: 0,
|
||||
widthCm: 0,
|
||||
heightCm: 0,
|
||||
unitsPerPackage: 1,
|
||||
pieces: 1,
|
||||
packageCount: 1,
|
||||
grossWeightKg: 0,
|
||||
volumeCbm: 0,
|
||||
boxSizeType: '',
|
||||
lengthCm: 0,
|
||||
widthCm: 0,
|
||||
heightCm: 0,
|
||||
netWeightKg: 0,
|
||||
unitDeclaredValue: 0,
|
||||
material: '',
|
||||
usage: '',
|
||||
model: '',
|
||||
brand: '',
|
||||
productLengthCm: 0,
|
||||
productWidthCm: 0,
|
||||
productHeightCm: 0,
|
||||
shipmentId: '',
|
||||
referenceId: '',
|
||||
containerType: '',
|
||||
containerCount: 0,
|
||||
remark: '',
|
||||
@@ -138,19 +116,20 @@ export function validateFirstLegOrderDraft(draft: FirstLegOrderDraft) {
|
||||
if (draft.items.length === 0) errors.push('请至少添加一条货物明细')
|
||||
|
||||
if (draft.destinationType === 'FBA') {
|
||||
if (!draft.referenceNo.trim()) errors.push('FBA 订单请填写客户参考号')
|
||||
if (!draft.amazonWarehouseCode.trim()) errors.push('FBA 订单请填写 Amazon 仓库编码')
|
||||
if (!draft.shipmentId.trim()) errors.push('FBA 订单请填写 Shipment ID')
|
||||
}
|
||||
|
||||
draft.items.forEach((item, index) => {
|
||||
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 (draft.destinationType === 'FBA' && !item.shipmentId.trim()) errors.push(`${line}缺少 Shipment ID`)
|
||||
if (draft.destinationType === 'FBA' && !item.referenceId.trim()) errors.push(`${line}缺少 Reference ID`)
|
||||
if (!Number.isInteger(item.pieces) || item.pieces <= 0) errors.push(`${line}件数必须为正整数`)
|
||||
if (!Number.isInteger(item.packageCount) || item.packageCount <= 0) errors.push(`${line}箱数必须为正整数`)
|
||||
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)) {
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
import { PRODUCT_IMAGE_MAX_SIZE, validateProductImageFile } from './productImage.ts'
|
||||
|
||||
test('accepts JPG and PNG product images up to 5MB', () => {
|
||||
assert.equal(validateProductImageFile({ name: 'product.JPG', size: PRODUCT_IMAGE_MAX_SIZE, type: 'image/jpeg' }), '')
|
||||
assert.equal(validateProductImageFile({ name: 'product.png', size: 1024, type: 'image/png' }), '')
|
||||
})
|
||||
|
||||
test('rejects empty, oversized, mismatched, and unsupported product image files', () => {
|
||||
assert.match(validateProductImageFile({ name: 'product.png', size: 0, type: 'image/png' }), /内容为空/)
|
||||
assert.match(validateProductImageFile({ name: 'product.png', size: PRODUCT_IMAGE_MAX_SIZE + 1, type: 'image/png' }), /超过 5MB/)
|
||||
assert.match(validateProductImageFile({ name: 'product.jpg', size: 1024, type: 'image/png' }), /仅支持/)
|
||||
assert.match(validateProductImageFile({ name: 'product.webp', size: 1024, type: 'image/webp' }), /仅支持/)
|
||||
})
|
||||
@@ -0,0 +1,14 @@
|
||||
const allowedTypes = new Set(['image/jpeg', 'image/png'])
|
||||
const allowedExtensions = new Set(['jpg', 'jpeg', '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)) {
|
||||
return '产品图片仅支持 JPG、JPEG、PNG 格式'
|
||||
}
|
||||
if (file.size <= 0) return '产品图片内容为空'
|
||||
if (file.size > PRODUCT_IMAGE_MAX_SIZE) return '产品图片不能超过 5MB'
|
||||
return ''
|
||||
}
|
||||
Reference in New Issue
Block a user