优化
This commit is contained in:
@@ -6,6 +6,45 @@ import ts from 'typescript'
|
||||
const view = readFileSync(new URL('./ReturnOrdersView.vue', import.meta.url), 'utf8')
|
||||
const api = readFileSync(new URL('../api/returnOrders.ts', import.meta.url), 'utf8')
|
||||
|
||||
function listHarness(list, summary) {
|
||||
const state = { query: { page: 1, size: 1, skuCode: 'FILTER' }, loading: { value: false }, listError: { value: '' }, statistics: { value: undefined }, rows: { value: [] }, total: { value: 0 } }
|
||||
const source = view.slice(view.indexOf('async function load()'), view.indexOf('\nfunction changePage('))
|
||||
const js = ts.transpileModule(source, { compilerOptions: { target: ts.ScriptTarget.ES2022 } }).outputText
|
||||
const load = new Function(...Object.keys(state), 'listReturnOrders', 'getReturnOrderStatistics', `let listRequest=0; ${js}; return load`)(...Object.values(state), list, summary)
|
||||
return { ...state, load }
|
||||
}
|
||||
|
||||
test('全筛选统计使用服务器总计,分页行只按单号映射载体,不重算SKU', async () => {
|
||||
const calls = []
|
||||
const totals = { orderCount: 3, skuTypes: 3, expectedQuantity: 19, receivedQuantity: 14, remainingQuantity: 5 }
|
||||
const h = listHarness(async p => { calls.push(p); return { total: '3', records: [{ returnNo: 'RT-1', receivedQuantity: 8 }] } }, async p => { calls.push(p); return { totals, pageDimensions: [{ returnNo: 'RT-1', receivedParcelCount: 1, receivedBoxCount: 1 }] } })
|
||||
await h.load()
|
||||
assert.deepEqual(h.statistics.value, totals)
|
||||
assert.equal(h.rows.value[0].receivedParcelCount, 1)
|
||||
h.query.page = 2
|
||||
await h.load()
|
||||
assert.equal(h.statistics.value.receivedQuantity, 14)
|
||||
assert.deepEqual(calls[2], calls[3])
|
||||
assert.equal(calls[3].page, 2)
|
||||
})
|
||||
|
||||
test('统计失败清空旧列表和汇总,并忽略较旧查询的迟到结果', async () => {
|
||||
let fail = false, finish
|
||||
const h = listHarness(async () => ({ total: 1, records: [{ returnNo: 'RT-1' }] }), async p => {
|
||||
if (fail) throw Error('统计服务不可用')
|
||||
if (p.page === 1) return new Promise(resolve => { finish = resolve })
|
||||
return { totals: { orderCount: 2 }, pageDimensions: [] }
|
||||
})
|
||||
const old = h.load(); h.query.page = 2; await h.load()
|
||||
finish({ totals: { orderCount: 999 }, pageDimensions: [] }); await old
|
||||
assert.equal(h.statistics.value.orderCount, 2)
|
||||
fail = true; await h.load()
|
||||
assert.equal(h.statistics.value, undefined)
|
||||
assert.deepEqual(h.rows.value, [])
|
||||
assert.equal(h.listError.value, '统计服务不可用')
|
||||
assert.equal(h.loading.value, false)
|
||||
})
|
||||
|
||||
function submitHarness(failure) {
|
||||
const source = view.slice(view.indexOf('async function submit()'), view.indexOf('\nfunction statusType('))
|
||||
const state = { requests: [], loads: 0, routes: [], errors: [], successes: [] }
|
||||
|
||||
@@ -11,6 +11,7 @@ import { getMerchantWarehouses, type MerchantWarehouse } from '@/modules/first-l
|
||||
import { createIdempotencyKey } from '@/utils/idempotencyKey'
|
||||
import { createReturnOrder, downloadReturnPhoto, getReturnOrder, listReturnOrders, updateReturnOrder, type ReturnOrderDetail, type ReturnOrderSummary, type ReturnReceipt } from '../api/returnOrders'
|
||||
import ReviewRejectionNotice from '../components/ReviewRejectionNotice.vue'
|
||||
import { getReturnOrderStatistics, type ReturnOrderStatistics } from '../api/returnOrders'
|
||||
import { getReturnInventoryLabel } from '../api/returnInventory'
|
||||
import { formatReturnVolume, receiptBoxDetails, receiptPhysicalDetails, receiptPrintableBoxCount, receiptTrackingNo } from '../utils/returnReceiptDisplay'
|
||||
|
||||
@@ -40,6 +41,9 @@ const printableBoxCount = computed(() => receiptPrintableBoxCount(printableRecei
|
||||
const photoUrls = reactive<Record<string, string>>({})
|
||||
const rows = ref<ReturnOrderSummary[]>([])
|
||||
const total = ref(0)
|
||||
const statistics = ref<ReturnOrderStatistics['totals']>()
|
||||
const listError = ref('')
|
||||
let listRequest = 0
|
||||
const operationWarehouses = ref<MerchantWarehouse[]>([])
|
||||
const operationWarehousesLoading = ref(false)
|
||||
const operationWarehousesError = ref('')
|
||||
@@ -62,8 +66,13 @@ const tableColumns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', slotName: 'actions', width: 220, fixed: 'left' },
|
||||
{ prop: 'returnNo', label: '退货单号', minWidth: 190 }, { prop: 'customerReferenceNo', label: '客户参考号', minWidth: 160 },
|
||||
{ key: 'warehouse', label: '操作仓', slotName: 'warehouse', minWidth: 200 }, { key: 'relabel', label: '换标', slotName: 'relabel', width: 100 },
|
||||
{ prop: 'skuTypes', label: 'SKU 种类', width: 105 }, { prop: 'expectedQuantity', label: '预报数量', width: 110 },
|
||||
{ prop: 'receivedQuantity', label: '实收数量', width: 110 }, { key: 'status', label: '状态', slotName: 'status', width: 150 },
|
||||
{ prop: 'skuTypes', label: 'SKU 种类', width: 105 }, { prop: 'expectedQuantity', label: '预报(件)', width: 110 },
|
||||
{ prop: 'receivedQuantity', label: '实收(件)', width: 110 },
|
||||
{ prop: 'remainingQuantity', label: '待收(件)', width: 110 },
|
||||
{ prop: 'receivedBoxCount', label: '签收箱数', width: 110 },
|
||||
{ prop: 'receivedParcelCount', label: '签收包裹数', width: 115 },
|
||||
{ prop: 'receivedPalletCount', label: '签收托数', width: 110 },
|
||||
{ key: 'status', label: '状态', slotName: 'status', width: 150 },
|
||||
{ key: 'relabelApplications', label: '换标子单', slotName: 'relabelApplications', minWidth: 340 },
|
||||
{ key: 'createdAt', label: '创建时间', slotName: 'createdAt', width: 180 },
|
||||
]
|
||||
@@ -147,9 +156,21 @@ async function openEdit(returnNo: string) {
|
||||
function addItem() { form.items.push(emptySku()) }
|
||||
function removeItem(index: number) { if (form.items.length > 1) form.items.splice(index, 1) }
|
||||
async function load() {
|
||||
const request = ++listRequest
|
||||
loading.value = true
|
||||
try { const response = await listReturnOrders({ ...query, returnNo: query.returnNo || undefined, trackingNo: query.trackingNo || undefined, skuCode: query.skuCode || undefined, status: query.status || undefined }); rows.value = response.records; total.value = Number(response.total); if (!Number.isFinite(total.value)) total.value = response.records.length }
|
||||
finally { loading.value = false }
|
||||
listError.value = ''; statistics.value = undefined
|
||||
try {
|
||||
const params = { ...query, returnNo: query.returnNo || undefined, trackingNo: query.trackingNo || undefined, skuCode: query.skuCode || undefined, status: query.status || undefined }
|
||||
const [response, summary] = await Promise.all([listReturnOrders(params), getReturnOrderStatistics(params)])
|
||||
if (request !== listRequest) return
|
||||
const dimensions = new Map(summary.pageDimensions.map(row => [row.returnNo, row]))
|
||||
rows.value = response.records.map(row => ({ ...row, ...dimensions.get(row.returnNo) }))
|
||||
total.value = Number(response.total); statistics.value = summary.totals
|
||||
} catch (error) {
|
||||
if (request !== listRequest) return
|
||||
rows.value = []; total.value = 0
|
||||
listError.value = error instanceof Error ? error.message : '退货列表及汇总加载失败'
|
||||
} finally { if (request === listRequest) loading.value = false }
|
||||
}
|
||||
function changePage(page: number) {
|
||||
if (page === query.page) return
|
||||
@@ -323,6 +344,11 @@ onBeforeUnmount(clearPhotoUrls)
|
||||
|
||||
<template v-else-if="!props.detailPage">
|
||||
<QueryToolbar v-model="queryModel" class="return-query" layout="grid" ariaLabel="退货签收筛选" :fields="queryFields" @search="search" />
|
||||
<el-alert v-if="listError" type="error" :closable="false" :title="listError"><el-button @click="load">重新加载</el-button></el-alert>
|
||||
<div v-if="statistics && !loading" class="return-filter-summary" role="status" aria-label="退货全筛选汇总">
|
||||
全筛选:{{ statistics.orderCount }} 票 · {{ statistics.skuTypes }} 种 SKU · 预报 {{ statistics.expectedQuantity }} 件 · 实收 {{ statistics.receivedQuantity }} 件 · 待收 {{ statistics.remainingQuantity }} 件 · 签收 {{ statistics.receivedBoxCount }} 箱 / {{ statistics.receivedParcelCount }} 包裹 / {{ statistics.receivedPalletCount }} 托
|
||||
<span>预报箱/包裹/托数:未提供</span>
|
||||
</div>
|
||||
<div class="return-table-card">
|
||||
<DataTable v-model:page="query.page" v-model:page-size="query.size" :data="rows" :columns="tableColumns" :loading="loading" :total="total" :page-sizes="[15, 30, 50, 100]" :row-actions="rowActions" row-key="returnNo" empty-text="暂无退货预报">
|
||||
<template #toolbar-actions><el-button v-if="canCreate" type="primary" @click="openCreate"><Plus :size="16" />新建退货预报</el-button></template>
|
||||
@@ -364,7 +390,7 @@ onBeforeUnmount(clearPhotoUrls)
|
||||
<section class="return-detail-section">
|
||||
<h3>签收概览</h3>
|
||||
<div class="return-detail-grid">
|
||||
<div><span>实收 / 预报</span><strong>{{ selected.order.receivedQuantity }} / {{ selected.order.expectedQuantity }}</strong></div>
|
||||
<div><span>预报(件)</span><strong>{{ selected.order.expectedQuantity }}</strong></div><div><span>实收(件)</span><strong>{{ selected.order.receivedQuantity }}</strong></div><div><span>待收(件)</span><strong>{{ Math.max(selected.order.expectedQuantity - selected.order.receivedQuantity, 0) }}</strong></div>
|
||||
<div><span>SKU 种类</span><strong>{{ selected.order.skuTypes }}</strong></div>
|
||||
<div><span>是否换标</span><strong>{{ selected.order.needRelabel ? '需要换标' : '否' }}</strong></div>
|
||||
<div><span>客户参考号</span><strong>{{ selected.order.customerReferenceNo || '—' }}</strong></div>
|
||||
@@ -475,6 +501,8 @@ onBeforeUnmount(clearPhotoUrls)
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.return-filter-summary { padding:12px 16px; margin-bottom:12px; border:1px solid var(--client-border); border-radius:8px; background:var(--client-surface,#fff); line-height:1.8; }
|
||||
.return-filter-summary span { margin-left:16px; color:var(--client-text-secondary,#64748b); }
|
||||
.return-query.query-toolbar--grid { display:grid; grid-template-columns:repeat(4,minmax(0,280px)) max-content; gap:14px 18px; align-items:end; }
|
||||
.return-query :deep(.query-toolbar__filters) { display:contents; }
|
||||
.return-query :deep(.query-toolbar__field) { display:flex; flex-direction:column; align-items:stretch; gap:6px; }
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -28,7 +28,7 @@ test('废弃来源包含通用仓位,选择后带出全部库存且不把选
|
||||
function selectionRows(boxes, selectedKeys) {
|
||||
const body = view.match(/function syncDiscardSelection\(\) \{([\s\S]*?)\n\}/)[1].replace('new Map<string, CargoRow>()', 'new Map()')
|
||||
const rows = { value: [] }
|
||||
new Function('discardSelectableBoxes', 'discardSelectedBoxKeys', 'cargoRows', 'createCargoRow', body)({ value: boxes }, { value: selectedKeys }, rows, () => ({}))
|
||||
new Function('discardSourceBoxes', 'discardSelectedBoxKeys', 'cargoRows', 'createCargoRow', body)({ value: boxes }, { value: selectedKeys }, rows, () => ({}))
|
||||
return rows.value
|
||||
}
|
||||
test('部分废弃只提交勾选的箱子,数量由箱子汇总,清空后无明细', () => {
|
||||
@@ -48,3 +48,12 @@ test('混合SKU箱按实物箱只计一次,退货箱仍整箱携带全部SKU',
|
||||
const returnRows = selectionRows([{ key: 'RETURN_CARTON:2', cartonId: '2', source: returned, skus }], ['RETURN_CARTON:2'])
|
||||
assert.deepEqual(returnRows.map(row => row.quantity), [2, 3]); assert.ok(returnRows.every(row => row.returnCartonId === '2' && row.discardCartonIds === undefined))
|
||||
})
|
||||
|
||||
test('通用混箱同仓位保留全部SKU,跨仓位相同SKU保持独立来源', () => {
|
||||
const skus = [{ skuCode: 'A', availablePackages: 1 }, { skuCode: 'B', availablePackages: 1 }]
|
||||
const source = { orderId: null, inventoryPositionId: '42', sourceType: 'INVENTORY', availablePackages: 1, skus }
|
||||
const boxes = [{ key: 'INVENTORY:1', cartonId: '1', source, skus }, { key: 'INVENTORY:2', cartonId: '2', source: { ...source, inventoryPositionId: '43' }, skus: [skus[0]] }]
|
||||
const rows = selectionRows(boxes, ['INVENTORY:1', 'INVENTORY:2'])
|
||||
assert.equal(rows.length, 3)
|
||||
assert.deepEqual(rows.map(row => [row.sourceInventoryPositionId, row.sku, row.quantity, row.discardCartonIds]), [['42', 'A', 1, ['1']], ['42', 'B', 1, ['1']], ['43', 'A', 1, ['2']]])
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user