This commit is contained in:
2026-10-09 17:36:34 +08:00
parent 07ba9fcb7c
commit 9891ac3e29
8 changed files with 99 additions and 21 deletions
+8 -2
View File
@@ -177,6 +177,11 @@ export interface MerchantUnifiedOrderQuery {
const unifiedOrderBaseUrl = '/merchant/v1/orders' 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<OrderQueue, number>
export interface MerchantOrderWorkbenchQuery { export interface MerchantOrderWorkbenchQuery {
cargoKeyword?: string cargoKeyword?: string
receivingWarehouseId?: string receivingWarehouseId?: string
@@ -186,14 +191,15 @@ export interface MerchantOrderWorkbenchQuery {
mainStatus?: string mainStatus?: string
createdFrom?: string createdFrom?: string
createdTo?: string createdTo?: string
queue?: 'all' | 'fee' | 'customs' | 'draft' queue?: OrderQueue
page?: number page?: number
size?: number size?: number
} }
export interface MerchantOrderWorkbench { export interface MerchantOrderWorkbench {
page: PageResult<MerchantUnifiedOrderRow> page: PageResult<MerchantUnifiedOrderRow>
queues: { all: number; fee: number; customs: number; draft: number } queues: OrderQueueCounts
progress: MerchantOrderProgress[]
channelNames: string[] channelNames: string[]
} }
@@ -236,5 +236,5 @@ test('keeps failed order loads distinct from empty results', () => {
assert.match(storeSource, /const loadError = ref\(''\)/) assert.match(storeSource, /const loadError = ref\(''\)/)
assert.match(storeSource, /orderResult\.reason instanceof Error/) assert.match(storeSource, /orderResult\.reason instanceof Error/)
assert.match(storeSource, /warehouseLoadError/) 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/)
}) })
+14 -5
View File
@@ -22,6 +22,8 @@ import {
getMerchantOrderWorkbench, getMerchantOrderWorkbench,
type MerchantUnifiedOrderDetail, type MerchantUnifiedOrderDetail,
type MerchantOrderWorkbenchQuery, type MerchantOrderWorkbenchQuery,
type MerchantOrderProgress,
type OrderQueueCounts,
type MerchantUnifiedOrderRow, type MerchantUnifiedOrderRow,
} from '@/modules/first-leg/api/unifiedOrders' } from '@/modules/first-leg/api/unifiedOrders'
import type { MerchantTenantStatus } from '@/modules/tenant-management/public' import type { MerchantTenantStatus } from '@/modules/tenant-management/public'
@@ -396,7 +398,8 @@ export const useFirstLegOrdersStore = defineStore('client-first-leg-orders', ()
const orders = ref<FirstLegOrder[]>([]) const orders = ref<FirstLegOrder[]>([])
const orderDetails = ref<Record<string, FirstLegOrder>>({}) const orderDetails = ref<Record<string, FirstLegOrder>>({})
const total = ref(0) const total = ref(0)
const queueCounts = ref({ all: 0, fee: 0, customs: 0, draft: 0 }) const queueCounts = ref<OrderQueueCounts>({ all: 0, fee: 0, customs: 0, draft: 0, loaded: 0, declared: 0, cleared: 0, scheduled: 0, delivering: 0, delivered: 0 })
const orderProgress = ref<Record<string, MerchantOrderProgress>>({})
const queryChannelNames = ref<string[]>([]) const queryChannelNames = ref<string[]>([])
let listRequest = 0 let listRequest = 0
const pendingSubmissions = new Map<string, { key: string; payload: { expectedVersion: number } }>() const pendingSubmissions = new Map<string, { key: string; payload: { expectedVersion: number } }>()
@@ -451,7 +454,8 @@ export const useFirstLegOrdersStore = defineStore('client-first-leg-orders', ()
loadError.value = '' loadError.value = ''
orders.value = [] orders.value = []
total.value = 0 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([ const [orderResult, warehouseResult] = await Promise.allSettled([
getMerchantOrderWorkbench(query), loadWarehouseType('DOMESTIC'), 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 || Number(result.page.size) !== size || Number(result.page.pages) !== pages
|| result.page.records.length !== Math.min(size, Math.max(0, count - (expectedPage - 1) * size)) || 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 || 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 || 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)) { || !Array.isArray(result.channelNames)) {
loadError.value = '订单分页数据不完整,请重新加载' loadError.value = '订单分页数据不完整,请重新加载'
} else { } else {
orders.value = result.page.records.map(item => mapUnifiedOrderRow(item, warehouses.value)) orders.value = result.page.records.map(item => mapUnifiedOrderRow(item, warehouses.value))
total.value = count 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 queryChannelNames.value = result.channelNames
} }
} catch { loadError.value = '订单分页数据不完整,请重新加载' } } catch { loadError.value = '订单分页数据不完整,请重新加载' }
@@ -629,5 +638,5 @@ export const useFirstLegOrdersStore = defineStore('client-first-leg-orders', ()
return loadOrderAggregate(id) 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 }
}) })
@@ -11,7 +11,8 @@ const row = (id, mainStatus = 'LOADED') => ({ orderId: String(id), orderNo: `FL-
function result(page = 1, size = 10, total = 215) { function result(page = 1, size = 10, total = 215) {
return { 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)) }, 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 } } 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.orders[0].id, '201')
assert.equal(h.store.total, 215) assert.equal(h.store.total, 215)
assert.deepEqual(h.calls, [query]) 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']) assert.deepEqual(h.store.queryChannelNames, ['AIR', 'SEA'])
await h.store.load({ page: 22, size: 10 }) await h.store.load({ page: 22, size: 10 })
assert.deepEqual(h.store.orders.map(item => item.id), ['211', '212', '213', '214', '215']) 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 => { test('queue total is checked against the selected queue rather than all orders', async t => {
const response = result(1, 10, 2) 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) const h = harness(t, () => response)
await h.store.load({ queue: 'fee' }) await h.store.load({ queue: 'fee' })
assert.equal(h.store.total, 2); assert.equal(h.store.queueCounts.all, 215) assert.equal(h.store.total, 2); assert.equal(h.store.queueCounts.all, 215)
assert.equal(h.store.loadError, '') 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, /分页数据不完整/)
})
@@ -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(), '—')
})
@@ -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}`
}
@@ -176,7 +176,7 @@ test('exports every visible order field with normalized dates', () => {
test('matches the prototype with selectable dense order-list columns', () => { test('matches the prototype with selectable dense order-list columns', () => {
assert.match(source, /aria-label="设置订单列表显示列"/) assert.match(source, /aria-label="设置订单列表显示列"/)
assert.match(source, /显示字段筛选/) 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, /key: 'inboundCode', label: '入仓码', width: 230, fixed: 'left'/)
assert.match(source, /<SlidersHorizontal :size="18"/) assert.match(source, /<SlidersHorizontal :size="18"/)
assert.match(source, /<el-checkbox-group v-model="visibleColumnKeys"/) assert.match(source, /<el-checkbox-group v-model="visibleColumnKeys"/)
@@ -272,7 +272,7 @@ test('keeps all progress stages and business summary usable on mobile', () => {
test('keeps the first viewport compact and moves create into the table toolbar', () => { 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"')) const tableToolbarSource = source.slice(source.indexOf('#toolbar-actions'), source.indexOf('class="order-eligibility is-pending"'))
assert.match(source, /#toolbar-actions/) 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.match(source, /activeOrderQueue\.value === 'draft'[\s\S]*order\.status === 'DRAFT'/)
assert.doesNotMatch(source, /共 \{\{ filteredOrders\.length \}\} 条/) 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"/) assert.match(source, /#toolbar-actions[\s\S]*order:first-leg:create[\s\S]*创建订单[\s\S]*class="order-eligibility is-pending"/)
@@ -59,6 +59,8 @@ import {
summarizeFirstLegCargo, summarizeFirstLegCargo,
validateFirstLegOrderDraft, validateFirstLegOrderDraft,
} from '@/modules/first-leg/utils/firstLegOrder' } 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 { validateProductImageFile } from '@/modules/first-leg/utils/productImage'
import { createPriceEstimateBreakdown } from '@/modules/first-leg/utils/priceEstimate' import { createPriceEstimateBreakdown } from '@/modules/first-leg/utils/priceEstimate'
@@ -168,6 +170,7 @@ const fixedOrderColumns: DataTableColumn[] = [
] ]
const selectableOrderColumns: DataTableColumn[] = [ const selectableOrderColumns: DataTableColumn[] = [
{ key: 'status', label: '当前订单状态', width: 126, align: 'center', headerAlign: 'center', slotName: 'status' }, { 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: 'channel', label: '渠道', width: 150, align: 'left', headerAlign: 'left', showOverflowTooltip: true, slotName: 'channel' },
{ key: 'transportMode', label: '运输方式', width: 104, align: 'center', headerAlign: 'center', slotName: 'transportMode' }, { key: 'transportMode', label: '运输方式', width: 104, align: 'center', headerAlign: 'center', slotName: 'transportMode' },
{ key: 'pickupMethod', label: '交货方式', width: 112, align: 'center', headerAlign: 'center', slotName: 'pickupMethod' }, { 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' }, { key: 'updatedAt', label: '更新时间', width: 164, align: 'center', headerAlign: 'center', slotName: 'updatedAt' },
] ]
const allSelectableColumnKeys = selectableOrderColumns.map((column) => String(column.key)) 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<string[]>([...defaultVisibleColumnKeys]) const visibleColumnKeys = ref<string[]>([...defaultVisibleColumnKeys])
const orderedColumnKeys = ref<string[]>([...allSelectableColumnKeys]) const orderedColumnKeys = ref<string[]>([...allSelectableColumnKeys])
const draggedColumnKey = ref('') const draggedColumnKey = ref('')
@@ -243,7 +246,6 @@ const selectedOrders = ref<FirstLegOrder[]>([])
const selectedOrderCount = computed(() => selectedOrders.value.length) const selectedOrderCount = computed(() => selectedOrders.value.length)
const selectedDraftOrders = computed(() => selectedOrders.value.filter((order) => order.status === 'DRAFT')) const selectedDraftOrders = computed(() => selectedOrders.value.filter((order) => order.status === 'DRAFT'))
const orderTableVersion = ref(0) const orderTableVersion = ref(0)
type OrderQueue = 'all' | 'fee' | 'customs' | 'draft'
const activeOrderQueue = ref<OrderQueue>('all') const activeOrderQueue = ref<OrderQueue>('all')
const editorOpen = ref(false) const editorOpen = ref(false)
const orderFormRef = ref<FormInstance>() const orderFormRef = ref<FormInstance>()
@@ -638,6 +640,7 @@ const emptyDescription = computed(() => {
if (activeOrderQueue.value === 'customs') return '暂无需要上传报关资料的订单' if (activeOrderQueue.value === 'customs') return '暂无需要上传报关资料的订单'
if (activeOrderQueue.value === 'fee') return '暂无需要确认计费重的订单' if (activeOrderQueue.value === 'fee') return '暂无需要确认计费重的订单'
if (activeOrderQueue.value === 'draft') return '暂无草稿订单' if (activeOrderQueue.value === 'draft') return '暂无草稿订单'
if (activeOrderQueue.value !== 'all') return `暂无${ORDER_QUEUES.find(q => q.key === activeOrderQueue.value)?.label ?? ''}的订单`
return '还没有订单' return '还没有订单'
}) })
@@ -1812,6 +1815,7 @@ function exportOrderInformation() {
displayListValue(order.destinationWarehouseName), displayListValue(order.destinationWarehouseName),
destinationTypeLabels[order.destinationType], destinationTypeLabels[order.destinationType],
getFirstLegStatusLabel(order.status), getFirstLegStatusLabel(order.status),
orderProgressLabel(store.orderProgress[order.id]),
formatListMetric(orderPackageCount(order)), formatListMetric(orderPackageCount(order)),
displayListValue(order.expressCompanyName), displayListValue(order.expressCompanyName),
displayListValue(order.trackingNumber), displayListValue(order.trackingNumber),
@@ -1830,7 +1834,7 @@ function exportOrderInformation() {
formatExportDateTime(order.createdAt), formatExportDateTime(order.createdAt),
]) ])
downloadCsv('订单信息.csv', [[ downloadCsv('订单信息.csv', [[
'入仓码', '渠道', '运输方式', '交货方式', '预约提货时间', '目的仓', '目的仓类型', '当前订单状态', '箱数', '快递公司', '快递单号', 'SKUS', '入仓码', '渠道', '运输方式', '交货方式', '预约提货时间', '目的仓', '目的仓类型', '当前订单状态', '履约进度', '箱数', '快递公司', '快递单号', 'SKUS',
'总毛重', '总净重', '总体积', '总数量', '计费单价', '计费体积', '计费重量', '备注', '总毛重', '总净重', '总体积', '总数量', '计费单价', '计费体积', '计费重量', '备注',
'货件编号', '跟踪编号', '国内仓入库时间', '创建时间', '货件编号', '跟踪编号', '国内仓入库时间', '创建时间',
], ...rows]) ], ...rows])
@@ -2106,10 +2110,12 @@ async function searchOrders(model: QueryModel) {
<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"> <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> <template #toolbar>
<nav class="order-queue-tabs" aria-label="订单事项分类"> <nav class="order-queue-tabs" aria-label="订单事项分类">
<button type="button" :class="{ active: activeOrderQueue === 'all' }" @click="selectOrderQueue('all')">全部</button> <button v-for="queue in ORDER_QUEUES" :key="queue.key" type="button"
<button type="button" :class="{ active: activeOrderQueue === 'fee' }" @click="selectOrderQueue('fee')">确认计费重<span v-if="orderQueueCounts.fee" class="queue-count">{{ orderQueueCounts.fee }}</span></button> :class="{ active: activeOrderQueue === queue.key }" :aria-pressed="activeOrderQueue === queue.key"
<button type="button" :class="{ active: activeOrderQueue === 'customs' }" @click="selectOrderQueue('customs')">上传资料<span v-if="orderQueueCounts.customs" class="queue-count">{{ orderQueueCounts.customs }}</span></button> @click="selectOrderQueue(queue.key)">
<button type="button" :class="{ active: activeOrderQueue === 'draft' }" @click="selectOrderQueue('draft')">草稿<span v-if="orderQueueCounts.draft" class="queue-count">{{ orderQueueCounts.draft }}</span></button> {{ queue.label }}<span v-if="queue.key !== 'all' && orderQueueCounts[queue.key]"
:class="queue.todo ? 'queue-count' : 'queue-stage-count'">{{ queue.todo ? orderQueueCounts[queue.key] : `(${orderQueueCounts[queue.key]})` }}</span>
</button>
</nav> </nav>
<span v-if="selectedOrderCount">已选 {{ selectedOrderCount }} 条</span> <span v-if="selectedOrderCount">已选 {{ selectedOrderCount }} 条</span>
</template> </template>
@@ -2185,6 +2191,7 @@ async function searchOrders(model: QueryModel) {
<template #receivingWarehouse="{ row }"><span class="table-ellipsis" :title="displayListValue((row as FirstLegOrder).receivingWarehouseName)">{{ displayListValue((row as FirstLegOrder).receivingWarehouseName) }}</span></template> <template #receivingWarehouse="{ row }"><span class="table-ellipsis" :title="displayListValue((row as FirstLegOrder).receivingWarehouseName)">{{ displayListValue((row as FirstLegOrder).receivingWarehouseName) }}</span></template>
<template #destinationWarehouse="{ row }"><span class="table-ellipsis" :title="displayListValue((row as FirstLegOrder).destinationWarehouseName)">{{ displayListValue((row as FirstLegOrder).destinationWarehouseName) }}</span></template> <template #destinationWarehouse="{ row }"><span class="table-ellipsis" :title="displayListValue((row as FirstLegOrder).destinationWarehouseName)">{{ displayListValue((row as FirstLegOrder).destinationWarehouseName) }}</span></template>
<template #destinationType="{ row }">{{ destinationTypeLabels[(row as FirstLegOrder).destinationType] }}</template> <template #destinationType="{ row }">{{ destinationTypeLabels[(row as FirstLegOrder).destinationType] }}</template>
<template #progress="{ row }">{{ orderProgressLabel(store.orderProgress[row.id]) }}</template>
<template #status="{ row }"><span class="client-status" :data-tone="getFirstLegStatusTone((row as FirstLegOrder).status)">{{ getFirstLegStatusLabel((row as FirstLegOrder).status) }}</span></template> <template #status="{ row }"><span class="client-status" :data-tone="getFirstLegStatusTone((row as FirstLegOrder).status)">{{ getFirstLegStatusLabel((row as FirstLegOrder).status) }}</span></template>
<template #packageCount="{ row }">{{ formatListMetric(orderPackageCount(row as FirstLegOrder)) }}</template> <template #packageCount="{ row }">{{ formatListMetric(orderPackageCount(row as FirstLegOrder)) }}</template>
<template #expressCompany="{ row }"><span class="table-ellipsis" :title="displayListValue((row as FirstLegOrder).expressCompanyName)">{{ displayListValue((row as FirstLegOrder).expressCompanyName) }}</span></template> <template #expressCompany="{ row }"><span class="table-ellipsis" :title="displayListValue((row as FirstLegOrder).expressCompanyName)">{{ displayListValue((row as FirstLegOrder).expressCompanyName) }}</span></template>
@@ -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; } .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-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-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 { 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:hover { color: var(--client-action); }
.order-queue-tabs button.active { border-color: #cfe0ff; background: #f3f7ff; color: var(--client-action); } .order-queue-tabs button.active { border-color: #cfe0ff; background: #f3f7ff; color: var(--client-action); }