# Conflicts:
#	src/modules/overseas-warehouse/views/relabelAttachments.test.mjs
This commit is contained in:
2026-09-10 15:55:12 +08:00
8 changed files with 50 additions and 27 deletions
+23 -4
View File
@@ -8,6 +8,23 @@ export interface PageResult<T> {
pages: number
}
interface PageResultWire<T> extends Omit<PageResult<T>, 'total' | 'page' | 'size' | 'pages'> {
total: number | string
page: number | string
size: number | string
pages: number | string
}
function normalizePageResult<T>(result: PageResultWire<T>): PageResult<T> {
return {
...result,
total: Number(result.total),
page: Number(result.page),
size: Number(result.size),
pages: Number(result.pages),
}
}
export interface FinanceAccount {
currencyCode: 'CNY'
actualBalance: string
@@ -71,20 +88,22 @@ export function getFinanceAccount() {
return apiRequest<FinanceAccount>({ method: 'GET', url: '/client/finance/v1/account' })
}
export function getAccountLedger(page: number, size: number) {
return apiRequest<PageResult<AccountLedgerEntry>>({
export async function getAccountLedger(page: number, size: number) {
const result = await apiRequest<PageResultWire<AccountLedgerEntry>>({
method: 'GET',
url: '/client/finance/v1/account/ledger',
params: { page, size },
})
return normalizePageResult(result)
}
export function getRechargeRequests(page: number, size: number) {
return apiRequest<PageResult<RechargeRequest>>({
export async function getRechargeRequests(page: number, size: number) {
const result = await apiRequest<PageResultWire<RechargeRequest>>({
method: 'GET',
url: '/client/finance/v1/recharge-requests',
params: { page, size },
})
return normalizePageResult(result)
}
export function getRechargeChannels() {
@@ -39,6 +39,11 @@ test('renders server balances, ledger and recharge states without internal ident
assert.match(viewSource, /v-loading="pageLoading"/)
assert.match(viewSource, /function formatAccountAmount[\s\S]*value == null \? '—' : formatAmount\(value\)/)
assert.match(viewSource, /formatAccountAmount\(account\?\.actualBalance\)/)
assert.match(viewSource, /:default-current-page="ledgerPage"/)
assert.match(viewSource, /:default-current-page="rechargePage"/)
assert.doesNotMatch(viewSource, /<el-pagination (?:v-model:current-page|:current-page)=/)
assert.match(apiSource, /function normalizePageResult<[\s\S]*total: Number\(result\.total\)/)
assert.match(apiSource, /page: Number\(result\.page\)/)
assert.doesNotMatch(viewSource, /requestId|ledgerId|customerId|tenantId/)
})
@@ -245,7 +245,7 @@ onMounted(() => { void loadAccountData() })
<el-table-column prop="remark" label="备注" min-width="180"><template #default="{ row }">{{ row.remark || '—' }}</template></el-table-column>
<el-table-column label="发生时间" width="170"><template #default="{ row }">{{ formatDateTime(row.createdAt) }}</template></el-table-column>
</el-table>
<div v-if="ledgerTotal" class="pagination-row"><span>共 {{ ledgerTotal }} 条</span><el-pagination :current-page="ledgerPage" :page-size="pageSize" layout="prev, pager, next" :total="ledgerTotal" @current-change="changeLedgerPage" /></div>
<div v-if="ledgerTotal" class="pagination-row"><span>共 {{ ledgerTotal }} 条</span><el-pagination :default-current-page="ledgerPage" :page-size="pageSize" layout="prev, pager, next" :total="ledgerTotal" @current-change="changeLedgerPage" /></div>
</section>
<section class="account-card">
@@ -261,7 +261,7 @@ onMounted(() => { void loadAccountData() })
<el-table-column label="提交时间" width="170"><template #default="{ row }">{{ formatDateTime(row.createdAt) }}</template></el-table-column>
<el-table-column label="审核时间" width="170"><template #default="{ row }">{{ formatDateTime(row.reviewedAt) }}</template></el-table-column>
</el-table>
<div v-if="rechargeTotal" class="pagination-row"><span>共 {{ rechargeTotal }} 条</span><el-pagination :current-page="rechargePage" :page-size="pageSize" layout="prev, pager, next" :total="rechargeTotal" @current-change="changeRechargePage" /></div>
<div v-if="rechargeTotal" class="pagination-row"><span>共 {{ rechargeTotal }} 条</span><el-pagination :default-current-page="rechargePage" :page-size="pageSize" layout="prev, pager, next" :total="rechargeTotal" @current-change="changeRechargePage" /></div>
</section>
</template>
@@ -32,7 +32,7 @@ test('keeps overseas warehouse operations first and renders readable list values
assert.match(overseas, /columns: \['操作', '废弃申请单号'/)
assert.match(overseas, /function statusLabel\(value: string\)/)
assert.match(overseas, /function deliveryMethodLabel\(value: string\)/)
assert.match(overseas, /function formatDateTime\(value: string\)/)
assert.match(overseas, /function formatDateTime\(value\?: string \| null\)/)
assert.match(overseas, /import SingleLineText from '@\/components\/common\/SingleLineText\.vue'/)
assert.match(overseas, /<SingleLineText :value="row\[String\(index\)\]"/)
assert.match(overseas, /<td><button class="warehouse-link"[^>]*>.*?查看<\/button><\/td><td v-for="\(_, index\) in config\.columns\.slice\(1\)"/s)
@@ -34,7 +34,7 @@ export interface RelabelInventoryCandidate {
}
export interface TransferCreate {
inboundCode: string; sourceLastlegOrderIds: string[]; operatingWarehouseId: string; fbaWarehouseCode?: string
operatingWarehouseId: string; fbaWarehouseCode?: string
deliveryAddress: string; packageCount: number; volumeCbm: number; deliveryMethod: string; estimatedArrivalAt: string
fbaShipmentNo?: string; trackingNo?: string; fbaBoxNos?: string; remark?: string; needRelabel: boolean; idempotencyKey: string
items: Array<{ skuCode: string; productNameZh: string; packageCount: number; shippingMark?: string }>
@@ -6,7 +6,7 @@ import SingleLineText from '@/components/common/SingleLineText.vue'
import {
createDiscard, createRelabel, createTransfer, getDiscard, getRelabel, getTransfer,
confirmRelabelCompletion, downloadRelabelCompletionPhoto, uploadRelabelShippingLabel,
listDiscardCandidates, listDiscards, listLastlegOrders, listRelabelInventoryCandidates, listRelabels, listTransfers,
listDiscardCandidates, listDiscards, listRelabelInventoryCandidates, listRelabels, listTransfers,
type RelabelInventoryCandidate,
type RelabelCreate,
type RelabelDetail,
@@ -28,7 +28,7 @@ interface ServiceConfig { title: string; createTitle: string; numberLabel: strin
const props = defineProps<{ service: ServiceType }>()
const configs: Record<ServiceType, ServiceConfig> = {
labeling: { title: '贴标申请', createTitle: '新增贴标申请', numberLabel: '贴标申请单号', emptyText: '暂无贴标申请', columns: ['操作', '贴标申请单号', '客户单号', '海外操作仓', '贴标要求', 'SKU 数', '总箱数', '状态', '创建时间'] },
transfer: { title: '仓储转运单', createTitle: '新增仓储转运单', numberLabel: '转运单号', emptyText: '暂无仓储转运订单', columns: ['操作', '转运单号', '进仓编号', '入库箱数', '中转仓', 'FBA 目的仓', '入仓方式', '是否换标', '状态', '创建时间'] },
transfer: { title: '仓储转运单', createTitle: '新增仓储转运单', numberLabel: '转运单号', emptyText: '暂无仓储转运订单', columns: ['操作', '转运单号', '入库箱数', '中转仓', 'FBA 目的仓', '入仓方式', '是否换标', '状态', '创建时间'] },
disposal: { title: '货物废弃', createTitle: '新增货物废弃申请', numberLabel: '废弃申请单号', emptyText: '暂无货物废弃申请', columns: ['操作', '废弃申请单号', '进仓编号', '操作仓', 'SKU 汇总', '废弃数量', '预估费用', '状态', '创建时间'] },
relabel: { title: '退货换标', createTitle: '新建退货换标单', numberLabel: '退货换标单号', emptyText: '暂无退货换标订单', columns: ['操作', '退货换标单号', '来源 FBA 仓', '目的 FBA 仓', 'SKU 汇总', '退件总数', '是否换标', '换标状态', '尾程状态', '创建时间'] },
}
@@ -301,7 +301,7 @@ async function load() {
} else {
const result = await listTransfers({ page: 1, size: 20, transferNo: filters.number || undefined, inboundCode: filters.inboundNo || undefined, operatingWarehouseId: filters.warehouse || undefined, fbaWarehouseCode: filters.destination || undefined, status: filters.status || undefined, needRelabel: props.service === 'relabel', createdFrom: dateTime(filters.start), createdTo: dateTime(filters.end, true) })
submittedRows.value = result.records.map((item) => props.service === 'transfer'
? row([item.transferNo, item.inboundCode, item.packageCount, '—', item.fbaWarehouseCode, deliveryMethodLabel(item.deliveryMethod), item.needRelabel ? '是' : '否', statusLabel(item.status), formatDateTime(item.createdAt)])
? row([item.transferNo, item.packageCount, '—', item.fbaWarehouseCode, deliveryMethodLabel(item.deliveryMethod), item.needRelabel ? '是 · 待绑定换标单' : '否', statusLabel(item.status), formatDateTime(item.createdAt)])
: row([item.transferNo, '—', item.fbaWarehouseCode, '—', item.packageCount, '是', item.relabelNo ? '已生成' : '待生成', statusLabel(item.status), formatDateTime(item.createdAt)]))
}
} catch (error) { ElMessage.error(error instanceof Error ? error.message : '列表加载失败') }
@@ -318,7 +318,7 @@ async function submit() {
? skuRelabelRows.value.some((item) => !item.cartonMark || !item.originalSku || !item.newSku || item.quantity < 1
|| (labelingMode.value === 'carton_and_sku' && (!item.fbaCartonMark || !item.destinationWarehouseId)))
: 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 === 'relabel' && !form.inboundNo) || (props.service !== 'disposal' && !primaryWarehouse) || invalidCargo) {
ElMessage.warning('请填写必填信息和完整的货物明细'); return
}
if (props.service === 'labeling' && (!form.labelDeadline || !form.contact || !form.phone || form.chargeAmount <= 0)) {
@@ -371,10 +371,8 @@ async function submit() {
})
await createDiscard({ items, idempotencyKey, remark: [form.reason, form.remark].filter(Boolean).join(';') || undefined })
} else {
const sources = await listLastlegOrders(form.inboundNo)
if (sources.records.length === 0) throw new Error('未找到该入仓码对应的尾程订单')
const packageCount = cargoRows.value.reduce((sum, item) => sum + item.quantity, 0)
await createTransfer({ inboundCode: form.inboundNo, sourceLastlegOrderIds: sources.records.map((item) => item.id), operatingWarehouseId: primaryWarehouse, fbaWarehouseCode: form.destination || undefined, deliveryAddress: form.address, packageCount, volumeCbm: form.volumeCbm, deliveryMethod: form.deliveryMethod, estimatedArrivalAt: form.estimatedArrivalAt, fbaShipmentNo: form.fbaShipmentNo || undefined, trackingNo: form.trackingNo || undefined, remark: form.remark || undefined, needRelabel: props.service === 'relabel' || form.relabel, idempotencyKey, items: cargoRows.value.map((item) => ({ skuCode: item.sku, productNameZh: item.name, packageCount: item.quantity, shippingMark: item.shippingMark || undefined })) })
await createTransfer({ operatingWarehouseId: primaryWarehouse, fbaWarehouseCode: form.destination || undefined, deliveryAddress: form.address, packageCount, volumeCbm: form.volumeCbm, deliveryMethod: form.deliveryMethod, estimatedArrivalAt: form.estimatedArrivalAt, fbaShipmentNo: form.fbaShipmentNo || undefined, trackingNo: form.deliveryMethod === 'CUSTOMER_DELIVERY' ? form.trackingNo || undefined : undefined, remark: form.remark || undefined, needRelabel: props.service === 'relabel' || form.relabel, idempotencyKey, items: cargoRows.value.map((item) => ({ skuCode: item.sku, productNameZh: item.name, packageCount: item.quantity })) })
}
ElMessage.success(`${config.value.title}提交成功`); mode.value = 'list'; await load()
} catch (error) { ElMessage.error(error instanceof Error ? error.message : '提交失败') }
@@ -512,7 +510,7 @@ function selectRelabelInventory(inboundOrderId: string) {
<div class="warehouse-card warehouse-form-section"><h3>{{ props.service === 'transfer' ? '入库与转运信息' : props.service === 'relabel' ? '退货信息' : props.service === 'labeling' ? '订单信息' : '基础信息' }}</h3>
<div class="warehouse-form-grid">
<label v-if="props.service === 'disposal'" class="warehouse-span-2"><span><i>*</i>选择入库单</span><el-select v-model="selectedDiscardOrderId" filterable :loading="discardCandidatesLoading" placeholder="请选择有可用库存的入库单" @change="selectDiscardInbound"><el-option v-for="candidate in discardCandidates" :key="candidate.orderId" :label="`${candidate.inboundOrderNo || candidate.inboundCode || '—'}|${candidate.inboundCode || '—'}|${candidate.overseasWarehouseName || '—'}|可用 ${candidate.availablePackages} 箱`" :value="candidate.orderId" /></el-select><small class="warehouse-hint">仅显示一件代发中仍有可用箱数的入库单。</small></label>
<label v-if="props.service === 'transfer' || props.service === 'relabel'"><span><i>*</i>进仓编号</span><el-input v-model="form.inboundNo" placeholder="请输入已有入仓编号" /></label>
<label v-if="props.service === 'relabel'"><span><i>*</i>进仓编号</span><el-input v-model="form.inboundNo" placeholder="请输入已有入仓编号" /></label>
<label v-if="props.service === 'relabel'"><span><i>*</i>来源 FBA 仓库</span><el-select v-model="form.sourceWarehouse" :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></label>
<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(fbaWarehousesError)" clearable placeholder="请选择国外中转仓" @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="props.service === 'labeling'" class="warehouse-hint">选择后仅查询当前客户在该中转仓的可用库存。</small><small v-else-if="props.service === 'transfer'" class="warehouse-hint">本批货物将先进入该仓完成签收和入库。</small></label>
@@ -543,9 +541,9 @@ function selectRelabelInventory(inboundOrderId: string) {
<label v-if="props.service === 'transfer' && transferHasFbaDestination"><span><i>*</i>FBA 货件编号</span><el-input v-model="form.fbaShipmentNo" placeholder="请输入 FBA 货件编号" /></label>
<label v-if="props.service === 'relabel'"><span><i>*</i>FBA 货件编号</span><el-input v-model="form.fbaShipmentNo" placeholder="请输入 FBA 货件编号" /></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 === 'relabel' || (props.service === 'transfer' && form.deliveryMethod === 'CUSTOMER_DELIVERY')"><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>
<div v-if="props.service === 'transfer'" class="warehouse-choice-field warehouse-span-2"><span><i>*</i>是否需要换标</span><el-radio-group v-model="form.relabel"><el-radio-button :value="false">不需要换标</el-radio-button><el-radio-button :value="true">需要换标</el-radio-button></el-radio-group><small v-if="form.relabel">提交后将同时生成待处理的换标任务。</small></div>
<div v-if="props.service === 'transfer'" class="warehouse-choice-field warehouse-span-2"><span><i>*</i>是否需要换标</span><el-radio-group v-model="form.relabel"><el-radio-button :value="false">不需要换标</el-radio-button><el-radio-button :value="true">需要换标</el-radio-button></el-radio-group><small v-if="form.relabel">转运单将显示换标标识,完成入库后再绑定换标单。</small></div>
</div>
</div>
<div class="warehouse-card warehouse-form-section">
@@ -570,7 +568,7 @@ function selectRelabelInventory(inboundOrderId: string) {
</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>
<div class="warehouse-table-scroll"><table class="warehouse-edit-table"><thead><tr><th>商品 SKU *</th><th>中文品名 *</th><th v-if="props.service === 'disposal'">可用箱数</th><th v-else>FNSKU / 箱唛</th><th>{{ props.service === 'disposal' ? '本次废弃箱数' : '箱数' }} *</th><th v-if="props.service !== 'disposal'">操作</th></tr></thead><tbody><tr v-for="row in cargoRows" :key="row.id"><td><el-input v-model="row.sku" :readonly="props.service === 'disposal'" placeholder="请输入商品 SKU" /></td><td><el-input v-model="row.name" :readonly="props.service === 'disposal'" placeholder="请输入中文品名" /></td><td v-if="props.service === 'disposal'"><strong>{{ row.availableQuantity }} 箱</strong></td><td v-else><el-input v-model="row.shippingMark" placeholder="请输入FNSKU或箱唛" /></td><td><el-input-number v-model="row.quantity" :min="props.service === 'disposal' ? 0 : 1" :max="row.availableQuantity" controls-position="right" placeholder="请输入箱数" /></td><td v-if="props.service !== 'disposal'"><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="props.service === 'disposal' ? 4 : 5"><div class="warehouse-empty">{{ props.service === 'disposal' ? '请先选择入库单,系统将自动带出 SKU 和可用箱数' : '暂无货物明细,请点击“添加行”' }}</div></td></tr></tbody></table></div>
<div class="warehouse-table-scroll"><table class="warehouse-edit-table"><thead><tr><th>商品 SKU *</th><th>中文品名 *</th><th v-if="props.service === 'disposal'">可用箱数</th><th v-if="props.service === 'relabel'">FNSKU / 箱唛</th><th>{{ props.service === 'disposal' ? '本次废弃箱数' : '预计箱数' }} *</th><th v-if="props.service !== 'disposal'">操作</th></tr></thead><tbody><tr v-for="row in cargoRows" :key="row.id"><td><el-input v-model="row.sku" :readonly="props.service === 'disposal'" placeholder="请输入商品 SKU" /></td><td><el-input v-model="row.name" :readonly="props.service === 'disposal'" placeholder="请输入中文品名" /></td><td v-if="props.service === 'disposal'"><strong>{{ row.availableQuantity }} 箱</strong></td><td v-if="props.service === 'relabel'"><el-input v-model="row.shippingMark" placeholder="请输入FNSKU或箱唛" /></td><td><el-input-number v-model="row.quantity" :min="props.service === 'disposal' ? 0 : 1" :max="row.availableQuantity" controls-position="right" placeholder="请输入箱数" /></td><td v-if="props.service !== 'disposal'"><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="props.service === 'disposal' ? 4 : props.service === 'relabel' ? 5 : 4"><div class="warehouse-empty">{{ props.service === 'disposal' ? '请先选择入库单,系统将自动带出 SKU 和可用箱数' : '暂无货物明细,请点击“添加行”' }}</div></td></tr></tbody></table></div>
</template>
</div>
<div class="warehouse-submit-bar"><el-button @click="resetForm">重置</el-button><el-button type="primary" native-type="submit" :loading="saving">提交{{ config.title }}</el-button></div>
@@ -42,14 +42,14 @@ test('贴标货物明细支持手动添加和编辑', () => {
assert.match(view, /<el-input v-model="row\.sku" :readonly="props\.service === 'disposal'"/)
})
test('贴标货物明细在同一页面支持三种换标类型', () => {
assert.match(view, /el-radio-group v-model="labelingMode" aria-label="换标类型"/)
assert.match(view, /el-radio-button value="carton">只换箱唛/)
assert.match(view, /el-radio-button value="sku">只换 SKU 标签/)
assert.match(view, /el-radio-button value="carton_and_sku">SKU 和箱唛都换/)
assert.doesNotMatch(view, /el-tab-pane label="箱唛换标"/)
assert.doesNotMatch(view, /el-tab-pane label="SKU 换标"/)
test('贴标货物明细通过单一换标类型选择三种模式', () => {
assert.match(view, /<el-radio-group v-model="labelingMode" aria-label="换标类型">/)
assert.match(view, /<el-radio-button value="carton">只换箱唛<\/el-radio-button>/)
assert.match(view, /<el-radio-button value="sku">只换 SKU 标签<\/el-radio-button>/)
assert.match(view, /<el-radio-button value="carton_and_sku">SKU 和箱唛都换<\/el-radio-button>/)
assert.doesNotMatch(view, /el-tab-pane label="(?:箱唛换标|SKU 换标)"/)
assert.match(view, /interface SkuRelabelRow/)
assert.match(view, /原 SKU \*/)
assert.match(view, /我方箱唛 \*/)
assert.match(view, /换标数量(件)\*/)
assert.match(view, /buildSkuRelabelItems/)
+2 -1
View File
@@ -59,7 +59,7 @@ test('keeps only implemented navigation groups visible', () => {
},
{
title: '海外仓',
children: ['贴标申请', '仓储转运', '货物废弃', '退货换标', '退货库存'],
children: ['贴标申请', '仓储转运', '货物废弃', '退货换标', '退货库存', '退货箱号管理'],
},
{
title: '订单管理',
@@ -107,6 +107,7 @@ test('flattens visible leaves with unique paths', () => {
'/overseas-warehouse/cargo-disposal',
'/overseas-warehouse/return-relabel',
'/overseas-warehouse/return-inventory',
'/overseas-warehouse/return-cartons',
'/orders/inbound',
'/orders/delivery',
'/finance/account',