From e5c4003e3d66169f4910b666f2be189d10c0e348 Mon Sep 17 00:00:00 2001 From: liuyihe <2513762445@qq.com> Date: Wed, 28 Oct 2026 16:33:35 +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 --- docs/business-index.md | 2 +- .../cross-border/businessOrders.test.mjs | 6 +- .../components/BusinessOrdersView.vue | 20 +-- .../components/TransportDemandForm.vue | 20 +-- .../cross-border/transportDemandForm.ts | 16 +- .../views/WarehouseServiceView.vue | 148 ++++++------------ .../views/relabelAttachments.test.mjs | 13 +- .../views/unifiedRelabelTemplate.test.mjs | 87 ++++++++++ .../views/warehouseTransferForm.test.mjs | 6 +- 9 files changed, 174 insertions(+), 144 deletions(-) create mode 100644 src/modules/overseas-warehouse/views/unifiedRelabelTemplate.test.mjs diff --git a/docs/business-index.md b/docs/business-index.md index 321959a..656ffe9 100644 --- a/docs/business-index.md +++ b/docs/business-index.md @@ -26,7 +26,7 @@ | 海外采购 | `src/modules/cross-border/routes.ts`、`views/ProcurementView.vue`、`components/ProcurementDetailPanel.vue`、`api/procurement.ts` | 独立列表、新建及详情路由;客户范围只取 Token,创建的 `customerName` 只取认证会话企业名称。七状态简化流程展示分批收货、分批出库与费用,待客户确认时提供当前客户确认动作;不开放内部文件或平台操作。Backend `lastleg-service/procurement/`;规则见 `baiyue-firstleg/lastleg-service/docs/procurement-workflow.md`(工作区路径) | | 美国本土卡与美本土散板 | `src/modules/cross-border/routes.ts`、`components/BusinessOrdersView.vue`、`api/businessOrders.ts` | 两个运输业务共用列表/表单组件,客户范围只取 Token;不消费采购字段或状态。Backend `lastleg-service/` 的 `trucking/`、`looseboard/` 客户接口 | | 海外仓入库订单 | `src/modules/order-management/routes.ts`、`views/InboundOrdersView.vue`、`api/inboundPlans.ts` | `orders/inbound` 为只读列表/详情,权限 `merchant:warehouse:inbound:list`;不展示或提交内部 ID | -| 海外仓贴标、仓储转运、货物废弃 | `src/modules/overseas-warehouse/routes.ts`、`views/WarehouseServiceView.vue`、`api/warehouseServices.ts` | 三个路由均为 `active`;退货预报另见本节独立入口;贴标申请先选国外中转仓和有库存历史入仓单,换标类型为只换 SKU、只换箱唛、SKU+箱唛。涉及箱唛换标时,整张贴标申请只选择一个 FBA 目的仓,全部箱级明细统一提交该目的仓,模板和明细不再逐行选择目的仓。箱唛换标明细一行固定代表一箱,箱数不可编辑,提交也固定为 1。货物明细外层始终提供“SKU 关联”“箱号关联”“SKU 混箱换标”三套模板的下载、上传,不随换标类型切换;上传只进入待导入关系区,客户可勾选部分或全选后导入本次明细,只有进入明细的库存才随申请提交和冻结。明细也支持手动新增编辑,组合换标以仓库唛头连接旧/新 SKU 和新 FBA 箱唛关系。涉及箱唛换标时必须选择卡车、自提或快递;快递可选择打印面单并上传 PDF/图片。PDA 逐箱换标、上传照片并由仓库复核后,客户详情展示完工照片和“换标完成确认”;客户确认前不得继续出库,确认后卡车进入预排车,自提/快递进入换标出库待创建。历史库存位置不能解释成箱唛与具体托盘快照,客户页面不暴露托盘、库位和内部库存 ID | +| 海外仓贴标、仓储转运、货物废弃 | `src/modules/overseas-warehouse/routes.ts`、`views/WarehouseServiceView.vue`、`api/warehouseServices.ts` | 三个路由均为 `active`;退货预报另见本节独立入口;贴标申请先选国外中转仓和有库存历史入仓单,换标类型为只换 SKU、只换箱唛、SKU+箱唛。涉及箱唛换标时,整张贴标申请只选择一个 FBA 目的仓,全部箱级明细统一提交该目的仓,模板和明细不再逐行选择目的仓。箱唛换标明细一行固定代表一箱,箱数不可编辑,提交也固定为 1。货物明细外层提供统一贴标明细模板,使用“贴标类型”列区分只换箱唛、只换 SKU 标签和 SKU+箱唛三种模式;上传只进入待导入关系区,客户可勾选部分或全选后导入本次明细,同一份文件不得混用多种类型,只有进入明细的库存才随申请提交和冻结。明细也支持手动新增编辑,组合换标以仓库唛头连接旧/新 SKU 和新 FBA 箱唛关系。涉及箱唛换标时必须选择卡车、自提或快递;快递可选择打印面单并上传 PDF/图片。PDA 逐箱换标、上传照片并由仓库复核后,客户详情展示完工照片和“换标完成确认”;客户确认前不得继续出库,确认后卡车进入预排车,自提/快递进入换标出库待创建。历史库存位置不能解释成箱唛与具体托盘快照,客户页面不暴露托盘、库位和内部库存 ID | ## Client 全量验收快速索引 diff --git a/src/modules/cross-border/businessOrders.test.mjs b/src/modules/cross-border/businessOrders.test.mjs index d5591c1..954bfab 100644 --- a/src/modules/cross-border/businessOrders.test.mjs +++ b/src/modules/cross-border/businessOrders.test.mjs @@ -150,7 +150,7 @@ test('truck requires both contacts vehicle and minute times without inheriting l truck.form.pickupAt = '2026-10-20T09:30:00' let failure truck.rules.value.deliveryAt[1].validator({}, '2026-10-20T09:29:00', error => { failure = error }) - assert.equal(failure.message, 'C点送达时间不能早于B点取货时间') + assert.equal(failure.message, '送达时间不能早于取货时间') }) test('truck retry submits the complete new contract with identical local times and idempotency key', async () => { @@ -176,8 +176,8 @@ test('truck details render saved minute times and unknown pool demand without ti current.detail.value = { summary: { vehicleType: null, pickupAt: '2026-10-20T09:30:00', deliveryAt: null }, originAddress: null, destinationAddress: null, remark: null } const fields = new Map(current.detailFields.value) assert.equal(fields.get('车辆类型'), '—') - assert.equal(fields.get('B点取货时间'), '2026-10-20 09:30') - assert.equal(fields.get('C点送达时间'), '—') + assert.equal(fields.get('取货时间'), '2026-10-20 09:30') + assert.equal(fields.get('送达时间'), '—') assert.equal(fields.has('柜型'), false) assert.equal(fields.has('托盘数'), false) }) diff --git a/src/modules/cross-border/components/BusinessOrdersView.vue b/src/modules/cross-border/components/BusinessOrdersView.vue index cce1172..44a3914 100644 --- a/src/modules/cross-border/components/BusinessOrdersView.vue +++ b/src/modules/cross-border/components/BusinessOrdersView.vue @@ -50,10 +50,10 @@ const configuration = computed(() => ({ })[props.kind]) function transportColumns(looseboard: boolean): ColumnDefinition[] { - const originLabel = looseboard ? '起运地' : 'B点取货地' - const destinationLabel = looseboard ? '目的地' : 'C点送达地' - const pickupLabel = looseboard ? '取货时间' : 'B点取货时间' - const deliveryLabel = looseboard ? '送货时间' : 'C点送达时间' + const originLabel = looseboard ? '起运地' : '取货地' + const destinationLabel = looseboard ? '目的地' : '送达地' + const pickupLabel = looseboard ? '取货时间' : '取货时间' + const deliveryLabel = looseboard ? '送货时间' : '送达时间' return [ { key: 'orderNo', label: '订单编号', width: 180 }, ...(!looseboard ? [{ key: 'vehicleType', label: '车辆类型', width: 110 }] : []), @@ -127,13 +127,13 @@ const detailFields = computed(() => { ...(props.kind === 'looseboard' ? [ ['商品名称', summary.productName], ['预计托数', summary.estimatedPalletCount], ] : [['车辆类型', vehicleTypeLabel(summary.vehicleType)]]), - [props.kind === 'trucking' ? 'B点取货地' : '起运地', read(summary, 'origin')], [props.kind === 'trucking' ? 'B点取货地址' : '起运地址', current.originAddress], - [props.kind === 'trucking' ? 'C点送达地' : '目的地', read(summary, 'destination')], [props.kind === 'trucking' ? 'C点送达地址' : '目的地址', current.destinationAddress], + [props.kind === 'trucking' ? '取货地' : '起运地', read(summary, 'origin')], [props.kind === 'trucking' ? '取货地址' : '起运地址', current.originAddress], + [props.kind === 'trucking' ? '送达地' : '目的地', read(summary, 'destination')], [props.kind === 'trucking' ? '送达地址' : '目的地址', current.destinationAddress], ['件数', read(summary, 'packageCount')], ['总重量(kg)', read(summary, 'totalWeightKg')], ['总体积(m³)', read(summary, 'totalVolumeCbm')], ['预算总价', money(read(summary, 'budgetAmount'), read(summary, 'currency'))], - [props.kind === 'trucking' ? 'B点联系人' : '起运地联系人', summary.originContactName], [props.kind === 'trucking' ? 'B点联系电话' : '起运地联系电话', summary.originContactPhone], - [props.kind === 'trucking' ? 'C点联系人' : '目的地联系人', summary.destinationContactName], [props.kind === 'trucking' ? 'C点联系电话' : '目的地联系电话', summary.destinationContactPhone], - [props.kind === 'trucking' ? 'B点取货时间' : '取货时间', localMinute(summary.pickupAt)], [props.kind === 'trucking' ? 'C点送达时间' : '送货时间', localMinute(summary.deliveryAt)], ['备注', current.remark], + [props.kind === 'trucking' ? '取货联系人' : '起运地联系人', summary.originContactName], [props.kind === 'trucking' ? '取货联系电话' : '起运地联系电话', summary.originContactPhone], + [props.kind === 'trucking' ? '送达联系人' : '目的地联系人', summary.destinationContactName], [props.kind === 'trucking' ? '送达联系电话' : '目的地联系电话', summary.destinationContactPhone], + [props.kind === 'trucking' ? '取货时间' : '取货时间', localMinute(summary.pickupAt)], [props.kind === 'trucking' ? '送达时间' : '送货时间', localMinute(summary.deliveryAt)], ['备注', current.remark], ] }) @@ -216,7 +216,7 @@ function getRowActions(candidate: object): TableAction[] { } function detailFieldClass(label: unknown) { - return ['备注', props.kind === 'trucking' ? 'B点取货地址' : '起运地址', props.kind === 'trucking' ? 'C点送达地址' : '目的地址'].includes(String(label)) ? 'is-wide' : '' + return ['备注', props.kind === 'trucking' ? '取货地址' : '起运地址', props.kind === 'trucking' ? '送达地址' : '目的地址'].includes(String(label)) ? 'is-wide' : '' } const listRouteName = computed(() => ({ diff --git a/src/modules/cross-border/components/TransportDemandForm.vue b/src/modules/cross-border/components/TransportDemandForm.vue index cc02418..2c45186 100644 --- a/src/modules/cross-border/components/TransportDemandForm.vue +++ b/src/modules/cross-border/components/TransportDemandForm.vue @@ -29,14 +29,14 @@ defineExpose({ - - - - - - - - + + + + + + + +
@@ -58,8 +58,8 @@ defineExpose({
- - + +
diff --git a/src/modules/cross-border/transportDemandForm.ts b/src/modules/cross-border/transportDemandForm.ts index b235b6a..ea45e41 100644 --- a/src/modules/cross-border/transportDemandForm.ts +++ b/src/modules/cross-border/transportDemandForm.ts @@ -33,12 +33,12 @@ export function createTransportDemandForm(): TransportDemandForm { } const requiredText = (kind: TransportDemandKind) => ({ - origin: kind === 'truck' ? 'B点取货地' : '起运地', - destination: kind === 'truck' ? 'C点送达地' : '目的地', - originContactName: kind === 'truck' ? 'B点联系人' : '起运地联系人', - originContactPhone: kind === 'truck' ? 'B点联系电话' : '起运地联系电话', - destinationContactName: kind === 'truck' ? 'C点联系人' : '目的地联系人', - destinationContactPhone: kind === 'truck' ? 'C点联系电话' : '目的地联系电话', + origin: kind === 'truck' ? '取货地' : '起运地', + destination: kind === 'truck' ? '送达地' : '目的地', + originContactName: kind === 'truck' ? '取货联系人' : '起运地联系人', + originContactPhone: kind === 'truck' ? '取货联系电话' : '起运地联系电话', + destinationContactName: kind === 'truck' ? '送达联系人' : '目的地联系人', + destinationContactPhone: kind === 'truck' ? '送达联系电话' : '目的地联系电话', } as const) export function localMinuteInput(value: unknown) { @@ -57,7 +57,7 @@ export function transportDemandRules(kind: TransportDemandKind, form: TransportD rules[field] = [{ required: true, type: 'number', min: 0, message: `${label}须为不小于 0 的数字`, trigger: 'change' }] } rules.budgetAmount = [{ type: 'number', min: 0, message: '预算总价不能小于 0', trigger: 'change' }] - const pickupLabel = kind === 'truck' ? 'B点取货时间' : '取货时间', deliveryLabel = kind === 'truck' ? 'C点送达时间' : '送货时间' + const pickupLabel = kind === 'truck' ? '取货时间' : '取货时间', deliveryLabel = kind === 'truck' ? '送达时间' : '送货时间' rules.pickupAt = [{ required: true, message: `请选择${pickupLabel}`, trigger: 'change' }] rules.deliveryAt = [{ required: true, message: `请选择${deliveryLabel}`, trigger: 'change' }, { validator: (_rule, value, callback) => callback(value && form.pickupAt && localMinuteInput(value) < localMinuteInput(form.pickupAt) ? new Error(`${deliveryLabel}不能早于${pickupLabel}`) : undefined), trigger: 'change', @@ -71,7 +71,7 @@ export function transportDemandError(kind: TransportDemandKind, form: TransportD for (const [field, label] of Object.entries(labels)) { if (!String(form[field as keyof typeof labels] || '').trim()) return `请输入${label}` } - const pickupLabel = kind === 'truck' ? 'B点取货时间' : '取货时间', deliveryLabel = kind === 'truck' ? 'C点送达时间' : '送货时间' + const pickupLabel = kind === 'truck' ? '取货时间' : '取货时间', deliveryLabel = kind === 'truck' ? '送达时间' : '送货时间' for (const [field, label] of [['pickupAt', pickupLabel], ['deliveryAt', deliveryLabel]] as const) { if (!form[field]) return `请选择${label}` if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?$/.test(form[field])) return `请选择有效的${label}` diff --git a/src/modules/overseas-warehouse/views/WarehouseServiceView.vue b/src/modules/overseas-warehouse/views/WarehouseServiceView.vue index c204706..a583831 100644 --- a/src/modules/overseas-warehouse/views/WarehouseServiceView.vue +++ b/src/modules/overseas-warehouse/views/WarehouseServiceView.vue @@ -428,9 +428,7 @@ function relabelOptionLabel(option: typeof relabelInboundOptions.value[number]) } const loading = ref(false) const saving = ref(false) -const cartonTemplateInput = ref(null) -const skuTemplateInput = ref(null) -const mixedTemplateInput = ref(null) +const unifiedTemplateInput = ref(null) const transferCartonMarkInput = ref(null) const relabelFileInput = ref(null) const relabelFileScope = ref<'CARTON_TOTAL' | 'SKU_TOTAL' | 'ITEM'>('CARTON_TOTAL') @@ -827,110 +825,62 @@ async function importTransferCartonMarks(event: Event) { for (const row of cargoRows.value) row.customerCartonMarks = imported.filter((item) => item.SKU.trim().toUpperCase() === row.sku.trim().toUpperCase()).map((item) => item['客户箱唛号'].trim()).join('\n') ElMessage.success(`已导入 ${imported.length} 个客户箱唛`) } -function downloadRelabelTemplate(templateType: RelabelTemplateType) { +function relabelTemplateTypeForMode(mode: LabelingMode): RelabelTemplateType { + return mode === 'carton' ? 'carton' : mode === 'sku' ? 'sku' : 'mixed' +} +function relabelTemplateTypeLabel(type: RelabelTemplateType) { + return type === 'carton' ? '只换箱唛' : type === 'sku' ? '只换 SKU 标签' : 'SKU 和箱唛都换' +} +function parseRelabelTemplateType(value: string): RelabelTemplateType | null { + const normalized = value.trim().toUpperCase().replaceAll(' ', '') + if (['只换箱唛', '箱号关联', '箱唛换标', 'CARTON', 'CARTON_LABEL'].includes(normalized)) return 'carton' + if (['只换SKU标签', 'SKU关联', 'SKU换标', 'SKU', 'SKU_LABEL'].includes(normalized)) return 'sku' + if (['SKU和箱唛都换', 'SKU混箱', 'SKU混箱换标', '混箱', 'MIXED', 'CARTON_AND_SKU'].includes(normalized)) return 'mixed' + return null +} +function downloadRelabelTemplate() { if (!cargoRows.value.length) { ElMessage.warning('请先选择有库存的历史入仓单'); return } - const cartonTemplate = templateType === 'carton' - const mixedTemplate = templateType === 'mixed' - const includeDestination = false + const templateType = relabelTemplateTypeForMode(labelingMode.value) const cartonMarkHeader = props.service === 'transfer' ? '客户箱唛号' : '仓库唛头' - const header = cartonTemplate - ? ['明细行标识', cartonMarkHeader, '新FBA箱唛', ...(includeDestination ? ['FBA目的仓'] : []), 'FBA货件编号', '备注'] - : ['明细行标识', cartonMarkHeader, '旧SKU标签', '新SKU标签', '换标数量(件)', - ...(mixedTemplate ? ['新FBA箱唛', ...(includeDestination ? ['FBA目的仓'] : []), 'FBA货件编号'] : []), '备注'] - const rows = cargoRows.value.map((item) => cartonTemplate - ? [item.id, item.cartonMark, item.fbaCartonMark, ...(includeDestination ? [item.destinationWarehouseId] : []), item.shipmentNo, item.remark] - : [item.id, item.cartonMark, item.sku, '', 1, - ...(mixedTemplate ? [item.fbaCartonMark, ...(includeDestination ? [item.destinationWarehouseId] : []), item.shipmentNo] : []), item.remark]) + const header = ['明细行标识', '贴标类型', cartonMarkHeader, '旧SKU标签', '新SKU标签', '换标数量(件)', '新FBA箱唛', 'FBA货件编号', '备注'] + const rows = cargoRows.value.map((item) => [item.id, relabelTemplateTypeLabel(templateType), props.service === 'transfer' ? item.customerCartonMarks : item.cartonMark, item.sku, '', 1, '', item.shipmentNo, item.remark]) const blob = new Blob([`\uFEFF${[header, ...rows].map((row) => row.map(csvCell).join(',')).join('\r\n')}\r\n`], { type: 'text/csv;charset=utf-8' }) - const url = URL.createObjectURL(blob) - const anchor = document.createElement('a') - anchor.href = url - anchor.download = cartonTemplate ? '贴标申请-箱号关联模板.csv' - : mixedTemplate ? '贴标申请-SKU混箱换标模板.csv' : '贴标申请-SKU关联模板.csv' - anchor.click(); URL.revokeObjectURL(url) + const url = URL.createObjectURL(blob); const anchor = document.createElement('a'); anchor.href = url; anchor.download = '贴标申请-统一明细关联模板.csv'; anchor.click(); URL.revokeObjectURL(url) } function parseCsvLine(line: string) { const values: string[] = []; let value = ''; let quoted = false - for (let index = 0; index < line.length; index += 1) { - const char = line[index] - if (char === '"') { if (quoted && line[index + 1] === '"') { value += '"'; index += 1 } else quoted = !quoted } - else if (char === ',' && !quoted) { values.push(value.trim()); value = '' } - else value += char - } + for (let index = 0; index < line.length; index += 1) { const char = line[index]; if (char === '"') { if (quoted && line[index + 1] === '"') { value += '"'; index += 1 } else quoted = !quoted } else if (char === ',' && !quoted) { values.push(value.trim()); value = '' } else value += char } values.push(value.trim()); return values } -async function importRelabelTemplate(event: Event, templateType: RelabelTemplateType) { - const input = event.target as HTMLInputElement; const file = input.files?.[0]; input.value = '' - if (!file) return +async function importRelabelTemplate(event: Event) { + const input = event.target as HTMLInputElement; const file = input.files?.[0]; input.value = ''; if (!file) return const lines = (await file.text()).replace(/^\uFEFF/, '').split(/\r?\n/).filter((line) => line.trim()) if (lines.length < 2) { ElMessage.error('模板中没有可导入的明细'); return } - // Accept both the current display name and legacy template header while keeping the internal key stable. const headers = parseCsvLine(lines[0]).map((name) => name === '仓库唛头' || name === '客户箱唛号' || name === '旧唛头' ? '旧箱唛' : name) - if (templateType !== 'carton') { - const mixedTemplate = templateType === 'mixed' - const required = ['明细行标识', '旧箱唛', '旧SKU标签', '新SKU标签', '换标数量(件)', - ...(mixedTemplate ? ['新FBA箱唛', 'FBA货件编号'] : [])] - if (required.some((name) => !headers.includes(name))) { - ElMessage.error('SKU 关联模板格式不正确,请重新下载 SKU 关联模板'); return - } - const imported = lines.slice(1).map(parseCsvLine) - .map((values) => Object.fromEntries(headers.map((key, index) => [key, values[index] || '']))) - .filter((item) => item['旧箱唛'] || item['新SKU标签'] || item['新FBA箱唛']) - if (!imported.length || imported.some((item) => required.some((key) => !item[key]))) { - ElMessage.error('仓库唛头、旧 SKU 标签、新 SKU 标签和换标数量必须填写完整'); return - } - const rows: ImportedRelabelBinding[] = [] - for (const item of imported) { - const quantity = Number(item['换标数量(件)']) - if (!Number.isInteger(quantity) || quantity < 1) { - ElMessage.error(`仓库唛头 ${item['旧箱唛']} 的换标数量必须是大于 0 的整数`); return - } - const source = cargoRows.value.find((row) => String(row.id) === item['明细行标识']) - if (!source) { ElMessage.error(`明细行 ${item['明细行标识']} 不属于当前库存,请重新下载模板`); return } - rows.push({ - id: ++skuRelabelSequence, - sourceRowId: source.id, - cartonMark: item['旧箱唛'].trim(), - originalSku: item['旧SKU标签'].trim(), - newSku: item['新SKU标签'].trim(), - quantity, - fbaCartonMark: mixedTemplate ? item['新FBA箱唛'].trim() : '', - destinationWarehouseId: form.destination, - remark: item['备注'] || '', - templateType, - selected: true, - shipmentNo: mixedTemplate ? item['FBA货件编号'].trim() : '', - }) - } - importedRelabelBindings.value = rows - ElMessage.success(`已读取 ${rows.length} 条绑定关系,请选择后导入到明细`) - return + const requiredHeaders = ['明细行标识', '贴标类型', '旧箱唛', '旧SKU标签', '新SKU标签', '换标数量(件)', '新FBA箱唛', 'FBA货件编号', '备注'] + if (requiredHeaders.some((name) => !headers.includes(name))) { ElMessage.error('统一明细关联模板格式不正确,请重新下载模板'); return } + const imported = lines.slice(1).map(parseCsvLine).map((values) => Object.fromEntries(headers.map((key, index) => [key, values[index] || '']))).filter((item) => item['明细行标识'] || item['贴标类型'] || item['旧箱唛'] || item['新SKU标签'] || item['新FBA箱唛']) + if (!imported.length) { ElMessage.error('模板中没有可导入的明细'); return } + const rows: ImportedRelabelBinding[] = []; let detectedType: RelabelTemplateType | null = null + for (const item of imported) { + const templateType = parseRelabelTemplateType(item['贴标类型']); if (!templateType) { ElMessage.error(`明细行 ${item['明细行标识']} 的贴标类型无效`); return } + if (detectedType && detectedType !== templateType) { ElMessage.error('同一份明细文件只能包含一种贴标类型,请拆分后分别提交换标单'); return } + detectedType = templateType + const source = cargoRows.value.find((row) => String(row.id) === item['明细行标识']); if (!source) { ElMessage.error(`明细行 ${item['明细行标识']} 不属于当前库存,请重新下载模板`); return } + const quantity = Number(item['换标数量(件)']); const requiresSku = templateType !== 'carton'; const requiresCarton = templateType !== 'sku' + if (!item['旧箱唛'] || (requiresSku && (!item['旧SKU标签'] || !item['新SKU标签'])) || !Number.isInteger(quantity) || quantity < 1 || (requiresCarton && (!item['新FBA箱唛'] || !item['FBA货件编号']))) { ElMessage.error(`明细行 ${item['明细行标识']} 未按“${relabelTemplateTypeLabel(templateType)}”填写完整`); return } + rows.push({ id: ++skuRelabelSequence, sourceRowId: source.id, cartonMark: item['旧箱唛'].trim(), originalSku: (item['旧SKU标签'] || source.sku).trim(), newSku: item['新SKU标签'].trim(), quantity: templateType === 'carton' ? 1 : quantity, fbaCartonMark: item['新FBA箱唛'].trim(), destinationWarehouseId: form.destination, remark: item['备注'] || '', templateType, selected: true, shipmentNo: item['FBA货件编号'].trim() }) } - const required = ['明细行标识', '旧箱唛', '新FBA箱唛', 'FBA货件编号'] - if (required.some((name) => !headers.includes(name))) { ElMessage.error('箱号关联模板格式不正确,请重新下载箱号关联模板'); return } - const imported = lines.slice(1).map(parseCsvLine) - .map((values) => Object.fromEntries(headers.map((key, index) => [key, values[index] || '']))) - .filter((item) => item['旧箱唛'] || item['新FBA箱唛']) - if (!imported.length || imported.some((item) => required.some((key) => !item[key]))) { - ElMessage.error('模板中的明细行、仓库唛头、新 FBA 箱唛和货件编号必须填写完整'); return + const keys = rows.map((row) => detectedType === 'carton' ? String(row.sourceRowId) : `${row.cartonMark.toUpperCase()}|${row.originalSku.toUpperCase()}`) + if (new Set(keys).size !== keys.length) { ElMessage.error('存在重复箱明细或重复的箱内 SKU,请合并后重新上传'); return } + const cartonTargets = new Map() + for (const row of rows) { + const key = row.cartonMark.toUpperCase() + const target = `${row.fbaCartonMark}|${row.shipmentNo}` + if (detectedType === 'mixed' && cartonTargets.has(key) && cartonTargets.get(key) !== target) { ElMessage.error('同一箱内的 SKU 必须填写相同的新 FBA 箱唛和货件编号'); return } + cartonTargets.set(key, target) } - if (new Set(imported.map((item) => item['明细行标识'])).size !== imported.length) { - ElMessage.error('模板中的明细行标识不能重复'); return - } - const importedByRow = new Map(imported.map((item) => [item['明细行标识'], item])) - if (importedByRow.size !== imported.length || [...importedByRow.keys()].some((id) => !cargoRows.value.some((row) => String(row.id) === id))) { - ElMessage.error('模板存在重复或不属于当前库存的明细行,请重新下载模板'); return - } - const bindings: ImportedRelabelBinding[] = [] - for (const [rowId, item] of importedByRow) { - const row = cargoRows.value.find((candidate) => String(candidate.id) === rowId)! - bindings.push({ id: ++skuRelabelSequence, sourceRowId: row.id, cartonMark: item['旧箱唛'], - originalSku: row.sku, newSku: '', quantity: 1, fbaCartonMark: item['新FBA箱唛'], - destinationWarehouseId: form.destination, remark: item['备注'] || '', templateType, - selected: true, shipmentNo: item['FBA货件编号'] }) - } - importedRelabelBindings.value = bindings - ElMessage.success(`已读取 ${bindings.length} 条箱号关联,请选择后导入到明细`) + importedRelabelBindings.value = rows; ElMessage.success(`已读取 ${rows.length} 条绑定关系,请选择后导入到明细`) } function selectAllImportedBindings(value: boolean) { for (const item of importedRelabelBindings.value) item.selected = value @@ -1795,13 +1745,11 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) {
2明细关联模板
按需使用
-
SKU 关联下载模板上传文件
-
箱号关联下载模板上传文件
-
SKU 混箱下载模板上传文件
+
统一贴标明细下载模板上传文件
-
+