优化
This commit is contained in:
Vendored
-2
@@ -21,8 +21,6 @@ declare module 'vue' {
|
|||||||
ElButton: typeof import('element-plus/es')['ElButton']
|
ElButton: typeof import('element-plus/es')['ElButton']
|
||||||
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
||||||
ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
|
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']
|
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
|
||||||
ElDialog: typeof import('element-plus/es')['ElDialog']
|
ElDialog: typeof import('element-plus/es')['ElDialog']
|
||||||
ElDrawer: typeof import('element-plus/es')['ElDrawer']
|
ElDrawer: typeof import('element-plus/es')['ElDrawer']
|
||||||
|
|||||||
@@ -150,12 +150,13 @@ export interface FirstLegOrderSaveRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface MerchantFirstLegOrder {
|
export interface MerchantFirstLegOrder {
|
||||||
id: number | string
|
id?: number | string
|
||||||
|
orderId?: number | string
|
||||||
orderNo: string | null
|
orderNo: string | null
|
||||||
customerId: number | string | null
|
customerId: number | string | null
|
||||||
customerName: string
|
customerName: string
|
||||||
status: ApiFirstLegStatus
|
status: ApiFirstLegStatus
|
||||||
transportMode: ApiTransportMode
|
transportMode?: ApiTransportMode
|
||||||
tradeTerm: string | null
|
tradeTerm: string | null
|
||||||
destinationCountryCode: string
|
destinationCountryCode: string
|
||||||
destinationWarehouseId: number | string | null
|
destinationWarehouseId: number | string | null
|
||||||
@@ -191,6 +192,7 @@ export interface MerchantFirstLegOrder {
|
|||||||
export interface MerchantFirstLegOrderCargo extends FirstLegOrderCargoPayload {
|
export interface MerchantFirstLegOrderCargo extends FirstLegOrderCargoPayload {
|
||||||
id: number | string
|
id: number | string
|
||||||
itemNo: number
|
itemNo: number
|
||||||
|
productUsage?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MerchantFirstLegOrderParty extends FirstLegOrderPartyPayload {
|
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', () => {
|
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\.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, /draft\.transportMode === 'ROAD'\) return 'TRUCK'/)
|
||||||
assert.match(storeSource, /status === 'SUBMITTED'\) return 'PENDING_REVIEW'/)
|
assert.match(storeSource, /status === 'SUBMITTED'\) return 'PENDING_REVIEW'/)
|
||||||
assert.match(storeSource, /status === 'REJECTED'\) 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, /destinationWarehouseId: draft\.destinationType === 'PRIVATE' \? undefined : draft\.destinationWarehouseId/)
|
||||||
assert.match(storeSource, /draft\.pickupMethod === 'PICKUP'.*draft\.shipperPartySnapshot/s)
|
assert.match(storeSource, /draft\.pickupMethod === 'PICKUP'.*draft\.shipperPartySnapshot/s)
|
||||||
assert.match(storeSource, /draft\.destinationType === 'PRIVATE'.*draft\.consigneePartySnapshot/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', () => {
|
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 shipper = detail ? partySnapshot(detail, 'SENDER') : null
|
||||||
const consignee = detail ? partySnapshot(detail, 'RECEIVER') : null
|
const consignee = detail ? partySnapshot(detail, 'RECEIVER') : null
|
||||||
return {
|
return {
|
||||||
id: String(order.id), orderNo: order.orderNo, referenceNo: order.referenceId ?? '', transportMode: clientTransport(order),
|
id: String(order.id ?? order.orderId ?? ''), orderNo: order.orderNo, referenceNo: order.referenceId ?? '',
|
||||||
businessType: 'EXPORT', origin: shipper?.city ?? '', destination: order.destinationWarehouseName || order.destinationCountryCode,
|
transportMode: order.transportMode ? clientTransport(order) : null,
|
||||||
|
businessType: 'EXPORT', origin: shipper?.city ?? '', destination: order.destinationWarehouseName || order.destinationCountryCode || '',
|
||||||
receivingWarehouseId: String(order.receivingWarehouseId), receivingWarehouseName: order.receivingWarehouseName,
|
receivingWarehouseId: String(order.receivingWarehouseId), receivingWarehouseName: order.receivingWarehouseName,
|
||||||
receivingWarehouseSnapshot: orderWarehouseSnapshot(order, warehouse), expectedDepartureDate: order.expectedShipDate ?? '', etd: '', eta: '',
|
receivingWarehouseSnapshot: orderWarehouseSnapshot(order, warehouse), expectedDepartureDate: order.expectedShipDate ?? '', etd: '', eta: '',
|
||||||
shipperPartyId: shipper?.id ?? '', shipperPartySnapshot: shipper, shipper: shipper?.name ?? '',
|
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,
|
customsHsCode: item.customsHsCode ?? '', lengthCm: item.lengthCm ?? 0, widthCm: item.widthCm ?? 0,
|
||||||
heightCm: item.heightCm ?? 0, unitsPerPackage: item.unitsPerPackage ?? 1,
|
heightCm: item.heightCm ?? 0, unitsPerPackage: item.unitsPerPackage ?? 1,
|
||||||
pieces: item.totalQuantity ?? item.packageCount, packageCount: item.packageCount, grossWeightKg: item.grossWeightKg,
|
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,
|
packageType: item.packageType, hsCode: item.hsCode ?? '', declaredValue: item.declaredValue ?? 0,
|
||||||
currency: item.currency ?? 'USD', dangerousGoods: item.dangerousGoods ?? false,
|
currency: item.currency ?? 'USD', dangerousGoods: item.dangerousGoods ?? false,
|
||||||
netWeightKg: item.netWeightKg ?? 0, unitDeclaredValue: item.unitDeclaredValue ?? 0,
|
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,
|
productLengthCm: item.productLengthCm ?? 0, productWidthCm: item.productWidthCm ?? 0,
|
||||||
productHeightCm: item.productHeightCm ?? 0, shipmentId: item.shipmentId ?? order.shipmentId ?? '',
|
productHeightCm: item.productHeightCm ?? 0, shipmentId: item.shipmentId ?? order.shipmentId ?? '',
|
||||||
referenceId: item.referenceId ?? order.referenceId ?? '',
|
referenceId: item.referenceId ?? order.referenceId ?? '',
|
||||||
|
|||||||
@@ -39,8 +39,6 @@ export interface FirstLegCargoItem {
|
|||||||
productHeightCm: number
|
productHeightCm: number
|
||||||
shipmentId: string
|
shipmentId: string
|
||||||
referenceId: string
|
referenceId: string
|
||||||
containerType: string
|
|
||||||
containerCount: number
|
|
||||||
remark: string
|
remark: string
|
||||||
packageType: string
|
packageType: string
|
||||||
hsCode: string
|
hsCode: string
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ function validDraft() {
|
|||||||
destinationType: 'FBA',
|
destinationType: 'FBA',
|
||||||
amazonWarehouseCode: 'ONT8',
|
amazonWarehouseCode: 'ONT8',
|
||||||
shipmentId: 'FBA-001',
|
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()
|
const invalid = validDraft()
|
||||||
invalid.receivingWarehouseId = ''
|
invalid.receivingWarehouseId = ''
|
||||||
invalid.items[0].grossWeightKg = 0
|
invalid.items[0].grossWeightKg = 0
|
||||||
invalid.items[0].containerType = ''
|
|
||||||
invalid.items[0].containerCount = 0
|
|
||||||
|
|
||||||
const errors = validateFirstLegOrderDraft(invalid)
|
const errors = validateFirstLegOrderDraft(invalid)
|
||||||
assert.ok(errors.includes('请选择平台收货仓'))
|
assert.ok(errors.includes('请选择平台收货仓'))
|
||||||
assert.ok(errors.includes('第 1 条货物毛重必须大于 0'))
|
assert.ok(errors.includes('第 1 条货物毛重必须大于 0'))
|
||||||
assert.ok(errors.includes('第 1 条货物需要填写箱型和柜量'))
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('only requires sender for pickup and receiver for a private destination', () => {
|
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('请填写收件人姓名'))
|
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()
|
const draft = validDraft()
|
||||||
draft.referenceNo = ''
|
draft.referenceNo = ''
|
||||||
draft.amazonWarehouseCode = ''
|
draft.amazonWarehouseCode = ''
|
||||||
draft.shipmentId = ''
|
draft.shipmentId = ''
|
||||||
draft.items[0].sku = ''
|
draft.items[0].sku = ''
|
||||||
draft.items[0].upc = ''
|
draft.items[0].upc = ''
|
||||||
|
draft.items[0].shipmentId = ''
|
||||||
|
draft.items[0].referenceId = ''
|
||||||
|
|
||||||
const errors = validateFirstLegOrderDraft(draft)
|
const errors = validateFirstLegOrderDraft(draft)
|
||||||
assert.ok(errors.includes('FBA 订单请填写客户参考号'))
|
|
||||||
assert.ok(errors.includes('FBA 订单请填写 Amazon 仓库编码'))
|
assert.ok(errors.includes('FBA 订单请填写 Amazon 仓库编码'))
|
||||||
assert.ok(errors.includes('FBA 订单请填写 Shipment ID'))
|
|
||||||
assert.ok(errors.includes('第 1 条货物缺少客户 SKU'))
|
assert.ok(errors.includes('第 1 条货物缺少客户 SKU'))
|
||||||
assert.ok(errors.includes('第 1 条货物缺少 UPC'))
|
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', () => {
|
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('私人地址不能引用平台公共仓库'))
|
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', () => {
|
test('summarizes cargo values from line items', () => {
|
||||||
const first = validDraft().items[0]
|
const first = validDraft().items[0]
|
||||||
const second = { ...first, id: 'cargo-2', pieces: 40, packageCount: 4, grossWeightKg: 300, volumeCbm: 1.25 }
|
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,
|
productHeightCm: 0,
|
||||||
shipmentId: '',
|
shipmentId: '',
|
||||||
referenceId: '',
|
referenceId: '',
|
||||||
containerType: '',
|
|
||||||
containerCount: 0,
|
|
||||||
remark: '',
|
remark: '',
|
||||||
packageType: 'CARTON',
|
packageType: 'CARTON',
|
||||||
hsCode: '',
|
hsCode: '',
|
||||||
@@ -132,9 +130,6 @@ export function validateFirstLegOrderDraft(draft: FirstLegOrderDraft) {
|
|||||||
if (!Number.isInteger(item.unitsPerPackage) || item.unitsPerPackage <= 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.grossWeightKg <= 0) errors.push(`${line}毛重必须大于 0`)
|
||||||
if (item.volumeCbm <= 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
|
return errors
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
const allowedTypes = new Set(['image/jpeg', 'image/png'])
|
const allowedImageTypes: Readonly<Record<string, ReadonlySet<string>>> = {
|
||||||
const allowedExtensions = new Set(['jpg', 'jpeg', 'png'])
|
'image/jpeg': new Set(['jpg', 'jpeg']),
|
||||||
|
'image/png': new Set(['png']),
|
||||||
|
}
|
||||||
|
|
||||||
export const PRODUCT_IMAGE_MAX_SIZE = 5 * 1024 * 1024
|
export const PRODUCT_IMAGE_MAX_SIZE = 5 * 1024 * 1024
|
||||||
|
|
||||||
export function validateProductImageFile(file: Pick<File, 'name' | 'size' | 'type'>) {
|
export function validateProductImageFile(file: Pick<File, 'name' | 'size' | 'type'>) {
|
||||||
const extension = file.name.split('.').pop()?.toLowerCase() ?? ''
|
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 格式'
|
return '产品图片仅支持 JPG、JPEG、PNG 格式'
|
||||||
}
|
}
|
||||||
if (file.size <= 0) return '产品图片内容为空'
|
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.ok(source.indexOf('<h3>目的地信息</h3>') < source.indexOf('<h3>货物明细</h3>'))
|
||||||
assert.match(source, /class="segmented-options"/)
|
assert.match(source, /class="segmented-options"/)
|
||||||
assert.match(source, /class="prototype-form-row prototype-form-row--tabs"/)
|
assert.match(source, /class="prototype-form-row prototype-form-row--tabs"/)
|
||||||
assert.match(source, /class="cargo-primary-grid"/)
|
for (const heading of ['商品信息', '箱规、数量与重量', '报关与产品资料', '货件与备注']) {
|
||||||
assert.match(source, /title="更多申报信息"/)
|
assert.match(source, new RegExp(`<h4>${heading}</h4>`))
|
||||||
|
}
|
||||||
assert.match(source, /参考运费/)
|
assert.match(source, /参考运费/)
|
||||||
assert.match(source, /仅供参考,最终以实际结算为准/)
|
assert.match(source, /仅供参考,最终以实际结算为准/)
|
||||||
assert.doesNotMatch(source, /v-model="form\.(?:businessType|origin|destination|priority|tradeTerm)"/)
|
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="consigneePartySnapshot\.address" required/)
|
||||||
assert.match(source, /:prop="`items\.\$\{index\}\.cargoNameCn`" required/)
|
assert.match(source, /:prop="`items\.\$\{index\}\.cargoNameCn`" required/)
|
||||||
assert.match(source, /:prop="`items\.\$\{index\}\.grossWeightKg`" 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, /placeholder="例如:蓝牙音箱"/)
|
||||||
assert.match(source, /\.order-form :deep\(\.el-form-item__error\).*position: static/)
|
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 ['订单与运输', '收发货信息', '关键业务摘要', '履约快递信息', '货物明细']) {
|
for (const heading of ['订单与运输', '收发货信息', '关键业务摘要', '履约快递信息', '货物明细']) {
|
||||||
assert.match(source, new RegExp(`<h3>${heading}<\\/h3>`))
|
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.match(source, new RegExp(field))
|
||||||
}
|
}
|
||||||
assert.doesNotMatch(detailSource, /承运商|待平台指派/)
|
assert.doesNotMatch(detailSource, /承运商|待平台指派/)
|
||||||
|
|||||||
@@ -394,7 +394,10 @@ watch(isCreatePage, (isCreate, wasCreate) => {
|
|||||||
}, { immediate: true })
|
}, { immediate: true })
|
||||||
|
|
||||||
watch(detailOpen, (open) => {
|
watch(detailOpen, (open) => {
|
||||||
if (!open) resetInboundBillingState()
|
if (!open) {
|
||||||
|
resetInboundBillingState()
|
||||||
|
if (detailOrder.value) releaseProductImagePreviews(detailOrder.value.items)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
@@ -942,7 +945,8 @@ function removeCargoItem(index: number) {
|
|||||||
ElMessage.warning('订单至少需要保留一条货物明细')
|
ElMessage.warning('订单至少需要保留一条货物明细')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
form.items.splice(index, 1)
|
const [removed] = form.items.splice(index, 1)
|
||||||
|
if (removed) releaseProductImagePreviews([removed])
|
||||||
resetEditorValidation()
|
resetEditorValidation()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1083,7 +1087,7 @@ async function searchOrders(model: QueryModel) {
|
|||||||
<template #order="{ row }">
|
<template #order="{ row }">
|
||||||
<button class="order-number-link" type="button" @click="openDetail(row as FirstLegOrder)">{{ (row as FirstLegOrder).orderNo || '未提交草稿' }}</button>
|
<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 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>
|
||||||
<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 #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>
|
<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">
|
<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>
|
<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 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>
|
<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>
|
<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>
|
</article>
|
||||||
@@ -1243,13 +1247,6 @@ async function searchOrders(model: QueryModel) {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</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>
|
</article>
|
||||||
<aside class="price-estimate" aria-live="polite">
|
<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 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>
|
<h3>订单与运输</h3>
|
||||||
<dl class="order-detail-grid">
|
<dl class="order-detail-grid">
|
||||||
<div v-if="detailOrder.businessSummary.inboundCode"><dt>入仓码</dt><dd>{{ detailOrder.businessSummary.inboundCode }}</dd></div>
|
<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>{{ formatDateTime(detailOrder.createdAt) }}</dd></div>
|
||||||
<div><dt>运输方式</dt><dd>{{ getFirstLegTransportModeLabel(detailOrder.transportMode) }}</dd></div>
|
<div><dt>运输方式</dt><dd>{{ getFirstLegTransportModeLabel(detailOrder.transportMode) }}</dd></div>
|
||||||
<div><dt>贸易条款</dt><dd>{{ displayValue(detailOrder.tradeTerm) }}</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>{{ destinationTypeLabels[detailOrder.destinationType] }}</dd></div>
|
||||||
<div><dt>目的国家 / 仓库</dt><dd>{{ detailOrder.destinationCountryCode || '-' }} · {{ detailOrder.destinationWarehouseName || '-' }}</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>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>{{ displayValue(detailOrder.expectedDepartureDate) }}</dd></div>
|
||||||
<div><dt>海关查验</dt><dd>{{ detailOrder.customsInspection ? '需要' : '不需要' }}</dd></div>
|
<div><dt>海关查验</dt><dd>{{ detailOrder.customsInspection ? '需要' : '不需要' }}</dd></div>
|
||||||
<div><dt>提交时间</dt><dd>{{ detailOrder.submittedAt ? formatDateTime(detailOrder.submittedAt) : '-' }}</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-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>
|
<div v-else class="express-empty"><PackageCheck :size="20" /><div><strong>平台尚未维护快递信息</strong><span>快递公司和快递单号由平台后台维护,客户端仅只读展示。</span></div></div>
|
||||||
</section>
|
</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" />
|
<OrderProgressPanel :order="detailOrder" />
|
||||||
<InboundBillingPanel
|
<InboundBillingPanel
|
||||||
:data="inboundBilling"
|
:data="inboundBilling"
|
||||||
@@ -1514,6 +1532,8 @@ async function searchOrders(model: QueryModel) {
|
|||||||
}
|
}
|
||||||
@media (min-width: 721px) and (max-width: 1050px) {
|
@media (min-width: 721px) and (max-width: 1050px) {
|
||||||
.first-leg-query-toolbar :deep(.query-toolbar__filters) { grid-template-columns: repeat(2, minmax(140px, 1fr)); }
|
.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) {
|
@media (max-width: 720px) {
|
||||||
.first-leg-query-toolbar { flex-wrap: wrap; }
|
.first-leg-query-toolbar { flex-wrap: wrap; }
|
||||||
|
|||||||
Reference in New Issue
Block a user