fix(client): request complete server-paged order workbench
This commit is contained in:
@@ -41,7 +41,7 @@ test('exposes real list, draft, submit, and detail workflow entries', () => {
|
||||
assert.match(source, /function submitOrder\(\)/)
|
||||
assert.match(source, /async function openDetail\(order: FirstLegOrder\)/)
|
||||
assert.match(source, /Promise\.allSettled\(\[/)
|
||||
assert.match(source, /const orderPageRequest = isCreatePage\.value \|\| isDetailPage\.value\s*\? store\.loadWarehouseType\('DOMESTIC', true\)\s*:\s*store\.load\(\{ includeDrafts: true \}\)/)
|
||||
assert.match(source, /const orderPageRequest = isCreatePage\.value \|\| isDetailPage\.value\s*\? store\.loadWarehouseType\('DOMESTIC', true\)\s*:\s*loadOrderPage\(\)/)
|
||||
assert.match(source, /Promise\.allSettled\(\[\s*orderPageRequest,\s*companyRequest/)
|
||||
assert.match(source, /tenantStore\.loadCompanyContext\(\)/)
|
||||
assert.match(source, /await store\.loadDetail\(orderId\)/)
|
||||
@@ -66,7 +66,7 @@ test('copies an order into the dedicated create page without reusing order ident
|
||||
})
|
||||
|
||||
test('keeps create-page support errors scoped to create-page requests', () => {
|
||||
assert.match(source, /if \(!isCreatePage\.value && store\.draftLoadError\)/)
|
||||
assert.doesNotMatch(source, /store\.draftLoadError/)
|
||||
})
|
||||
|
||||
test('blocks order submission before company approval while preserving draft actions', () => {
|
||||
@@ -114,7 +114,7 @@ test('uses the shared query toolbar and data table without metric cards', () =>
|
||||
assert.match(source, /<QueryToolbar v-model="filters" class="first-leg-query-toolbar" layout="grid" :fields="queryFields" ariaLabel="订单查询" @search="searchOrders" \/>/)
|
||||
assert.match(source, /async function searchOrders\(model: QueryModel\)/)
|
||||
assert.match(source, /await store\.load\(\{/)
|
||||
assert.match(source, /<DataTable v-model:page="tablePage" v-model:page-size="tablePageSize" :data="pagedOrders" :columns="visibleOrderColumns"/)
|
||||
assert.match(source, /<DataTable :page="tablePage" :page-size="tablePageSize" @update:page="changeOrderPage" @update:page-size="changeOrderPageSize" :data="pagedOrders" :columns="visibleOrderColumns"/)
|
||||
assert.match(source, /\{ key: 'actions', label: '操作', width: 284, fixed: 'left', align: 'left', headerAlign: 'left', slotName: 'actions' \}/)
|
||||
assert.doesNotMatch(source, /order-metrics|订单概览/)
|
||||
})
|
||||
@@ -247,9 +247,9 @@ test('uses only fields present on C02 rows before loading full detail', () => {
|
||||
assert.match(source, /displayListValue/)
|
||||
assert.doesNotMatch(source, /key: 'operator'/)
|
||||
assert.doesNotMatch(source, /key: 'transportMode', type: 'select'/)
|
||||
assert.match(source, /store\.draftLoadError/)
|
||||
assert.match(source, /mainStatus: statusValue && statusValue !== 'DRAFT' && statusValue !== 'CANCELLED' \? statusValue : undefined/)
|
||||
assert.match(source, /draftOnly: statusValue === 'DRAFT'/)
|
||||
assert.match(source, /:total="store\.total"/)
|
||||
assert.match(source, /mainStatus: text\('status'\)/)
|
||||
assert.match(source, /queue: activeOrderQueue\.value, page: requestedPage, size: tablePageSize\.value/)
|
||||
assert.match(source, /createdFrom: range\[0\] \? `\$\{range\[0\]\}T00:00:00`/)
|
||||
})
|
||||
|
||||
|
||||
@@ -112,7 +112,7 @@ async function cancelOrder(order: FirstLegOrder) {
|
||||
await store.cancelOrder(command.id, command.expectedVersion, command.reason)
|
||||
pendingCancellation.value = undefined
|
||||
ElMessage.success('订单已取消')
|
||||
await store.load({ includeDrafts: true })
|
||||
await loadOrderPage()
|
||||
if (isDetailPage.value) await router.push({ name: 'first-leg-orders' })
|
||||
} catch (error) {
|
||||
if (error === 'cancel' || error === 'close') return
|
||||
@@ -135,7 +135,7 @@ const appliedFilters = ref<QueryModel>({ ...filters.value })
|
||||
const queryFields = computed<QueryField[]>(() => [
|
||||
{ key: 'cargoKeyword', label: '入仓码 / SKU', type: 'input', placeholder: '请输入入仓码或 SKU', props: { 'aria-label': '入仓码或 SKU' } },
|
||||
{ key: 'receivingWarehouseId', label: '收货仓', type: 'select', placeholder: '全部', options: availableWarehouses.value.map((item) => ({ label: item.name, value: item.id })), props: { 'aria-label': '收货仓' } },
|
||||
{ key: 'channelName', label: '渠道名称', type: 'select', placeholder: '全部', options: [...new Set(store.orders.map((item) => item.channelName).filter(Boolean))].map((name) => ({ label: name, value: name })), props: { 'aria-label': '渠道名称' } },
|
||||
{ key: 'channelName', label: '渠道名称', type: 'select', placeholder: '全部', options: store.queryChannelNames.map((name) => ({ label: name, value: name })), props: { 'aria-label': '渠道名称' } },
|
||||
{ key: 'createdRange', label: '创建时间', type: 'daterange', placeholder: '请选择创建时间', props: { 'aria-label': '创建时间' } },
|
||||
{ key: 'status', label: '当前状态', type: 'select', placeholder: '全部', options: FIRST_LEG_ORDER_STATUSES.filter(item => item.code !== 'CANCELLED').map((item) => ({ label: item.label, value: item.code })), props: { 'aria-label': '当前状态' } },
|
||||
{ key: 'destinationType', label: '目的仓类型', type: 'select', placeholder: '全部', options: [{ label: 'FBA 仓', value: 'FBA' }, { label: '海外仓', value: 'OVERSEAS' }, { label: '私人地址', value: 'PRIVATE' }], props: { 'aria-label': '目的仓类型' } },
|
||||
@@ -500,49 +500,47 @@ useUnsavedWorkRegistration({
|
||||
dirty: editorDirty,
|
||||
})
|
||||
|
||||
function matchesAppliedFilters(order: FirstLegOrder) {
|
||||
const statusValue = appliedFilters.value.status
|
||||
const createdRangeValue = appliedFilters.value.createdRange
|
||||
const textValue = (key: string) => typeof appliedFilters.value[key] === 'string' ? String(appliedFilters.value[key]).trim().toLowerCase() : ''
|
||||
const receivingWarehouseId = textValue('receivingWarehouseId')
|
||||
const channelName = textValue('channelName')
|
||||
const cargoKeyword = textValue('cargoKeyword')
|
||||
const destinationType = textValue('destinationType')
|
||||
const trackingNumber = textValue('trackingNumber')
|
||||
const status = typeof statusValue === 'string' ? statusValue as FirstLegOrderStatus | '' : ''
|
||||
const createdRange = Array.isArray(createdRangeValue) ? createdRangeValue.filter((value): value is string => typeof value === 'string') : []
|
||||
const matchesWarehouse = !receivingWarehouseId || order.receivingWarehouseId.toLowerCase() === receivingWarehouseId
|
||||
const matchesChannel = !channelName || order.channelName.toLowerCase() === channelName
|
||||
const matchesCargo = !cargoKeyword || [order.businessSummary.inboundCode, ...order.skuNos, ...order.items.map((item) => item.sku)]
|
||||
.filter(Boolean).some((value) => String(value).toLowerCase().includes(cargoKeyword))
|
||||
const matchesDestinationType = !destinationType || order.destinationType.toLowerCase() === destinationType
|
||||
const matchesTrackingNumber = !trackingNumber || order.trackingNumber.toLowerCase().includes(trackingNumber)
|
||||
const matchesStatus = !status || order.status === status
|
||||
const matchesDate = createdRange.length !== 2 || (order.createdAt.slice(0, 10) >= createdRange[0] && order.createdAt.slice(0, 10) <= createdRange[1])
|
||||
return matchesWarehouse && matchesChannel && matchesCargo && matchesDestinationType && matchesTrackingNumber && matchesStatus && matchesDate
|
||||
}
|
||||
const orderQueueCounts = computed(() => store.queueCounts)
|
||||
const pagedOrders = computed(() => store.orders)
|
||||
|
||||
const queryMatchedOrders = computed(() => store.orders.filter(matchesAppliedFilters))
|
||||
const orderQueueCounts = computed(() => ({
|
||||
all: queryMatchedOrders.value.length,
|
||||
fee: queryMatchedOrders.value.filter((order) => order.businessSummary.customerFeeConfirmationRequired).length,
|
||||
customs: queryMatchedOrders.value.filter((order) => needsCustomsUpload(eligibleCustomsDetail(order))).length,
|
||||
draft: queryMatchedOrders.value.filter((order) => order.status === 'DRAFT').length,
|
||||
}))
|
||||
const filteredOrders = computed(() => queryMatchedOrders.value.filter((order) => {
|
||||
if (activeOrderQueue.value === 'fee') return order.businessSummary.customerFeeConfirmationRequired
|
||||
if (activeOrderQueue.value === 'customs') return needsCustomsUpload(eligibleCustomsDetail(order))
|
||||
if (activeOrderQueue.value === 'draft') return order.status === 'DRAFT'
|
||||
return true
|
||||
}))
|
||||
const pagedOrders = computed(() => {
|
||||
const start = (tablePage.value - 1) * tablePageSize.value
|
||||
return filteredOrders.value.slice(start, start + tablePageSize.value)
|
||||
})
|
||||
|
||||
function selectOrderQueue(queue: OrderQueue) {
|
||||
async function selectOrderQueue(queue: OrderQueue) {
|
||||
activeOrderQueue.value = queue
|
||||
tablePage.value = 1
|
||||
await loadOrderPage()
|
||||
}
|
||||
|
||||
async function changeOrderPage(page: number) {
|
||||
tablePage.value = page
|
||||
await loadOrderPage()
|
||||
}
|
||||
|
||||
async function changeOrderPageSize(size: number) {
|
||||
tablePageSize.value = size
|
||||
tablePage.value = 1
|
||||
await loadOrderPage()
|
||||
}
|
||||
|
||||
let orderPageRequestVersion = 0
|
||||
async function loadOrderPage() {
|
||||
const requestVersion = ++orderPageRequestVersion
|
||||
const text = (key: string) => typeof appliedFilters.value[key] === 'string' ? String(appliedFilters.value[key]).trim() || undefined : undefined
|
||||
const range = Array.isArray(appliedFilters.value.createdRange) ? appliedFilters.value.createdRange : []
|
||||
const requestedPage = tablePage.value
|
||||
await store.load({
|
||||
cargoKeyword: text('cargoKeyword'), receivingWarehouseId: text('receivingWarehouseId'),
|
||||
channelName: text('channelName'), destinationType: text('destinationType'),
|
||||
trackingNumber: text('trackingNumber'), mainStatus: text('status'),
|
||||
createdFrom: range[0] ? `${range[0]}T00:00:00` : undefined,
|
||||
createdTo: range[1] ? `${range[1]}T23:59:59.999` : undefined,
|
||||
queue: activeOrderQueue.value, page: requestedPage, size: tablePageSize.value,
|
||||
})
|
||||
if (requestVersion !== orderPageRequestVersion) return
|
||||
const lastPage = Math.max(1, Math.ceil(store.total / tablePageSize.value))
|
||||
if (!store.loadError && requestedPage > lastPage) {
|
||||
tablePage.value = lastPage
|
||||
await loadOrderPage()
|
||||
return
|
||||
}
|
||||
selectedOrders.value = []
|
||||
orderTableVersion.value += 1
|
||||
}
|
||||
@@ -586,7 +584,9 @@ function openInboundCodeAssociation(inboundCode: string) {
|
||||
}
|
||||
|
||||
function updateCustomsDetail(value: MerchantCustomsContainer) {
|
||||
const previous = customsByOrder[customsDialogOrderId.value]
|
||||
if (customsDialogOrderId.value) customsByOrder[customsDialogOrderId.value] = value
|
||||
if (!isCreatePage.value && !isDetailPage.value && previous && previous.merchantStatus !== value.merchantStatus) void loadOrderPage()
|
||||
}
|
||||
|
||||
const detailOrder = computed(() => detailId.value ? store.getById(detailId.value) : undefined)
|
||||
@@ -652,7 +652,7 @@ async function loadPageData() {
|
||||
: Promise.resolve()
|
||||
const orderPageRequest = isCreatePage.value || isDetailPage.value
|
||||
? store.loadWarehouseType('DOMESTIC', true)
|
||||
: store.load({ includeDrafts: true })
|
||||
: loadOrderPage()
|
||||
const addressRequest = isCreatePage.value ? loadOrderAddresses() : Promise.resolve()
|
||||
const [ordersResult, companyResult, destinationResult, policyResult, addressResult] = await Promise.allSettled([
|
||||
orderPageRequest,
|
||||
@@ -673,7 +673,6 @@ async function loadPageData() {
|
||||
}
|
||||
if (!isCreatePage.value && ordersResult.status === 'rejected' && !store.loadError) supportingDataErrors.value.push(`订单加载失败:${errorMessage(ordersResult.reason)}`)
|
||||
if (store.warehouseLoadError) supportingDataErrors.value.push(`公共仓库加载失败:${store.warehouseLoadError}`)
|
||||
if (!isCreatePage.value && store.draftLoadError) supportingDataErrors.value.push(`订单草稿加载失败:${store.draftLoadError}`)
|
||||
if (destinationResult.status === 'rejected') supportingDataErrors.value.push(`目的仓加载失败:${errorMessage(destinationResult.reason)}`)
|
||||
if (addressResult.status === 'rejected') supportingDataErrors.value.push(`企业地址簿加载失败:${errorMessage(addressResult.reason)}`)
|
||||
await autofillWarehouseSelections()
|
||||
@@ -1528,6 +1527,7 @@ async function retryInboundBilling() {
|
||||
}
|
||||
|
||||
function handleInboundFeeConfirmed() {
|
||||
if (!isCreatePage.value && !isDetailPage.value) void loadOrderPage()
|
||||
if (!feeConfirmationOpen.value) void retryInboundBilling()
|
||||
}
|
||||
|
||||
@@ -1953,7 +1953,7 @@ async function submitBatchImport() {
|
||||
batchImportIdempotencyKey.value,
|
||||
)
|
||||
const result = batchImportResult.value.result
|
||||
if (result.succeeded && auth.hasPermission('order:first-leg:list')) await store.load({ includeDrafts: true })
|
||||
if (result.succeeded && auth.hasPermission('order:first-leg:list')) await loadOrderPage()
|
||||
if (result.failed) ElMessage.warning(`导入完成:成功 ${result.succeeded} 条,失败 ${result.failed} 条`)
|
||||
else ElMessage.success(`已成功导入 ${result.succeeded} 条订单草稿`)
|
||||
} catch (error) {
|
||||
@@ -2087,37 +2087,13 @@ async function submitOrder() {
|
||||
}
|
||||
|
||||
async function searchOrders(model: QueryModel) {
|
||||
const createdRange = model.createdRange
|
||||
appliedFilters.value = {
|
||||
...model,
|
||||
createdRange: Array.isArray(createdRange) ? createdRange.filter((value): value is string => typeof value === 'string') : [],
|
||||
appliedFilters.value = { ...model,
|
||||
createdRange: Array.isArray(model.createdRange) ? model.createdRange.filter((value): value is string => typeof value === 'string') : [],
|
||||
}
|
||||
tablePage.value = 1
|
||||
try {
|
||||
const statusValue = typeof model.status === 'string' ? model.status as FirstLegOrderStatus | '' : ''
|
||||
const cargoKeyword = typeof model.cargoKeyword === 'string' ? model.cargoKeyword.trim() : ''
|
||||
const range = Array.isArray(createdRange) ? createdRange.filter((value): value is string => typeof value === 'string') : []
|
||||
const query = {
|
||||
keyword: undefined,
|
||||
inboundCode: cargoKeyword || undefined,
|
||||
receivingWarehouseId: typeof model.receivingWarehouseId === 'string' && model.receivingWarehouseId ? model.receivingWarehouseId : undefined,
|
||||
mainStatus: statusValue && statusValue !== 'DRAFT' && statusValue !== 'CANCELLED' ? statusValue : undefined,
|
||||
draftOnly: statusValue === 'DRAFT',
|
||||
includeDrafts: !statusValue,
|
||||
createdFrom: range[0] ? `${range[0]}T00:00:00` : undefined,
|
||||
createdTo: range[1] ? `${range[1]}T23:59:59` : undefined,
|
||||
}
|
||||
await store.load(query)
|
||||
const cargoMatched = cargoKeyword && store.orders.some((order) => order.status !== 'DRAFT' && orderMatchesCargoKeyword(order, cargoKeyword))
|
||||
if (cargoKeyword && !cargoMatched && statusValue !== 'DRAFT') {
|
||||
await store.load({ ...query, keyword: undefined, inboundCode: undefined, fbaCode: cargoKeyword })
|
||||
}
|
||||
selectedOrders.value = []
|
||||
orderTableVersion.value += 1
|
||||
} catch {
|
||||
// Store keeps the recoverable error state rendered below the query toolbar.
|
||||
}
|
||||
await loadOrderPage()
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -2126,7 +2102,7 @@ async function searchOrders(model: QueryModel) {
|
||||
<QueryToolbar v-model="filters" class="first-leg-query-toolbar" layout="grid" :fields="queryFields" ariaLabel="订单查询" @search="searchOrders" />
|
||||
|
||||
<div class="client-card order-table-card">
|
||||
<DataTable v-model:page="tablePage" v-model:page-size="tablePageSize" :data="pagedOrders" :columns="visibleOrderColumns" :key="orderTableVersion" :loading="store.loading" :total="filteredOrders.length" :page-sizes="[10, 20, 50]" row-key="id" empty-text="暂无订单" class="prototype-order-table" :row-actions="getOrderRowActions" @selection-change="handleOrderSelectionChange">
|
||||
<DataTable :page="tablePage" :page-size="tablePageSize" @update:page="changeOrderPage" @update:page-size="changeOrderPageSize" :data="pagedOrders" :columns="visibleOrderColumns" :key="orderTableVersion" :loading="store.loading" :total="store.total" :page-sizes="[10, 20, 50]" row-key="id" empty-text="暂无订单" class="prototype-order-table" :row-actions="getOrderRowActions" @selection-change="handleOrderSelectionChange">
|
||||
<template #toolbar>
|
||||
<nav class="order-queue-tabs" aria-label="订单事项分类">
|
||||
<button type="button" :class="{ active: activeOrderQueue === 'all' }" @click="selectOrderQueue('all')">全部</button>
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { ref } from 'vue'
|
||||
import ts from 'typescript'
|
||||
|
||||
const source = readFileSync(new URL('./FirstLegOrdersView.vue', import.meta.url), 'utf8').match(/<script setup lang="ts">([\s\S]*?)<\/script>/)[1]
|
||||
const ast = ts.createSourceFile('view.ts', source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS)
|
||||
const names = ['loadOrderPage', 'changeOrderPage', 'changeOrderPageSize', 'selectOrderQueue', 'searchOrders']
|
||||
const functions = ast.statements.filter(n => ts.isFunctionDeclaration(n) && names.includes(n.name?.text)).map(n => n.getText(ast)).join('\n')
|
||||
const code = ts.transpileModule('let orderPageRequestVersion = 0;\n' + functions, { compilerOptions: { target: ts.ScriptTarget.ES2022 } }).outputText
|
||||
function harness() {
|
||||
const calls = []
|
||||
const values = {
|
||||
tablePage: ref(1), tablePageSize: ref(10), activeOrderQueue: ref('all'), appliedFilters: ref({}),
|
||||
selectedOrders: ref(['previous']), orderTableVersion: ref(0),
|
||||
store: { total: 215, loadError: '', load: async q => { calls.push(q) } },
|
||||
}
|
||||
const handlers = new Function(...Object.keys(values), code + ';return {' + names.join(',') + '}')(...Object.values(values))
|
||||
return { ...values, ...handlers, calls }
|
||||
}
|
||||
test('page events request server pages; size, queue and search reset to the first page', async () => {
|
||||
const h = harness()
|
||||
await h.changeOrderPage(21); assert.equal(h.calls.at(-1).page, 21)
|
||||
await h.changeOrderPageSize(50); assert.equal(h.calls.at(-1).page, 1); assert.equal(h.calls.at(-1).size, 50)
|
||||
await h.selectOrderQueue('customs'); assert.equal(h.calls.at(-1).queue, 'customs'); assert.equal(h.tablePage.value, 1)
|
||||
await h.searchOrders({ cargoKeyword: ' SKU ', status: 'DRAFT', createdRange: ['2026-10-01', '2026-10-03'] })
|
||||
assert.equal(h.calls.at(-1).cargoKeyword, 'SKU'); assert.equal(h.calls.at(-1).mainStatus, 'DRAFT')
|
||||
assert.equal(h.calls.at(-1).createdFrom, '2026-10-01T00:00:00')
|
||||
assert.equal(h.calls.at(-1).createdTo, '2026-10-03T23:59:59.999')
|
||||
assert.deepEqual(h.selectedOrders.value, [])
|
||||
})
|
||||
test('deletion on the last page clamps and reloads once; failed reads retain the requested page for retry', async () => {
|
||||
const h = harness(); h.store.total = 200
|
||||
await h.changeOrderPage(22)
|
||||
assert.deepEqual(h.calls.map(q => q.page), [22, 20]); assert.equal(h.tablePage.value, 20)
|
||||
h.store.total = 0; h.store.loadError = 'failed'
|
||||
await h.changeOrderPage(22)
|
||||
assert.equal(h.tablePage.value, 22); assert.equal(h.calls.length, 3)
|
||||
})
|
||||
test('old same-page response cannot clamp a newer query or clear its current selection', async () => {
|
||||
const h = harness(); let release
|
||||
h.store.load = () => new Promise(resolve => { release = resolve })
|
||||
const first = h.loadOrderPage(); const oldRelease = release
|
||||
const second = h.searchOrders({ cargoKeyword: 'new' })
|
||||
h.store.total = 215; release(); await second
|
||||
h.selectedOrders.value = ['new-selection']
|
||||
oldRelease(); await first
|
||||
assert.deepEqual(h.selectedOrders.value, ['new-selection'])
|
||||
assert.equal(h.orderTableVersion.value, 1)
|
||||
})
|
||||
Reference in New Issue
Block a user