From 9891ac3e29ae525629e6a43ae82b49cf1ced5ef4 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Fri, 9 Oct 2026 17:36:34 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/first-leg/api/unifiedOrders.ts | 10 +++++-- .../first-leg/store/firstLegOrders.test.mjs | 2 +- src/modules/first-leg/store/firstLegOrders.ts | 19 +++++++++---- .../store/firstLegWorkbench.test.mjs | 22 +++++++++++++-- .../first-leg/utils/orderProgress.test.mjs | 15 ++++++++++ src/modules/first-leg/utils/orderProgress.ts | 20 +++++++++++++ .../views/FirstLegOrdersView.test.mjs | 4 +-- .../first-leg/views/FirstLegOrdersView.vue | 28 +++++++++++++------ 8 files changed, 99 insertions(+), 21 deletions(-) create mode 100644 src/modules/first-leg/utils/orderProgress.test.mjs create mode 100644 src/modules/first-leg/utils/orderProgress.ts diff --git a/src/modules/first-leg/api/unifiedOrders.ts b/src/modules/first-leg/api/unifiedOrders.ts index fdde238..e7fd13b 100644 --- a/src/modules/first-leg/api/unifiedOrders.ts +++ b/src/modules/first-leg/api/unifiedOrders.ts @@ -177,6 +177,11 @@ export interface MerchantUnifiedOrderQuery { const unifiedOrderBaseUrl = '/merchant/v1/orders' +export type OrderQueue = 'all' | 'fee' | 'customs' | 'draft' | 'loaded' | 'declared' | 'cleared' | 'scheduled' | 'delivering' | 'delivered' +export type OrderProgressStage = 'none' | 'loaded' | 'declared' | 'cleared' | 'scheduled' | 'delivering' | 'delivered' +export interface MerchantOrderProgress { orderId: string | number; stage: OrderProgressStage; partial: boolean } +export type OrderQueueCounts = Record + export interface MerchantOrderWorkbenchQuery { cargoKeyword?: string receivingWarehouseId?: string @@ -186,14 +191,15 @@ export interface MerchantOrderWorkbenchQuery { mainStatus?: string createdFrom?: string createdTo?: string - queue?: 'all' | 'fee' | 'customs' | 'draft' + queue?: OrderQueue page?: number size?: number } export interface MerchantOrderWorkbench { page: PageResult - queues: { all: number; fee: number; customs: number; draft: number } + queues: OrderQueueCounts + progress: MerchantOrderProgress[] channelNames: string[] } diff --git a/src/modules/first-leg/store/firstLegOrders.test.mjs b/src/modules/first-leg/store/firstLegOrders.test.mjs index 34c3cd7..8a369db 100644 --- a/src/modules/first-leg/store/firstLegOrders.test.mjs +++ b/src/modules/first-leg/store/firstLegOrders.test.mjs @@ -236,5 +236,5 @@ test('keeps failed order loads distinct from empty results', () => { assert.match(storeSource, /const loadError = ref\(''\)/) assert.match(storeSource, /orderResult\.reason instanceof Error/) assert.match(storeSource, /warehouseLoadError/) - assert.match(storeSource, /return \{ orders, orderDetails, total, queueCounts, queryChannelNames, warehouses, loading, loadedFromApi, loadError, warehouseLoadError/) + assert.match(storeSource, /return \{ orders, orderDetails, total, queueCounts, orderProgress, queryChannelNames, warehouses, loading, loadedFromApi, loadError, warehouseLoadError/) }) diff --git a/src/modules/first-leg/store/firstLegOrders.ts b/src/modules/first-leg/store/firstLegOrders.ts index 64932d0..bce11ef 100644 --- a/src/modules/first-leg/store/firstLegOrders.ts +++ b/src/modules/first-leg/store/firstLegOrders.ts @@ -22,6 +22,8 @@ import { getMerchantOrderWorkbench, type MerchantUnifiedOrderDetail, type MerchantOrderWorkbenchQuery, + type MerchantOrderProgress, + type OrderQueueCounts, type MerchantUnifiedOrderRow, } from '@/modules/first-leg/api/unifiedOrders' import type { MerchantTenantStatus } from '@/modules/tenant-management/public' @@ -396,7 +398,8 @@ export const useFirstLegOrdersStore = defineStore('client-first-leg-orders', () const orders = ref([]) const orderDetails = ref>({}) const total = ref(0) - const queueCounts = ref({ all: 0, fee: 0, customs: 0, draft: 0 }) + const queueCounts = ref({ all: 0, fee: 0, customs: 0, draft: 0, loaded: 0, declared: 0, cleared: 0, scheduled: 0, delivering: 0, delivered: 0 }) + const orderProgress = ref>({}) const queryChannelNames = ref([]) let listRequest = 0 const pendingSubmissions = new Map() @@ -451,7 +454,8 @@ export const useFirstLegOrdersStore = defineStore('client-first-leg-orders', () loadError.value = '' orders.value = [] total.value = 0 - queueCounts.value = { all: 0, fee: 0, customs: 0, draft: 0 } + queueCounts.value = { all: 0, fee: 0, customs: 0, draft: 0, loaded: 0, declared: 0, cleared: 0, scheduled: 0, delivering: 0, delivered: 0 } + orderProgress.value = {} const [orderResult, warehouseResult] = await Promise.allSettled([ getMerchantOrderWorkbench(query), loadWarehouseType('DOMESTIC'), ]) @@ -466,14 +470,19 @@ export const useFirstLegOrdersStore = defineStore('client-first-leg-orders', () || Number(result.page.size) !== size || Number(result.page.pages) !== pages || result.page.records.length !== Math.min(size, Math.max(0, count - (expectedPage - 1) * size)) || new Set(result.page.records.map(row => String(row.orderId))).size !== result.page.records.length - || ['all', 'fee', 'customs', 'draft'].some(key => { const value = result.queues[key as keyof typeof result.queues]; return !Number.isSafeInteger(Number(value)) || Number(value) < 0 }) + || ['all', 'fee', 'customs', 'draft', 'loaded', 'declared', 'cleared', 'scheduled', 'delivering', 'delivered'].some(key => { const value = result.queues[key as keyof typeof result.queues]; return !Number.isSafeInteger(Number(value)) || Number(value) < 0 }) || Number(result.queues[query.queue ?? 'all']) !== count + || !Array.isArray(result.progress) || result.progress.length !== result.page.records.length + || new Set(result.progress.map(p => String(p.orderId))).size !== result.progress.length + || result.progress.some(p => !result.page.records.some(row => String(row.orderId) === String(p.orderId)) + || !['none', 'loaded', 'declared', 'cleared', 'scheduled', 'delivering', 'delivered'].includes(p.stage) || typeof p.partial !== 'boolean') || !Array.isArray(result.channelNames)) { loadError.value = '订单分页数据不完整,请重新加载' } else { orders.value = result.page.records.map(item => mapUnifiedOrderRow(item, warehouses.value)) total.value = count - queueCounts.value = { all: Number(result.queues.all), fee: Number(result.queues.fee), customs: Number(result.queues.customs), draft: Number(result.queues.draft) } + queueCounts.value = Object.fromEntries(Object.entries(result.queues).map(([key, value]) => [key, Number(value)])) as OrderQueueCounts + orderProgress.value = Object.fromEntries(result.progress.map(p => [String(p.orderId), p])) queryChannelNames.value = result.channelNames } } catch { loadError.value = '订单分页数据不完整,请重新加载' } @@ -629,5 +638,5 @@ export const useFirstLegOrdersStore = defineStore('client-first-leg-orders', () return loadOrderAggregate(id) } - return { orders, orderDetails, total, queueCounts, queryChannelNames, warehouses, loading, loadedFromApi, loadError, warehouseLoadError, getById, load, loadDetail, loadDraftDetail, loadWarehouseDetail, loadWarehouseType, beginDraftEdit, saveDraft, prepareCancellation, cancelOrder, saveDraftBatch, submitSaved, hasPendingSubmission, isSubmissionRetry, beginNewSubmissionEdit, saveDraftForSubmission } + return { orders, orderDetails, total, queueCounts, orderProgress, queryChannelNames, warehouses, loading, loadedFromApi, loadError, warehouseLoadError, getById, load, loadDetail, loadDraftDetail, loadWarehouseDetail, loadWarehouseType, beginDraftEdit, saveDraft, prepareCancellation, cancelOrder, saveDraftBatch, submitSaved, hasPendingSubmission, isSubmissionRetry, beginNewSubmissionEdit, saveDraftForSubmission } }) diff --git a/src/modules/first-leg/store/firstLegWorkbench.test.mjs b/src/modules/first-leg/store/firstLegWorkbench.test.mjs index 0e35218..4d0986f 100644 --- a/src/modules/first-leg/store/firstLegWorkbench.test.mjs +++ b/src/modules/first-leg/store/firstLegWorkbench.test.mjs @@ -11,7 +11,8 @@ const row = (id, mainStatus = 'LOADED') => ({ orderId: String(id), orderNo: `FL- function result(page = 1, size = 10, total = 215) { return { page: { page, size, total, pages: Math.ceil(total / size), records: Array.from({ length: Math.min(size, Math.max(0, total - (page - 1) * size)) }, (_, i) => row((page - 1) * size + i + 1)) }, - queues: { all: total, fee: 2, customs: 1, draft: 3 }, channelNames: ['AIR', 'SEA'], + queues: { all: total, fee: 2, customs: 1, draft: 3, loaded: 0, declared: 0, cleared: 0, scheduled: 0, delivering: 0, delivered: 0 }, channelNames: ['AIR', 'SEA'], + progress: Array.from({ length: Math.min(size, Math.max(0, total - (page - 1) * size)) }, (_, i) => ({ orderId: String((page - 1) * size + i + 1), stage: 'loaded', partial: false })), } } function deferred() { let resolve, reject; const promise = new Promise((a, b) => { resolve = a; reject = b }); return { promise, resolve, reject } } @@ -44,7 +45,7 @@ test('loads beyond 200 with server totals and forwards every filter using one re assert.equal(h.store.orders[0].id, '201') assert.equal(h.store.total, 215) assert.deepEqual(h.calls, [query]) - assert.deepEqual(h.store.queueCounts, { all: 215, fee: 2, customs: 1, draft: 3 }) + assert.deepEqual(h.store.queueCounts, { all: 215, fee: 2, customs: 1, draft: 3, loaded: 0, declared: 0, cleared: 0, scheduled: 0, delivering: 0, delivered: 0 }) assert.deepEqual(h.store.queryChannelNames, ['AIR', 'SEA']) await h.store.load({ page: 22, size: 10 }) assert.deepEqual(h.store.orders.map(item => item.id), ['211', '212', '213', '214', '215']) @@ -109,9 +110,24 @@ test('draft and cancelled rows retain authoritative versions; off-page detail ca test('queue total is checked against the selected queue rather than all orders', async t => { const response = result(1, 10, 2) - response.queues = { all: 215, fee: 2, customs: 1, draft: 3 } + response.queues = { all: 215, fee: 2, customs: 1, draft: 3, loaded: 0, declared: 0, cleared: 0, scheduled: 0, delivering: 0, delivered: 0 } const h = harness(t, () => response) await h.store.load({ queue: 'fee' }) assert.equal(h.store.total, 2); assert.equal(h.store.queueCounts.all, 215) assert.equal(h.store.loadError, '') }) + + +test('retains server progress and rejects missing or foreign progress without inferring milestones', async t => { + let response = result() + response.progress[0] = { orderId: '1', stage: 'scheduled', partial: true } + const h = harness(t, () => response) + await h.store.load() + assert.deepEqual(h.store.orderProgress['1'], response.progress[0]) + response = result(); response.progress[0].orderId = 'foreign' + await h.store.load() + assert.match(h.store.loadError, /分页数据不完整/) + assert.deepEqual(h.store.orderProgress, {}) + response = result(); delete response.queues.cleared + await h.store.load(); assert.match(h.store.loadError, /分页数据不完整/) +}) diff --git a/src/modules/first-leg/utils/orderProgress.test.mjs b/src/modules/first-leg/utils/orderProgress.test.mjs new file mode 100644 index 0000000..efc8626 --- /dev/null +++ b/src/modules/first-leg/utils/orderProgress.test.mjs @@ -0,0 +1,15 @@ +import assert from 'node:assert/strict' +import test from 'node:test' +import { readFileSync } from 'node:fs' +import ts from 'typescript' +const source = readFileSync(new URL('./orderProgress.ts', import.meta.url), 'utf8') +const code = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS } }).outputText +const exports = {} +new Function('exports', code)(exports) +test('prototype queues preserve order and distinguish partial progress from completion', () => { + assert.deepEqual(exports.ORDER_QUEUES.slice(0, 7).map(q => q.key), ['all','fee','loaded','declared','cleared','scheduled','delivering']) + assert.equal(exports.orderProgressLabel({ stage: 'scheduled', partial: true }), '部分已排车') + assert.equal(exports.orderProgressLabel({ stage: 'cleared', partial: false }), '已清关') + assert.equal(exports.orderProgressLabel({ stage: 'none', partial: false }), '—') + assert.equal(exports.orderProgressLabel(), '—') +}) diff --git a/src/modules/first-leg/utils/orderProgress.ts b/src/modules/first-leg/utils/orderProgress.ts new file mode 100644 index 0000000..fe3ce02 --- /dev/null +++ b/src/modules/first-leg/utils/orderProgress.ts @@ -0,0 +1,20 @@ +import type { MerchantOrderProgress, OrderQueue } from '../api/unifiedOrders' + +export const ORDER_QUEUES: ReadonlyArray<{ key: OrderQueue; label: string; todo?: boolean }> = [ + { key: 'all', label: '全部' }, + { key: 'fee', label: '确认计费重', todo: true }, + { key: 'loaded', label: '装柜完成' }, + { key: 'declared', label: '已报关' }, + { key: 'cleared', label: '已清关' }, + { key: 'scheduled', label: '已排车' }, + { key: 'delivering', label: '派送中' }, + { key: 'delivered', label: '派送完成' }, + { key: 'customs', label: '上传资料', todo: true }, + { key: 'draft', label: '草稿' }, +] + +export function orderProgressLabel(progress?: MerchantOrderProgress) { + if (!progress || progress.stage === 'none') return '—' + const label = ORDER_QUEUES.find(q => q.key === progress.stage)?.label ?? '—' + return `${progress.partial ? '部分' : ''}${label}` +} diff --git a/src/modules/first-leg/views/FirstLegOrdersView.test.mjs b/src/modules/first-leg/views/FirstLegOrdersView.test.mjs index 4b8c255..4d498ff 100644 --- a/src/modules/first-leg/views/FirstLegOrdersView.test.mjs +++ b/src/modules/first-leg/views/FirstLegOrdersView.test.mjs @@ -176,7 +176,7 @@ test('exports every visible order field with normalized dates', () => { test('matches the prototype with selectable dense order-list columns', () => { assert.match(source, /aria-label="设置订单列表显示列"/) assert.match(source, /显示字段筛选/) - assert.match(source, /const defaultVisibleColumnKeys = \['status', 'channel', 'transportMode', 'pickupMethod', 'receivingWarehouse', 'createdAt'\]/) + assert.match(source, /const defaultVisibleColumnKeys = \['status', 'progress', 'channel', 'transportMode', 'pickupMethod', 'receivingWarehouse', 'createdAt'\]/) assert.match(source, /key: 'inboundCode', label: '入仓码', width: 230, fixed: 'left'/) assert.match(source, / { test('keeps the first viewport compact and moves create into the table toolbar', () => { const tableToolbarSource = source.slice(source.indexOf('#toolbar-actions'), source.indexOf('class="order-eligibility is-pending"')) assert.match(source, /#toolbar-actions/) - assert.match(source, /class="order-queue-tabs"[\s\S]*确认计费重[\s\S]*上传资料[\s\S]*草稿/) + assert.match(source, /class="order-queue-tabs"[\s\S]*v-for="queue in ORDER_QUEUES"[\s\S]*orderQueueCounts\[queue.key\]/) assert.match(source, /activeOrderQueue\.value === 'draft'[\s\S]*order\.status === 'DRAFT'/) assert.doesNotMatch(source, /共 \{\{ filteredOrders\.length \}\} 条/) assert.match(source, /#toolbar-actions[\s\S]*order:first-leg:create[\s\S]*创建订单[\s\S]*class="order-eligibility is-pending"/) diff --git a/src/modules/first-leg/views/FirstLegOrdersView.vue b/src/modules/first-leg/views/FirstLegOrdersView.vue index 8fba950..a868dc1 100644 --- a/src/modules/first-leg/views/FirstLegOrdersView.vue +++ b/src/modules/first-leg/views/FirstLegOrdersView.vue @@ -59,6 +59,8 @@ import { summarizeFirstLegCargo, validateFirstLegOrderDraft, } from '@/modules/first-leg/utils/firstLegOrder' +import { ORDER_QUEUES, orderProgressLabel } from '../utils/orderProgress' +import type { OrderQueue } from '../api/unifiedOrders' import { validateProductImageFile } from '@/modules/first-leg/utils/productImage' import { createPriceEstimateBreakdown } from '@/modules/first-leg/utils/priceEstimate' @@ -168,6 +170,7 @@ const fixedOrderColumns: DataTableColumn[] = [ ] const selectableOrderColumns: DataTableColumn[] = [ { key: 'status', label: '当前订单状态', width: 126, align: 'center', headerAlign: 'center', slotName: 'status' }, + { key: 'progress', label: '履约进度', width: 142, align: 'center', headerAlign: 'center', slotName: 'progress' }, { key: 'channel', label: '渠道', width: 150, align: 'left', headerAlign: 'left', showOverflowTooltip: true, slotName: 'channel' }, { key: 'transportMode', label: '运输方式', width: 104, align: 'center', headerAlign: 'center', slotName: 'transportMode' }, { key: 'pickupMethod', label: '交货方式', width: 112, align: 'center', headerAlign: 'center', slotName: 'pickupMethod' }, @@ -198,7 +201,7 @@ const selectableOrderColumns: DataTableColumn[] = [ { key: 'updatedAt', label: '更新时间', width: 164, align: 'center', headerAlign: 'center', slotName: 'updatedAt' }, ] const allSelectableColumnKeys = selectableOrderColumns.map((column) => String(column.key)) -const defaultVisibleColumnKeys = ['status', 'channel', 'transportMode', 'pickupMethod', 'receivingWarehouse', 'createdAt'] +const defaultVisibleColumnKeys = ['status', 'progress', 'channel', 'transportMode', 'pickupMethod', 'receivingWarehouse', 'createdAt'] const visibleColumnKeys = ref([...defaultVisibleColumnKeys]) const orderedColumnKeys = ref([...allSelectableColumnKeys]) const draggedColumnKey = ref('') @@ -243,7 +246,6 @@ const selectedOrders = ref([]) const selectedOrderCount = computed(() => selectedOrders.value.length) const selectedDraftOrders = computed(() => selectedOrders.value.filter((order) => order.status === 'DRAFT')) const orderTableVersion = ref(0) -type OrderQueue = 'all' | 'fee' | 'customs' | 'draft' const activeOrderQueue = ref('all') const editorOpen = ref(false) const orderFormRef = ref() @@ -638,6 +640,7 @@ const emptyDescription = computed(() => { if (activeOrderQueue.value === 'customs') return '暂无需要上传报关资料的订单' if (activeOrderQueue.value === 'fee') return '暂无需要确认计费重的订单' if (activeOrderQueue.value === 'draft') return '暂无草稿订单' + if (activeOrderQueue.value !== 'all') return `暂无${ORDER_QUEUES.find(q => q.key === activeOrderQueue.value)?.label ?? ''}的订单` return '还没有订单' }) @@ -1812,6 +1815,7 @@ function exportOrderInformation() { displayListValue(order.destinationWarehouseName), destinationTypeLabels[order.destinationType], getFirstLegStatusLabel(order.status), + orderProgressLabel(store.orderProgress[order.id]), formatListMetric(orderPackageCount(order)), displayListValue(order.expressCompanyName), displayListValue(order.trackingNumber), @@ -1830,7 +1834,7 @@ function exportOrderInformation() { formatExportDateTime(order.createdAt), ]) downloadCsv('订单信息.csv', [[ - '入仓码', '渠道', '运输方式', '交货方式', '预约提货时间', '目的仓', '目的仓类型', '当前订单状态', '箱数', '快递公司', '快递单号', 'SKUS', + '入仓码', '渠道', '运输方式', '交货方式', '预约提货时间', '目的仓', '目的仓类型', '当前订单状态', '履约进度', '箱数', '快递公司', '快递单号', 'SKUS', '总毛重', '总净重', '总体积', '总数量', '计费单价', '计费体积', '计费重量', '备注', '货件编号', '跟踪编号', '国内仓入库时间', '创建时间', ], ...rows]) @@ -2106,10 +2110,12 @@ async function searchOrders(model: QueryModel) { @@ -2185,6 +2191,7 @@ async function searchOrders(model: QueryModel) { + @@ -2570,7 +2577,12 @@ async function searchOrders(model: QueryModel) { .supporting-data-error span { overflow-wrap: anywhere; color: var(--muted); font-size: 11px; line-height: 1.55; } .order-row-actions, .editor-actions { display: flex; justify-content: flex-end; gap: 8px; } .order-table-card { padding: 0; overflow: hidden; border: 1px solid var(--client-border); border-radius: var(--client-radius); box-shadow: none; } -.order-queue-tabs { display: flex; align-items: center; gap: 4px; } +.prototype-order-table :deep(.data-table__toolbar) { flex-wrap: nowrap; } +.prototype-order-table :deep(.data-table__toolbar-heading) { min-width: 0; flex: 1; flex-wrap: nowrap; } +.prototype-order-table :deep(.data-table__toolbar-actions) { flex-shrink: 0; } +.order-queue-tabs { display: flex; min-width: 0; align-items: center; gap: 4px; overflow-x: auto; padding: 7px 0 10px; scrollbar-width: thin; } +.order-queue-tabs button { flex-shrink: 0; white-space: nowrap; } +.queue-stage-count { margin-left: 3px; } .order-queue-tabs button { position: relative; min-height: 34px; padding: 0 12px; border: 1px solid transparent; border-radius: 7px; background: transparent; color: #667085; cursor: pointer; font: inherit; font-weight: 600; } .order-queue-tabs button:hover { color: var(--client-action); } .order-queue-tabs button.active { border-color: #cfe0ff; background: #f3f7ff; color: var(--client-action); }