This commit is contained in:
2026-09-10 15:56:39 +08:00
parent 56398e98eb
commit 5c4578ce00
4 changed files with 82 additions and 10 deletions
+1 -1
View File
@@ -54,7 +54,7 @@
| 客户资金账户、授信与充值申请 | `src/modules/finance/views/FinanceAccountView.vue`、`api/account.ts` | 显示当前企业 CNY 实际余额、冻结金额、授信额度、已用授信、冻结授信、总可用余额和不可变流水;数据加载前显示“—”,不得先伪装为零。充值只展示服务端启用的微信/支付宝静态收款码,客户上传 JPG/PNG 付款截图后进入待审核。客户范围只取登录态,无收款码时禁止提交,不模拟支付成功 |
| 海外采购、美国本土卡与美本土散板 | `src/modules/cross-border/routes.ts`、`components/BusinessOrdersView.vue`、`api/businessOrders.ts` | 三个路由均为 `active`,客户范围只取 Token;采购创建的 `customerName` 只取当前认证会话企业名称,不允许自由输入或冒用其他客户。Backend `lastleg-service/` 的 `procurement/`、`trucking/`、`looseboard/` 客户接口 |
| 海外仓入库订单 | `src/modules/order-management/routes.ts`、`views/InboundOrdersView.vue`、`api/inboundPlans.ts` | `orders/inbound` 为只读列表/详情,权限 `merchant:warehouse:inbound:list`;不展示或提交内部 ID |
| 海外仓贴标、仓储转运、货物废弃、退货换标 | `src/modules/overseas-warehouse/routes.ts`、`views/WarehouseServiceView.vue`、`api/warehouseServices.ts` | 四个路由均为 `active`;贴标申请先选国外中转仓和有库存历史入仓单,换标类型为只换 SKU、只换箱唛、SKU+箱唛。涉及箱唛换标时必须选择卡车、自提或快递;快递可选择打印面单并上传 PDF/图片。PDA 逐箱换标、上传照片并由仓库复核后,客户详情展示完工照片和“换标完成确认”;客户确认前不得继续出库,确认后卡车进入预排车,自提/快递进入换标出库待创建。历史库存位置不能解释成箱唛与具体托盘快照,客户页面不暴露托盘、库位和内部库存 ID |
| 海外仓贴标、仓储转运、货物废弃、退货换标 | `src/modules/overseas-warehouse/routes.ts`、`views/WarehouseServiceView.vue`、`api/warehouseServices.ts` | 四个路由均为 `active`;贴标申请先选国外中转仓和有库存历史入仓单,换标类型为只换 SKU、只换箱唛、SKU+箱唛。模板上传和下载固定在货物明细外层:箱唛模式使用箱唛模板,SKU 模式使用包含我方箱唛、旧 SKU 标签、新 SKU 标签和换标数量的 SKU 模板,SKU+箱唛模板另含新 FBA 箱唛与目的仓。涉及箱唛换标时必须选择卡车、自提或快递;快递可选择打印面单并上传 PDF/图片。PDA 逐箱换标、上传照片并由仓库复核后,客户详情展示完工照片和“换标完成确认”;客户确认前不得继续出库,确认后卡车进入预排车,自提/快递进入换标出库待创建。历史库存位置不能解释成箱唛与具体托盘快照,客户页面不暴露托盘、库位和内部库存 ID |
## 租户、认证与数据安全
@@ -189,13 +189,24 @@ function clearRelabelRows() { cargoRows.value = []; selectedRelabelInboundOrderI
function csvCell(value: string | number | undefined) { return `"${String(value ?? '').replaceAll('"', '""')}"` }
function downloadRelabelTemplate() {
if (!cargoRows.value.length) { ElMessage.warning('请先选择有库存的历史入仓单'); return }
const header = ['明细行标识', '本次箱数', '商品SKU', '中文品名', 'FBA货件编号', '我方箱唛', 'FBA箱唛', 'FBA目的仓', '备注']
const rows = cargoRows.value.map((item) => [item.id, item.quantity, item.sku, item.name, item.shipmentNo,
item.cartonMark, item.fbaCartonMark, item.destinationWarehouseId, item.remark])
const cartonTemplate = labelingMode.value === 'carton'
const combinedTemplate = labelingMode.value === 'carton_and_sku'
const header = cartonTemplate
? ['明细行标识', '本次箱数', '商品SKU', '中文品名', 'FBA货件编号', '我方箱唛', 'FBA箱唛', 'FBA目的仓', '备注']
: ['我方箱唛', '旧SKU标签', '新SKU标签', '换标数量(件)',
...(combinedTemplate ? ['新FBA箱唛', 'FBA目的仓'] : []), '备注']
const rows = cartonTemplate
? cargoRows.value.map((item) => [item.id, item.quantity, item.sku, item.name, item.shipmentNo,
item.cartonMark, item.fbaCartonMark, item.destinationWarehouseId, item.remark])
: skuRelabelRows.value.map((item) => [item.cartonMark, item.originalSku, item.newSku, item.quantity,
...(combinedTemplate ? [item.fbaCartonMark, item.destinationWarehouseId] : []), item.remark])
const blob = new Blob([`\uFEFF${[header, ...rows].map((row) => row.map(csvCell).join(',')).join('\r\n')}\r\n`], { type: 'text/csv;charset=utf-8' })
const url = URL.createObjectURL(blob)
const anchor = document.createElement('a')
anchor.href = url; anchor.download = '贴标申请-关联面单模板.csv'; anchor.click(); URL.revokeObjectURL(url)
anchor.href = url
anchor.download = cartonTemplate ? '贴标申请-箱唛换标模板.csv'
: combinedTemplate ? '贴标申请-SKU和箱唛换标模板.csv' : '贴标申请-SKU换标模板.csv'
anchor.click(); URL.revokeObjectURL(url)
}
function parseCsvLine(line: string) {
const values: string[] = []; let value = ''; let quoted = false
@@ -213,6 +224,41 @@ async function importRelabelTemplate(event: Event) {
const lines = (await file.text()).replace(/^\uFEFF/, '').split(/\r?\n/).filter((line) => line.trim())
if (lines.length < 2) { ElMessage.error('模板中没有可导入的明细'); return }
const headers = parseCsvLine(lines[0])
if (labelingMode.value !== 'carton') {
const combinedTemplate = labelingMode.value === 'carton_and_sku'
const required = ['我方箱唛', '旧SKU标签', '新SKU标签', '换标数量(件)',
...(combinedTemplate ? ['新FBA箱唛', 'FBA目的仓'] : [])]
if (required.some((name) => !headers.includes(name))) {
ElMessage.error('SKU 换标模板格式不正确,请重新下载当前换标类型的模板'); return
}
const imported = lines.slice(1).map(parseCsvLine)
.map((values) => Object.fromEntries(headers.map((key, index) => [key, values[index] || ''])))
.filter((item) => Object.values(item).some(Boolean))
if (!imported.length || imported.some((item) => required.some((key) => !item[key]))) {
ElMessage.error('我方箱唛、旧 SKU 标签、新 SKU 标签和换标数量必须填写完整'); return
}
const rows: SkuRelabelRow[] = []
for (const item of imported) {
const quantity = Number(item['换标数量(件)'])
if (!Number.isInteger(quantity) || quantity < 1) {
ElMessage.error(`箱唛 ${item['我方箱唛']} 的换标数量必须是大于 0 的整数`); return
}
rows.push({
id: ++skuRelabelSequence,
sourceRowId: null,
cartonMark: item['我方箱唛'].trim(),
originalSku: item['旧SKU标签'].trim(),
newSku: item['新SKU标签'].trim(),
quantity,
fbaCartonMark: combinedTemplate ? item['新FBA箱唛'].trim() : '',
destinationWarehouseId: combinedTemplate ? item['FBA目的仓'].trim() : '',
remark: item['备注'] || '',
})
}
skuRelabelRows.value = rows
ElMessage.success(`已导入 ${rows.length} 条 SKU 换标明细`)
return
}
const required = ['明细行标识', '本次箱数', 'FBA货件编号', '我方箱唛', 'FBA箱唛', 'FBA目的仓']
if (required.some((name) => !headers.includes(name))) { ElMessage.error('模板格式不正确,请重新下载模板填写'); return }
const imported = lines.slice(1).map(parseCsvLine)
@@ -548,7 +594,14 @@ function selectRelabelInventory(inboundOrderId: string) {
</div>
<div class="warehouse-card warehouse-form-section">
<template v-if="props.service === 'labeling'">
<h3>货物明细</h3>
<div class="warehouse-section-heading warehouse-labeling-heading">
<h3>货物明细</h3>
<div class="warehouse-template-actions">
<el-button size="small" :disabled="!cargoRows.length" @click="downloadRelabelTemplate"><Download :size="15" />下载{{ labelingMode === 'carton' ? '箱唛' : 'SKU' }}模板</el-button>
<el-button size="small" :disabled="!cargoRows.length" @click="templateInput?.click()"><Upload :size="15" />上传{{ labelingMode === 'carton' ? '箱唛' : 'SKU' }}模板</el-button>
<input ref="templateInput" class="warehouse-hidden-input" type="file" accept=".csv,text/csv" @change="importRelabelTemplate">
</div>
</div>
<div class="warehouse-labeling-mode">
<span>换标类型</span>
<el-radio-group v-model="labelingMode" aria-label="换标类型">
@@ -560,7 +613,7 @@ function selectRelabelInventory(inboundOrderId: string) {
</div>
<div class="warehouse-section-heading warehouse-tab-actions">
<p>系统只提供历史入仓单的大概库存位置,不代表箱唛已绑定具体托盘。</p>
<div v-if="labelingMode === 'carton'"><el-button size="small" @click="downloadRelabelTemplate"><Download :size="15" />下载关联模板</el-button><el-button size="small" :disabled="!cargoRows.length" @click="templateInput?.click()"><Upload :size="15" />上传关联面单</el-button><input ref="templateInput" class="warehouse-hidden-input" type="file" accept=".csv,text/csv" @change="importRelabelTemplate"><el-button type="primary" size="small" @click="addRelabelCargoRow"><Plus :size="15" />手动添加行</el-button><el-button size="small" @click="clearRelabelRows">清空</el-button></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 cargoRows" :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><el-input-number v-model="row.quantity" :min="1" :max="row.availableQuantity" controls-position="right" placeholder="请输入箱数" /></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="cargoRows.length === 0"><td colspan="9"><div class="warehouse-empty">请先选择仍有库存的历史入仓单,系统将带出可用库存明细</div></td></tr></tbody></table></div>
@@ -606,7 +659,7 @@ function selectRelabelInventory(inboundOrderId: string) {
.warehouse-choice-field{display:grid;grid-template-columns:150px auto 1fr;align-items:center;gap:14px;min-height:40px;padding:12px 14px;border:1px solid #e1e8f2;border-radius:5px;background:#f8faff}.warehouse-choice-field>span{color:#34445c;font-size:13px;font-weight:650}.warehouse-choice-field>span i{margin-right:3px;color:#e24b4b;font-style:normal}.warehouse-choice-field>small{color:#7a879a;font-size:12px}.warehouse-receiving-card{display:grid;grid-template-columns:2fr 1fr 1fr;gap:22px;padding:15px 16px;border:1px solid #cfe0f5;border-radius:5px;background:#f6faff}.warehouse-receiving-card div{display:flex;min-width:0;flex-direction:column;gap:6px}.warehouse-receiving-card span{color:#7c899c;font-size:12px}.warehouse-receiving-card strong{overflow-wrap:anywhere;color:#314561;font-size:13px;font-weight:600}
.warehouse-hidden-input{display:none}.warehouse-label-table{min-width:1120px}.warehouse-attachment-tip{margin:10px 0 12px;color:#7d8797;font-size:12px}.warehouse-mark-mapping{display:flex;flex-wrap:wrap;gap:8px;margin:4px 0 8px;padding:12px;border:1px solid #dce7f5;border-radius:5px;background:#f7faff}.warehouse-mark-mapping strong{width:100%;color:#34445c}.warehouse-mark-mapping span{padding:5px 9px;border-radius:3px;background:#eaf2fc;color:#3e6593;font-size:12px}
.warehouse-unmaintained{margin-left:8px;color:#e6a23c;font-size:12px;font-style:normal}
.warehouse-labeling-tabs{margin-top:-8px}.warehouse-tab-actions{align-items:center;margin:2px 0 14px}.warehouse-tab-actions p{margin:0;color:#7d8797;font-size:12px}.warehouse-tab-actions>div{display:flex;gap:8px}.warehouse-sku-notice{margin:0 0 14px;padding:10px 12px;border:1px solid #f2d39b;border-radius:4px;background:#fff8e8;color:#8a6116;font-size:12px}.warehouse-sku-label-table{min-width:980px}.warehouse-sku-label-table :deep(.el-input-number){width:160px}:deep(.warehouse-labeling-tabs>.el-tabs__header){margin-bottom:18px}:deep(.warehouse-labeling-tabs .el-tabs__item){height:42px;padding:0 26px;font-weight:600}.warehouse-labeling-mode{display:flex;align-items:center;gap:14px;margin:0 0 16px;padding:14px 16px;border:1px solid #dce5f5;border-radius:5px;background:#f7f9ff}.warehouse-labeling-mode>span{color:#34445c;font-size:13px;font-weight:650;white-space:nowrap}.warehouse-labeling-mode p{margin:0;color:#71809a;font-size:12px}.warehouse-labeling-mode :deep(.el-radio-group){display:flex;flex-shrink:0}.warehouse-labeling-mode :deep(.el-radio-button__inner){padding:8px 14px}
.warehouse-labeling-tabs{margin-top:-8px}.warehouse-labeling-heading{align-items:flex-start}.warehouse-labeling-heading h3{margin-bottom:16px}.warehouse-template-actions{display:flex;gap:8px;padding-bottom:12px}.warehouse-tab-actions{align-items:center;margin:2px 0 14px}.warehouse-tab-actions p{margin:0;color:#7d8797;font-size:12px}.warehouse-tab-actions>div{display:flex;gap:8px}.warehouse-sku-notice{margin:0 0 14px;padding:10px 12px;border:1px solid #f2d39b;border-radius:4px;background:#fff8e8;color:#8a6116;font-size:12px}.warehouse-sku-label-table{min-width:980px}.warehouse-sku-label-table :deep(.el-input-number){width:160px}:deep(.warehouse-labeling-tabs>.el-tabs__header){margin-bottom:18px}:deep(.warehouse-labeling-tabs .el-tabs__item){height:42px;padding:0 26px;font-weight:600}.warehouse-labeling-mode{display:flex;align-items:center;gap:14px;margin:0 0 16px;padding:14px 16px;border:1px solid #dce5f5;border-radius:5px;background:#f7f9ff}.warehouse-labeling-mode>span{color:#34445c;font-size:13px;font-weight:650;white-space:nowrap}.warehouse-labeling-mode p{margin:0;color:#71809a;font-size:12px}.warehouse-labeling-mode :deep(.el-radio-group){display:flex;flex-shrink:0}.warehouse-labeling-mode :deep(.el-radio-button__inner){padding:8px 14px}
.warehouse-relabel-detail-table{min-width:1420px}.warehouse-relabel-detail-table td strong{color:#253f67}
.warehouse-native-file{min-height:32px;padding:5px 8px;border:1px solid #dcdfe6;border-radius:4px;background:#fff;color:#606266}.warehouse-fulfillment-grid{margin-bottom:18px}.warehouse-evidence{display:flex;align-items:center;gap:16px;padding:14px;border:1px solid #e7ebf1;border-radius:4px;background:#f8fafc}.warehouse-evidence>span{color:#71809a;font-size:12px}.warehouse-evidence>strong{color:#8a95a5;font-size:12px;font-weight:400}.warehouse-photo-actions{display:flex;flex-wrap:wrap;gap:8px}.warehouse-completion-remark{margin:10px 0 0;color:#596579;font-size:12px}
</style>
@@ -27,9 +27,9 @@ test('客户贴标表单不展示客户单号、托盘和内部库存来源列',
test('贴标申请保留关联模板下载和上传入口', () => {
assert.match(view, /downloadRelabelTemplate/)
assert.match(view, /下载关联模板/)
assert.match(view, /下载\{\{ labelingMode === 'carton' \? '箱唛' : 'SKU' \}\}模板/)
assert.match(view, /templateInput\?\.click\(\)/)
assert.match(view, /上传关联面单/)
assert.match(view, /上传\{\{ labelingMode === 'carton' \? '箱唛' : 'SKU' \}\}模板/)
assert.match(view, /importRelabelTemplate/)
assert.match(view, /明细行标识/)
assert.doesNotMatch(view, /库存位置ID/)
@@ -52,6 +52,9 @@ test('贴标货物明细通过单一换标类型选择三种模式', () => {
assert.match(view, /原 SKU \*/)
assert.match(view, /我方箱唛 \*/)
assert.match(view, /换标数量(件)\*/)
assert.match(view, /旧SKU标签/)
assert.match(view, /新SKU标签/)
assert.match(view, /已导入 \$\{rows\.length\} 条 SKU 换标明细/)
assert.match(view, /buildSkuRelabelItems/)
assert.match(view, /CARTON_AND_SKU/)
assert.match(view, /changeCarton/)
@@ -23,3 +23,19 @@ test('仓储转运允许暂未确定 FBA 去向并显式选择是否换标', ()
assert.match(api, /fbaWarehouseCode\?: string/)
assert.match(api, /fbaShipmentNo\?: string/)
})
test('仓储转运不要求客户填写内部入仓号和箱唛', () => {
const transferConfig = view.match(/transfer:\s*\{[^\n]+/)?.[0] || ''
const transferSubmit = view.match(/await createTransfer\(\{[^\n]+/)?.[0] || ''
const transferCargoTable = view.match(/<template v-else>[\s\S]*?<\/template>\s*<\/div>\s*<div class="warehouse-submit-bar"/)?.[0] || ''
assert.doesNotMatch(transferConfig, /进仓编号/)
assert.doesNotMatch(transferSubmit, /inboundCode|sourceLastlegOrderIds|shippingMark/)
assert.match(transferCargoTable, /预计箱数/)
assert.doesNotMatch(transferCargoTable, /props\.service === 'transfer'.*FNSKU \/ 箱唛/)
})
test('上门提货不提交物流号,需要换标仅提示入库后绑定', () => {
assert.match(view, /trackingNo:\s*form\.deliveryMethod === 'CUSTOMER_DELIVERY'\s*\?[^:]+:\s*undefined/)
assert.match(view, /完成入库后再绑定换标单/)
assert.match(view, /form\.deliveryMethod === 'CUSTOMER_DELIVERY'.*物流跟踪号/s)
})