This commit is contained in:
2026-09-09 13:35:51 +08:00
parent e5039f2707
commit 7dc02d8327
6 changed files with 71 additions and 60 deletions
+1 -1
View File
@@ -54,7 +54,7 @@
| 客户资金账户、授信与充值申请 | `src/modules/finance/views/FinanceAccountView.vue`、`api/account.ts` | 显示当前企业 CNY 实际余额、冻结金额、授信额度、已用授信、冻结授信、总可用余额和不可变流水;数据加载前显示“—”,不得先伪装为零。充值只展示服务端启用的微信/支付宝静态收款码,客户上传 JPG/PNG 付款截图后进入待审核。客户范围只取登录态,无收款码时禁止提交,不模拟支付成功 | | 客户资金账户、授信与充值申请 | `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/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/order-management/routes.ts`、`views/InboundOrdersView.vue`、`api/inboundPlans.ts` | `orders/inbound` 为只读列表/详情,权限 `merchant:warehouse:inbound:list`;不展示或提交内部 ID |
| 海外仓贴标、仓储转运、货物废弃、退货换标 | `src/modules/overseas-warehouse/routes.ts`、`views/WarehouseServiceView.vue`、`api/warehouseServices.ts` | 四个路由均为 `active`;贴标申请先从共享仓库主数据选择国外中转仓,并自动带出海外仓签收联系人和联系电话,再从当前客户已经完成入库且仍有可用库存的历史入仓单中单选。货物明细支持箱唛换标、SKU 换标和同箱组合换标:SKU 明细按来源库存箱聚合为一个箱唛任务,整箱只冻结一次,SKU 数量按件保存为子明细;PDA 扫我方箱唛定位任务,返回新 FBA 箱唛和 SKU 子明细,照片完工并复核通过后释放原库存,后续仍进入既有排车出库。客户页面不暴露托盘、库位和内部库存 ID;换标申请必须提交正数 CNY `chargeAmount`,页面展示服务端钱包冻结/结算/释放状态及错误,不本地推导余额结果。Backend `lastleg-service/` 的 `relabel/`、`relabel/pda/`、`transfer/`、`discard/`,请求统一经 `src/api/overseasHttp.ts` 与 `gateway-overseas/` | | 海外仓贴标、仓储转运、货物废弃、退货换标 | `src/modules/overseas-warehouse/routes.ts`、`views/WarehouseServiceView.vue`、`api/warehouseServices.ts` | 四个路由均为 `active`;贴标申请先从共享仓库主数据选择国外中转仓,并自动带出海外仓签收联系人和联系电话,再从当前客户已经完成入库且仍有可用库存的历史入仓单中单选。货物明细在同一页面通过“换标类型”单选区分只换 SKU 标签、只换箱唛、SKU+箱唛都换,避免两个 Tab 造成语义混淆:客户自行填写我方箱唛,同一箱唛的多条 SKU 明细聚合为一个箱任务,整箱只冻结一次,SKU 数量按件保存为子明细;历史入仓库存只提供大概位置和可用箱数,不能据此推断箱唛与具体托盘的绑定关系。PDA 扫我方箱唛定位任务,仓库人员在大概位置内找到实物后完成换标,返回新 FBA 箱唛和 SKU 子明细,照片完工并复核通过后释放原库存,后续仍进入既有排车出库。客户页面不暴露托盘、库位和内部库存 ID;换标申请必须提交正数 CNY `chargeAmount`,页面展示服务端钱包冻结/结算/释放状态及错误,不本地推导余额结果。Backend `lastleg-service/` 的 `relabel/`、`relabel/pda/`、`transfer/`、`discard/`,请求统一经 `src/api/overseasHttp.ts` 与 `gateway-overseas/` |
## 租户、认证与数据安全 ## 租户、认证与数据安全
@@ -3,7 +3,7 @@ import type { PageResult } from './warehouseServices'
export interface ReturnOrderSummary { export interface ReturnOrderSummary {
returnNo: string; overseasWarehouseId: string; customerReferenceNo?: string; status: string returnNo: string; overseasWarehouseId: string; customerReferenceNo?: string; status: string
skuTypes: number; expectedQuantity: number; receivedQuantity: number; createdAt: string skuTypes: number; expectedQuantity: number; receivedQuantity: number; needRelabel: boolean; createdAt: string
} }
export interface ReturnOrderItem { export interface ReturnOrderItem {
skuCode: string; productNameZh?: string; expectedQuantity: number; receivedQuantity: number skuCode: string; productNameZh?: string; expectedQuantity: number; receivedQuantity: number
@@ -14,7 +14,7 @@ export interface ReturnReceipt {
} }
export interface ReturnOrderDetail { order: ReturnOrderSummary; remark?: string; items: ReturnOrderItem[]; receipts: ReturnReceipt[] } export interface ReturnOrderDetail { order: ReturnOrderSummary; remark?: string; items: ReturnOrderItem[]; receipts: ReturnReceipt[] }
export interface ReturnOrderCreate { export interface ReturnOrderCreate {
overseasWarehouseId: string; customerReferenceNo?: string; remark?: string; idempotencyKey: string overseasWarehouseId: string; customerReferenceNo?: string; remark?: string; needRelabel: boolean; idempotencyKey: string
items: Array<{ skuCode: string; productNameZh?: string; expectedQuantity: number }> items: Array<{ skuCode: string; productNameZh?: string; expectedQuantity: number }>
} }
+1 -1
View File
@@ -26,7 +26,7 @@ export const overseasWarehouseModule = {
{ {
path: 'overseas-warehouse/return-relabel', name: 'overseas-return-relabel', path: 'overseas-warehouse/return-relabel', name: 'overseas-return-relabel',
component: () => import('./views/ReturnOrdersView.vue'), release: 'active', component: () => import('./views/ReturnOrdersView.vue'), release: 'active',
meta: { title: '退货签收', description: '创建退货预报并查看仓库签收结果', permission: 'dashboard:view' }, meta: { title: '退货换标', description: '创建退货预报并查看仓库签收结果', permission: 'dashboard:view' },
navigation: { key: 'overseas-return-relabel', icon: RefreshCcw }, navigation: { key: 'overseas-return-relabel', icon: RefreshCcw },
}, },
{ {
@@ -19,7 +19,7 @@ const query = reactive({ page: 1, size: 20, returnNo: '', trackingNo: '', skuCod
interface SkuDraft { id: number; skuCode: string; productNameZh: string; expectedQuantity: number } interface SkuDraft { id: number; skuCode: string; productNameZh: string; expectedQuantity: number }
let skuSequence = 0 let skuSequence = 0
function emptySku(): SkuDraft { return { id: ++skuSequence, skuCode: '', productNameZh: '', expectedQuantity: 1 } } function emptySku(): SkuDraft { return { id: ++skuSequence, skuCode: '', productNameZh: '', expectedQuantity: 1 } }
const form = reactive({ overseasWarehouseId: '', customerReferenceNo: '', remark: '', items: [emptySku()] }) const form = reactive({ overseasWarehouseId: '', customerReferenceNo: '', remark: '', needRelabel: false, items: [emptySku()] })
function normalizedSku(value: string) { return value.trim().toUpperCase() } function normalizedSku(value: string) { return value.trim().toUpperCase() }
const duplicateSkuIds = computed(() => { const duplicateSkuIds = computed(() => {
const grouped = new Map<string, number[]>() const grouped = new Map<string, number[]>()
@@ -29,7 +29,7 @@ const duplicateSkuIds = computed(() => {
} }
return new Set([...grouped.values()].filter(ids => ids.length > 1).flat()) return new Set([...grouped.values()].filter(ids => ids.length > 1).flat())
}) })
function resetForm() { Object.assign(form, { overseasWarehouseId: '', customerReferenceNo: '', remark: '', items: [emptySku()] }) } function resetForm() { Object.assign(form, { overseasWarehouseId: '', customerReferenceNo: '', remark: '', needRelabel: false, items: [emptySku()] }) }
function warehouseLabel(warehouse: MerchantWarehouse) { return `${warehouse.warehouseName} · ${warehouse.warehouseCode}` } function warehouseLabel(warehouse: MerchantWarehouse) { return `${warehouse.warehouseName} · ${warehouse.warehouseCode}` }
async function loadOperationWarehouses() { async function loadOperationWarehouses() {
operationWarehousesLoading.value = true operationWarehousesLoading.value = true
@@ -63,7 +63,7 @@ async function submit() {
if (duplicateSkuIds.value.size) { ElMessage.warning('同一退货单不能重复填写相同 SKU'); return } if (duplicateSkuIds.value.size) { ElMessage.warning('同一退货单不能重复填写相同 SKU'); return }
creating.value = true creating.value = true
try { try {
const detail = await createReturnOrder({ overseasWarehouseId: form.overseasWarehouseId, customerReferenceNo: form.customerReferenceNo || undefined, remark: form.remark || undefined, idempotencyKey: crypto.randomUUID(), items: items.map(item => ({ ...item, productNameZh: item.productNameZh || undefined })) }) const detail = await createReturnOrder({ overseasWarehouseId: form.overseasWarehouseId, customerReferenceNo: form.customerReferenceNo || undefined, remark: form.remark || undefined, needRelabel: form.needRelabel, idempotencyKey: crypto.randomUUID(), items: items.map(item => ({ ...item, productNameZh: item.productNameZh || undefined })) })
ElMessage.success('退货预报已创建'); showCreate.value = false; resetForm(); await load(); await openDetail(detail.order.returnNo) ElMessage.success('退货预报已创建'); showCreate.value = false; resetForm(); await load(); await openDetail(detail.order.returnNo)
} finally { creating.value = false } } finally { creating.value = false }
} }
@@ -85,6 +85,7 @@ onMounted(load)
<el-table-column prop="returnNo" label="退货单号" min-width="190" /> <el-table-column prop="returnNo" label="退货单号" min-width="190" />
<el-table-column prop="customerReferenceNo" label="客户参考号" min-width="150" /> <el-table-column prop="customerReferenceNo" label="客户参考号" min-width="150" />
<el-table-column prop="overseasWarehouseId" label="操作仓" min-width="120" /> <el-table-column prop="overseasWarehouseId" label="操作仓" min-width="120" />
<el-table-column label="换标" width="100"><template #default="{ row }"><el-tag v-if="row.needRelabel" type="warning">需要换标</el-tag><span v-else>否</span></template></el-table-column>
<el-table-column prop="skuTypes" label="SKU种类" width="100" /> <el-table-column prop="skuTypes" label="SKU种类" width="100" />
<el-table-column prop="expectedQuantity" label="预报数量" width="110" /> <el-table-column prop="expectedQuantity" label="预报数量" width="110" />
<el-table-column prop="receivedQuantity" label="实收数量" width="110" /> <el-table-column prop="receivedQuantity" label="实收数量" width="110" />
@@ -97,6 +98,7 @@ onMounted(load)
<el-dialog v-model="showCreate" title="新建退货预报" width="920px" destroy-on-close> <el-dialog v-model="showCreate" title="新建退货预报" width="920px" destroy-on-close>
<el-form label-width="150px"><el-form-item label="海外操作仓(中转仓)" required><el-select v-model="form.overseasWarehouseId" filterable clearable :loading="operationWarehousesLoading" :disabled="Boolean(operationWarehousesError)" placeholder="请选择国外中转仓"><el-option v-for="warehouse in operationWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="String(warehouse.id)" /></el-select><div v-if="operationWarehousesError" class="warehouse-error">{{ operationWarehousesError }}</div></el-form-item><el-form-item label="客户参考号"><el-input v-model="form.customerReferenceNo" placeholder="可填写客户侧参考号" /></el-form-item><el-form-item label="备注"><el-input v-model="form.remark" type="textarea" placeholder="填写退货说明(选填)" /></el-form-item></el-form> <el-form label-width="150px"><el-form-item label="海外操作仓(中转仓)" required><el-select v-model="form.overseasWarehouseId" filterable clearable :loading="operationWarehousesLoading" :disabled="Boolean(operationWarehousesError)" placeholder="请选择国外中转仓"><el-option v-for="warehouse in operationWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="String(warehouse.id)" /></el-select><div v-if="operationWarehousesError" class="warehouse-error">{{ operationWarehousesError }}</div></el-form-item><el-form-item label="客户参考号"><el-input v-model="form.customerReferenceNo" placeholder="可填写客户侧参考号" /></el-form-item><el-form-item label="备注"><el-input v-model="form.remark" type="textarea" placeholder="填写退货说明(选填)" /></el-form-item></el-form>
<div class="items-title"><span>SKU 预报明细</span><el-button text type="primary" @click="addItem"><Plus :size="15" />添加 SKU</el-button></div> <div class="items-title"><span>SKU 预报明细</span><el-button text type="primary" @click="addItem"><Plus :size="15" />添加 SKU</el-button></div>
<div class="return-relabel-option"><span>换标</span><el-checkbox v-model="form.needRelabel">需要换标</el-checkbox></div>
<div class="items-hint">客户只需填写预计退回的 SKU 和数量;快递单号由仓库签收时按实际包裹扫描绑定。</div> <div class="items-hint">客户只需填写预计退回的 SKU 和数量;快递单号由仓库签收时按实际包裹扫描绑定。</div>
<el-table :data="form.items" row-key="id" size="small"> <el-table :data="form.items" row-key="id" size="small">
<el-table-column label="SKU" min-width="200"><template #default="{ row }"><div><el-input v-model="row.skuCode" :class="{ 'duplicate-sku-input': duplicateSkuIds.has(row.id) }" placeholder="填写 SKU" @blur="row.skuCode = row.skuCode.trim()" /><div v-if="duplicateSkuIds.has(row.id)" class="duplicate-sku-error">该 SKU 已在明细中填写</div></div></template></el-table-column> <el-table-column label="SKU" min-width="200"><template #default="{ row }"><div><el-input v-model="row.skuCode" :class="{ 'duplicate-sku-input': duplicateSkuIds.has(row.id) }" placeholder="填写 SKU" @blur="row.skuCode = row.skuCode.trim()" /><div v-if="duplicateSkuIds.has(row.id)" class="duplicate-sku-error">该 SKU 已在明细中填写</div></div></template></el-table-column>
@@ -14,9 +14,9 @@ import { getMerchantWarehouses, type MerchantWarehouse } from '@/modules/first-l
type ServiceType = 'labeling' | 'transfer' | 'disposal' | 'relabel' type ServiceType = 'labeling' | 'transfer' | 'disposal' | 'relabel'
type PageMode = 'list' | 'create' | 'detail' type PageMode = 'list' | 'create' | 'detail'
type LabelingMode = 'carton' | 'sku' type LabelingMode = 'carton' | 'sku' | 'carton_and_sku'
interface CargoRow { id: number; sku: string; name: string; quantity: number; availableQuantity?: number; sourceOrderId?: string; sourceInventoryBalanceId?: string; sourceInventoryPositionId?: string; sourceInboundOrderId?: string; sourceInboundOrderNo?: string; sourceInboundCode?: string; sourceWarehouseName?: string; palletNo?: string; warehouseLocationCode?: string; fnsku: string; shipmentNo: string; newSkuCode: string; shippingMark: string; cartonMark: string; fbaCartonMark: string; destinationWarehouseId: string; remark: string } interface CargoRow { id: number; sku: string; name: string; quantity: number; availableQuantity?: number; sourceOrderId?: string; sourceInventoryBalanceId?: string; sourceInventoryPositionId?: string; sourceInboundOrderId?: string; sourceInboundOrderNo?: string; sourceInboundCode?: string; sourceWarehouseName?: string; palletNo?: string; warehouseLocationCode?: string; fnsku: string; shipmentNo: string; newSkuCode: string; shippingMark: string; cartonMark: string; fbaCartonMark: string; destinationWarehouseId: string; remark: string }
interface SkuRelabelRow { id: number; sourceRowId: number | null; cartonMark: string; originalSku: string; newSku: string; quantity: number; changeCarton: boolean; 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 ServiceConfig { title: string; createTitle: string; numberLabel: string; emptyText: string; columns: string[] } interface ServiceConfig { title: string; createTitle: string; numberLabel: string; emptyText: string; columns: string[] }
const props = defineProps<{ service: ServiceType }>() const props = defineProps<{ service: ServiceType }>()
@@ -38,10 +38,11 @@ const form = reactive({ customerNo: '', inboundNo: '', sourceWarehouse: '', ware
const cargoRows = ref<CargoRow[]>([]) const cargoRows = ref<CargoRow[]>([])
const labelingMode = ref<LabelingMode>('carton') const labelingMode = ref<LabelingMode>('carton')
const skuRelabelRows = ref<SkuRelabelRow[]>([]) const skuRelabelRows = ref<SkuRelabelRow[]>([])
const cartonMarkOptions = computed(() => cargoRows.value.map((item) => ({ const labelingModeHint = computed(() => labelingMode.value === 'carton'
value: item.id, ? '只替换外箱箱唛,不修改箱内商品 SKU 标签;按箱填写新的 FBA 箱唛和目的仓。'
label: `${item.cartonMark || item.sku || '库存货物'}|可用 ${item.availableQuantity || item.quantity} 箱`, : labelingMode.value === 'sku'
}))) ? '只替换箱内商品 SKU 标签,保留原箱唛;同一箱唛可以添加多条 SKU 明细。'
: '同一箱同时替换商品 SKU 标签和外箱箱唛;同一箱唛的多条 SKU 使用同一个新 FBA 箱唛和目的仓。')
const submittedRows = ref<Record<string, string>[]>([]) const submittedRows = ref<Record<string, string>[]>([])
const selectedDetail = ref<Record<string, string> | null>(null) const selectedDetail = ref<Record<string, string> | null>(null)
const selectedRelabelDetail = ref<RelabelDetail | null>(null) const selectedRelabelDetail = ref<RelabelDetail | null>(null)
@@ -77,7 +78,7 @@ let cargoSequence = 0
let skuRelabelSequence = 0 let skuRelabelSequence = 0
watch(() => props.service, () => { mode.value = 'list'; resetFilters(); resetForm(); void load() }) watch(() => props.service, () => { mode.value = 'list'; resetFilters(); resetForm(); void load() })
watch(labelingMode, (value) => { if (value === 'sku' && !skuRelabelRows.value.length) addSkuRelabelRow() }) watch(labelingMode, (value) => { if (value !== 'carton' && !skuRelabelRows.value.length) addSkuRelabelRow() })
onMounted(() => { void load() }) onMounted(() => { void load() })
function resetFilters() { Object.assign(filters, { number: '', inboundNo: '', warehouse: '', destination: '', status: '', start: '', end: '' }) } function resetFilters() { Object.assign(filters, { number: '', inboundNo: '', warehouse: '', destination: '', status: '', start: '', end: '' }) }
function resetForm() { function resetForm() {
@@ -101,36 +102,36 @@ async function openCreate() {
finally { discardCandidatesLoading.value = false } finally { discardCandidatesLoading.value = false }
} }
function addCargoRow() { cargoRows.value.push({ id: ++cargoSequence, sku: '', name: '', quantity: 1, fnsku: '', shipmentNo: '', newSkuCode: '', shippingMark: '', cartonMark: '', fbaCartonMark: '', destinationWarehouseId: '', remark: '' }) } function addCargoRow() { cargoRows.value.push({ id: ++cargoSequence, sku: '', name: '', quantity: 1, fnsku: '', shipmentNo: '', newSkuCode: '', shippingMark: '', cartonMark: '', fbaCartonMark: '', destinationWarehouseId: '', remark: '' }) }
function addSkuRelabelRow() { skuRelabelRows.value.push({ id: ++skuRelabelSequence, sourceRowId: null, cartonMark: '', originalSku: '', newSku: '', quantity: 1, changeCarton: false, fbaCartonMark: '', destinationWarehouseId: '', remark: '' }) } function addSkuRelabelRow() {
function removeSkuRelabelRow(id: number) { skuRelabelRows.value = skuRelabelRows.value.filter((row) => row.id !== id) } const source = cargoRows.value[Math.min(skuRelabelRows.value.length, Math.max(0, cargoRows.value.length - 1))]
function selectSkuSource(row: SkuRelabelRow) { skuRelabelRows.value.push({ id: ++skuRelabelSequence, sourceRowId: source?.id || null,
const source = cargoRows.value.find((item) => item.id === row.sourceRowId) cartonMark: '', originalSku: source?.sku || '', newSku: '', quantity: 1,
if (!source) return fbaCartonMark: '', destinationWarehouseId: source?.destinationWarehouseId || '', remark: '' })
row.cartonMark = source.cartonMark || ''
row.originalSku = row.originalSku || source.sku
row.destinationWarehouseId = row.destinationWarehouseId || source.destinationWarehouseId
} }
function removeSkuRelabelRow(id: number) { skuRelabelRows.value = skuRelabelRows.value.filter((row) => row.id !== id) }
function buildSkuRelabelItems(): RelabelCreate['items'] { function buildSkuRelabelItems(): RelabelCreate['items'] {
const groups = new Map<number, SkuRelabelRow[]>() const groups = new Map<string, SkuRelabelRow[]>()
for (const row of skuRelabelRows.value) { for (const row of skuRelabelRows.value) {
if (row.sourceRowId == null) throw new Error('请选择每条 SKU 明细对应的来源库存箱') const cartonMark = row.cartonMark.trim().toUpperCase()
const rows = groups.get(row.sourceRowId) || [] if (!cartonMark) throw new Error('请填写每个箱子的我方箱唛')
const rows = groups.get(cartonMark) || []
rows.push(row) rows.push(row)
groups.set(row.sourceRowId, rows) groups.set(cartonMark, rows)
} }
return [...groups.entries()].map(([sourceRowId, rows]) => { const anchors = cargoRows.value.flatMap((item) => Array.from(
const source = cargoRows.value.find((item) => item.id === sourceRowId) { length: Number(item.availableQuantity || 0) }, () => item,
))
if (!anchors.length) throw new Error('请先选择有库存的历史入仓单')
if (groups.size > anchors.length) {
throw new Error('填写的箱唛数量超过当前入仓单可用库存箱数')
}
return [...groups.entries()].map(([_, rows], groupIndex) => {
const source = anchors[groupIndex]
if (!source?.sourceInventoryBalanceId || !source.sourceInventoryPositionId || !source.sourceInboundOrderId) { if (!source?.sourceInventoryBalanceId || !source.sourceInventoryPositionId || !source.sourceInboundOrderId) {
throw new Error('来源库存已变化,请重新选择有库存的历史入仓单') throw new Error('来源库存已变化,请重新选择有库存的历史入仓单')
} }
const cartonMark = rows[0].cartonMark.trim() const cartonMark = rows[0].cartonMark.trim()
if (!cartonMark || rows.some((row) => row.cartonMark.trim() !== cartonMark)) { const changeCarton = labelingMode.value === 'carton_and_sku'
throw new Error('同一来源库存箱的 SKU 明细必须使用同一个我方箱唛')
}
const changeCarton = rows[0].changeCarton
if (rows.some((row) => row.changeCarton !== changeCarton)) {
throw new Error('同一我方箱唛的 SKU 明细必须统一是否同时更换箱唛')
}
const fbaCartonMark = rows[0].fbaCartonMark.trim() const fbaCartonMark = rows[0].fbaCartonMark.trim()
const destinationWarehouseId = rows[0].destinationWarehouseId const destinationWarehouseId = rows[0].destinationWarehouseId
if (changeCarton && (!fbaCartonMark || !destinationWarehouseId if (changeCarton && (!fbaCartonMark || !destinationWarehouseId
@@ -143,8 +144,8 @@ function buildSkuRelabelItems(): RelabelCreate['items'] {
sourceInboundOrderId: source.sourceInboundOrderId, sourceInboundOrderId: source.sourceInboundOrderId,
skuCode: source.sku || rows[0].originalSku, skuCode: source.sku || rows[0].originalSku,
productNameZh: source.name, productNameZh: source.name,
totalPackageCount: source.quantity, totalPackageCount: 1,
notifiedPackageCount: source.quantity, notifiedPackageCount: 1,
shipmentNo: source.shipmentNo || cartonMark, shipmentNo: source.shipmentNo || cartonMark,
trackingCode: form.trackingNo || undefined, trackingCode: form.trackingNo || undefined,
newSkuCode: rows[0].newSku, newSkuCode: rows[0].newSku,
@@ -169,8 +170,8 @@ function clearRelabelRows() { cargoRows.value = []; selectedRelabelInboundOrderI
function csvCell(value: string | number | undefined) { return `"${String(value ?? '').replaceAll('"', '""')}"` } function csvCell(value: string | number | undefined) { return `"${String(value ?? '').replaceAll('"', '""')}"` }
function downloadRelabelTemplate() { function downloadRelabelTemplate() {
if (!cargoRows.value.length) { ElMessage.warning('请先选择有库存的历史入仓单'); return } if (!cargoRows.value.length) { ElMessage.warning('请先选择有库存的历史入仓单'); return }
const header = ['明细行标识', '本次箱数', '商品SKU', '中文品名', 'FBA货件编号', '换标新SKU', '我方箱唛', 'FBA箱唛', 'FBA目的仓', '备注'] const header = ['明细行标识', '本次箱数', '商品SKU', '中文品名', 'FBA货件编号', '我方箱唛', 'FBA箱唛', 'FBA目的仓', '备注']
const rows = cargoRows.value.map((item) => [item.id, item.quantity, item.sku, item.name, item.shipmentNo, item.newSkuCode, const rows = cargoRows.value.map((item) => [item.id, item.quantity, item.sku, item.name, item.shipmentNo,
item.cartonMark, item.fbaCartonMark, item.destinationWarehouseId, item.remark]) item.cartonMark, 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 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 url = URL.createObjectURL(blob)
@@ -193,13 +194,13 @@ async function importRelabelTemplate(event: Event) {
const lines = (await file.text()).replace(/^\uFEFF/, '').split(/\r?\n/).filter((line) => line.trim()) const lines = (await file.text()).replace(/^\uFEFF/, '').split(/\r?\n/).filter((line) => line.trim())
if (lines.length < 2) { ElMessage.error('模板中没有可导入的明细'); return } if (lines.length < 2) { ElMessage.error('模板中没有可导入的明细'); return }
const headers = parseCsvLine(lines[0]) const headers = parseCsvLine(lines[0])
const required = ['明细行标识', '本次箱数', 'FBA货件编号', '换标新SKU', '我方箱唛', 'FBA箱唛', 'FBA目的仓'] const required = ['明细行标识', '本次箱数', 'FBA货件编号', '我方箱唛', 'FBA箱唛', 'FBA目的仓']
if (required.some((name) => !headers.includes(name))) { ElMessage.error('模板格式不正确,请重新下载模板填写'); return } if (required.some((name) => !headers.includes(name))) { ElMessage.error('模板格式不正确,请重新下载模板填写'); return }
const imported = lines.slice(1).map(parseCsvLine) const imported = lines.slice(1).map(parseCsvLine)
.map((values) => Object.fromEntries(headers.map((key, index) => [key, values[index] || '']))) .map((values) => Object.fromEntries(headers.map((key, index) => [key, values[index] || ''])))
.filter((item) => item['明细行标识']) .filter((item) => item['明细行标识'])
if (!imported.length || imported.some((item) => required.some((key) => !item[key]))) { if (!imported.length || imported.some((item) => required.some((key) => !item[key]))) {
ElMessage.error('模板中的明细行、箱数、货件、换标 SKU、箱唛和 FBA 目的仓必须填写完整'); return ElMessage.error('模板中的明细行、箱数、货件、箱唛和 FBA 目的仓必须填写完整'); return
} }
if (new Set(imported.map((item) => item['明细行标识'])).size !== imported.length) { if (new Set(imported.map((item) => item['明细行标识'])).size !== imported.length) {
ElMessage.error('模板中的明细行标识不能重复'); return ElMessage.error('模板中的明细行标识不能重复'); return
@@ -216,7 +217,6 @@ async function importRelabelTemplate(event: Event) {
} }
row.quantity = quantity row.quantity = quantity
row.shipmentNo = item['FBA货件编号'] row.shipmentNo = item['FBA货件编号']
row.newSkuCode = item['换标新SKU']
row.cartonMark = item['我方箱唛'] row.cartonMark = item['我方箱唛']
row.fbaCartonMark = item['FBA箱唛'] row.fbaCartonMark = item['FBA箱唛']
row.destinationWarehouseId = item['FBA目的仓'] row.destinationWarehouseId = item['FBA目的仓']
@@ -285,13 +285,13 @@ async function load() {
function query() { void load() } function query() { void load() }
async function submit() { async function submit() {
const primaryWarehouse = props.service === 'relabel' ? form.sourceWarehouse : form.warehouse const primaryWarehouse = props.service === 'relabel' ? form.sourceWarehouse : form.warehouse
const relabelRows = labelingMode.value === 'sku' ? skuRelabelRows.value : cargoRows.value const relabelRows = labelingMode.value === 'carton' ? cargoRows.value : skuRelabelRows.value
const invalidCargo = props.service === 'disposal' const invalidCargo = props.service === 'disposal'
? !selectedDiscardOrderId.value || cargoRows.value.every((item) => item.quantity < 1) ? !selectedDiscardOrderId.value || cargoRows.value.every((item) => item.quantity < 1)
|| cargoRows.value.some((item) => item.quantity < 0 || item.quantity > Number(item.availableQuantity || 0)) || cargoRows.value.some((item) => item.quantity < 0 || item.quantity > Number(item.availableQuantity || 0))
: relabelRows.length === 0 || (props.service === 'labeling' && labelingMode.value === 'sku' : relabelRows.length === 0 || (props.service === 'labeling' && labelingMode.value !== 'carton'
? skuRelabelRows.value.some((item) => !item.sourceRowId || !item.cartonMark || !item.originalSku || !item.newSku || item.quantity < 1 ? skuRelabelRows.value.some((item) => !item.cartonMark || !item.originalSku || !item.newSku || item.quantity < 1
|| (item.changeCarton && (!item.fbaCartonMark || !item.destinationWarehouseId))) || (labelingMode.value === 'carton_and_sku' && (!item.fbaCartonMark || !item.destinationWarehouseId)))
: cargoRows.value.some((item) => (props.service !== 'labeling' && !item.sku) || !item.name || item.quantity < 1)) : cargoRows.value.some((item) => (props.service !== 'labeling' && !item.sku) || !item.name || item.quantity < 1))
if ((props.service !== 'labeling' && !form.customerNo && !form.inboundNo) || (props.service !== 'disposal' && !primaryWarehouse) || invalidCargo) { if ((props.service !== 'labeling' && !form.customerNo && !form.inboundNo) || (props.service !== 'disposal' && !primaryWarehouse) || invalidCargo) {
ElMessage.warning('请填写必填信息和完整的货物明细'); return ElMessage.warning('请填写必填信息和完整的货物明细'); return
@@ -299,7 +299,7 @@ async function submit() {
if (props.service === 'labeling' && (!form.labelDeadline || !form.contact || !form.phone || form.chargeAmount <= 0)) { if (props.service === 'labeling' && (!form.labelDeadline || !form.contact || !form.phone || form.chargeAmount <= 0)) {
ElMessage.warning('请填写贴标截止时间、联系人和贴标费用'); return ElMessage.warning('请填写贴标截止时间、联系人和贴标费用'); return
} }
if (props.service === 'labeling' && labelingMode.value === 'carton' && cargoRows.value.some((item) => !item.shipmentNo || !item.newSkuCode || !item.cartonMark || !item.fbaCartonMark)) { if (props.service === 'labeling' && labelingMode.value === 'carton' && cargoRows.value.some((item) => !item.shipmentNo || !item.cartonMark || !item.fbaCartonMark)) {
ElMessage.warning('请上传关联面单,确保每箱的我方箱唛和 FBA 箱唛完整'); return ElMessage.warning('请上传关联面单,确保每箱的我方箱唛和 FBA 箱唛完整'); return
} }
if (props.service === 'labeling' && labelingMode.value === 'carton' && cargoRows.value.some((item) => !item.destinationWarehouseId || !item.sourceInventoryPositionId)) { if (props.service === 'labeling' && labelingMode.value === 'carton' && cargoRows.value.some((item) => !item.destinationWarehouseId || !item.sourceInventoryPositionId)) {
@@ -318,7 +318,7 @@ async function submit() {
if (props.service === 'labeling') { 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 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' const items = labelingMode.value === 'carton'
? cargoRows.value.map((item) => ({ sourceInventoryBalanceId: item.sourceInventoryBalanceId!, sourceInventoryPositionId: item.sourceInventoryPositionId!, sourceInboundOrderId: item.sourceInboundOrderId!, skuCode: item.sku, productNameZh: item.name, totalPackageCount: item.quantity, notifiedPackageCount: item.quantity, shipmentNo: item.shipmentNo, trackingCode: form.trackingNo || undefined, newSkuCode: item.newSkuCode, labelRequirement: form.requirement || undefined, cartonMark: item.cartonMark, fbaCartonMark: item.fbaCartonMark, destinationFbaWarehouseCode: item.destinationWarehouseId, operationType: 'CARTON_LABEL' as const, skuItems: [], remark: item.remark || undefined })) ? cargoRows.value.map((item) => ({ sourceInventoryBalanceId: item.sourceInventoryBalanceId!, sourceInventoryPositionId: item.sourceInventoryPositionId!, sourceInboundOrderId: item.sourceInboundOrderId!, skuCode: item.sku, productNameZh: item.name, totalPackageCount: item.quantity, notifiedPackageCount: item.quantity, shipmentNo: item.shipmentNo, trackingCode: form.trackingNo || undefined, newSkuCode: undefined, labelRequirement: form.requirement || undefined, cartonMark: item.cartonMark, fbaCartonMark: item.fbaCartonMark, destinationFbaWarehouseCode: item.destinationWarehouseId, operationType: 'CARTON_LABEL' as const, skuItems: [], remark: item.remark || undefined }))
: buildSkuRelabelItems() : buildSkuRelabelItems()
await createRelabel({ customerOrderNo: sourceInboundOrders || '贴标申请', operationWarehouseId: form.warehouse, labelDeadline: form.labelDeadline, contactName: form.contact, contactPhone: form.phone, chargeAmount: form.chargeAmount, remark: form.remark || undefined, idempotencyKey, items }) await createRelabel({ customerOrderNo: sourceInboundOrders || '贴标申请', operationWarehouseId: form.warehouse, labelDeadline: form.labelDeadline, contactName: form.contact, contactPhone: form.phone, chargeAmount: form.chargeAmount, remark: form.remark || undefined, idempotencyKey, items })
} else if (props.service === 'disposal') { } else if (props.service === 'disposal') {
@@ -397,6 +397,7 @@ function selectRelabelInventory(inboundOrderId: string) {
} }
})) }))
skuRelabelRows.value = [] skuRelabelRows.value = []
if (labelingMode.value !== 'carton') addSkuRelabelRow()
} }
</script> </script>
@@ -450,21 +451,28 @@ function selectRelabelInventory(inboundOrderId: string) {
<label v-if="props.service === 'labeling'"><span>贴标要求</span><el-input v-model="form.requirement" placeholder="请输入贴标要求" /></label> <label v-if="props.service === 'labeling'"><span>贴标要求</span><el-input v-model="form.requirement" placeholder="请输入贴标要求" /></label>
<label v-else-if="props.service !== 'disposal'"><span>物流跟踪号</span><el-input v-model="form.trackingNo" placeholder="请输入物流跟踪号" /></label> <label v-else-if="props.service !== 'disposal'"><span>物流跟踪号</span><el-input v-model="form.trackingNo" placeholder="请输入物流跟踪号" /></label>
<label class="warehouse-span-2"><span>订单备注</span><el-input v-model="form.remark" type="textarea" :rows="3" placeholder="请输入备注信息" /></label> <label class="warehouse-span-2"><span>订单备注</span><el-input v-model="form.remark" type="textarea" :rows="3" placeholder="请输入备注信息" /></label>
<div v-if="props.service === 'transfer'" class="warehouse-checkbox-field warehouse-span-2"><span>是否换标</span><el-checkbox v-model="form.relabel">需要换标</el-checkbox></div>
</div> </div>
</div> </div>
<div class="warehouse-card warehouse-form-section"> <div class="warehouse-card warehouse-form-section">
<template v-if="props.service === 'labeling'"> <template v-if="props.service === 'labeling'">
<h3>货物明细</h3> <h3>货物明细</h3>
<el-tabs v-model="labelingMode" class="warehouse-labeling-tabs"> <div class="warehouse-labeling-mode">
<el-tab-pane label="箱唛换标" name="carton"> <span>换标类型</span>
<div class="warehouse-section-heading warehouse-tab-actions"><p>按箱关联我方箱唛与新的 FBA 箱唛,提交后按所选库存箱数冻结。</p><div><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> <el-radio-group v-model="labelingMode" aria-label="换标类型">
<div class="warehouse-table-scroll"><table class="warehouse-edit-table warehouse-label-table"><thead><tr><th>商品 SKU</th><th>中文品名 *</th><th>FBA 货件编号 *</th><th>换标新 SKU *</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 v-model="row.newSkuCode" placeholder="请输入换标新SKU" /></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="10"><div class="warehouse-empty">请先选择仍有库存的历史入仓单,系统将带出可用库存明细</div></td></tr></tbody></table></div> <el-radio-button value="carton">只换箱唛</el-radio-button>
</el-tab-pane> <el-radio-button value="sku">只换 SKU 标签</el-radio-button>
<el-tab-pane label="SKU 换标" name="sku"> <el-radio-button value="carton_and_sku">SKU 和箱唛都换</el-radio-button>
<div class="warehouse-section-heading warehouse-tab-actions"><p>指定我方箱唛后,可为同一箱唛添加多条 SKU 换标明细,数量按件填写。</p><div><el-button type="primary" size="small" @click="addSkuRelabelRow"><Plus :size="15" />添加 SKU 明细</el-button><el-button size="small" @click="skuRelabelRows = []">清空</el-button></div></div> </el-radio-group>
<div class="warehouse-table-scroll"><table class="warehouse-edit-table warehouse-sku-label-table"><thead><tr><th>来源库存箱 *</th><th>我方箱唛 *</th><th>原 SKU *</th><th>换标后 SKU *</th><th>换标数量(件)*</th><th>同时更换箱唛</th><th>新 FBA 箱唛</th><th>FBA 目的仓</th><th>备注</th><th>操作</th></tr></thead><tbody><tr v-for="row in skuRelabelRows" :key="row.id"><td><el-select v-model="row.sourceRowId" filterable placeholder="选择来源库存箱" @change="selectSkuSource(row)"><el-option v-for="source in cartonMarkOptions" :key="source.value" :label="source.label" :value="source.value" /></el-select></td><td><el-input v-model="row.cartonMark" placeholder="请输入或核对我方箱唛" /></td><td><el-input v-model="row.originalSku" placeholder="请输入原 SKU" /></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><el-checkbox v-model="row.changeCarton">换箱唛</el-checkbox></td><td><el-input v-model="row.fbaCartonMark" :disabled="!row.changeCarton" placeholder="组合换标时填写" /></td><td><el-select v-model="row.destinationWarehouseId" :loading="fbaWarehousesLoading" :disabled="!row.changeCarton || Boolean(fbaWarehousesError)" placeholder="组合换标时选择"><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="10"><div class="warehouse-empty">暂无 SKU 换标明细,请点击“添加 SKU 明细”</div></td></tr></tbody></table></div> <p>{{ labelingModeHint }}</p>
</el-tab-pane> </div>
</el-tabs> <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-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>
<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="请输入我方箱唛" /></td><td><el-input v-model="row.originalSku" placeholder="请输入原 SKU" /></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>
</template> </template>
<template v-else> <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> <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>
@@ -492,8 +500,9 @@ function selectRelabelInventory(inboundOrderId: string) {
<style scoped> <style scoped>
.warehouse-service-page{color:#25324a}.warehouse-breadcrumb{display:flex;align-items:center;gap:9px;margin:0 0 14px;color:#9aa3b2;font-size:12px}.warehouse-breadcrumb b{font-weight:400}.warehouse-breadcrumb strong{color:#606b7c}.warehouse-breadcrumb button{padding:0;border:0;background:transparent;color:#4376b8;cursor:pointer}.warehouse-page-heading{display:flex;align-items:center;justify-content:space-between;margin-bottom:16px}.warehouse-page-heading h2{margin:0;color:#17233a;font-size:22px;letter-spacing:0}.warehouse-card{margin-bottom:12px;border:1px solid #e7ebf1;border-radius:4px;background:#fff;box-shadow:0 1px 3px rgba(25,42,70,.04)}.warehouse-filter-card{padding:20px 22px 16px}.warehouse-filter-grid{display:grid;grid-template-columns:repeat(4,minmax(180px,1fr));gap:16px 20px}.warehouse-filter-grid label,.warehouse-form-grid label{display:flex;min-width:0;flex-direction:column;gap:7px}.warehouse-filter-grid label>span,.warehouse-form-grid label>span{color:#596579;font-size:12px}.warehouse-filter-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:18px}.warehouse-table-card{overflow:hidden}.warehouse-table-scroll{overflow-x:auto}table{width:100%;min-width:940px;border-collapse:collapse;font-size:12px}th{height:44px;padding:0 14px;border-bottom:1px solid #e8edf3;background:#f7f9fc;color:#4d5a6e;font-weight:650;text-align:left;white-space:nowrap}td{height:48px;padding:8px 14px;border-bottom:1px solid #eef1f5;color:#526176;white-space:nowrap}.warehouse-empty{display:grid;height:150px;color:#a0a9b7;place-items:center}.warehouse-link,.warehouse-delete{display:inline-flex;align-items:center;gap:5px;padding:4px;border:0;background:transparent;color:#2176d2;cursor:pointer}.warehouse-delete{color:#d64545}.warehouse-pagination{display:flex;align-items:center;justify-content:flex-end;gap:8px;padding:14px 18px;color:#778398;font-size:12px}.warehouse-pagination span{margin-right:8px}.warehouse-page-number{width:28px;height:28px;border:1px solid #2176d2;border-radius:3px;background:#2176d2;color:#fff}.warehouse-form-section{padding:22px}.warehouse-form-section h3{margin:0 0 18px;padding-bottom:12px;border-bottom:1px solid #edf0f4;color:#26364f;font-size:15px;letter-spacing:0}.warehouse-form-grid{display:grid;grid-template-columns:repeat(2,minmax(240px,1fr));gap:18px 28px}.warehouse-form-grid i{margin-right:3px;color:#e24b4b;font-style:normal}.warehouse-span-2{grid-column:1/-1}.warehouse-section-heading{display:flex;align-items:center;justify-content:space-between}.warehouse-section-heading h3{flex:1}.warehouse-edit-table td{padding:9px 10px}.warehouse-edit-table :deep(.el-input-number){width:130px}.warehouse-hint{margin:12px 0 0;color:#8792a2;font-size:12px}.warehouse-submit-bar{display:flex;justify-content:center;gap:10px;padding:12px 0 30px}.warehouse-detail-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:24px}.warehouse-detail-grid div{display:flex;flex-direction:column;gap:7px}.warehouse-detail-grid span{color:#8a95a5;font-size:12px}.warehouse-detail-grid strong{color:#334157;font-size:13px}:deep(.el-button){border-radius:4px}:deep(.el-button span){display:inline-flex;align-items:center;gap:6px}:deep(.el-select),:deep(.el-date-editor.el-input){width:100%}@media(max-width:1100px){.warehouse-filter-grid{grid-template-columns:repeat(2,1fr)}.warehouse-detail-grid{grid-template-columns:repeat(2,1fr)}} .warehouse-service-page{color:#25324a}.warehouse-breadcrumb{display:flex;align-items:center;gap:9px;margin:0 0 14px;color:#9aa3b2;font-size:12px}.warehouse-breadcrumb b{font-weight:400}.warehouse-breadcrumb strong{color:#606b7c}.warehouse-breadcrumb button{padding:0;border:0;background:transparent;color:#4376b8;cursor:pointer}.warehouse-page-heading{display:flex;align-items:center;justify-content:space-between;margin-bottom:16px}.warehouse-page-heading h2{margin:0;color:#17233a;font-size:22px;letter-spacing:0}.warehouse-card{margin-bottom:12px;border:1px solid #e7ebf1;border-radius:4px;background:#fff;box-shadow:0 1px 3px rgba(25,42,70,.04)}.warehouse-filter-card{padding:20px 22px 16px}.warehouse-filter-grid{display:grid;grid-template-columns:repeat(4,minmax(180px,1fr));gap:16px 20px}.warehouse-filter-grid label,.warehouse-form-grid label{display:flex;min-width:0;flex-direction:column;gap:7px}.warehouse-filter-grid label>span,.warehouse-form-grid label>span{color:#596579;font-size:12px}.warehouse-filter-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:18px}.warehouse-table-card{overflow:hidden}.warehouse-table-scroll{overflow-x:auto}table{width:100%;min-width:940px;border-collapse:collapse;font-size:12px}th{height:44px;padding:0 14px;border-bottom:1px solid #e8edf3;background:#f7f9fc;color:#4d5a6e;font-weight:650;text-align:left;white-space:nowrap}td{height:48px;padding:8px 14px;border-bottom:1px solid #eef1f5;color:#526176;white-space:nowrap}.warehouse-empty{display:grid;height:150px;color:#a0a9b7;place-items:center}.warehouse-link,.warehouse-delete{display:inline-flex;align-items:center;gap:5px;padding:4px;border:0;background:transparent;color:#2176d2;cursor:pointer}.warehouse-delete{color:#d64545}.warehouse-pagination{display:flex;align-items:center;justify-content:flex-end;gap:8px;padding:14px 18px;color:#778398;font-size:12px}.warehouse-pagination span{margin-right:8px}.warehouse-page-number{width:28px;height:28px;border:1px solid #2176d2;border-radius:3px;background:#2176d2;color:#fff}.warehouse-form-section{padding:22px}.warehouse-form-section h3{margin:0 0 18px;padding-bottom:12px;border-bottom:1px solid #edf0f4;color:#26364f;font-size:15px;letter-spacing:0}.warehouse-form-grid{display:grid;grid-template-columns:repeat(2,minmax(240px,1fr));gap:18px 28px}.warehouse-form-grid i{margin-right:3px;color:#e24b4b;font-style:normal}.warehouse-span-2{grid-column:1/-1}.warehouse-section-heading{display:flex;align-items:center;justify-content:space-between}.warehouse-section-heading h3{flex:1}.warehouse-edit-table td{padding:9px 10px}.warehouse-edit-table :deep(.el-input-number){width:130px}.warehouse-hint{margin:12px 0 0;color:#8792a2;font-size:12px}.warehouse-submit-bar{display:flex;justify-content:center;gap:10px;padding:12px 0 30px}.warehouse-detail-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:24px}.warehouse-detail-grid div{display:flex;flex-direction:column;gap:7px}.warehouse-detail-grid span{color:#8a95a5;font-size:12px}.warehouse-detail-grid strong{color:#334157;font-size:13px}:deep(.el-button){border-radius:4px}:deep(.el-button span){display:inline-flex;align-items:center;gap:6px}:deep(.el-select),:deep(.el-date-editor.el-input){width:100%}@media(max-width:1100px){.warehouse-filter-grid{grid-template-columns:repeat(2,1fr)}.warehouse-detail-grid{grid-template-columns:repeat(2,1fr)}}
.warehouse-checkbox-field{display:flex;min-height:32px;align-items:center;justify-content:flex-start;gap:16px}.warehouse-checkbox-field>span{color:#596579;font-size:12px}.warehouse-checkbox-field :deep(.el-checkbox){margin-right:0}
.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-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-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-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-relabel-detail-table{min-width:1420px}.warehouse-relabel-detail-table td strong{color:#253f67} .warehouse-relabel-detail-table{min-width:1420px}.warehouse-relabel-detail-table td strong{color:#253f67}
</style> </style>
+1 -1
View File
@@ -59,7 +59,7 @@ test('keeps only implemented navigation groups visible', () => {
}, },
{ {
title: '海外仓', title: '海外仓',
children: ['贴标申请', '仓储转运', '货物废弃', '退货签收', '退货库存'], children: ['贴标申请', '仓储转运', '货物废弃', '退货换标', '退货库存'],
}, },
{ {
title: '订单管理', title: '订单管理',