diff --git a/src/modules/first-leg/api/unifiedOrders.ts b/src/modules/first-leg/api/unifiedOrders.ts index 7c83e33..8daee30 100644 --- a/src/modules/first-leg/api/unifiedOrders.ts +++ b/src/modules/first-leg/api/unifiedOrders.ts @@ -1,4 +1,4 @@ -import { ApiError, apiRequest } from '@/api/http' +import { apiRequest } from '@/api/http' import type { ApiContainerMode, ApiDestinationType, ApiFirstLegStatus, ApiPickupMethod, ApiTaxMode, MerchantFirstLegOrderDetail, PageResult } from '@/modules/first-leg/api/firstLeg' export type ApiUnifiedOrderMainStatus = @@ -60,11 +60,12 @@ export interface MerchantUnifiedTransportSummary { export interface MerchantUnifiedOrderRow { orderId: number | string + orderVersion: number tenantId: number | string orderNo: string | null fulfillmentType?: 'STANDARD' | 'ONE_CLICK_DROPSHIP' | null lxCode?: string | null - mainStatus: ApiUnifiedOrderMainStatus + mainStatus: ApiUnifiedOrderMainStatus | 'DRAFT' | 'CANCELLED' nextAction: string | null customerFeeConfirmationRequired: boolean internalOrderStatus: ApiFirstLegStatus | null @@ -158,6 +159,31 @@ export interface MerchantUnifiedOrderQuery { const unifiedOrderBaseUrl = '/merchant/v1/orders' +export interface MerchantOrderWorkbenchQuery { + cargoKeyword?: string + receivingWarehouseId?: string + channelName?: string + destinationType?: string + trackingNumber?: string + mainStatus?: string + createdFrom?: string + createdTo?: string + queue?: 'all' | 'fee' | 'customs' | 'draft' + page?: number + size?: number +} + +export interface MerchantOrderWorkbench { + page: PageResult + queues: { all: number; fee: number; customs: number; draft: number } + channelNames: string[] +} + +export function getMerchantOrderWorkbench(params: MerchantOrderWorkbenchQuery = {}) { + return apiRequest({ method: 'GET', url: `${unifiedOrderBaseUrl}/workbench`, + params: { ...params, page: params.page ?? 1, size: params.size ?? 10 } }) +} + export function getMerchantUnifiedOrders(params: MerchantUnifiedOrderQuery = {}) { const { page, size, ...filters } = params return apiRequest>({ @@ -173,7 +199,3 @@ export function getMerchantUnifiedOrder(orderId: number | string) { url: `${unifiedOrderBaseUrl}/${orderId}`, }) } - -export function isUnifiedOrderQueryUnavailable(error: unknown) { - return error instanceof ApiError && (error.status === 404 || error.code === 'A0404') -} diff --git a/src/modules/first-leg/store/firstLegDraftCommands.test.mjs b/src/modules/first-leg/store/firstLegDraftCommands.test.mjs index 07941fb..9847ce6 100644 --- a/src/modules/first-leg/store/firstLegDraftCommands.test.mjs +++ b/src/modules/first-leg/store/firstLegDraftCommands.test.mjs @@ -32,7 +32,6 @@ function harness(t) { '@/modules/first-leg/api/firstLeg': api, '@/modules/first-leg/api/unifiedOrders': { getMerchantUnifiedOrder: async () => ({ summary: { orderId: 'one', mainStatus: null }, order: { order: { id: 'one', status: behavior.status, version: behavior.version, receivingWarehouseId: 'warehouse', receivingWarehouseAddress: behavior.receivingWarehouseAddress } }, timeline: [] }), - isUnifiedOrderQueryUnavailable: () => false, }, '@/modules/first-leg/utils/firstLegOrder': { canClientEditFirstLegOrder: status => status === 'DRAFT', getFirstLegStatusLabel: value => value }, '@/modules/first-leg/utils/warehousePagination': {}, @@ -131,7 +130,7 @@ test('edit freezes detail version rather than cached list version and replays sa const h = harness(t) const original = await h.store.loadDraftDetail('one') const session = h.store.beginDraftEdit(original) - h.store.orders[0].version = 0 + h.store.orders = [{ ...original, version: 0 }] h.behavior.failWrite = true await assert.rejects(h.store.saveDraft(h.draft, '测试企业', 'one', session), /响应丢失/) const first = h.calls.find(call => call.method === 'PUT') @@ -233,6 +232,7 @@ function cancellationViewHarness() { } const ElMessage = { error: message => messages.push(message), warning: message => messages.push(message), success: message => messages.push(message) } const globals = { canCancelOrder, cancellingOrderId, pendingCancellation, store, ElMessageBox, ElMessage, ApiError, + loadOrderPage: async () => calls.push(['load']), isDetailPage: vue.ref(true), router: { push: async route => calls.push(['route', route]) } } const handler = new Function(...Object.keys(globals), code + '\nreturn cancelOrder')(...Object.values(globals)) return { handler, calls, messages, prompts, canCancelOrder, pendingCancellation, behavior, ApiError } diff --git a/src/modules/first-leg/store/firstLegOrders.test.mjs b/src/modules/first-leg/store/firstLegOrders.test.mjs index 566225a..34c3cd7 100644 --- a/src/modules/first-leg/store/firstLegOrders.test.mjs +++ b/src/modules/first-leg/store/firstLegOrders.test.mjs @@ -27,22 +27,10 @@ test('renders estimated sailing from the current API without substituting actual assert.equal(map({ transport: null }).estimatedPortArrivalAt, '') }) -test('maps the first-leg aggregate list fields without loading detail rows', () => { - assert.match(apiSource, /interface MerchantFirstLegOrderRow/) - assert.match(apiSource, /skuNos: string \| string\[\] \| null/) - assert.match(apiSource, /cargoes\?: Array<\{ skuNo\?: string \| null \}>/) - assert.match(apiSource, /apiRequest>/) - assert.match(storeSource, /function mapFirstLegOrderRow/) - for (const field of ['inboundId', 'inboundCode', 'channelName', 'destinationWarehouseName', 'pickupMethod', 'packageCount', 'expressCompany', 'trackingNo', 'totalGrossWeightKg', 'totalNetWeightKg', 'totalVolumeCbm', 'totalQuantity', 'remark', 'shipmentId', 'fulfillmentType', 'lxCode', 'referenceId', 'createdAt']) { - assert.match(storeSource, new RegExp(`row\\.${field}`)) - } - assert.match(storeSource, /row\.skuNos/) - assert.match(storeSource, /row\.cargoes/) - assert.match(storeSource, /chargeableWeightKg: null/) - assert.match(storeSource, /chargeableVolumeCbm: null/) - assert.match(apiSource, /pickupMethod: ApiPickupMethod \\| null/) - assert.match(storeSource, /orderResult\.value\.records\.map\(mapFirstLegOrderRow\)/) - assert.match(storeSource, /orderResult\.value\.records\.map\(\(item\) => mapUnifiedOrderRow\(item, warehouses\.value\)\)/) +test('maps the current workbench row without loading detail rows', () => { + assert.match(storeSource, /result\.page\.records\.map\(item => mapUnifiedOrderRow\(item, warehouses\.value\)\)/) + assert.match(storeSource, /version: summary\.orderVersion/) + assert.doesNotMatch(storeSource, /function mapFirstLegOrderRow/) }) test('declares the documented merchant warehouse and first-leg endpoints', () => { @@ -85,14 +73,10 @@ test('maps transport and status fields instead of treating container mode as tra assert.doesNotMatch(storeSource, /status === 'COMPLETED'\) return 'DELIVERED'/) }) -test('prefers unified reads and only falls back to the real legacy API when C02 is unavailable', () => { +test('uses only the formal workbench read and surfaces failures', () => { + assert.match(storeSource, /getMerchantOrderWorkbench\(query\)/) assert.match(storeSource, /orders\.value = \[\]/) - assert.match(storeSource, /warehouses\.value = \[\]/) - assert.match(storeSource, /Promise\.allSettled\(\[getOrdersForClient\(query\), loadWarehouseType\('DOMESTIC', true\)\]\)/) - assert.match(storeSource, /getMerchantUnifiedOrders\(unifiedQuery\)/) - assert.match(storeSource, /isUnifiedOrderQueryUnavailable\(error\)/) - assert.match(storeSource, /getMerchantFirstLegOrders\(legacyQuery\(query\)\)/) - assert.doesNotMatch(storeSource, /DEMO_PLATFORM_RECEIVING_WAREHOUSES|initialOrders/) + assert.doesNotMatch(storeSource, /isUnifiedOrderQueryUnavailable|getOrdersForClient|getMerchantFirstLegOrders|initialOrders/) }) test('declares the final C02 row and detail contract in the API adapter', () => { @@ -115,7 +99,6 @@ test('declares the final C02 row and detail contract in the API adapter', () => assert.match(unifiedApiSource, /order: MerchantFirstLegOrderDetail/) assert.match(unifiedApiSource, /timeline: MerchantUnifiedTimelineEvent\[\]/) assert.match(unifiedApiSource, /customNodes\?: MerchantUnifiedCustomNode\[\]/) - assert.match(unifiedApiSource, /error\.status === 404 \|\| error\.code === 'A0404'/) }) test('keeps unified order pagination valid when the store omits page and size', () => { @@ -123,21 +106,10 @@ test('keeps unified order pagination valid when the store omits page and size', assert.match(unifiedApiSource, /params: \{ page: page \?\? 1, size: size \?\? 200, \.\.\.filters \}/) }) -test('keeps drafts on the legacy read path without assigning a unified main status', () => { - assert.match(storeSource, /getMerchantFirstLegOrders\(legacyQuery\(query, 'DRAFT'\)\)/) - assert.match(storeSource, /const listOrder = getById\(id\)/) - assert.match(storeSource, /listOrder\?\.status === 'DRAFT' \? loadLegacyDetail\(id, listOrder\) : loadUnifiedDetail\(id\)/) - assert.match(storeSource, /detail\.order\.lxCode == null && listOrder\?\.lxCode/) - assert.match(storeSource, /return loadLegacyDetail\(String\(createdId\)\)/) - assert.match(storeSource, /draftLoadError/) -}) - -test('routes supported filters to C02 without sending DRAFT as a main status', () => { - assert.match(storeSource, /interface ClientOrderReadQuery extends MerchantUnifiedOrderQuery/) - assert.match(storeSource, /if \(query\.draftOnly\)/) - assert.match(storeSource, /const unifiedQuery: MerchantUnifiedOrderQuery/) - assert.match(storeSource, /inboundCode: query\.inboundCode/) - assert.match(storeSource, /getMerchantUnifiedOrders\(unifiedQuery\)/) +test('draft editing reads the order aggregate while navigation uses unified detail', () => { + assert.match(storeSource, /return loadOrderAggregate\(id\)/) + assert.match(storeSource, /const detail = await getMerchantUnifiedOrder\(id\)/) + assert.doesNotMatch(storeSource, /loadLegacyDetail|draftOnly|includeDrafts/) }) test('maps C02 authoritative status, transport, cargo and pickup snapshots', () => { @@ -195,7 +167,7 @@ test('separates domestic receiving warehouses from public destinations', () => { assert.match(apiSource, /warehouseType\?: MerchantWarehouseType/) assert.match(apiSource, /params: \{ page: 1, size: 200, status: 'ACTIVE', \.\.\.params \}/) assert.match(storeSource, /loadAllWarehouseRecords\(warehouseType, getMerchantWarehouses\)/) - assert.match(storeSource, /loadWarehouseType\('DOMESTIC', true\)/) + assert.match(storeSource, /loadWarehouseType\('DOMESTIC'\)/) assert.match(storeSource, /warehouse\.warehouseType !== 'DOMESTIC'/) assert.match(storeSource, /item\.warehouseType === draft\.destinationType/) assert.match(storeSource, /draft\.amazonWarehouseCode = draft\.destinationType === 'FBA' \? destinationWarehouse\.code : ''/) @@ -264,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, warehouses, loading, loadedFromApi, loadError, warehouseLoadError, draftLoadError/) + assert.match(storeSource, /return \{ orders, orderDetails, total, queueCounts, 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 fdde68a..a862856 100644 --- a/src/modules/first-leg/store/firstLegOrders.ts +++ b/src/modules/first-leg/store/firstLegOrders.ts @@ -1,11 +1,10 @@ -import { computed, ref } from 'vue' +import { ref } from 'vue' import { defineStore } from 'pinia' import { createMerchantFirstLegOrderBatch, createMerchantFirstLegOrder, createIdempotencyKey, getMerchantFirstLegOrder, - getMerchantFirstLegOrders, getMerchantWarehouseDetail, getMerchantWarehouses, submitMerchantFirstLegOrder, @@ -14,17 +13,15 @@ import { type ApiFirstLegStatus, type FirstLegOrderSaveRequest, type MerchantFirstLegOrder, - type MerchantFirstLegOrderRow, type MerchantFirstLegOrderDetail, type MerchantBatchDraftResponse, type MerchantWarehouse, } from '@/modules/first-leg/api/firstLeg' import { getMerchantUnifiedOrder, - getMerchantUnifiedOrders, - isUnifiedOrderQueryUnavailable, + getMerchantOrderWorkbench, type MerchantUnifiedOrderDetail, - type MerchantUnifiedOrderQuery, + type MerchantOrderWorkbenchQuery, type MerchantUnifiedOrderRow, } from '@/modules/first-leg/api/unifiedOrders' import type { MerchantTenantStatus } from '@/modules/tenant-management/public' @@ -342,7 +339,7 @@ function mapUnifiedOrderRow(summary: MerchantUnifiedOrderRow, warehouses: Platfo trackingNumber: summary.expressTrackingNo ?? summary.delivery?.trackingNo ?? '', skuNos: summary.skuNos ?? [], totalNetWeightKg: summary.totalNetWeightKg ?? null, totalQuantity: summary.totalQuantity ?? null, chargeableVolumeCbm: summary.chargeableVolumeCbm ?? null, inboundedAt: summary.inboundCompletedAt ?? '', businessDate: '', priority: 'NORMAL', customerRemark: summary.customerRemark ?? '', operatorName: '', - status: summary.mainStatus, version: 0, items: [], timeline: [], businessSummary: businessSummary(summary), + status: summary.mainStatus, version: summary.orderVersion, items: [], timeline: [], businessSummary: businessSummary(summary), createdAt: summary.createdAt, updatedAt: summary.updatedAt, tradeTerm: '', destinationCountryCode: '', destinationWarehouseId: summary.destinationWarehouseId == null ? '' : String(summary.destinationWarehouseId), destinationWarehouseName: summary.destinationWarehouseName ?? '', pickupMethod: summary.pickupMethod ?? 'SELF_EXPRESS', channelCode: '', @@ -352,36 +349,6 @@ function mapUnifiedOrderRow(summary: MerchantUnifiedOrderRow, warehouses: Platfo } } -function mapFirstLegOrderRow(row: MerchantFirstLegOrderRow): FirstLegOrder { - const skuNos = Array.isArray(row.skuNos) - ? row.skuNos - : (row.skuNos ?? '').split('、').map((value) => value.trim()).filter(Boolean) - const resolvedSkuNos = skuNos.length ? skuNos : (row.cargoes ?? []) - .map((cargo) => cargo.skuNo?.trim()) - .filter((value): value is string => Boolean(value)) - const uniqueSkuNos = [...new Set(resolvedSkuNos)] - return { - id: String(row.orderId), orderNo: row.orderNo, referenceNo: row.referenceId ?? '', transportMode: null, - businessType: 'EXPORT', fulfillmentType: row.fulfillmentType ?? 'STANDARD', lxCode: row.lxCode ?? '', origin: '', destination: row.destinationWarehouseName ?? '', receivingWarehouseId: '', - receivingWarehouseName: '', receivingWarehouseSnapshot: null, expectedDepartureDate: '', etd: '', eta: '', - shipperPartyId: '', shipperPartySnapshot: null, shipper: '', consigneePartyId: '', consigneePartySnapshot: null, - consignee: row.receivingCompanyName ?? '', expressCompanyName: row.expressCompany ?? '', trackingNumber: row.trackingNo ?? '', - skuNos: uniqueSkuNos, totalNetWeightKg: row.totalNetWeightKg, totalQuantity: row.totalQuantity, - chargeableVolumeCbm: null, inboundedAt: row.inboundedAt ?? '', businessDate: '', priority: 'NORMAL', customerRemark: row.remark ?? '', - operatorName: '', status: clientStatus(row.status), version: 0, items: [], timeline: [], - businessSummary: { - ...businessSummary(), totalPackageCount: row.packageCount, actualGrossWeightKg: row.totalGrossWeightKg ?? row.weightKg, - actualVolumeCbm: row.totalVolumeCbm, chargeableWeightKg: null, - inboundId: row.inboundId == null ? '' : String(row.inboundId), inboundCode: row.inboundCode ?? '', - }, - createdAt: row.createdAt, updatedAt: row.createdAt, tradeTerm: '', destinationCountryCode: '', destinationWarehouseId: '', - destinationWarehouseName: row.destinationWarehouseName ?? '', pickupMethod: row.pickupMethod ?? 'SELF_EXPRESS', channelCode: '', - channelName: row.channelName ?? '', pricingTaxMode: 'TAX_INCLUDED', carrierId: '', carrierName: '', taxMode: 'BUY_DECLARATION', - customsInspection: false, destinationType: row.destinationType ?? 'PRIVATE', amazonWarehouseCode: '', - shipmentId: row.shipmentId ?? row.internationalOrderNo ?? '', requestedPickupAt: '', receivingTime: '', submittedAt: '', completedAt: '', - } -} - function mapUnifiedOrderDetail(detail: MerchantUnifiedOrderDetail, warehouses: PlatformReceivingWarehouse[]): FirstLegOrder { if (!detail?.summary || !detail.order?.order) throw new Error('订单详情数据不完整,请刷新列表后重试') const mapped = mapOrder(detail.order.order, warehouses, detail.order) @@ -410,59 +377,6 @@ function mapUnifiedOrderDetail(detail: MerchantUnifiedOrderDetail, warehouses: P return mapped } -type OrderListReadResult = - | { source: 'first-leg'; records: MerchantFirstLegOrderRow[] } - | { source: 'unified'; records: MerchantUnifiedOrderRow[]; drafts: MerchantFirstLegOrderRow[] } - -interface ClientOrderReadQuery extends MerchantUnifiedOrderQuery { - fbaCode?: string - draftOnly?: boolean - includeDrafts?: boolean -} - -function legacyQuery(query: ClientOrderReadQuery, status?: ApiFirstLegStatus) { - return { - keyword: query.keyword, - status, - createdFrom: query.createdFrom, - createdTo: query.createdTo, - } -} - -async function getOrdersForClient(query: ClientOrderReadQuery = {}): Promise { - if (query.draftOnly) { - const response = await getMerchantFirstLegOrders(legacyQuery(query, 'DRAFT')) - return { source: 'first-leg', records: response.records } - } - - const unifiedQuery: MerchantUnifiedOrderQuery = { - keyword: query.keyword, - orderNo: query.orderNo, - mainStatus: query.mainStatus, - receivingWarehouseId: query.receivingWarehouseId, - destinationWarehouseId: query.destinationWarehouseId, - inboundCode: query.inboundCode, - createdFrom: query.createdFrom, - createdTo: query.createdTo, - fbaCode: query.fbaCode, - page: query.page, - size: query.size, - } - try { - const [response, draftResponse] = await Promise.all([ - getMerchantUnifiedOrders(unifiedQuery), - query.includeDrafts - ? getMerchantFirstLegOrders(legacyQuery(query, 'DRAFT')) - : Promise.resolve({ records: [] as MerchantFirstLegOrderRow[] }), - ]) - return { source: 'unified', records: response.records, drafts: draftResponse.records } - } catch (error) { - if (!isUnifiedOrderQueryUnavailable(error)) throw error - const response = await getMerchantFirstLegOrders(legacyQuery(query)) - return { source: 'first-leg', records: response.records } - } -} - function mapWarehouse(item: MerchantWarehouse): ClientWarehouse { return { id: String(item.id), code: item.warehouseCode, name: item.warehouseName, province: '', @@ -479,6 +393,11 @@ export interface DraftEditSession { 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 queryChannelNames = ref([]) + let listRequest = 0 const pendingSubmissions = new Map() const submissionForms = new Map() const submittingIds = new Set() @@ -494,13 +413,10 @@ export const useFirstLegOrdersStore = defineStore('client-first-leg-orders', () const loadedFromApi = ref(false) const loadError = ref('') const warehouseLoadError = ref('') - const draftLoadError = ref('') const loadedWarehouseTypes = ref([]) const warehouseTypeRequests = new Map>() - const activeCount = computed(() => orders.value.filter((item) => item.status !== 'DRAFT' && item.status !== 'DELIVERED').length) - const pendingCount = computed(() => orders.value.filter((item) => item.status === 'PENDING_REVIEW').length) - function getById(id: string) { return orders.value.find((item) => item.id === id) } + function getById(id: string) { return orders.value.find((item) => item.id === id) ?? orderDetails.value[id] } async function loadWarehouseType(warehouseType: ClientWarehouseType, force = false) { if (warehouseType === 'DOMESTIC' && force) warehouseLoadError.value = '' @@ -527,28 +443,40 @@ export const useFirstLegOrdersStore = defineStore('client-first-leg-orders', () } } - async function load(query: ClientOrderReadQuery = {}) { + async function load(query: MerchantOrderWorkbenchQuery = {}) { + const request = ++listRequest loading.value = true loadedFromApi.value = true loadError.value = '' - warehouseLoadError.value = '' - draftLoadError.value = '' - loadedWarehouseTypes.value = [] - warehouseTypeRequests.clear() orders.value = [] - warehouses.value = [] - const [orderResult, warehouseResult] = await Promise.allSettled([getOrdersForClient(query), loadWarehouseType('DOMESTIC', true)]) - if (warehouseResult.status === 'rejected' && !warehouseLoadError.value) warehouseLoadError.value = warehouseResult.reason instanceof Error ? warehouseResult.reason.message : '公共仓库数据加载失败' + total.value = 0 + queueCounts.value = { all: 0, fee: 0, customs: 0, draft: 0 } + const [orderResult, warehouseResult] = await Promise.allSettled([ + getMerchantOrderWorkbench(query), loadWarehouseType('DOMESTIC'), + ]) + if (request !== listRequest) return 0 + if (warehouseResult.status === 'rejected') warehouseLoadError.value = warehouseResult.reason instanceof Error ? warehouseResult.reason.message : '公共仓库数据加载失败' if (orderResult.status === 'fulfilled') { - orders.value = orderResult.value.source === 'unified' - ? [ - ...orderResult.value.drafts.map(mapFirstLegOrderRow), - ...orderResult.value.records.map((item) => mapUnifiedOrderRow(item, warehouses.value)), - ] - : orderResult.value.records.map(mapFirstLegOrderRow) - } else { - loadError.value = orderResult.reason instanceof Error ? orderResult.reason.message : '订单数据加载失败' - } + try { + const result = orderResult.value + const expectedPage = query.page ?? 1, size = query.size ?? 10 + const count = Number(result.page.total), pages = Math.ceil(count / size) + if (!Number.isSafeInteger(count) || count < 0 || Number(result.page.page) !== expectedPage + || 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 }) + || Number(result.queues[query.queue ?? 'all']) !== count + || !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) } + queryChannelNames.value = result.channelNames + } + } catch { loadError.value = '订单分页数据不完整,请重新加载' } + } else loadError.value = orderResult.reason instanceof Error ? orderResult.reason.message : '订单数据加载失败' loading.value = false return orders.value.length } @@ -556,38 +484,18 @@ export const useFirstLegOrdersStore = defineStore('client-first-leg-orders', () function upsertOrder(mapped: FirstLegOrder) { const index = orders.value.findIndex((item) => item.id === mapped.id) if (index >= 0) orders.value[index] = mapped - else orders.value.unshift(mapped) + orderDetails.value[mapped.id] = mapped return mapped } - async function loadLegacyDetail(id: string, listOrder?: FirstLegOrder) { + async function loadOrderAggregate(id: string) { const detail = await getMerchantFirstLegOrder(id) - const mapped = mapOrder(detail.order, warehouses.value, detail) - // Older detail responses may omit newly added dropship fields. Keep the - // values already returned by the list row so editing a draft does not - // clear the persisted LX code in the form. - if (detail.order.fulfillmentType == null && listOrder?.fulfillmentType) { - mapped.fulfillmentType = listOrder.fulfillmentType - } - if (detail.order.lxCode == null && listOrder?.lxCode) { - mapped.lxCode = listOrder.lxCode - } - return upsertOrder(mapped) - } - - async function loadUnifiedDetail(id: string) { - try { - const detail = await getMerchantUnifiedOrder(id) - return upsertOrder(mapUnifiedOrderDetail(detail, warehouses.value)) - } catch (error) { - if (!isUnifiedOrderQueryUnavailable(error)) throw error - return loadLegacyDetail(id) - } + return upsertOrder(mapOrder(detail.order, warehouses.value, detail)) } async function loadDetail(id: string) { - const listOrder = getById(id) - return listOrder?.status === 'DRAFT' ? loadLegacyDetail(id, listOrder) : loadUnifiedDetail(id) + const detail = await getMerchantUnifiedOrder(id) + return upsertOrder(mapUnifiedOrderDetail(detail, warehouses.value)) } async function loadWarehouseDetail(id: string) { @@ -621,12 +529,12 @@ export const useFirstLegOrdersStore = defineStore('client-first-leg-orders', () pendingEdits.set(editSession, pending) } await updateMerchantFirstLegOrder(id, pending.payload, pending.key) - const saved = await loadLegacyDetail(id) + const saved = await loadOrderAggregate(id) pendingEdits.delete(editSession) return saved } const createdId = await createMerchantFirstLegOrder(savePayload(draft, warehouse, customerName)) - return loadLegacyDetail(String(createdId)) + return loadOrderAggregate(String(createdId)) } async function cancelOrder(id: string, expectedVersion: number, reason: string) { @@ -680,7 +588,7 @@ export const useFirstLegOrdersStore = defineStore('client-first-leg-orders', () pendingSubmissions.set(id, pending) } await submitMerchantFirstLegOrder(id, pending.payload, pending.key) - const submitted = await loadLegacyDetail(id) + const submitted = await loadOrderAggregate(id) pendingSubmissions.delete(id) submissionForms.delete(id) return submitted @@ -717,8 +625,8 @@ export const useFirstLegOrdersStore = defineStore('client-first-leg-orders', () } async function loadDraftDetail(id: string) { - return loadLegacyDetail(id, getById(id)) + return loadOrderAggregate(id) } - return { orders, warehouses, loading, loadedFromApi, loadError, warehouseLoadError, draftLoadError, activeCount, pendingCount, getById, load, loadDetail, loadDraftDetail, loadWarehouseDetail, loadWarehouseType, beginDraftEdit, saveDraft, prepareCancellation, cancelOrder, saveDraftBatch, submitSaved, hasPendingSubmission, isSubmissionRetry, beginNewSubmissionEdit, saveDraftForSubmission } + 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 } }) diff --git a/src/modules/first-leg/store/firstLegWorkbench.test.mjs b/src/modules/first-leg/store/firstLegWorkbench.test.mjs new file mode 100644 index 0000000..0e35218 --- /dev/null +++ b/src/modules/first-leg/store/firstLegWorkbench.test.mjs @@ -0,0 +1,117 @@ +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 * as pinia from 'pinia' + +const source = readFileSync(new URL('./firstLegOrders.ts', import.meta.url), 'utf8') +const code = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 } }).outputText +const row = (id, mainStatus = 'LOADED') => ({ orderId: String(id), orderNo: `FL-${id}`, mainStatus, orderVersion: 7 }) +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'], + } +} +function deferred() { let resolve, reject; const promise = new Promise((a, b) => { resolve = a; reject = b }); return { promise, resolve, reject } } +function harness(t, read) { + const context = pinia.createPinia(); pinia.setActivePinia(context) + t.after(() => pinia.disposePinia(context)) + const calls = [], details = [], warehouseCalls = [] + const deps = { + vue, pinia, + '@/modules/first-leg/api/firstLeg': {}, + '@/modules/first-leg/api/unifiedOrders': { + getMerchantOrderWorkbench: query => { calls.push(structuredClone(query)); return read(query) }, + getMerchantUnifiedOrder: async id => { + details.push(id) + return { summary: row(id, 'DRAFT'), order: { order: { id, status: 'DRAFT', version: 7 } }, timeline: [] } + }, + }, + '@/modules/first-leg/utils/firstLegOrder': { getFirstLegStatusLabel: value => value }, + '@/modules/first-leg/utils/warehousePagination': { loadAllWarehouseRecords: async type => { warehouseCalls.push(type); return [] } }, + } + const exports = {} + new Function('require', 'exports', code)(id => { assert.ok(id in deps, id); return deps[id] }, exports) + return { store: exports.useFirstLegOrdersStore(), calls, details, warehouseCalls } +} + +test('loads beyond 200 with server totals and forwards every filter using one request per page', async t => { + const h = harness(t, q => result(q.page, q.size)) + const query = { page: 21, size: 10, cargoKeyword: 'SKU', channelName: 'SEA', receivingWarehouseId: '42', destinationType: 'FBA', trackingNumber: 'TRACK', mainStatus: 'LOADED', queue: 'all', createdFrom: '2026-10-01T00:00:00', createdTo: '2026-10-03T23:59:59.999' } + assert.equal(await h.store.load(query), 10) + 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.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']) + assert.equal(h.calls.length, 2) + assert.deepEqual(h.warehouseCalls, ['DOMESTIC']) +}) + +test('late success cannot replace newer rows, counts, channels or loading state', async t => { + const a = deferred(), b = deferred() + const h = harness(t, q => q.page === 1 ? a.promise : b.promise) + const first = h.store.load({ page: 1 }), second = h.store.load({ page: 2 }) + b.resolve(result(2)); await second + a.resolve(result(1, 10, 999)); await first + assert.equal(h.store.orders[0].id, '11'); assert.equal(h.store.total, 215) + assert.equal(h.store.queueCounts.all, 215); assert.equal(h.store.loading, false) +}) + +test('late failure cannot clear the current page and an older completion cannot stop loading a newer page', async t => { + const a = deferred(), b = deferred(), c = deferred() + const h = harness(t, q => [a, b, c][q.page - 1].promise) + const first = h.store.load({ page: 1 }), second = h.store.load({ page: 2 }) + a.resolve(result(1)); await first + assert.equal(h.store.loading, true); assert.equal(h.store.orders.length, 0) + const third = h.store.load({ page: 3 }) + c.resolve(result(3)); await third + b.reject(new Error('old failure')); await second + assert.equal(h.store.orders[0].id, '21'); assert.equal(h.store.loadError, '') +}) + +test('current failure clears stale records and totals, never falls back, and retry restores the page', async t => { + let fail = false + const h = harness(t, () => fail ? Promise.reject(new Error('查询失败')) : result()) + await h.store.load(); fail = true; await h.store.load() + assert.equal(h.store.orders.length, 0); assert.equal(h.store.total, 0) + assert.equal(h.store.queueCounts.all, 0); assert.equal(h.store.loadError, '查询失败') + assert.equal(h.store.loading, false) + fail = false; await h.store.load(); assert.equal(h.store.orders.length, 10) + assert.equal(h.calls.length, 3); assert.equal(h.store.loadError, '') +}) + +test('rejects inconsistent metadata, truncated pages and duplicate rows without displaying partial data', async t => { + let response + const h = harness(t, () => response) + const corruptions = [r => { r.page.total = -1 }, r => { r.page.pages = 1 }, r => { r.page.page = 2 }, r => { r.page.records.pop() }, r => { r.page.records[1] = r.page.records[0] }, r => { r.queues.all = 99 }, r => { delete r.queues }, r => { r.channelNames = null }] + for (const corrupt of corruptions) { + response = result(); corrupt(response); await h.store.load() + assert.equal(h.store.orders.length, 0); assert.equal(h.store.total, 0) + assert.match(h.store.loadError, /分页数据不完整/); assert.equal(h.store.loading, false) + } +}) + +test('draft and cancelled rows retain authoritative versions; off-page detail cannot inflate list or totals', async t => { + const response = result(1, 10, 2) + response.page.records = [row(1, 'DRAFT'), row(2, 'CANCELLED')] + const h = harness(t, () => response) + await h.store.load() + assert.deepEqual(h.store.orders.map(item => [item.status, item.version]), [['DRAFT', 7], ['CANCELLED', 7]]) + await h.store.loadDetail('900') + assert.equal(h.store.orders.length, 2); assert.equal(h.store.total, 2) + assert.equal(h.store.getById('900').status, 'DRAFT') +}) + +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 } + 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, '') +}) diff --git a/src/modules/first-leg/utils/customsEligibility.test.mjs b/src/modules/first-leg/utils/customsEligibility.test.mjs index c81a6de..87c8df6 100644 --- a/src/modules/first-leg/utils/customsEligibility.test.mjs +++ b/src/modules/first-leg/utils/customsEligibility.test.mjs @@ -2,7 +2,7 @@ import test from 'node:test' import assert from 'node:assert/strict' import { readFileSync } from 'node:fs' import ts from 'typescript' -import { computed, ref } from 'vue' +import { ref } from 'vue' const source = readFileSync(new URL('./customsEligibility.ts', import.meta.url), 'utf8') const exports = {} @@ -17,17 +17,16 @@ test('upload queue contains only customer uploads still required, while complete for (const status of ['LOADED', 'DELIVERED', 'COMPLETED']) assert.equal(exports.isCustomsOrderCandidate({ status, taxMode: 'TAX_REFUND' }), true) }) -test('actual page count and queue filter both drop a record when its upload is complete', () => { +test('upload status changes refresh server counts while initial detail reads do not reload the list', async () => { const page = readFileSync(new URL('../views/FirstLegOrdersView.vue', import.meta.url), 'utf8') - const block = page.slice(page.indexOf('const orderQueueCounts ='), page.indexOf('const pagedOrders =')) - const rows = ref([{ id: 'waiting', businessSummary: {} }, { id: 'completed', businessSummary: {} }]) - const states = ref({ waiting: { status: 'WAITING_CUSTOMER_DOCUMENTS', merchantStatus: 'WAITING_UPLOAD' }, completed: { status: 'COMPLETED', merchantStatus: 'CUSTOMS_COMPLETED' } }) - const queue = ref('customs') - const read = new Function('computed', 'queryMatchedOrders', 'activeOrderQueue', 'eligibleCustomsDetail', 'needsCustomsUpload', block + '\nreturn { orderQueueCounts, filteredOrders }') - const view = read(computed, rows, queue, order => states.value[order.id], exports.needsCustomsUpload) - assert.equal(view.orderQueueCounts.value.customs, 1) - assert.deepEqual(view.filteredOrders.value.map(row => row.id), ['waiting']) - states.value.waiting.merchantStatus = 'DOCUMENT_UPLOADED' - assert.equal(view.orderQueueCounts.value.customs, 0); assert.equal(view.filteredOrders.value.length, 0) - queue.value = 'all'; assert.equal(view.filteredOrders.value.length, 2) + const block = page.slice(page.indexOf('function updateCustomsDetail('), page.indexOf('const detailOrder =')) + const js = ts.transpileModule(block, { compilerOptions: { target: ts.ScriptTarget.ES2022 } }).outputText + const states = {}, id = ref('one'), create = ref(false), detail = ref(false) + let reloads = 0 + const update = new Function('customsByOrder', 'customsDialogOrderId', 'isCreatePage', 'isDetailPage', 'loadOrderPage', js + ';return updateCustomsDetail')(states, id, create, detail, async () => reloads++) + update({ merchantStatus: 'WAITING_UPLOAD' }); assert.equal(reloads, 0) + update({ merchantStatus: 'WAITING_UPLOAD' }); assert.equal(reloads, 0) + update({ merchantStatus: 'DOCUMENT_UPLOADED' }); assert.equal(reloads, 1) + assert.equal(states.one.merchantStatus, 'DOCUMENT_UPLOADED') + detail.value = true; update({ merchantStatus: 'WAITING_UPLOAD' }); assert.equal(reloads, 1) }) diff --git a/src/modules/first-leg/views/FirstLegOrdersView.test.mjs b/src/modules/first-leg/views/FirstLegOrdersView.test.mjs index 2e9173a..4b8c255 100644 --- a/src/modules/first-leg/views/FirstLegOrdersView.test.mjs +++ b/src/modules/first-leg/views/FirstLegOrdersView.test.mjs @@ -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, //) assert.match(source, /async function searchOrders\(model: QueryModel\)/) assert.match(source, /await store\.load\(\{/) - assert.match(source, / { 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`/) }) diff --git a/src/modules/first-leg/views/FirstLegOrdersView.vue b/src/modules/first-leg/views/FirstLegOrdersView.vue index 3e659ab..1f1e1cf 100644 --- a/src/modules/first-leg/views/FirstLegOrdersView.vue +++ b/src/modules/first-leg/views/FirstLegOrdersView.vue @@ -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({ ...filters.value }) const queryFields = computed(() => [ { 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() } +