fix(client): keep delivery filters and latest query result consistent

This commit is contained in:
2026-10-02 10:58:00 +08:00
parent d19b1b2f8c
commit 162e9a2336
2 changed files with 108 additions and 3 deletions
@@ -0,0 +1,88 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import { readFileSync } from 'node:fs'
import ts from 'typescript'
import * as vue from 'vue'
import dayjs from 'dayjs'
function harness(t) {
const source = readFileSync(new URL('./views/DeliveryOrdersView.vue', import.meta.url), 'utf8')
.match(/<script setup lang="ts">([\s\S]*?)<\/script>/)[1]
const code = ts.transpileModule(source + '\nexport { filters, queryFields, tableColumns, page, pageSize, search, reset, loadOrders, records, total, loading, errorMessage }', {
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022, esModuleInterop: true },
}).outputText
const calls = [], mounted = []
const dependencies = {
vue: { ...vue, onMounted: callback => mounted.push(callback) }, dayjs,
'vue-router': { useRouter: () => ({ push() {} }) },
'@/modules/auth/public': { useAuthStore: () => ({ hasPermission: () => true }) },
'../api/deliveryOrders': { listDeliveryOrders: query => new Promise((resolve, reject) => calls.push({ query, resolve, reject })) },
}
const scope = vue.effectScope(), view = {}
scope.run(() => new Function('require', 'exports', code)(id => {
assert.ok(id in dependencies, id)
return dependencies[id]
}, view))
t.after(() => scope.stop())
return { view, calls, mount: () => mounted.forEach(fn => fn()) }
}
const result = name => ({ records: [{ deliveryNo: name }], total: 1 })
test('formal delivery filters submit exact values and format each known method in Chinese', async t => {
const { view, calls } = harness(t)
const expected = { FBA_PICKUP: 'FBA 自提', CUSTOMER_PICKUP: '客户自提', TRUCK_PALLET: '卡派打托', TRUCK_FLOOR: '卡派地板', EXPRESS: '快递', OUTSOURCED: '外配', DEDICATED_TRUCK: '专车直送', LTL: '零担派送', TRUCK: '卡车派送' }
const options = view.queryFields.find(f => f.key === 'deliveryMethod').options
assert.deepEqual(Object.fromEntries(options.map(o => [o.value, o.label])), expected)
const column = view.tableColumns.find(c => c.prop === 'deliveryMethod')
for (const [value, label] of Object.entries(expected)) {
view.filters.deliveryMethod = value
const pending = view.loadOrders()
assert.equal(calls.at(-1).query.deliveryMethod, value)
calls.at(-1).resolve(result(value)); await pending
assert.equal(column.formatter({}, {}, value), label)
}
assert.equal(column.formatter({}, {}, null), '—')
})
test('newest result survives older success and older failure', async t => {
for (const oldFailure of [false, true]) {
const { view, calls } = harness(t)
view.filters.documentNo = 'old'; const old = view.loadOrders()
view.filters.documentNo = 'new'; const current = view.loadOrders()
calls[1].resolve(result('new')); await current
if (oldFailure) calls[0].reject(new Error('old failed'))
else calls[0].resolve(result('old'))
await old
assert.equal(view.records.value[0].deliveryNo, 'new')
assert.equal(view.errorMessage.value, '')
assert.equal(view.total.value, 1)
assert.equal(calls.length, 2)
}
})
test('older completion cannot clear current loading; current failure can be retried', async t => {
const { view, calls } = harness(t)
const old = view.loadOrders(), current = view.loadOrders()
calls[0].resolve(result('old')); await old
assert.equal(view.loading.value, true)
assert.equal(view.records.value.length, 0)
calls[1].reject(new Error('current failed')); await current
assert.equal(view.loading.value, false)
assert.equal(view.errorMessage.value, 'current failed')
const retry = view.loadOrders(); calls[2].resolve(result('retry')); await retry
assert.equal(view.records.value[0].deliveryNo, 'retry')
assert.equal(view.errorMessage.value, '')
})
test('mount, search from later page, page-size change and reset each issue one request', async t => {
const { view, calls, mount } = harness(t)
mount(); await vue.nextTick(); assert.equal(calls.length, 1)
view.page.value = 3; await vue.nextTick(); assert.equal(calls.length, 2)
view.search(); await vue.nextTick(); assert.equal(calls.length, 3)
assert.equal(calls[2].query.page, 1)
view.page.value = 4; await vue.nextTick(); assert.equal(calls.length, 4)
view.pageSize.value = 50; await vue.nextTick(); assert.equal(calls.length, 5)
assert.equal(calls[4].query.page, 1); assert.equal(calls[4].query.size, 50)
view.reset(); await vue.nextTick(); assert.equal(calls.length, 6)
calls.forEach(call => call.resolve(result('resolved')))
})
@@ -19,6 +19,20 @@ const page = ref(1)
const pageSize = ref(20)
const filters = reactive({ documentNo: '', dispatchNo: '', waybillNo: '', status: '', deliveryMethod: '', plannedRange: [] as string[] })
const canCreate = computed(() => auth.hasPermission('merchant:warehouse:delivery:create'))
let requestSequence = 0
const deliveryMethodOptions = [
{ value: 'FBA_PICKUP', label: 'FBA 自提' },
{ value: 'CUSTOMER_PICKUP', label: '客户自提' },
{ value: 'TRUCK_PALLET', label: '卡派打托' },
{ value: 'TRUCK_FLOOR', label: '卡派地板' },
{ value: 'EXPRESS', label: '快递' },
{ value: 'OUTSOURCED', label: '外配' },
{ value: 'DEDICATED_TRUCK', label: '专车直送' },
{ value: 'LTL', label: '零担派送' },
{ value: 'TRUCK', label: '卡车派送' },
]
const deliveryMethodLabels: Record<string, string> = Object.fromEntries(deliveryMethodOptions.map(item => [item.value, item.label]))
const statusOptions = [
{ value: 'WAIT_DELIVERY', label: '待派送' },
@@ -37,14 +51,14 @@ const queryFields: QueryField[] = [
{ key: 'dispatchNo', label: '排车单号', type: 'input', placeholder: '请输入排车单号' },
{ key: 'waybillNo', label: '运单号', type: 'input', placeholder: '请输入运单号' },
{ key: 'status', label: '派送状态', type: 'select', options: statusOptions },
{ key: 'deliveryMethod', label: '派送方式', type: 'select', options: [{ label: '卡车派送', value: 'TRUCK' }, { label: '快递派送', value: 'COURIER' }, { label: '客户自提', value: 'SELF_PICKUP' }] },
{ key: 'deliveryMethod', label: '派送方式', type: 'select', options: deliveryMethodOptions },
{ key: 'plannedRange', label: '计划时间', type: 'daterange' },
]
const tableColumns: DataTableColumn[] = [
{ key: 'actions', label: '操作', slotName: 'actions', width: 90, fixed: 'left' }, { prop: 'deliveryNo', label: '派送单号', minWidth: 180 },
{ prop: 'dispatchNo', label: '排车单号', minWidth: 170 }, { prop: 'outboundNo', label: '出库单号', minWidth: 170 },
{ key: 'status', label: '状态', slotName: 'status', width: 110 }, { prop: 'overseasWarehouseName', label: '海外仓', minWidth: 160 },
{ prop: 'deliveryMethod', label: '派送方式', width: 120 }, { prop: 'waybillNo', label: '运单号', minWidth: 150 },
{ prop: 'deliveryMethod', label: '派送方式', width: 120, formatter: (_row: object, _column: unknown, value: string | null | undefined) => value ? deliveryMethodLabels[value] ?? value : '—' }, { prop: 'waybillNo', label: '运单号', minWidth: 150 },
{ prop: 'carrierName', label: '承运商', minWidth: 140 }, { key: 'planned', label: '计划派送', slotName: 'planned', width: 160 },
{ key: 'arrival', label: '预计到达', slotName: 'arrival', width: 160 }, { key: 'createdAt', label: '创建时间', slotName: 'createdAt', width: 160 },
]
@@ -84,18 +98,21 @@ function buildQuery(): DeliveryQuery {
}
async function loadOrders() {
const sequence = ++requestSequence
loading.value = true
errorMessage.value = ''
try {
const result = await listDeliveryOrders(buildQuery())
if (sequence !== requestSequence) return
records.value = result.records
total.value = result.total
} catch (error) {
if (sequence !== requestSequence) return
records.value = []
total.value = 0
errorMessage.value = error instanceof Error ? error.message : '派送单加载失败,请重试'
} finally {
loading.value = false
if (sequence === requestSequence) loading.value = false
}
}