From 958da32b001d7425298a686ad1ca1f84d06a613e Mon Sep 17 00:00:00 2001 From: liuyihe <2513762445@qq.com> Date: Thu, 17 Sep 2026 16:34:51 +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 +- .../api/warehouseServices.ts | 4 +- .../views/WarehouseServiceView.vue | 139 ++++++++++++------ .../views/relabelAttachments.test.mjs | 15 +- .../views/warehouseTransferForm.test.mjs | 19 ++- 5 files changed, 122 insertions(+), 57 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index ce0f91d..da6bea4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -56,7 +56,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`;退货预报另见本节独立入口;贴标申请先选国外中转仓和有库存历史入仓单,换标申请必须提交正数 CNY `chargeAmount`,换标类型为只换 SKU、只换箱唛、SKU+箱唛。箱唛换标明细一行固定代表一箱,箱数不可编辑,提交也固定为 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 | ## 租户、认证与数据安全 diff --git a/src/modules/overseas-warehouse/api/warehouseServices.ts b/src/modules/overseas-warehouse/api/warehouseServices.ts index 59506a6..1f7b2ca 100644 --- a/src/modules/overseas-warehouse/api/warehouseServices.ts +++ b/src/modules/overseas-warehouse/api/warehouseServices.ts @@ -31,6 +31,8 @@ export interface RelabelInventoryCandidate { destinationFbaWarehouseCode?: string; destinationFbaWarehouseName?: string; skuCode?: string; productNameZh?: string availablePackages: number; version: number positions: Array<{ positionId: string; palletNo?: string; batchNo?: string; warehouseLocationCode?: string; availablePackages: number }> + sourceType?: 'INBOUND' | 'RETURN_CARTON'; returnCartonId?: string; cartonMark?: string + skuItems?: Array<{ skuCode: string; newSkuCode?: string; quantity: number; remark?: string }> } export interface TransferCreate { @@ -46,7 +48,7 @@ export interface TransferCreate { export interface RelabelCreate { customerOrderNo: string; operationWarehouseId: string; labelDeadline: string; contactName: string; contactPhone: string; chargeAmount?: number remark?: string; idempotencyKey: string; deliveryMethod?: RelabelDeliveryMethod; printShippingLabel?: boolean - items: Array<{ sourceInventoryBalanceId: string; sourceInventoryPositionId: string; sourceInboundOrderId: string; skuCode?: string; productNameZh: string; totalPackageCount: number; notifiedPackageCount: number; shipmentNo: string; trackingCode?: string; newSkuCode?: string; labelRequirement?: string + items: Array<{ sourceInventoryBalanceId?: string; sourceInventoryPositionId?: string; sourceInboundOrderId?: string; returnCartonId?: string; skuCode?: string; productNameZh: string; totalPackageCount: number; notifiedPackageCount: number; shipmentNo: string; trackingCode?: string; newSkuCode?: string; labelRequirement?: string cartonMark: string; fbaCartonMark?: string; destinationFbaWarehouseCode?: string; operationType: 'CARTON_LABEL' | 'SKU_LABEL' | 'CARTON_AND_SKU'; skuItems?: Array<{ skuCode: string; newSkuCode: string; quantity: number; remark?: string }>; remark?: string }> } diff --git a/src/modules/overseas-warehouse/views/WarehouseServiceView.vue b/src/modules/overseas-warehouse/views/WarehouseServiceView.vue index a3ef7ae..2856644 100644 --- a/src/modules/overseas-warehouse/views/WarehouseServiceView.vue +++ b/src/modules/overseas-warehouse/views/WarehouseServiceView.vue @@ -26,7 +26,7 @@ type ServiceType = 'labeling' | 'transfer' | 'disposal' | 'relabel' type PageMode = 'list' | 'create' | 'edit' | 'detail' type LabelingMode = 'carton' | 'sku' | 'carton_and_sku' 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; customerCartonMarks: string; cartonMark: string; fbaCartonMark: string; destinationWarehouseId: string; remark: string } +interface CargoRow { id: number; included?: boolean; sku: string; name: string; quantity: number; availableQuantity?: number; sourceOrderId?: string; sourceInventoryBalanceId?: string; sourceInventoryPositionId?: string; sourceInboundOrderId?: string; returnCartonId?: string; sourceInboundOrderNo?: string; sourceInboundCode?: string; sourceWarehouseName?: string; palletNo?: string; warehouseLocationCode?: string; fnsku: string; shipmentNo: string; newSkuCode: string; shippingMark: string; customerCartonMarks: 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[] } @@ -67,16 +67,17 @@ const operationWarehouseAddress = computed(() => { }) const cargoRows = ref([]) const transferRelabelRows = ref([]) +const transferRelabelDefaultDestination = 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 箱唛和目的仓。' + ? '只替换外箱箱唛,不修改箱内商品 SKU 标签;整张申请统一使用订单信息中的 FBA 目的仓。' : labelingMode.value === 'sku' ? '只替换箱内商品 SKU 标签,保留原箱唛;同一箱唛可以添加多条 SKU 明细。' - : '同一箱同时替换商品 SKU 标签和外箱箱唛;同一箱唛的多条 SKU 使用同一个新 FBA 箱唛和目的仓。') + : '同一箱同时替换商品 SKU 标签和外箱箱唛;整张申请统一使用订单信息中的 FBA 目的仓。') const submittedRows = ref[]>([]) const page = ref(1) const pageSize = ref(15) @@ -123,13 +124,13 @@ const relabelCandidatesLoading = ref(false) const relabelInboundOptions = computed(() => { const grouped = new Map() for (const candidate of relabelCandidates.value) { - const value = String(candidate.inboundOrderId) + const value = candidate.sourceType === 'RETURN_CARTON' ? `RETURN_CARTON:${candidate.returnCartonId}` : String(candidate.inboundOrderId) const current = grouped.get(value) if (current) current.availablePackages += candidate.availablePackages else grouped.set(value, { value, - inboundOrderNo: candidate.inboundOrderNo || '—', - inboundCode: candidate.inboundCode || '—', + inboundOrderNo: candidate.sourceType === 'RETURN_CARTON' ? `退货合箱 ${candidate.cartonMark || '—'}` : (candidate.inboundOrderNo || '—'), + inboundCode: candidate.sourceType === 'RETURN_CARTON' ? '仓库唛头库存' : (candidate.inboundCode || '—'), availablePackages: candidate.availablePackages, }) } @@ -160,6 +161,12 @@ watch(() => form.relabel, (value) => { } }) watch(labelingMode, (value) => { if (value !== 'carton' && !skuRelabelRows.value.length) addSkuRelabelRow() }) +watch(() => form.destination, (value) => { + if (props.service !== 'labeling') return + for (const row of cargoRows.value) row.destinationWarehouseId = value + for (const row of skuRelabelRows.value) row.destinationWarehouseId = value + for (const row of importedRelabelBindings.value) row.destinationWarehouseId = value +}) function refreshWarehouseMarkLabels() { if (props.service !== 'labeling') return const root = document.querySelector('.warehouse-service-page') @@ -181,6 +188,7 @@ function resetForm() { Object.assign(form, { customerNo: '', inboundNo: '', sourceWarehouse: '', warehouse: '', destination: '', privateDestinationAddress: '', privateDestinationPostalCode: '', privateDestinationContact: '', privateDestinationPhone: '', address: '', contact: '', phone: '', pickupContact: '', pickupPhone: '', relabelContact: '', relabelPhone: '', deliveryMethod: '', relabelDeliveryMethod: '', trackingNo: '', reason: '', requirement: '', relabel: false, remark: '', labelDeadline: '', estimatedArrivalAt: '', estimatedShipAt: '', fbaShipmentNo: '', volumeCbm: 0.01 }) cargoRows.value = [] transferRelabelRows.value = [] + transferRelabelDefaultDestination.value = '' labelingMode.value = 'carton' skuRelabelRows.value = [] importedRelabelBindings.value = [] @@ -234,6 +242,8 @@ async function openEdit(transferNo: string) { fbaCartonMark: binding.fbaCartonMark || '', destinationWarehouseId: binding.destinationWarehouseCode || '', remark: binding.remark || '', })) + const destinations = [...new Set(bindings.map((binding) => binding.destinationWarehouseCode).filter(Boolean))] + transferRelabelDefaultDestination.value = destinations.length === 1 ? destinations[0]! : '' } if (form.warehouse) { try { selectedOperationWarehouse.value = await getMerchantWarehouseDetail(form.warehouse) } catch { selectedOperationWarehouse.value = null } @@ -265,7 +275,7 @@ function addSkuRelabelRow() { const source = cargoRows.value[Math.min(skuRelabelRows.value.length, Math.max(0, cargoRows.value.length - 1))] skuRelabelRows.value.push({ id: ++skuRelabelSequence, sourceRowId: source?.id || null, cartonMark: '', originalSku: source?.sku || '', newSku: '', quantity: 1, - fbaCartonMark: '', destinationWarehouseId: source?.destinationWarehouseId || '', remark: '' }) + fbaCartonMark: '', destinationWarehouseId: form.destination, remark: '' }) } function removeSkuRelabelRow(id: number) { skuRelabelRows.value = skuRelabelRows.value.filter((row) => row.id !== id) } function applyExistingSkuBinding(row: SkuRelabelRow) { @@ -315,21 +325,21 @@ function buildSkuRelabelItems(): RelabelCreate['items'] { } return [...groups.entries()].map(([_, rows], groupIndex) => { const source = cargoRows.value.find((item) => item.id === rows[0].sourceRowId) || anchors[groupIndex] - if (!source?.sourceInventoryBalanceId || !source.sourceInventoryPositionId || !source.sourceInboundOrderId) { + if (!source?.returnCartonId && (!source?.sourceInventoryBalanceId || !source.sourceInventoryPositionId || !source.sourceInboundOrderId)) { throw new Error('来源库存已变化,请重新选择有库存的历史入仓单') } const cartonMark = rows[0].cartonMark.trim() const changeCarton = labelingMode.value === 'carton_and_sku' const fbaCartonMark = rows[0].fbaCartonMark.trim() - const destinationWarehouseId = rows[0].destinationWarehouseId - if (changeCarton && (!fbaCartonMark || !destinationWarehouseId - || rows.some((row) => row.fbaCartonMark.trim() !== fbaCartonMark || row.destinationWarehouseId !== destinationWarehouseId))) { - throw new Error('同一我方箱唛的组合换标必须使用相同的新 FBA 箱唛和目的仓') + if (changeCarton && (!fbaCartonMark || !form.destination + || rows.some((row) => row.fbaCartonMark.trim() !== fbaCartonMark))) { + throw new Error('同一我方箱唛的组合换标必须使用相同的新 FBA 箱唛,并选择整票 FBA 目的仓') } return { sourceInventoryBalanceId: source.sourceInventoryBalanceId, sourceInventoryPositionId: source.sourceInventoryPositionId, sourceInboundOrderId: source.sourceInboundOrderId, + returnCartonId: source.returnCartonId, skuCode: source.sku || rows[0].originalSku, productNameZh: source.name, totalPackageCount: 1, @@ -340,7 +350,7 @@ function buildSkuRelabelItems(): RelabelCreate['items'] { labelRequirement: form.requirement || undefined, cartonMark, fbaCartonMark: changeCarton ? fbaCartonMark : undefined, - destinationFbaWarehouseCode: changeCarton ? destinationWarehouseId : undefined, + destinationFbaWarehouseCode: changeCarton ? form.destination : undefined, operationType: changeCarton ? 'CARTON_AND_SKU' as const : 'SKU_LABEL' as const, skuItems: rows.map((row) => ({ skuCode: row.originalSku.trim(), newSkuCode: row.newSku.trim(), quantity: row.quantity, remark: row.remark || undefined })), remark: undefined, @@ -402,14 +412,16 @@ function downloadRelabelTemplate(templateType: RelabelTemplateType) { if (!cargoRows.value.length) { ElMessage.warning('请先选择有库存的历史入仓单'); return } const cartonTemplate = templateType === 'carton' const mixedTemplate = templateType === 'mixed' + const includeDestination = false + const cartonMarkHeader = props.service === 'transfer' ? '客户箱唛号' : '仓库唛头' const header = cartonTemplate - ? ['明细行标识', '仓库唛头', '新FBA箱唛', 'FBA目的仓', 'FBA货件编号', '备注'] - : ['明细行标识', '仓库唛头', '旧SKU标签', '新SKU标签', '换标数量(件)', - ...(mixedTemplate ? ['新FBA箱唛', 'FBA目的仓', 'FBA货件编号'] : []), '备注'] + ? ['明细行标识', 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, item.destinationWarehouseId, item.shipmentNo, item.remark] + ? [item.id, item.cartonMark, item.fbaCartonMark, ...(includeDestination ? [item.destinationWarehouseId] : []), item.shipmentNo, item.remark] : [item.id, item.cartonMark, item.sku, '', 1, - ...(mixedTemplate ? [item.fbaCartonMark, item.destinationWarehouseId, item.shipmentNo] : []), item.remark]) + ...(mixedTemplate ? [item.fbaCartonMark, ...(includeDestination ? [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') @@ -434,11 +446,11 @@ async function importRelabelTemplate(event: Event, templateType: RelabelTemplate 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) + 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目的仓', 'FBA货件编号'] : [])] + ...(mixedTemplate ? ['新FBA箱唛', 'FBA货件编号'] : [])] if (required.some((name) => !headers.includes(name))) { ElMessage.error('SKU 关联模板格式不正确,请重新下载 SKU 关联模板'); return } @@ -464,7 +476,7 @@ async function importRelabelTemplate(event: Event, templateType: RelabelTemplate newSku: item['新SKU标签'].trim(), quantity, fbaCartonMark: mixedTemplate ? item['新FBA箱唛'].trim() : '', - destinationWarehouseId: mixedTemplate ? item['FBA目的仓'].trim() : '', + destinationWarehouseId: form.destination, remark: item['备注'] || '', templateType, selected: true, @@ -475,13 +487,13 @@ async function importRelabelTemplate(event: Event, templateType: RelabelTemplate ElMessage.success(`已读取 ${rows.length} 条绑定关系,请选择后导入到明细`) return } - const required = ['明细行标识', '旧箱唛', '新FBA箱唛', 'FBA目的仓', 'FBA货件编号'] + 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 箱唛、FBA 目的仓和货件编号必须填写完整'); return + ElMessage.error('模板中的明细行、仓库唛头、新 FBA 箱唛和货件编号必须填写完整'); return } if (new Set(imported.map((item) => item['明细行标识'])).size !== imported.length) { ElMessage.error('模板中的明细行标识不能重复'); return @@ -495,7 +507,7 @@ async function importRelabelTemplate(event: Event, templateType: RelabelTemplate 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, + destinationWarehouseId: form.destination, remark: item['备注'] || '', templateType, selected: true, shipmentNo: item['FBA货件编号'] }) } importedRelabelBindings.value = bindings @@ -509,13 +521,34 @@ function importSelectedBindingsToDetails() { if (!selected.length) { ElMessage.warning('请至少选择一条绑定关系'); return } const templateType = selected[0].templateType if (selected.some((item) => item.templateType !== templateType)) { ElMessage.warning('请重新上传同一种绑定模板'); return } + if (props.service === 'transfer') { + const importedRows = selected.map((binding) => ({ + ...createCargoRow(), + sku: binding.originalSku, + newSkuCode: binding.newSku, + quantity: binding.quantity, + customerCartonMarks: binding.cartonMark, + fbaCartonMark: binding.fbaCartonMark, + destinationWarehouseId: '', + remark: binding.remark, + })) + const importedMarks = new Set(importedRows.flatMap(transferCustomerMarks).map((mark) => mark.toUpperCase())) + transferRelabelRows.value = [ + ...transferRelabelRows.value.filter((row) => !transferCustomerMarks(row).some((mark) => importedMarks.has(mark.toUpperCase()))), + ...importedRows, + ] + labelingMode.value = templateType === 'carton' ? 'carton' : templateType === 'sku' ? 'sku' : 'carton_and_sku' + importedRelabelBindings.value = importedRelabelBindings.value.filter((item) => !item.selected) + ElMessage.success(`已导入 ${importedRows.length} 条仓储转运换标明细`) + 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 }) + destinationWarehouseId: form.destination, remark: binding.remark }) } labelingMode.value = 'carton' } else { @@ -523,12 +556,13 @@ function importSelectedBindingsToDetails() { 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())) + item.destinationWarehouseId = form.destination 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 }) + fbaCartonMark: item.fbaCartonMark, destinationWarehouseId: form.destination, shipmentNo }) } } labelingMode.value = templateType === 'mixed' ? 'carton_and_sku' : 'sku' @@ -634,7 +668,7 @@ async function submit(action: 'draft' | 'submit' = 'submit') { || cargoRows.value.some((item) => item.quantity < 0 || item.quantity > Number(item.availableQuantity || 0)) : (props.service === 'labeling' || props.service === 'relabel') && (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))) + || (labelingMode.value === 'carton_and_sku' && !item.fbaCartonMark)) : cargoRows.value.some((item) => (props.service !== 'labeling' && !item.sku) || !item.name || (props.service !== 'labeling' && item.quantity < 1)))) if ((props.service === 'relabel' && !form.inboundNo) || !primaryWarehouse || invalidCargo) { @@ -646,6 +680,9 @@ async function submit(action: 'draft' | 'submit' = 'submit') { if (props.service === 'labeling' && labelingMode.value !== 'sku' && !form.deliveryMethod) { ElMessage.warning('请选择换标完成后的派送方式'); return } + if (props.service === 'labeling' && labelingMode.value !== 'sku' && !form.destination) { + ElMessage.warning('请选择本张贴标申请统一使用的 FBA 目的仓'); return + } if (props.service === 'labeling' && labelingMode.value !== 'sku' && (form.deliveryMethod === 'EXPRESS' || form.deliveryMethod === 'EXPRESS_PICKUP') && form.relabel && !shippingLabelFile.value) { ElMessage.warning('选择打印面单时必须上传面单文件'); return @@ -653,8 +690,8 @@ async function submit(action: 'draft' | 'submit' = 'submit') { 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' && cartonDetailRows.value.some((item) => !item.destinationWarehouseId || !item.sourceInventoryPositionId)) { - ElMessage.warning('请选择有库存的历史入仓单,并为每项填写 FBA 目的仓'); return + if (props.service === 'labeling' && labelingMode.value === 'carton' && cartonDetailRows.value.some((item) => !item.sourceInventoryPositionId)) { + ElMessage.warning('请选择有库存的历史入仓单'); return } if (props.service === 'disposal' && !form.reason) { ElMessage.warning('请选择废弃原因'); return @@ -674,13 +711,13 @@ async function submit(action: 'draft' | 'submit' = 'submit') { } if (props.service === 'transfer' && form.relabel) { if (!form.labelDeadline || !form.relabelContact || !form.relabelPhone || (labelingMode.value !== 'sku' && !form.relabelDeliveryMethod)) { ElMessage.warning('请填写换标截止时间、换标联系人和换标后派送方式'); return } + if (!transferRelabelDefaultDestination.value) { ElMessage.warning('请选择整票换标货物的 FBA 目的仓'); return } const relabelRows = transferRelabelRows.value const allMarks = relabelRows.flatMap(transferCustomerMarks) if (!relabelRows.length || relabelRows.some((row) => transferCustomerMarks(row).length !== row.quantity)) { ElMessage.warning('每个换标明细的客户箱唛数量必须与换标数量一致'); return } if (new Set(allMarks.map((mark) => mark.toUpperCase())).size !== allMarks.length) { ElMessage.warning('客户箱唛号不能重复'); return } if (allMarks.join(',').length > 2000 || relabelRows.some((row) => transferCustomerMarks(row).join(',').length > 255)) { ElMessage.warning('客户箱唛内容过长,请减少单次提交箱数或缩短箱唛号'); return } - if (labelingMode.value !== 'sku' && !form.destination) { ElMessage.warning('箱唛换标需要先在入库与转运信息中选择目的 FBA 仓'); return } - if (relabelRows.some((row) => row.quantity < 1 || (labelingMode.value !== 'carton' && (!row.sku.trim() || !row.newSkuCode.trim())) || (labelingMode.value !== 'sku' && !row.fbaCartonMark.trim()))) { ElMessage.warning('请按当前换标类型完整填写换标明细'); return } + if (relabelRows.some((row) => row.quantity < 1 || (labelingMode.value !== 'carton' && (!row.sku.trim() || !row.newSkuCode.trim())) || (labelingMode.value !== 'sku' && !row.fbaCartonMark.trim()))) { ElMessage.warning('请按当前换标类型填写完整明细'); return } } if (props.service !== 'transfer' || action === 'submit') { try { await ElMessageBox.confirm(`确认提交${config.value.title}吗?`, '提交确认', { confirmButtonText: '提交', cancelButtonText: '取消' }) } catch { return } @@ -691,7 +728,7 @@ async function submit(action: 'draft' | 'submit' = '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' - ? 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 })) + ? cartonDetailRows.value.map((item) => ({ sourceInventoryBalanceId: item.sourceInventoryBalanceId, sourceInventoryPositionId: item.sourceInventoryPositionId, sourceInboundOrderId: item.sourceInboundOrderId, returnCartonId: item.returnCartonId, 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: form.destination, 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, remark: form.remark || undefined, idempotencyKey, items, deliveryMethod: labelingMode.value === 'sku' ? undefined : form.deliveryMethod as 'TRUCK' | 'EXPRESS' | 'TRUCK_PICKUP' | 'EXPRESS_PICKUP', @@ -722,7 +759,7 @@ async function submit(action: 'draft' | 'submit' = 'submit') { const transferRemark = [form.remark, form.relabel && form.requirement ? `换标要求:${form.requirement}` : ''].filter(Boolean).join(';') const operationType = labelingMode.value === 'carton' ? 'CARTON_LABEL' : labelingMode.value === 'sku' ? 'SKU_LABEL' : 'CARTON_AND_SKU' const selectedFba = fbaWarehouses.value.find((warehouse) => warehouse.warehouseCode === form.destination) - const payload: TransferCreate = { operatingWarehouseId: primaryWarehouse, destinationType: transferDestinationType.value, fbaWarehouseCode: transferDestinationType.value === 'FBA' ? form.destination || undefined : undefined, destinationWarehouseId: transferDestinationType.value === 'FBA' ? String(selectedFba?.id || '') || undefined : undefined, destinationWarehouseCode: transferDestinationType.value === 'FBA' ? form.destination || undefined : undefined, destinationWarehouseName: transferDestinationType.value === 'FBA' ? selectedFba?.warehouseName : undefined, privateDestinationAddress: transferDestinationType.value === 'PRIVATE' ? form.privateDestinationAddress : undefined, privateDestinationPostalCode: transferDestinationType.value === 'PRIVATE' ? form.privateDestinationPostalCode : undefined, privateDestinationContactName: transferDestinationType.value === 'PRIVATE' ? form.privateDestinationContact : undefined, privateDestinationContactPhone: transferDestinationType.value === 'PRIVATE' ? form.privateDestinationPhone : undefined, deliveryAddress: form.address, packageCount, volumeCbm: form.volumeCbm, deliveryMethod: form.deliveryMethod, estimatedArrivalAt: form.estimatedArrivalAt, estimatedShipAt: form.estimatedShipAt || undefined, fbaShipmentNo: transferDestinationType.value === 'FBA' ? form.fbaShipmentNo || undefined : undefined, trackingNo: form.deliveryMethod === 'CUSTOMER_DELIVERY' ? form.trackingNo || undefined : undefined, fbaBoxNos: customerCartonMarks.length ? customerCartonMarks.join(',') : undefined, remark: transferRemark || undefined, needRelabel: props.service === 'relabel' || form.relabel, labelDeadline: form.relabel ? form.labelDeadline : undefined, pickupContactName: form.deliveryMethod === 'WAREHOUSE_PICKUP' ? form.pickupContact : undefined, pickupContactPhone: form.deliveryMethod === 'WAREHOUSE_PICKUP' ? form.pickupPhone : undefined, relabelContactName: form.relabel ? form.relabelContact : undefined, relabelContactPhone: form.relabel ? form.relabelPhone : undefined, relabelDeliveryMethod: form.relabel && labelingMode.value !== 'sku' ? form.relabelDeliveryMethod as 'TRUCK' | 'EXPRESS' | 'TRUCK_PICKUP' | 'EXPRESS_PICKUP' : undefined, idempotencyKey, items: cargoRows.value.map((item) => ({ skuCode: item.sku, productNameZh: item.name, packageCount: item.quantity, shippingMark: transferCustomerMarks(item).join(',') || undefined })), relabelItems: form.relabel ? transferRelabelRows.value.map((item) => ({ operationType, customerCartonMarks: transferCustomerMarks(item), originalSku: item.sku, newSku: item.newSkuCode || undefined, quantity: item.quantity, fbaCartonMark: item.fbaCartonMark || undefined, destinationWarehouseCode: labelingMode.value === 'sku' ? undefined : form.destination || undefined, remark: item.remark || undefined })) : undefined } + const payload: TransferCreate = { operatingWarehouseId: primaryWarehouse, destinationType: transferDestinationType.value, fbaWarehouseCode: transferDestinationType.value === 'FBA' ? form.destination || undefined : undefined, destinationWarehouseId: transferDestinationType.value === 'FBA' ? String(selectedFba?.id || '') || undefined : undefined, destinationWarehouseCode: transferDestinationType.value === 'FBA' ? form.destination || undefined : undefined, destinationWarehouseName: transferDestinationType.value === 'FBA' ? selectedFba?.warehouseName : undefined, privateDestinationAddress: transferDestinationType.value === 'PRIVATE' ? form.privateDestinationAddress : undefined, privateDestinationPostalCode: transferDestinationType.value === 'PRIVATE' ? form.privateDestinationPostalCode : undefined, privateDestinationContactName: transferDestinationType.value === 'PRIVATE' ? form.privateDestinationContact : undefined, privateDestinationContactPhone: transferDestinationType.value === 'PRIVATE' ? form.privateDestinationPhone : undefined, deliveryAddress: form.address, packageCount, volumeCbm: form.volumeCbm, deliveryMethod: form.deliveryMethod, estimatedArrivalAt: form.estimatedArrivalAt, estimatedShipAt: form.estimatedShipAt || undefined, fbaShipmentNo: transferDestinationType.value === 'FBA' ? form.fbaShipmentNo || undefined : undefined, trackingNo: form.deliveryMethod === 'CUSTOMER_DELIVERY' ? form.trackingNo || undefined : undefined, fbaBoxNos: customerCartonMarks.length ? customerCartonMarks.join(',') : undefined, remark: transferRemark || undefined, needRelabel: props.service === 'relabel' || form.relabel, labelDeadline: form.relabel ? form.labelDeadline : undefined, pickupContactName: form.deliveryMethod === 'WAREHOUSE_PICKUP' ? form.pickupContact : undefined, pickupContactPhone: form.deliveryMethod === 'WAREHOUSE_PICKUP' ? form.pickupPhone : undefined, relabelContactName: form.relabel ? form.relabelContact : undefined, relabelContactPhone: form.relabel ? form.relabelPhone : undefined, relabelDeliveryMethod: form.relabel && labelingMode.value !== 'sku' ? form.relabelDeliveryMethod as 'TRUCK' | 'EXPRESS' | 'TRUCK_PICKUP' | 'EXPRESS_PICKUP' : undefined, idempotencyKey, items: cargoRows.value.map((item) => ({ skuCode: item.sku, productNameZh: item.name, packageCount: item.quantity, shippingMark: transferCustomerMarks(item).join(',') || undefined })), relabelItems: form.relabel ? transferRelabelRows.value.map((item) => ({ operationType, customerCartonMarks: transferCustomerMarks(item), originalSku: item.sku, newSku: item.newSkuCode || undefined, quantity: item.quantity, fbaCartonMark: item.fbaCartonMark || undefined, destinationWarehouseCode: transferRelabelDefaultDestination.value || undefined, remark: item.remark || undefined })) : undefined } const created = editingTransferNo.value ? await updateTransfer(editingTransferNo.value, payload) : await createTransfer(payload) @@ -912,9 +949,16 @@ async function selectOperationWarehouse() { finally { relabelCandidatesLoading.value = false } } function selectRelabelInventory(inboundOrderId: string) { - cargoRows.value = relabelCandidates.value - .filter((candidate) => String(candidate.inboundOrderId) === inboundOrderId) - .flatMap((candidate) => candidate.positions.flatMap((position) => Array.from({ length: Math.min(position.availablePackages, 500) }, () => ({ + const rows: CargoRow[] = [] + for (const candidate of relabelCandidates.value.filter((item) => (item.sourceType === 'RETURN_CARTON' ? `RETURN_CARTON:${item.returnCartonId}` : String(item.inboundOrderId)) === inboundOrderId)) { + if (candidate.sourceType === 'RETURN_CARTON') rows.push({ + id: ++cargoSequence, sku: candidate.skuCode || '', name: candidate.productNameZh || '退货合箱库存', + included: true, quantity: 1, availableQuantity: 1, returnCartonId: String(candidate.returnCartonId), + sourceInboundOrderNo: candidate.inboundOrderNo, sourceWarehouseName: candidate.overseasWarehouseName, + fnsku: '', shipmentNo: candidate.inboundOrderNo || candidate.cartonMark || '', newSkuCode: '', shippingMark: '', customerCartonMarks: '', + cartonMark: candidate.cartonMark || '', fbaCartonMark: '', destinationWarehouseId: candidate.destinationFbaWarehouseCode || '', remark: '', + }) + else for (const position of candidate.positions) rows.push(...Array.from({ length: Math.min(position.availablePackages, 500) }, () => ({ id: ++cargoSequence, sku: candidate.skuCode || '', name: candidate.productNameZh || '库存货物', included: false, quantity: 1, availableQuantity: 1, sourceInventoryBalanceId: String(candidate.inventoryBalanceId), sourceInventoryPositionId: String(position.positionId), @@ -923,7 +967,9 @@ function selectRelabelInventory(inboundOrderId: string) { palletNo: position.palletNo, warehouseLocationCode: position.warehouseLocationCode, fnsku: '', shipmentNo: '', newSkuCode: '', shippingMark: '', customerCartonMarks: '', cartonMark: '', fbaCartonMark: '', destinationWarehouseId: candidate.destinationFbaWarehouseCode || '', remark: '', - })))).slice(0, 500) + }))) + } + cargoRows.value = rows.slice(0, 500) if (cargoRows.value.length) cargoRows.value[0].included = true skuRelabelRows.value = [] importedRelabelBindings.value = [] @@ -975,6 +1021,7 @@ function selectRelabelInventory(inboundOrderId: string) { +
*货物如何送到海外仓仓库上门提货客户送货到仓仓库上门提货:填写货物当前地址;客户送货到仓:下方自动展示海外仓收货信息。
@@ -999,7 +1046,7 @@ function selectRelabelInventory(inboundOrderId: string) {
*是否有目的去向暂未确定FBA 仓私人仓暂未确定也可以提交;完成入库后可在新建出库单中批量绑定目的地。
- + @@ -1047,15 +1094,15 @@ function selectRelabelInventory(inboundOrderId: string) {
待导入绑定关系

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

全选导入到明细
-
选择关系类型仓库唛头旧 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数量新 FBA 箱唛
{{ item.templateType === 'mixed' ? 'SKU 混箱换标' : item.templateType === 'carton' ? '箱号关联' : 'SKU 关联' }}{{ item.cartonMark }}{{ item.originalSku || '—' }}{{ item.newSku || '—' }}{{ item.quantity }}{{ item.fbaCartonMark || '—' }}
绑定明细

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

手动添加行清空
添加 SKU 明细清空
-
商品 SKU中文品名 *FBA 货件编号 *箱数仓库唛头 *新 FBA 箱唛 *FBA 目的仓 *备注操作
1 箱
暂无本次换标明细,请上传关系后导入或手动添加
-
仓库唛头 *旧 SKU *新 SKU *换标数量(件)*新 FBA 箱唛 *FBA 目的仓 *备注操作
暂无绑定明细,请点击“添加 SKU 明细”
+
商品 SKU中文品名 *FBA 货件编号 *箱数仓库唛头 *新 FBA 箱唛 *备注操作
1 箱
暂无本次换标明细,请上传关系后导入或手动添加
+
仓库唛头 *旧 SKU *新 SKU *换标数量(件)*新 FBA 箱唛 *备注操作
暂无绑定明细,请点击“添加 SKU 明细”