From b506e4e59a2afc69d993c41089b627ef9a04b1a5 Mon Sep 17 00:00:00 2001 From: liuyihe <2513762445@qq.com> Date: Thu, 10 Sep 2026 17:12:52 +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 --- AGENTS.md | 2 +- .../views/WarehouseServiceView.vue | 210 ++++++++++++------ .../views/relabelAttachments.test.mjs | 46 +++- 3 files changed, 186 insertions(+), 72 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 4a9d048..2f076d3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -54,7 +54,7 @@ | 客户资金账户、授信与充值申请 | `src/modules/finance/views/FinanceAccountView.vue`、`api/account.ts` | 显示当前企业 CNY 实际余额、冻结金额、授信额度、已用授信、冻结授信、总可用余额和不可变流水;数据加载前显示“—”,不得先伪装为零。充值只展示服务端启用的微信/支付宝静态收款码,客户上传 JPG/PNG 付款截图后进入待审核。客户范围只取登录态,无收款码时禁止提交,不模拟支付成功 | | 海外采购、美国本土卡与美本土散板 | `src/modules/cross-border/routes.ts`、`components/BusinessOrdersView.vue`、`api/businessOrders.ts` | 三个路由均为 `active`,客户范围只取 Token;采购创建的 `customerName` 只取当前认证会话企业名称,不允许自由输入或冒用其他客户。Backend `lastleg-service/` 的 `procurement/`、`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+箱唛。模板上传和下载固定在货物明细外层:箱唛模式使用箱唛模板,SKU 模式使用包含我方箱唛、旧 SKU 标签、新 SKU 标签和换标数量的 SKU 模板,SKU+箱唛模板另含新 FBA 箱唛与目的仓。涉及箱唛换标时必须选择卡车、自提或快递;快递可选择打印面单并上传 PDF/图片。PDA 逐箱换标、上传照片并由仓库复核后,客户详情展示完工照片和“换标完成确认”;客户确认前不得继续出库,确认后卡车进入预排车,自提/快递进入换标出库待创建。历史库存位置不能解释成箱唛与具体托盘快照,客户页面不暴露托盘、库位和内部库存 ID | +| 海外仓贴标、仓储转运、货物废弃、退货换标 | `src/modules/overseas-warehouse/routes.ts`、`views/WarehouseServiceView.vue`、`api/warehouseServices.ts` | 四个路由均为 `active`;贴标申请先选国外中转仓和有库存历史入仓单,换标类型为只换 SKU、只换箱唛、SKU+箱唛。箱唛换标明细一行固定代表一箱,箱数不可编辑,提交也固定为 1。货物明细外层始终提供“SKU 关联”“箱号关联”“SKU 混箱换标”三套模板的下载、上传,不随换标类型切换;上传只进入待导入关系区,客户可勾选部分或全选后导入本次明细,只有进入明细的库存才随申请提交和冻结。明细也支持手动新增编辑,组合换标以旧箱唛连接旧/新 SKU 和旧箱唛/新 FBA 箱唛关系。涉及箱唛换标时必须选择卡车、自提或快递;快递可选择打印面单并上传 PDF/图片。PDA 逐箱换标、上传照片并由仓库复核后,客户详情展示完工照片和“换标完成确认”;客户确认前不得继续出库,确认后卡车进入预排车,自提/快递进入换标出库待创建。历史库存位置不能解释成箱唛与具体托盘快照,客户页面不暴露托盘、库位和内部库存 ID | ## 租户、认证与数据安全 diff --git a/src/modules/overseas-warehouse/views/WarehouseServiceView.vue b/src/modules/overseas-warehouse/views/WarehouseServiceView.vue index ee1486c..00de9c1 100644 --- a/src/modules/overseas-warehouse/views/WarehouseServiceView.vue +++ b/src/modules/overseas-warehouse/views/WarehouseServiceView.vue @@ -21,8 +21,10 @@ import { type ServiceType = 'labeling' | 'transfer' | 'disposal' | 'relabel' type PageMode = 'list' | 'create' | 'detail' type LabelingMode = 'carton' | 'sku' | 'carton_and_sku' -interface CargoRow { id: number; sku: string; name: string; quantity: number; availableQuantity?: number; sourceOrderId?: string; sourceInventoryBalanceId?: string; sourceInventoryPositionId?: string; sourceInboundOrderId?: string; sourceInboundOrderNo?: string; sourceInboundCode?: string; sourceWarehouseName?: string; palletNo?: string; warehouseLocationCode?: string; fnsku: string; shipmentNo: string; newSkuCode: string; shippingMark: string; cartonMark: string; fbaCartonMark: string; destinationWarehouseId: string; remark: string } +type RelabelTemplateType = 'carton' | 'sku' | 'mixed' +interface CargoRow { id: number; included?: boolean; sku: string; name: string; quantity: number; availableQuantity?: number; sourceOrderId?: string; sourceInventoryBalanceId?: string; sourceInventoryPositionId?: string; sourceInboundOrderId?: string; sourceInboundOrderNo?: string; sourceInboundCode?: string; sourceWarehouseName?: string; palletNo?: string; warehouseLocationCode?: string; fnsku: string; shipmentNo: string; newSkuCode: string; shippingMark: string; cartonMark: string; fbaCartonMark: string; destinationWarehouseId: string; remark: string } interface SkuRelabelRow { id: number; sourceRowId: number | null; cartonMark: string; originalSku: string; newSku: string; quantity: number; fbaCartonMark: string; destinationWarehouseId: string; remark: string } +interface ImportedRelabelBinding extends SkuRelabelRow { templateType: RelabelTemplateType; selected: boolean; shipmentNo: string } interface ServiceConfig { title: string; createTitle: string; numberLabel: string; emptyText: string; columns: string[] } const props = defineProps<{ service: ServiceType }>() @@ -54,6 +56,8 @@ const cargoRows = ref([]) const labelingMode = ref('carton') const shippingLabelFile = ref(null) const skuRelabelRows = ref([]) +const importedRelabelBindings = ref([]) +const cartonDetailRows = computed(() => cargoRows.value.filter((row) => row.included)) const labelingModeHint = computed(() => labelingMode.value === 'carton' ? '只替换外箱箱唛,不修改箱内商品 SKU 标签;按箱填写新的 FBA 箱唛和目的仓。' : labelingMode.value === 'sku' @@ -89,7 +93,9 @@ const relabelInboundOptions = computed(() => { }) const loading = ref(false) const saving = ref(false) -const templateInput = ref(null) +const cartonTemplateInput = ref(null) +const skuTemplateInput = ref(null) +const mixedTemplateInput = ref(null) let cargoSequence = 0 let skuRelabelSequence = 0 @@ -102,6 +108,7 @@ function resetForm() { cargoRows.value = [] labelingMode.value = 'carton' skuRelabelRows.value = [] + importedRelabelBindings.value = [] discardCandidates.value = [] selectedDiscardOrderId.value = '' relabelCandidates.value = [] @@ -127,6 +134,22 @@ function addSkuRelabelRow() { fbaCartonMark: '', destinationWarehouseId: source?.destinationWarehouseId || '', remark: '' }) } function removeSkuRelabelRow(id: number) { skuRelabelRows.value = skuRelabelRows.value.filter((row) => row.id !== id) } +function applyExistingSkuBinding(row: SkuRelabelRow) { + if (!row.originalSku.trim() || row.newSku.trim()) return + const mapping = skuRelabelRows.value.find((item) => item.id !== row.id + && item.originalSku.trim().toUpperCase() === row.originalSku.trim().toUpperCase() && item.newSku.trim()) + if (mapping) row.newSku = mapping.newSku +} +function applyExistingCartonBinding(row: SkuRelabelRow) { + if (!row.cartonMark.trim()) return + const cartonMark = row.cartonMark.trim().toUpperCase() + const mapping = cargoRows.value.find((item) => item.cartonMark.trim().toUpperCase() === cartonMark && item.fbaCartonMark.trim()) + || skuRelabelRows.value.find((item) => item.id !== row.id + && item.cartonMark.trim().toUpperCase() === cartonMark && item.fbaCartonMark.trim()) + if (!mapping) return + if (!row.fbaCartonMark.trim()) row.fbaCartonMark = mapping.fbaCartonMark + if (!row.destinationWarehouseId) row.destinationWarehouseId = mapping.destinationWarehouseId +} function selectShippingLabel(event: Event) { shippingLabelFile.value = (event.target as HTMLInputElement).files?.[0] || null } function buildSkuRelabelItems(): RelabelCreate['items'] { const groups = new Map() @@ -145,7 +168,7 @@ function buildSkuRelabelItems(): RelabelCreate['items'] { throw new Error('填写的箱唛数量超过当前入仓单可用库存箱数') } return [...groups.entries()].map(([_, rows], groupIndex) => { - const source = anchors[groupIndex] + const source = cargoRows.value.find((item) => item.id === rows[0].sourceRowId) || anchors[groupIndex] if (!source?.sourceInventoryBalanceId || !source.sourceInventoryPositionId || !source.sourceInboundOrderId) { throw new Error('来源库存已变化,请重新选择有库存的历史入仓单') } @@ -180,32 +203,40 @@ function buildSkuRelabelItems(): RelabelCreate['items'] { } function addRelabelCargoRow() { if (!selectedRelabelInboundOrderId.value) { ElMessage.warning('请先选择有库存的历史入仓单'); return } - ElMessage.info('货物明细已按所选入仓单的可用库存带出,请直接编辑现有明细') + const source = cargoRows.value.find((row) => !row.included) + if (!source) { ElMessage.warning('所选入仓单没有更多可添加的库存箱'); return } + source.included = true } function removeCargoRow(id: number) { + const item = cargoRows.value.find((row) => row.id === id) + if (props.service === 'labeling' && item?.sourceInventoryBalanceId) { + Object.assign(item, { included: false, shipmentNo: '', cartonMark: '', fbaCartonMark: '', destinationWarehouseId: '', remark: '' }) + return + } cargoRows.value = cargoRows.value.filter((row) => row.id !== id) } -function clearRelabelRows() { cargoRows.value = []; selectedRelabelInboundOrderId.value = '' } +function clearRelabelRows() { + for (const row of cargoRows.value) Object.assign(row, { included: false, shipmentNo: '', cartonMark: '', fbaCartonMark: '', destinationWarehouseId: '', remark: '' }) +} function csvCell(value: string | number | undefined) { return `"${String(value ?? '').replaceAll('"', '""')}"` } -function downloadRelabelTemplate() { +function downloadRelabelTemplate(templateType: RelabelTemplateType) { if (!cargoRows.value.length) { ElMessage.warning('请先选择有库存的历史入仓单'); return } - const cartonTemplate = labelingMode.value === 'carton' - const combinedTemplate = labelingMode.value === 'carton_and_sku' + const cartonTemplate = templateType === 'carton' + const mixedTemplate = templateType === 'mixed' const header = cartonTemplate - ? ['明细行标识', '本次箱数', '商品SKU', '中文品名', 'FBA货件编号', '我方箱唛', 'FBA箱唛', 'FBA目的仓', '备注'] - : ['我方箱唛', '旧SKU标签', '新SKU标签', '换标数量(件)', - ...(combinedTemplate ? ['新FBA箱唛', 'FBA目的仓'] : []), '备注'] - const rows = cartonTemplate - ? cargoRows.value.map((item) => [item.id, item.quantity, item.sku, item.name, item.shipmentNo, - item.cartonMark, item.fbaCartonMark, item.destinationWarehouseId, item.remark]) - : skuRelabelRows.value.map((item) => [item.cartonMark, item.originalSku, item.newSku, item.quantity, - ...(combinedTemplate ? [item.fbaCartonMark, item.destinationWarehouseId] : []), item.remark]) + ? ['明细行标识', '旧箱唛', '新FBA箱唛', 'FBA目的仓', 'FBA货件编号', '备注'] + : ['明细行标识', '旧箱唛', '旧SKU标签', '新SKU标签', '换标数量(件)', + ...(mixedTemplate ? ['新FBA箱唛', 'FBA目的仓', 'FBA货件编号'] : []), '备注'] + const rows = cargoRows.value.map((item) => cartonTemplate + ? [item.id, item.cartonMark, item.fbaCartonMark, item.destinationWarehouseId, item.shipmentNo, item.remark] + : [item.id, item.cartonMark, item.sku, '', 1, + ...(mixedTemplate ? [item.fbaCartonMark, item.destinationWarehouseId, 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' - : combinedTemplate ? '贴标申请-SKU和箱唛换标模板.csv' : '贴标申请-SKU换标模板.csv' + anchor.download = cartonTemplate ? '贴标申请-箱号关联模板.csv' + : mixedTemplate ? '贴标申请-SKU混箱换标模板.csv' : '贴标申请-SKU关联模板.csv' anchor.click(); URL.revokeObjectURL(url) } function parseCsvLine(line: string) { @@ -218,76 +249,112 @@ function parseCsvLine(line: string) { } values.push(value.trim()); return values } -async function importRelabelTemplate(event: Event) { +async function importRelabelTemplate(event: Event, templateType: RelabelTemplateType) { 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 } const headers = parseCsvLine(lines[0]) - if (labelingMode.value !== 'carton') { - const combinedTemplate = labelingMode.value === 'carton_and_sku' - const required = ['我方箱唛', '旧SKU标签', '新SKU标签', '换标数量(件)', - ...(combinedTemplate ? ['新FBA箱唛', 'FBA目的仓'] : [])] + if (templateType !== 'carton') { + const mixedTemplate = templateType === 'mixed' + const required = ['明细行标识', '旧箱唛', '旧SKU标签', '新SKU标签', '换标数量(件)', + ...(mixedTemplate ? ['新FBA箱唛', 'FBA目的仓', 'FBA货件编号'] : [])] if (required.some((name) => !headers.includes(name))) { - ElMessage.error('SKU 换标模板格式不正确,请重新下载当前换标类型的模板'); return + 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) => Object.values(item).some(Boolean)) + .filter((item) => item['旧箱唛'] || item['新SKU标签'] || item['新FBA箱唛']) if (!imported.length || imported.some((item) => required.some((key) => !item[key]))) { - ElMessage.error('我方箱唛、旧 SKU 标签、新 SKU 标签和换标数量必须填写完整'); return + ElMessage.error('旧箱唛、旧 SKU 标签、新 SKU 标签和换标数量必须填写完整'); return } - const rows: SkuRelabelRow[] = [] + const rows: ImportedRelabelBinding[] = [] for (const item of imported) { const quantity = Number(item['换标数量(件)']) if (!Number.isInteger(quantity) || quantity < 1) { - ElMessage.error(`箱唛 ${item['我方箱唛']} 的换标数量必须是大于 0 的整数`); return + 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: null, - cartonMark: item['我方箱唛'].trim(), + sourceRowId: source.id, + cartonMark: item['旧箱唛'].trim(), originalSku: item['旧SKU标签'].trim(), newSku: item['新SKU标签'].trim(), quantity, - fbaCartonMark: combinedTemplate ? item['新FBA箱唛'].trim() : '', - destinationWarehouseId: combinedTemplate ? item['FBA目的仓'].trim() : '', + fbaCartonMark: mixedTemplate ? item['新FBA箱唛'].trim() : '', + destinationWarehouseId: mixedTemplate ? item['FBA目的仓'].trim() : '', remark: item['备注'] || '', + templateType, + selected: true, + shipmentNo: mixedTemplate ? item['FBA货件编号'].trim() : '', }) } - skuRelabelRows.value = rows - ElMessage.success(`已导入 ${rows.length} 条 SKU 换标明细`) + importedRelabelBindings.value = rows + ElMessage.success(`已读取 ${rows.length} 条绑定关系,请选择后导入到明细`) return } - const required = ['明细行标识', '本次箱数', 'FBA货件编号', '我方箱唛', 'FBA箱唛', 'FBA目的仓'] - if (required.some((name) => !headers.includes(name))) { ElMessage.error('模板格式不正确,请重新下载模板填写'); return } + const required = ['明细行标识', '旧箱唛', '新FBA箱唛', '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['明细行标识']) + .filter((item) => item['旧箱唛'] || item['新FBA箱唛']) if (!imported.length || imported.some((item) => required.some((key) => !item[key]))) { - ElMessage.error('模板中的明细行、箱数、货件、箱唛和 FBA 目的仓必须填写完整'); return + ElMessage.error('模板中的明细行、旧箱唛、新 FBA 箱唛、FBA 目的仓和货件编号必须填写完整'); return } 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 !== cargoRows.value.length || cargoRows.value.some((row) => !importedByRow.has(String(row.id)))) { - ElMessage.error('模板明细与当前已选入仓单库存不一致,请重新下载模板'); return + if (importedByRow.size !== imported.length || [...importedByRow.keys()].some((id) => !cargoRows.value.some((row) => String(row.id) === id))) { + ElMessage.error('模板存在重复或不属于当前库存的明细行,请重新下载模板'); return } - for (const row of cargoRows.value) { - const item = importedByRow.get(String(row.id))! - const quantity = Number(item['本次箱数']) - if (!Number.isInteger(quantity) || quantity < 1 || quantity > Number(row.availableQuantity || 0)) { - ElMessage.error(`明细行 ${row.id} 的本次箱数不正确或超过可用库存`); 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: item['FBA目的仓'], remark: item['备注'] || '', templateType, + selected: true, shipmentNo: item['FBA货件编号'] }) + } + importedRelabelBindings.value = bindings + ElMessage.success(`已读取 ${bindings.length} 条箱号关联,请选择后导入到明细`) +} +function selectAllImportedBindings(value: boolean) { + for (const item of importedRelabelBindings.value) item.selected = value +} +function importSelectedBindingsToDetails() { + const selected = importedRelabelBindings.value.filter((item) => item.selected) + if (!selected.length) { ElMessage.warning('请至少选择一条绑定关系'); return } + const templateType = selected[0].templateType + if (selected.some((item) => item.templateType !== templateType)) { ElMessage.warning('请重新上传同一种绑定模板'); return } + if (templateType === 'carton') { + for (const binding of selected) { + const source = cargoRows.value.find((row) => row.id === binding.sourceRowId) + if (!source) continue + Object.assign(source, { included: true, quantity: 1, shipmentNo: binding.shipmentNo, + cartonMark: binding.cartonMark, fbaCartonMark: binding.fbaCartonMark, + destinationWarehouseId: binding.destinationWarehouseId, remark: binding.remark }) } - row.quantity = quantity - row.shipmentNo = item['FBA货件编号'] - row.cartonMark = item['我方箱唛'] - row.fbaCartonMark = item['FBA箱唛'] - row.destinationWarehouseId = item['FBA目的仓'] - row.remark = item['备注'] || '' + labelingMode.value = 'carton' + } else { + const importedRows = selected.map(({ templateType: _, selected: __, shipmentNo, ...item }) => item) + for (const item of importedRows) { + skuRelabelRows.value = skuRelabelRows.value.filter((row) => !(row.cartonMark.trim().toUpperCase() === item.cartonMark.trim().toUpperCase() + && row.originalSku.trim().toUpperCase() === item.originalSku.trim().toUpperCase())) + skuRelabelRows.value.push(item) + if (templateType === 'mixed') { + const source = cargoRows.value.find((row) => row.id === item.sourceRowId) + const shipmentNo = selected.find((binding) => binding.id === item.id)?.shipmentNo || '' + if (source) Object.assign(source, { included: true, cartonMark: item.cartonMark, + fbaCartonMark: item.fbaCartonMark, destinationWarehouseId: item.destinationWarehouseId, shipmentNo }) + } + } + labelingMode.value = templateType === 'mixed' ? 'carton_and_sku' : 'sku' } - ElMessage.success(`已导入 ${imported.length} 条关联面单明细`) + importedRelabelBindings.value = importedRelabelBindings.value.filter((item) => !item.selected) + ElMessage.success(`已将 ${selected.length} 条绑定关系导入本次换标明细`) } function selectDiscardInbound(orderId: string) { const candidate = discardCandidates.value.find((item) => item.orderId === orderId) @@ -356,14 +423,15 @@ async function load() { function query() { void load() } async function submit() { const primaryWarehouse = props.service === 'relabel' ? form.sourceWarehouse : form.warehouse - const relabelRows = labelingMode.value === 'carton' ? cargoRows.value : skuRelabelRows.value + const relabelRows = labelingMode.value === 'carton' ? cartonDetailRows.value : skuRelabelRows.value const invalidCargo = props.service === 'disposal' ? !selectedDiscardOrderId.value || cargoRows.value.every((item) => item.quantity < 1) || cargoRows.value.some((item) => item.quantity < 0 || item.quantity > Number(item.availableQuantity || 0)) : relabelRows.length === 0 || (props.service === 'labeling' && labelingMode.value !== 'carton' ? skuRelabelRows.value.some((item) => !item.cartonMark || !item.originalSku || !item.newSku || item.quantity < 1 || (labelingMode.value === 'carton_and_sku' && (!item.fbaCartonMark || !item.destinationWarehouseId))) - : cargoRows.value.some((item) => (props.service !== 'labeling' && !item.sku) || !item.name || item.quantity < 1)) + : cargoRows.value.some((item) => (props.service !== 'labeling' && !item.sku) || !item.name + || (props.service !== 'labeling' && item.quantity < 1))) if ((props.service === 'relabel' && !form.inboundNo) || (props.service !== 'disposal' && !primaryWarehouse) || invalidCargo) { ElMessage.warning('请填写必填信息和完整的货物明细'); return } @@ -377,10 +445,10 @@ async function submit() { && form.relabel && !shippingLabelFile.value) { ElMessage.warning('选择打印面单时必须上传面单文件'); return } - if (props.service === 'labeling' && labelingMode.value === 'carton' && cargoRows.value.some((item) => !item.shipmentNo || !item.cartonMark || !item.fbaCartonMark)) { + if (props.service === 'labeling' && labelingMode.value === 'carton' && cartonDetailRows.value.some((item) => !item.shipmentNo || !item.cartonMark || !item.fbaCartonMark)) { ElMessage.warning('请上传关联面单,确保每箱的我方箱唛和 FBA 箱唛完整'); return } - if (props.service === 'labeling' && labelingMode.value === 'carton' && cargoRows.value.some((item) => !item.destinationWarehouseId || !item.sourceInventoryPositionId)) { + if (props.service === 'labeling' && labelingMode.value === 'carton' && cartonDetailRows.value.some((item) => !item.destinationWarehouseId || !item.sourceInventoryPositionId)) { ElMessage.warning('请选择有库存的历史入仓单,并为每项填写 FBA 目的仓'); return } if (props.service === 'disposal' && !form.reason) { @@ -402,7 +470,7 @@ async function submit() { if (props.service === 'labeling') { const sourceInboundOrders = [...new Set(cargoRows.value.map((item) => item.sourceInboundOrderNo || item.sourceInboundCode).filter((value): value is string => Boolean(value)))].join('、').slice(0, 128) const items = labelingMode.value === 'carton' - ? cargoRows.value.map((item) => ({ sourceInventoryBalanceId: item.sourceInventoryBalanceId!, sourceInventoryPositionId: item.sourceInventoryPositionId!, sourceInboundOrderId: item.sourceInboundOrderId!, skuCode: item.sku, productNameZh: item.name, totalPackageCount: item.quantity, notifiedPackageCount: item.quantity, shipmentNo: item.shipmentNo, trackingCode: form.trackingNo || undefined, newSkuCode: undefined, labelRequirement: form.requirement || undefined, cartonMark: item.cartonMark, fbaCartonMark: item.fbaCartonMark, destinationFbaWarehouseCode: item.destinationWarehouseId, operationType: 'CARTON_LABEL' as const, skuItems: [], remark: item.remark || undefined })) + ? cartonDetailRows.value.map((item) => ({ sourceInventoryBalanceId: item.sourceInventoryBalanceId!, sourceInventoryPositionId: item.sourceInventoryPositionId!, sourceInboundOrderId: item.sourceInboundOrderId!, skuCode: item.sku, productNameZh: item.name, totalPackageCount: 1, notifiedPackageCount: 1, shipmentNo: item.shipmentNo, trackingCode: form.trackingNo || undefined, newSkuCode: undefined, labelRequirement: form.requirement || undefined, cartonMark: item.cartonMark, fbaCartonMark: item.fbaCartonMark, destinationFbaWarehouseCode: item.destinationWarehouseId, operationType: 'CARTON_LABEL' as const, skuItems: [], remark: item.remark || undefined })) : buildSkuRelabelItems() const created = await createRelabel({ customerOrderNo: sourceInboundOrders || '贴标申请', operationWarehouseId: form.warehouse, labelDeadline: form.labelDeadline, contactName: form.contact, contactPhone: form.phone, chargeAmount: form.chargeAmount, remark: form.remark || undefined, idempotencyKey, items, deliveryMethod: labelingMode.value === 'sku' ? undefined : form.deliveryMethod as 'TRUCK' | 'EXPRESS' | 'TRUCK_PICKUP' | 'EXPRESS_PICKUP', @@ -496,6 +564,7 @@ function selectTransferFbaAvailability(value: string | number | boolean | undefi async function selectOperationWarehouse() { cargoRows.value = [] skuRelabelRows.value = [] + importedRelabelBindings.value = [] selectedRelabelInboundOrderId.value = '' relabelCandidates.value = [] const warehouse = operationWarehouses.value.find((item) => String(item.id) === form.warehouse) @@ -512,7 +581,7 @@ function selectRelabelInventory(inboundOrderId: string) { .filter((candidate) => String(candidate.inboundOrderId) === inboundOrderId) .flatMap((candidate) => candidate.positions.flatMap((position) => Array.from({ length: Math.min(position.availablePackages, 500) }, () => ({ id: ++cargoSequence, sku: candidate.skuCode || '', name: candidate.productNameZh || '库存货物', - quantity: 1, availableQuantity: 1, + included: false, quantity: 1, availableQuantity: 1, sourceInventoryBalanceId: String(candidate.inventoryBalanceId), sourceInventoryPositionId: String(position.positionId), sourceInboundOrderId: String(candidate.inboundOrderId), sourceInboundOrderNo: candidate.inboundOrderNo, sourceInboundCode: candidate.inboundCode, sourceWarehouseName: candidate.overseasWarehouseName, @@ -520,7 +589,9 @@ function selectRelabelInventory(inboundOrderId: string) { fnsku: '', shipmentNo: '', newSkuCode: '', shippingMark: '', cartonMark: '', fbaCartonMark: '', destinationWarehouseId: candidate.destinationFbaWarehouseCode || '', remark: '', })))).slice(0, 500) + if (cargoRows.value.length) cargoRows.value[0].included = true skuRelabelRows.value = [] + importedRelabelBindings.value = [] shippingLabelFile.value = null if (labelingMode.value !== 'carton') addSkuRelabelRow() } @@ -597,9 +668,15 @@ function selectRelabelInventory(inboundOrderId: string) {

货物明细

- 下载{{ labelingMode === 'carton' ? '箱唛' : 'SKU' }}模板 - 上传{{ labelingMode === 'carton' ? '箱唛' : 'SKU' }}模板 - + 下载 SKU 关联模板 + 上传 SKU 关联 + 下载箱号关联模板 + 上传箱号关联 + 下载 SKU 混箱模板 + 上传 SKU 混箱 + + +
@@ -611,13 +688,17 @@ function selectRelabelInventory(inboundOrderId: string) {

{{ labelingModeHint }}

+
+
待导入绑定关系

可只勾选本次需要换标的部分,也可以全选后一次导入。

全选导入到明细
+
选择关系类型旧箱唛旧 SKU新 SKU数量新 FBA 箱唛FBA 目的仓
{{ item.templateType === 'mixed' ? 'SKU 混箱换标' : item.templateType === 'carton' ? '箱号关联' : 'SKU 关联' }}{{ item.cartonMark }}{{ item.originalSku || '—' }}{{ item.newSku || '—' }}{{ item.quantity }}{{ item.fbaCartonMark || '—' }}{{ warehouseDisplay(item.destinationWarehouseId) }}
+
-

系统只提供历史入仓单的大概库存位置,不代表箱唛已绑定具体托盘。

+
绑定明细

上传后在下方回显,也可以手动添加;SKU 与箱号关联通过旧箱唛合并。系统只提供大概库存位置,不代表箱唛已绑定具体托盘。

手动添加行清空
添加 SKU 明细清空
-
商品 SKU中文品名 *FBA 货件编号 *箱数 *我方箱唛 *新 FBA 箱唛 *FBA 目的仓 *备注操作
请先选择仍有库存的历史入仓单,系统将带出可用库存明细
-
我方箱唛 *原 SKU *换标后 SKU *换标数量(件)*新 FBA 箱唛 *FBA 目的仓 *备注操作
暂无换标明细,请点击“添加 SKU 明细”
+
商品 SKU中文品名 *FBA 货件编号 *箱数旧箱唛 *新 FBA 箱唛 *FBA 目的仓 *备注操作
1 箱
暂无本次换标明细,请上传关系后导入或手动添加
+
旧箱唛 *旧 SKU *新 SKU *换标数量(件)*新 FBA 箱唛 *FBA 目的仓 *备注操作
暂无绑定明细,请点击“添加 SKU 明细”