This commit is contained in:
2026-09-17 16:34:51 +08:00
parent 8024fd4717
commit 958da32b00
5 changed files with 122 additions and 57 deletions
+1 -1
View File
@@ -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 |
## 租户、认证与数据安全
@@ -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 }>
}
@@ -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<CargoRow[]>([])
const transferRelabelRows = ref<CargoRow[]>([])
const transferRelabelDefaultDestination = ref('')
const labelingMode = ref<LabelingMode>('carton')
const shippingLabelFile = ref<File | null>(null)
const skuRelabelRows = ref<SkuRelabelRow[]>([])
const importedRelabelBindings = ref<ImportedRelabelBinding[]>([])
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<Record<string, string>[]>([])
const page = ref(1)
const pageSize = ref(15)
@@ -123,13 +124,13 @@ const relabelCandidatesLoading = ref(false)
const relabelInboundOptions = computed(() => {
const grouped = new Map<string, { value: string; inboundOrderNo: string; inboundCode: string; availablePackages: number }>()
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) {
<label v-else-if="props.service === 'disposal'"><span>进仓编号</span><el-input v-model="form.inboundNo" readonly /></label>
<label v-else><span><i>*</i>{{ props.service === 'labeling' ? '海外操作仓(中转仓)' : props.service === 'transfer' ? '入库海外仓' : '操作仓' }}</span><el-select v-model="form.warehouse" :loading="fbaWarehousesLoading || operationWarehouseDetailLoading" :disabled="Boolean(operationWarehousesError)" clearable :placeholder="props.service === 'transfer' ? '请选择海外仓' : '请选择国外中转仓'" @change="props.service === 'labeling' ? selectOperationWarehouse() : props.service === 'transfer' && selectTransferWarehouse()"><el-option v-for="warehouse in operationWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="String(warehouse.id)" /></el-select><small v-if="operationWarehousesError" class="warehouse-hint">{{ operationWarehousesError }}</small><small v-else-if="props.service === 'labeling'" class="warehouse-hint">选择后仅查询当前客户在该中转仓的可用库存。</small><small v-else-if="props.service === 'transfer'" class="warehouse-hint">本批货物将先进入该海外仓完成签收和入库。</small></label>
<label v-if="props.service === 'labeling'" class="warehouse-span-2"><span><i>*</i>选择有库存的历史入仓单</span><el-select v-model="selectedRelabelInboundOrderId" filterable clearable :loading="relabelCandidatesLoading" :disabled="!form.warehouse" no-data-text="当前中转仓暂无已入库可用库存" placeholder="请选择曾经下单且当前仍有库存的入仓单" @change="selectRelabelInventory"><el-option v-for="option in relabelInboundOptions" :key="option.value" :label="`${option.inboundOrderNo}|入仓码 ${option.inboundCode}|可用 ${option.availablePackages} 箱`" :value="option.value" /></el-select><small class="warehouse-hint">仅显示当前客户在所选中转仓已经完成入库且仍有可用库存的历史入仓单。</small></label>
<label v-if="props.service === 'labeling' && labelingMode !== 'sku'"><span><i>*</i>FBA 目的仓</span><el-select v-model="form.destination" :loading="fbaWarehousesLoading" :disabled="Boolean(fbaWarehousesError)" filterable placeholder="请选择本张申请统一的 FBA 目的仓"><el-option v-for="warehouse in fbaWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="warehouse.warehouseCode" /></el-select><small class="warehouse-hint">整张贴标申请只能选择一个目的仓,全部明细统一使用该目的仓。</small></label>
<div v-if="props.service === 'transfer'" class="warehouse-choice-field warehouse-span-2"><span><i>*</i>货物如何送到海外仓</span><el-radio-group v-model="form.deliveryMethod" @change="selectTransferInboundMethod"><el-radio-button value="WAREHOUSE_PICKUP">仓库上门提货</el-radio-button><el-radio-button value="CUSTOMER_DELIVERY">客户送货到仓</el-radio-button></el-radio-group><small>仓库上门提货:填写货物当前地址;客户送货到仓:下方自动展示海外仓收货信息。</small></div>
<label v-if="props.service === 'transfer' && form.deliveryMethod === 'WAREHOUSE_PICKUP'" class="pickup-address-field"><span><i>*</i>提货地址</span><el-input v-model="form.address" placeholder="请输入仓库上门提货的详细地址" /></label>
<label v-if="props.service === 'transfer' && form.deliveryMethod === 'WAREHOUSE_PICKUP'"><span><i>*</i>提货联系人</span><el-input v-model="form.pickupContact" placeholder="请输入提货联系人" maxlength="128" /></label>
@@ -999,7 +1046,7 @@ function selectRelabelInventory(inboundOrderId: string) {
<label v-if="props.service === 'transfer' || props.service === 'relabel'"><span><i>*</i>{{ props.service === 'transfer' ? '预计入仓时间' : '预计到达时间' }}</span><el-date-picker v-model="form.estimatedArrivalAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" :placeholder="props.service === 'transfer' ? '请选择预计入仓时间' : '请选择预计到达时间'" /></label>
<label v-if="props.service === 'transfer'"><span>预计发货时间</span><el-date-picker v-model="form.estimatedShipAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="选填,可线下确认后补充" /><small class="warehouse-hint">暂未确定时可不填写,不影响货物先入海外仓。</small></label>
<div v-if="props.service === 'transfer'" class="warehouse-choice-field warehouse-span-2"><span><i>*</i>是否有目的去向</span><el-radio-group v-model="transferDestinationType" @change="selectTransferDestinationType"><el-radio-button value="NONE">暂未确定</el-radio-button><el-radio-button value="FBA">FBA 仓</el-radio-button><el-radio-button value="PRIVATE">私人仓</el-radio-button></el-radio-group><small>暂未确定也可以提交;完成入库后可在新建出库单中批量绑定目的地。</small></div>
<label v-if="props.service === 'transfer' && transferDestinationType === 'FBA'"><span><i>*</i>目的 FBA 仓</span><el-select v-model="form.destination" :loading="fbaWarehousesLoading" :disabled="Boolean(fbaWarehousesError)" filterable placeholder="请选择平台维护的 FBA 仓"><el-option v-for="warehouse in fbaWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="warehouse.warehouseCode" /></el-select><small class="warehouse-hint">整张转运单统一使用该目的仓,换标明细自动继承。</small></label>
<label v-if="props.service === 'transfer' && transferDestinationType === 'FBA'"><span><i>*</i>未换标货物目的 FBA 仓</span><el-select v-model="form.destination" :loading="fbaWarehousesLoading" :disabled="Boolean(fbaWarehousesError)" filterable placeholder="请选择平台维护的 FBA 仓"><el-option v-for="warehouse in fbaWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="warehouse.warehouseCode" /></el-select><small class="warehouse-hint">此处只表示不换标箱子的去向;需要换箱唛的箱子在换标明细中单独选择目的仓。</small></label>
<label v-if="props.service === 'transfer' && transferDestinationType === 'FBA'"><span>FBA 货件编号</span><el-input v-model="form.fbaShipmentNo" placeholder="选填,可后续补充" /></label>
<label v-if="props.service === 'transfer' && transferDestinationType === 'PRIVATE'" class="warehouse-span-2"><span><i>*</i>私人仓地址</span><el-input v-model="form.privateDestinationAddress" maxlength="1000" placeholder="请输入私人仓完整地址" /></label>
<label v-if="props.service === 'transfer' && transferDestinationType === 'PRIVATE'"><span><i>*</i>邮编</span><el-input v-model="form.privateDestinationPostalCode" maxlength="32" placeholder="请输入邮编" /></label>
@@ -1047,15 +1094,15 @@ function selectRelabelInventory(inboundOrderId: string) {
</div>
<div v-if="importedRelabelBindings.length" class="warehouse-import-preview">
<div class="warehouse-section-heading"><div><strong>待导入绑定关系</strong><p>可只勾选本次需要换标的部分,也可以全选后一次导入。</p></div><div><el-checkbox :model-value="importedRelabelBindings.every((item) => item.selected)" @change="selectAllImportedBindings(Boolean($event))">全选</el-checkbox><el-button type="primary" size="small" @click="importSelectedBindingsToDetails">导入到明细</el-button></div></div>
<div class="warehouse-table-scroll"><table class="warehouse-binding-preview-table"><thead><tr><th>选择</th><th>关系类型</th><th>仓库唛头</th><th>旧 SKU</th><th>新 SKU</th><th>数量</th><th>新 FBA 箱唛</th><th>FBA 目的仓</th></tr></thead><tbody><tr v-for="item in importedRelabelBindings" :key="item.id"><td><el-checkbox v-model="item.selected" /></td><td>{{ item.templateType === 'mixed' ? 'SKU 混箱换标' : item.templateType === 'carton' ? '箱号关联' : 'SKU 关联' }}</td><td>{{ item.cartonMark }}</td><td>{{ item.originalSku || '—' }}</td><td>{{ item.newSku || '—' }}</td><td>{{ item.quantity }}</td><td>{{ item.fbaCartonMark || '—' }}</td><td>{{ warehouseDisplay(item.destinationWarehouseId) }}</td></tr></tbody></table></div>
<div class="warehouse-table-scroll"><table class="warehouse-binding-preview-table"><thead><tr><th>选择</th><th>关系类型</th><th>仓库唛头</th><th>旧 SKU</th><th>新 SKU</th><th>数量</th><th>新 FBA 箱唛</th></tr></thead><tbody><tr v-for="item in importedRelabelBindings" :key="item.id"><td><el-checkbox v-model="item.selected" /></td><td>{{ item.templateType === 'mixed' ? 'SKU 混箱换标' : item.templateType === 'carton' ? '箱号关联' : 'SKU 关联' }}</td><td>{{ item.cartonMark }}</td><td>{{ item.originalSku || '—' }}</td><td>{{ item.newSku || '—' }}</td><td>{{ item.quantity }}</td><td>{{ item.fbaCartonMark || '—' }}</td></tr></tbody></table></div>
</div>
<div class="warehouse-section-heading warehouse-tab-actions">
<div><strong>绑定明细</strong><p>上传后在下方回显,也可以手动添加;SKU 与箱号关联通过仓库唛头合并。系统只提供大概库存位置,不代表箱唛已绑定具体托盘。</p></div>
<div v-if="labelingMode === 'carton'"><el-button type="primary" size="small" @click="addRelabelCargoRow"><Plus :size="15" />手动添加行</el-button><el-button size="small" @click="clearRelabelRows">清空</el-button></div>
<div v-else><el-button type="primary" size="small" @click="addSkuRelabelRow"><Plus :size="15" />添加 SKU 明细</el-button><el-button size="small" @click="skuRelabelRows = []">清空</el-button></div>
</div>
<div v-if="labelingMode === 'carton'" class="warehouse-table-scroll"><table class="warehouse-edit-table warehouse-label-table"><thead><tr><th>商品 SKU</th><th>中文品名 *</th><th>FBA 货件编号 *</th><th>箱数</th><th>仓库唛头 *</th><th>新 FBA 箱唛 *</th><th>FBA 目的仓 *</th><th>备注</th><th>操作</th></tr></thead><tbody><tr v-for="row in cartonDetailRows" :key="row.id"><td><el-input v-model="row.sku" placeholder="选填" /></td><td><el-input v-model="row.name" placeholder="请输入中文品名" /></td><td><el-input v-model="row.shipmentNo" placeholder="请输入FBA货件编号" /></td><td><strong>1 箱</strong></td><td><el-input v-model="row.cartonMark" placeholder="请输入仓库唛头" /></td><td><el-input v-model="row.fbaCartonMark" placeholder="请输入新的 FBA 箱唛" /></td><td><el-select v-model="row.destinationWarehouseId" :loading="fbaWarehousesLoading" :disabled="Boolean(fbaWarehousesError)" placeholder="请选择 FBA 目的仓"><el-option v-for="warehouse in fbaWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="warehouse.warehouseCode" /></el-select></td><td><el-input v-model="row.remark" type="textarea" :rows="2" maxlength="1000" show-word-limit placeholder="请输入明细备注" /></td><td><button type="button" class="warehouse-delete" title="移出本次明细" @click="removeCargoRow(row.id)"><Trash2 :size="16" />移出</button></td></tr><tr v-if="cartonDetailRows.length === 0"><td colspan="9"><div class="warehouse-empty">暂无本次换标明细,请上传关系后导入或手动添加</div></td></tr></tbody></table></div>
<div v-else class="warehouse-table-scroll"><table class="warehouse-edit-table warehouse-sku-label-table"><thead><tr><th>仓库唛头 *</th><th>旧 SKU *</th><th>新 SKU *</th><th>换标数量(件)*</th><th v-if="labelingMode === 'carton_and_sku'">新 FBA 箱唛 *</th><th v-if="labelingMode === 'carton_and_sku'">FBA 目的仓 *</th><th>备注</th><th>操作</th></tr></thead><tbody><tr v-for="row in skuRelabelRows" :key="row.id"><td><el-input v-model="row.cartonMark" placeholder="请输入仓库唛头" @change="applyExistingCartonBinding(row)" /></td><td><el-input v-model="row.originalSku" placeholder="请输入旧 SKU" @change="applyExistingSkuBinding(row)" /></td><td><el-input v-model="row.newSku" placeholder="请输入新 SKU" /></td><td><el-input-number v-model="row.quantity" :min="1" :precision="0" controls-position="right" placeholder="请输入件数" /></td><td v-if="labelingMode === 'carton_and_sku'"><el-input v-model="row.fbaCartonMark" placeholder="请输入新的 FBA 箱唛" /></td><td v-if="labelingMode === 'carton_and_sku'"><el-select v-model="row.destinationWarehouseId" :loading="fbaWarehousesLoading" :disabled="Boolean(fbaWarehousesError)" placeholder="请选择 FBA 目的仓"><el-option v-for="warehouse in fbaWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="warehouse.warehouseCode" /></el-select></td><td><el-input v-model="row.remark" placeholder="请输入备注" /></td><td><button type="button" class="warehouse-delete" title="删除" @click="removeSkuRelabelRow(row.id)"><Trash2 :size="16" />删除</button></td></tr><tr v-if="skuRelabelRows.length === 0"><td :colspan="labelingMode === 'carton_and_sku' ? 8 : 6"><div class="warehouse-empty">暂无绑定明细,请点击“添加 SKU 明细”</div></td></tr></tbody></table></div>
<div v-if="labelingMode === 'carton'" class="warehouse-table-scroll"><table class="warehouse-edit-table warehouse-label-table"><thead><tr><th>商品 SKU</th><th>中文品名 *</th><th>FBA 货件编号 *</th><th>箱数</th><th>仓库唛头 *</th><th>新 FBA 箱唛 *</th><th>备注</th><th>操作</th></tr></thead><tbody><tr v-for="row in cartonDetailRows" :key="row.id"><td><el-input v-model="row.sku" placeholder="选填" /></td><td><el-input v-model="row.name" placeholder="请输入中文品名" /></td><td><el-input v-model="row.shipmentNo" placeholder="请输入FBA货件编号" /></td><td><strong>1 箱</strong></td><td><el-input v-model="row.cartonMark" placeholder="请输入仓库唛头" /></td><td><el-input v-model="row.fbaCartonMark" placeholder="请输入新的 FBA 箱唛" /></td><td><el-input v-model="row.remark" type="textarea" :rows="2" maxlength="1000" show-word-limit placeholder="请输入明细备注" /></td><td><button type="button" class="warehouse-delete" title="移出本次明细" @click="removeCargoRow(row.id)"><Trash2 :size="16" />移出</button></td></tr><tr v-if="cartonDetailRows.length === 0"><td colspan="8"><div class="warehouse-empty">暂无本次换标明细,请上传关系后导入或手动添加</div></td></tr></tbody></table></div>
<div v-else class="warehouse-table-scroll"><table class="warehouse-edit-table warehouse-sku-label-table"><thead><tr><th>仓库唛头 *</th><th>旧 SKU *</th><th>新 SKU *</th><th>换标数量(件)*</th><th v-if="labelingMode === 'carton_and_sku'">新 FBA 箱唛 *</th><th>备注</th><th>操作</th></tr></thead><tbody><tr v-for="row in skuRelabelRows" :key="row.id"><td><el-input v-model="row.cartonMark" placeholder="请输入仓库唛头" @change="applyExistingCartonBinding(row)" /></td><td><el-input v-model="row.originalSku" placeholder="请输入旧 SKU" @change="applyExistingSkuBinding(row)" /></td><td><el-input v-model="row.newSku" placeholder="请输入新 SKU" /></td><td><el-input-number v-model="row.quantity" :min="1" :precision="0" controls-position="right" placeholder="请输入件数" /></td><td v-if="labelingMode === 'carton_and_sku'"><el-input v-model="row.fbaCartonMark" placeholder="请输入新的 FBA 箱唛" /></td><td><el-input v-model="row.remark" placeholder="请输入备注" /></td><td><button type="button" class="warehouse-delete" title="删除" @click="removeSkuRelabelRow(row.id)"><Trash2 :size="16" />删除</button></td></tr><tr v-if="skuRelabelRows.length === 0"><td :colspan="labelingMode === 'carton_and_sku' ? 7 : 6"><div class="warehouse-empty">暂无绑定明细,请点击“添加 SKU 明细”</div></td></tr></tbody></table></div>
</template>
<template v-else>
<div class="warehouse-section-heading"><h3>货物明细</h3><div v-if="props.service !== 'disposal'"><el-button type="primary" size="small" @click="addCargoRow"><Plus :size="15" />添加行</el-button><el-button size="small" @click="cargoRows = []">清空</el-button></div></div>
@@ -1064,11 +1111,11 @@ function selectRelabelInventory(inboundOrderId: string) {
</div>
<div v-if="props.service === 'transfer' && form.relabel" class="warehouse-card warehouse-form-section warehouse-transfer-relabel">
<div class="warehouse-section-heading warehouse-labeling-heading"><div><h3>货物明细</h3><p>换标明细与贴标申请保持一致;客户只填写自己的箱唛,我方箱唛由系统生成。</p></div><div class="warehouse-template-actions"><el-button size="small" :disabled="!cargoRows.length" @click="downloadRelabelTemplate('sku')"><Download :size="15" />下载 SKU 关联模板</el-button><el-button size="small" :disabled="!cargoRows.length" @click="skuTemplateInput?.click()"><Upload :size="15" />上传 SKU 关联</el-button><el-button size="small" :disabled="!cargoRows.length" @click="downloadRelabelTemplate('carton')"><Download :size="15" />下载箱号关联模板</el-button><el-button size="small" :disabled="!cargoRows.length" @click="cartonTemplateInput?.click()"><Upload :size="15" />上传箱号关联</el-button><el-button size="small" :disabled="!cargoRows.length" @click="downloadRelabelTemplate('mixed')"><Download :size="15" />下载 SKU 混箱模板</el-button><el-button size="small" :disabled="!cargoRows.length" @click="mixedTemplateInput?.click()"><Upload :size="15" />上传 SKU 混箱</el-button><input ref="skuTemplateInput" class="warehouse-hidden-input" type="file" accept=".csv,text/csv" @change="importRelabelTemplate($event, 'sku')"><input ref="cartonTemplateInput" class="warehouse-hidden-input" type="file" accept=".csv,text/csv" @change="importRelabelTemplate($event, 'carton')"><input ref="mixedTemplateInput" class="warehouse-hidden-input" type="file" accept=".csv,text/csv" @change="importRelabelTemplate($event, 'mixed')"></div></div>
<div class="warehouse-form-grid warehouse-transfer-relabel-fields"><label><span><i>*</i>换标截止时间</span><el-date-picker v-model="form.labelDeadline" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="请选择截止时间" /></label><label><span><i>*</i>换标联系人</span><el-input v-model="form.relabelContact" placeholder="选择海外仓后自动带出,也可手动填写" maxlength="128" /></label><label><span><i>*</i>换标联系人电话</span><el-input v-model="form.relabelPhone" placeholder="选择海外仓后自动带出,也可手动填写" maxlength="64" /></label><label v-if="labelingMode !== 'sku'"><span><i>*</i>换标后派送方式</span><el-select v-model="form.relabelDeliveryMethod" placeholder="请选择派送方式"><el-option label="卡车派送" value="TRUCK" /><el-option label="快递派送" value="EXPRESS" /><el-option label="卡车自提" value="TRUCK_PICKUP" /><el-option label="快递自提" value="EXPRESS_PICKUP" /></el-select></label><label class="warehouse-span-2"><span>贴标要求</span><el-input v-model="form.requirement" type="textarea" :rows="2" maxlength="1000" show-word-limit placeholder="请输入贴标要求(选填)" /></label></div>
<div class="warehouse-form-grid warehouse-transfer-relabel-fields"><label><span><i>*</i>换标截止时间</span><el-date-picker v-model="form.labelDeadline" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="请选择截止时间" /></label><label><span><i>*</i>换标联系人</span><el-input v-model="form.relabelContact" placeholder="选择海外仓后自动带出,也可手动填写" maxlength="128" /></label><label><span><i>*</i>换标联系人电话</span><el-input v-model="form.relabelPhone" placeholder="选择海外仓后自动带出,也可手动填写" maxlength="64" /></label><label v-if="labelingMode !== 'sku'"><span><i>*</i>换标后派送方式</span><el-select v-model="form.relabelDeliveryMethod" placeholder="请选择派送方式"><el-option label="卡车派送" value="TRUCK" /><el-option label="快递派送" value="EXPRESS" /><el-option label="卡车自提" value="TRUCK_PICKUP" /><el-option label="快递自提" value="EXPRESS_PICKUP" /></el-select></label><label class="warehouse-span-2"><span><i>*</i>整票换标货物目的 FBA 仓</span><el-select v-model="transferRelabelDefaultDestination" :loading="fbaWarehousesLoading" :disabled="Boolean(fbaWarehousesError)" filterable placeholder="请选择整票货物的目的仓"><el-option v-for="warehouse in fbaWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="warehouse.warehouseCode" /></el-select><small class="warehouse-hint">本票所有换标货物统一前往该目的仓,暂不支持一票多个目的仓。</small></label><label class="warehouse-span-2"><span>贴标要求</span><el-input v-model="form.requirement" type="textarea" :rows="2" maxlength="1000" show-word-limit placeholder="请输入贴标要求(选填)" /></label></div>
<div class="warehouse-labeling-mode"><span>换标类型</span><el-radio-group v-model="labelingMode" aria-label="换标类型"><el-radio-button value="carton">只换箱唛</el-radio-button><el-radio-button value="sku">只换 SKU 标签</el-radio-button><el-radio-button value="carton_and_sku">SKU 和箱唛都换</el-radio-button></el-radio-group><p>{{ labelingModeHint }}</p></div>
<div v-if="importedRelabelBindings.length" class="warehouse-import-preview"><div class="warehouse-section-heading"><div><strong>待导入绑定关系</strong><p>可只勾选本次需要换标的部分,也可以全选后一次导入。</p></div><div><el-checkbox :model-value="importedRelabelBindings.every((item) => item.selected)" @change="selectAllImportedBindings(Boolean($event))">全选</el-checkbox><el-button type="primary" size="small" @click="importSelectedBindingsToDetails">导入到明细</el-button></div></div><div class="warehouse-table-scroll"><table class="warehouse-binding-preview-table"><thead><tr><th>选择</th><th>关系类型</th><th>仓库唛头</th><th>旧 SKU</th><th>新 SKU</th><th>数量</th><th>新 FBA 箱唛</th></tr></thead><tbody><tr v-for="item in importedRelabelBindings" :key="item.id"><td><el-checkbox v-model="item.selected" /></td><td>{{ item.templateType === 'mixed' ? 'SKU 混箱换标' : item.templateType === 'carton' ? '箱号关联' : 'SKU 关联' }}</td><td>{{ item.cartonMark }}</td><td>{{ item.originalSku || '—' }}</td><td>{{ item.newSku || '—' }}</td><td>{{ item.quantity }}</td><td>{{ item.fbaCartonMark || '—' }}</td></tr></tbody></table></div></div>
<div v-if="importedRelabelBindings.length" class="warehouse-import-preview"><div class="warehouse-section-heading"><div><strong>待导入绑定关系</strong><p>可只勾选本次需要换标的部分,也可以全选后一次导入。</p></div><div><el-checkbox :model-value="importedRelabelBindings.every((item) => item.selected)" @change="selectAllImportedBindings(Boolean($event))">全选</el-checkbox><el-button type="primary" size="small" @click="importSelectedBindingsToDetails">导入到明细</el-button></div></div><div class="warehouse-table-scroll"><table class="warehouse-binding-preview-table"><thead><tr><th>选择</th><th>关系类型</th><th>客户箱唛号</th><th>旧 SKU</th><th>新 SKU</th><th>数量</th><th>新 FBA 箱唛</th></tr></thead><tbody><tr v-for="item in importedRelabelBindings" :key="item.id"><td><el-checkbox v-model="item.selected" /></td><td>{{ item.templateType === 'mixed' ? 'SKU 混箱换标' : item.templateType === 'carton' ? '箱号关联' : 'SKU 关联' }}</td><td>{{ item.cartonMark }}</td><td>{{ item.originalSku || '—' }}</td><td>{{ item.newSku || '—' }}</td><td>{{ item.quantity }}</td><td>{{ item.fbaCartonMark || '—' }}</td></tr></tbody></table></div></div>
<div class="warehouse-section-heading warehouse-tab-actions"><div><strong>绑定明细</strong><p>上传后在下方回显,也可以手动添加;客户箱唛作为仓库唛头来源,系统我方箱唛不需要手填。</p></div><div><el-button type="primary" size="small" @click="addTransferRelabelRow"><Plus :size="15" />添加 SKU 明细</el-button><el-button size="small" @click="clearTransferRelabelRows">清空</el-button></div></div>
<div v-if="labelingMode === 'carton'" class="warehouse-table-scroll"><table class="warehouse-edit-table warehouse-transfer-mark-table"><thead><tr><th>客户箱唛号 *</th><th>旧 SKU</th><th>新 FBA 箱唛 *</th><th>备注</th><th>操作</th></tr></thead><tbody><tr v-for="row in transferRelabelRows" :key="row.id"><td><el-input v-model="row.customerCartonMarks" type="textarea" :rows="2" placeholder="请输入客户箱唛号" /></td><td><el-input :model-value="row.sku" readonly /></td><td><el-input v-model="row.fbaCartonMark" placeholder="请输入新的 FBA 箱唛" /></td><td><el-input v-model="row.remark" type="textarea" :rows="2" placeholder="请输入备注" /></td><td><button type="button" class="warehouse-delete" title="删除该明细" @click="transferRelabelRows = transferRelabelRows.filter((item) => item.id !== row.id)"><Trash2 :size="16" />删除</button></td></tr><tr v-if="transferRelabelRows.length === 0"><td colspan="5"><div class="warehouse-empty">暂无绑定明细,请点击“添加 SKU 明细”</div></td></tr></tbody></table></div>
<div v-if="labelingMode === 'carton'" class="warehouse-table-scroll"><table class="warehouse-edit-table warehouse-transfer-mark-table"><thead><tr><th>客户箱唛号 *</th><th>旧 SKU</th><th>新 FBA 箱唛 *</th><th>备注</th><th>操作</th></tr></thead><tbody><tr v-for="row in transferRelabelRows" :key="row.id"><td><el-input v-model="row.customerCartonMarks" type="textarea" :rows="2" placeholder="请输入客户箱唛号" /></td><td><el-input :model-value="row.sku" readonly /></td><td><el-input v-model="row.fbaCartonMark" placeholder="请输入新的 FBA 箱唛" /></td><td><el-input v-model="row.remark" type="textarea" :rows="2" placeholder="请输入备注" /></td><td><button type="button" class="warehouse-delete" title="删除该明细" @click="transferRelabelRows = transferRelabelRows.filter((item) => item.id !== row.id)"><Trash2 :size="16" />删除</button></td></tr><tr v-if="transferRelabelRows.length === 0"><td colspan="5"><div class="warehouse-empty">暂无绑定明细,请点击“添加换标明细”</div></td></tr></tbody></table></div>
<div v-else-if="labelingMode === 'sku'" class="warehouse-table-scroll"><table class="warehouse-edit-table warehouse-transfer-mark-table"><thead><tr><th>客户箱唛号 *</th><th>旧 SKU *</th><th>新 SKU *</th><th>换标数量(件)*</th><th>备注</th><th>操作</th></tr></thead><tbody><tr v-for="row in transferRelabelRows" :key="row.id"><td><el-input v-model="row.customerCartonMarks" type="textarea" :rows="2" placeholder="请输入客户箱唛号" /></td><td><el-input :model-value="row.sku" readonly /></td><td><el-input v-model="row.newSkuCode" placeholder="请输入新 SKU" /></td><td><el-input-number v-model="row.quantity" :min="1" :precision="0" controls-position="right" placeholder="请输入箱数" /></td><td><el-input v-model="row.remark" type="textarea" :rows="2" placeholder="请输入备注" /></td><td><button type="button" class="warehouse-delete" title="删除该明细" @click="transferRelabelRows = transferRelabelRows.filter((item) => item.id !== row.id)"><Trash2 :size="16" />删除</button></td></tr><tr v-if="transferRelabelRows.length === 0"><td colspan="6"><div class="warehouse-empty">暂无绑定明细,请点击“添加 SKU 明细”</div></td></tr></tbody></table></div>
<div v-else class="warehouse-table-scroll"><table class="warehouse-edit-table warehouse-transfer-mark-table"><thead><tr><th>客户箱唛号 *</th><th>旧 SKU *</th><th>新 SKU *</th><th>换标数量(件)*</th><th>新 FBA 箱唛 *</th><th>备注</th><th>操作</th></tr></thead><tbody><tr v-for="row in transferRelabelRows" :key="row.id"><td><el-input v-model="row.customerCartonMarks" type="textarea" :rows="2" placeholder="请输入客户箱唛号" /></td><td><el-input :model-value="row.sku" readonly /></td><td><el-input v-model="row.newSkuCode" placeholder="请输入新 SKU" /></td><td><el-input-number v-model="row.quantity" :min="1" :precision="0" controls-position="right" placeholder="请输入数量" /></td><td><el-input v-model="row.fbaCartonMark" placeholder="请输入新的 FBA 箱唛" /></td><td><el-input v-model="row.remark" type="textarea" :rows="2" placeholder="请输入备注" /></td><td><button type="button" class="warehouse-delete" title="删除该明细" @click="transferRelabelRows = transferRelabelRows.filter((item) => item.id !== row.id)"><Trash2 :size="16" />删除</button></td></tr><tr v-if="transferRelabelRows.length === 0"><td colspan="7"><div class="warehouse-empty">暂无绑定明细,请点击“添加 SKU 明细”</div></td></tr></tbody></table></div>
</div>
@@ -1084,6 +1131,10 @@ function selectRelabelInventory(inboundOrderId: string) {
<div class="warehouse-section-heading"><h3>货物明细</h3><el-button v-if="selectedDetail?.status !== 'DRAFT'" native-type="button" type="primary" @click="printTransferCartons(String(selectedDetail?.transferNo || selectedDetail?.['0'] || ''))"><Printer :size="16" />打印箱唛</el-button></div>
<div class="warehouse-table-scroll"><table class="warehouse-edit-table"><thead><tr><th>商品 SKU</th><th>中文品名</th><th>预计箱数</th><th>客户箱唛号</th></tr></thead><tbody><tr v-for="(item, index) in selectedDetail?.items || []" :key="`${item.skuCode}-${index}`"><td>{{ item.skuCode || '—' }}</td><td>{{ item.productNameZh || '—' }}</td><td>{{ item.packageCount ?? '—' }}</td><td>{{ item.shippingMark || '—' }}</td></tr><tr v-if="!selectedDetail?.items?.length"><td colspan="4"><div class="warehouse-empty">暂无货物明细</div></td></tr></tbody></table></div>
</div>
<div v-if="props.service === 'transfer' && selectedDetail?.needRelabel" class="warehouse-card warehouse-form-section">
<h3>换标箱级去向</h3>
<div class="warehouse-table-scroll"><table><thead><tr><th>换标类型</th><th>客户箱唛号</th><th>旧 SKU</th><th>新 SKU</th><th>新 FBA 箱唛</th><th>实际 FBA 目的仓</th><th>数量</th><th>备注</th></tr></thead><tbody><tr v-for="(binding, index) in selectedDetail?.relabelBindings || []" :key="`${binding.customerCartonMark}-${index}`"><td>{{ binding.operationType === 'CARTON_AND_SKU' ? 'SKU 和箱唛都换' : binding.operationType === 'SKU_LABEL' ? '只换 SKU' : '只换箱唛' }}</td><td>{{ binding.customerCartonMark || '—' }}</td><td>{{ binding.originalSku || '—' }}</td><td>{{ binding.newSku || '—' }}</td><td>{{ binding.fbaCartonMark || '—' }}</td><td>{{ binding.operationType === 'SKU_LABEL' ? '沿用原箱去向' : warehouseDisplay(binding.destinationWarehouseCode) }}</td><td>{{ binding.quantity ?? '—' }}</td><td>{{ binding.remark || '—' }}</td></tr><tr v-if="!selectedDetail?.relabelBindings?.length"><td colspan="8"><div class="warehouse-empty">暂无换标绑定明细</div></td></tr></tbody></table></div>
</div>
<div v-if="props.service === 'transfer'" class="warehouse-card warehouse-form-section warehouse-transfer-times"><h3>发货时间</h3><div class="warehouse-detail-grid"><div><span>预计发货时间</span><strong>{{ formatDateTime(selectedDetail?.estimatedShipAt) }}</strong></div><div><span>实际发货时间</span><strong>{{ formatDateTime(selectedDetail?.actualShipAt) }}</strong></div></div><p>实际发货时间由仓库出库或 PDA 完成后回填,客户不能手动修改。</p></div>
<div v-if="props.service === 'labeling'" class="warehouse-card warehouse-form-section">
<div class="warehouse-section-heading"><h3>换标完成与出库</h3></div>
@@ -49,7 +49,8 @@ test('贴标申请保留关联模板下载和上传入口', () => {
assert.match(view, /importRelabelTemplate/)
assert.match(view, /明细行标识/)
assert.match(view, /'明细行标识', '旧箱唛', '旧SKU标签', '新SKU标签'/)
assert.match(view, /'旧箱唛', '新FBA箱唛', 'FBA目的仓'/)
assert.match(view, /const includeDestination = false/)
assert.doesNotMatch(view, /const includeDestination = props\.service !== 'transfer'/)
assert.match(view, /SKU 与箱号关联通过仓库唛头合并/)
assert.match(view, /<th>仓库唛头 \*<\/th>/)
assert.doesNotMatch(view, /库存位置ID/)
@@ -114,21 +115,23 @@ test('箱唛相关换标必须选择四选一派送方式', () => {
assert.match(view, /labelingMode\.value !== 'sku' && !form\.deliveryMethod/)
})
test('一次申请单选入仓单且每项保留原库存归属', () => {
test('一次申请单选库存来源且保留普通入仓或退货合箱归属', () => {
assert.doesNotMatch(view, /multiple filterable collapse-tags/)
assert.match(view, /String\(candidate\.inboundOrderId\) === inboundOrderId/)
assert.match(view, /RETURN_CARTON:\$\{item\.returnCartonId\}/)
assert.match(view, /sourceInventoryBalanceId: String\(candidate\.inventoryBalanceId\)/)
assert.match(view, /sourceInventoryPositionId: String\(position\.positionId\)/)
assert.match(view, /sourceInboundOrderId: String\(candidate\.inboundOrderId\)/)
assert.match(api, /sourceInventoryBalanceId: string; sourceInventoryPositionId: string; sourceInboundOrderId: string/)
assert.match(view, /returnCartonId: String\(candidate\.returnCartonId\)/)
assert.match(api, /sourceInventoryBalanceId\?: string; sourceInventoryPositionId\?: string; sourceInboundOrderId\?: string; returnCartonId\?: string/)
})
test('海外操作仓和FBA目的仓使用独立字段', () => {
assert.match(api, /operationWarehouseId: string/)
assert.match(api, /destinationFbaWarehouseCode\?: string/)
assert.match(view, /operationWarehouseId: form\.warehouse/)
assert.match(view, /destinationFbaWarehouseCode: item\.destinationWarehouseId/)
assert.match(view, /<th>FBA 目的仓<\/th>/)
assert.match(view, /destinationFbaWarehouseCode: form\.destination/)
assert.match(view, /整张贴标申请只能选择一个目的仓/)
assert.doesNotMatch(view, /v-model="row\.destinationWarehouseId"[^\n]+请选择 FBA 目的仓/)
})
test('操作仓和FBA仓均从商户仓库目录按类型加载', () => {
@@ -29,16 +29,25 @@ test('仓储转运支持无目的地、FBA 仓和私人仓并显式选择是否
assert.doesNotMatch(view, /relabelChargeAmount:/)
})
test('仓储转运目的仓使用主单字段且 FBA 货件号选填', () => {
assert.match(view, /props\.service === 'transfer' && transferDestinationType === 'FBA'[^\n]+目的 FBA 仓/)
assert.match(view, /整张转运单统一使用该目的仓,换标明细自动继承/)
test('仓储转运换标整票只允许一个目的仓', () => {
assert.match(view, /props\.service === 'transfer' && transferDestinationType === 'FBA'[^\n]+未换标货物目的 FBA 仓/)
assert.match(view, /此处只表示不换标箱子的去向/)
assert.match(view, /FBA 货件编号<\/span><el-input v-model="form\.fbaShipmentNo" placeholder="选填,可后续补充"/)
assert.match(view, /transferDestinationType\.value === 'FBA' && !form\.destination/)
assert.match(view, /transferDestinationType\.value === 'PRIVATE'.*!form\.privateDestinationPostalCode/)
assert.match(view, /destinationWarehouseCode: labelingMode\.value === 'sku' \? undefined : form\.destination \|\| undefined/)
assert.match(view, /整票换标货物目的 FBA 仓/)
assert.match(view, /暂不支持一票多个目的仓/)
assert.match(view, /if \(!transferRelabelDefaultDestination\.value\) \{ ElMessage\.warning\('请选择整票换标货物的 FBA 目的仓'\)/)
assert.match(view, /destinationWarehouseCode: transferRelabelDefaultDestination\.value \|\| undefined/)
const transferRelabelSection = view.split('warehouse-transfer-relabel">')[1]?.split('warehouse-transfer-submit-bar')[0] || ''
assert.doesNotMatch(transferRelabelSection, /v-model="row\.destinationWarehouseId"/)
assert.doesNotMatch(transferRelabelSection, /<th>FBA 目的仓/)
assert.doesNotMatch(transferRelabelSection, /<th>FBA 目的仓 \*<\/th>/)
assert.doesNotMatch(view, /applyTransferRelabelDestination/)
assert.match(view, /destinationWarehouseId: binding\.destinationWarehouseCode \|\| ''/)
assert.match(view, /props\.service === 'transfer' \? '客户箱唛号' : '仓库唛头'/)
assert.match(view, /已导入 \$\{importedRows\.length\} 条仓储转运换标明细/)
assert.doesNotMatch(transferRelabelSection, /新 FBA 箱唛<\/th><th>FBA 目的仓<\/th>/)
assert.match(view, /const includeDestination = false/)
})
test('仓储转运不要求客户填写内部入仓号和我方箱唛,但换标时填写客户箱唛', () => {