diff --git a/AGENTS.md b/AGENTS.md index 26ff0ff..4a9d048 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+箱唛。涉及箱唛换标时必须选择卡车、自提或快递;快递可选择打印面单并上传 PDF/图片。PDA 逐箱换标、上传照片并由仓库复核后,客户详情展示完工照片和“换标完成确认”;客户确认前不得继续出库,确认后卡车进入预排车,自提/快递进入换标出库待创建。历史库存位置不能解释成箱唛与具体托盘快照,客户页面不暴露托盘、库位和内部库存 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 | ## 租户、认证与数据安全 diff --git a/src/modules/overseas-warehouse/views/WarehouseServiceView.vue b/src/modules/overseas-warehouse/views/WarehouseServiceView.vue index 13e0205..ee1486c 100644 --- a/src/modules/overseas-warehouse/views/WarehouseServiceView.vue +++ b/src/modules/overseas-warehouse/views/WarehouseServiceView.vue @@ -189,13 +189,24 @@ function clearRelabelRows() { cargoRows.value = []; selectedRelabelInboundOrderI function csvCell(value: string | number | undefined) { return `"${String(value ?? '').replaceAll('"', '""')}"` } function downloadRelabelTemplate() { if (!cargoRows.value.length) { ElMessage.warning('请先选择有库存的历史入仓单'); return } - const header = ['明细行标识', '本次箱数', '商品SKU', '中文品名', 'FBA货件编号', '我方箱唛', 'FBA箱唛', 'FBA目的仓', '备注'] - const rows = cargoRows.value.map((item) => [item.id, item.quantity, item.sku, item.name, item.shipmentNo, - item.cartonMark, item.fbaCartonMark, item.destinationWarehouseId, item.remark]) + const cartonTemplate = labelingMode.value === 'carton' + const combinedTemplate = labelingMode.value === 'carton_and_sku' + 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]) 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 = '贴标申请-关联面单模板.csv'; anchor.click(); URL.revokeObjectURL(url) + anchor.href = url + anchor.download = cartonTemplate ? '贴标申请-箱唛换标模板.csv' + : combinedTemplate ? '贴标申请-SKU和箱唛换标模板.csv' : '贴标申请-SKU换标模板.csv' + anchor.click(); URL.revokeObjectURL(url) } function parseCsvLine(line: string) { const values: string[] = []; let value = ''; let quoted = false @@ -213,6 +224,41 @@ async function importRelabelTemplate(event: Event) { 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 (required.some((name) => !headers.includes(name))) { + ElMessage.error('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)) + if (!imported.length || imported.some((item) => required.some((key) => !item[key]))) { + ElMessage.error('我方箱唛、旧 SKU 标签、新 SKU 标签和换标数量必须填写完整'); return + } + const rows: SkuRelabelRow[] = [] + for (const item of imported) { + const quantity = Number(item['换标数量(件)']) + if (!Number.isInteger(quantity) || quantity < 1) { + ElMessage.error(`箱唛 ${item['我方箱唛']} 的换标数量必须是大于 0 的整数`); return + } + rows.push({ + id: ++skuRelabelSequence, + sourceRowId: null, + cartonMark: item['我方箱唛'].trim(), + originalSku: item['旧SKU标签'].trim(), + newSku: item['新SKU标签'].trim(), + quantity, + fbaCartonMark: combinedTemplate ? item['新FBA箱唛'].trim() : '', + destinationWarehouseId: combinedTemplate ? item['FBA目的仓'].trim() : '', + remark: item['备注'] || '', + }) + } + skuRelabelRows.value = rows + ElMessage.success(`已导入 ${rows.length} 条 SKU 换标明细`) + return + } const required = ['明细行标识', '本次箱数', 'FBA货件编号', '我方箱唛', 'FBA箱唛', 'FBA目的仓'] if (required.some((name) => !headers.includes(name))) { ElMessage.error('模板格式不正确,请重新下载模板填写'); return } const imported = lines.slice(1).map(parseCsvLine) @@ -548,7 +594,14 @@ function selectRelabelInventory(inboundOrderId: string) {