diff --git a/src/modules/first-leg/api/unifiedOrders.ts b/src/modules/first-leg/api/unifiedOrders.ts index f3a2042..3f24b87 100644 --- a/src/modules/first-leg/api/unifiedOrders.ts +++ b/src/modules/first-leg/api/unifiedOrders.ts @@ -129,10 +129,11 @@ export interface MerchantUnifiedOrderQuery { const unifiedOrderBaseUrl = '/merchant/v1/orders' export function getMerchantUnifiedOrders(params: MerchantUnifiedOrderQuery = {}) { + const { page, size, ...filters } = params return apiRequest>({ method: 'GET', url: unifiedOrderBaseUrl, - params: { page: 1, size: 200, ...params }, + params: { page: page ?? 1, size: size ?? 200, ...filters }, }) } diff --git a/src/modules/first-leg/components/FirstLegOrderEditorShell.vue b/src/modules/first-leg/components/FirstLegOrderEditorShell.vue index 73d4f95..60036da 100644 --- a/src/modules/first-leg/components/FirstLegOrderEditorShell.vue +++ b/src/modules/first-leg/components/FirstLegOrderEditorShell.vue @@ -44,7 +44,10 @@ const emit = defineEmits<{ .editor-page-nav { display: flex; min-height: 52px; align-items: center; justify-content: space-between; gap: 16px; padding: 8px 20px; border-bottom: 1px solid var(--border); } .editor-page-nav span { color: var(--muted); font-size: 12px; } .editor-page-body { padding: 22px 24px 30px; } -.editor-page-footer { position: sticky; z-index: 3; bottom: 0; display: flex; justify-content: flex-end; padding: 12px 24px; border-top: 1px solid var(--border); background: rgba(255, 255, 255, 0.96); } +.editor-page-footer { position: sticky; z-index: 3; bottom: 0; display: flex; justify-content: center; padding: 12px 24px; border-top: 1px solid var(--border); background: rgba(255, 255, 255, 0.96); box-shadow: 0 -6px 18px rgba(15, 31, 58, .06); backdrop-filter: blur(10px); } +.editor-page-footer :deep(.editor-actions) { width: min(100%, 560px); justify-content: center; } +.editor-page-footer :deep(.editor-actions > .el-button) { min-width: 132px; min-height: 42px; } +.editor-page-footer :deep(.editor-actions > .el-button:last-child) { min-width: 172px; } @media (max-width: 720px) { .editor-page-nav { align-items: flex-start; flex-direction: column; gap: 2px; padding: 10px 16px; } diff --git a/src/modules/first-leg/store/firstLegOrders.test.mjs b/src/modules/first-leg/store/firstLegOrders.test.mjs index 3fc77e5..571b207 100644 --- a/src/modules/first-leg/store/firstLegOrders.test.mjs +++ b/src/modules/first-leg/store/firstLegOrders.test.mjs @@ -48,6 +48,11 @@ test('declares the final C02 row and detail contract in the API adapter', () => assert.match(unifiedApiSource, /error\.status === 404 \|\| error\.code === 'A0404'/) }) +test('keeps unified order pagination valid when the store omits page and size', () => { + assert.match(unifiedApiSource, /const \{ page, size, \.\.\.filters \} = params/) + 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, /getById\(id\)\?\.status === 'DRAFT' \? loadLegacyDetail\(id\) : loadUnifiedDetail\(id\)/) @@ -85,8 +90,7 @@ test('sends backend-required channel, customs, destination and cargo fields', () 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, /getMerchantWarehouses\(\{ warehouseType: warehouseType as MerchantWarehouseType, page, size: 200 \}\)/) - assert.match(storeSource, /while \(page <= pages\)/) + assert.match(storeSource, /loadAllWarehouseRecords\(warehouseType, getMerchantWarehouses\)/) assert.match(storeSource, /loadWarehouseType\('DOMESTIC', true\)/) assert.match(storeSource, /warehouse\.warehouseType !== 'DOMESTIC'/) assert.match(storeSource, /item\.warehouseType === draft\.destinationType/) diff --git a/src/modules/first-leg/store/firstLegOrders.ts b/src/modules/first-leg/store/firstLegOrders.ts index 3a85fdd..ef62edc 100644 --- a/src/modules/first-leg/store/firstLegOrders.ts +++ b/src/modules/first-leg/store/firstLegOrders.ts @@ -14,7 +14,6 @@ import { type MerchantFirstLegOrder, type MerchantFirstLegOrderDetail, type MerchantWarehouse, - type MerchantWarehouseType, } from '@/modules/first-leg/api/firstLeg' import { getMerchantUnifiedOrder, @@ -29,6 +28,7 @@ import type { PlatformReceivingWarehouse, PlatformReceivingWarehouseSnapshot } f import type { FirstLegPartySnapshot } from '@/modules/first-leg/data/firstLegParties' import type { FirstLegOrder, FirstLegOrderDraft, FirstLegOrderTimelineItem } from '@/modules/first-leg/types/firstLegOrder' import { canClientEditFirstLegOrder, getFirstLegStatusLabel } from '@/modules/first-leg/utils/firstLegOrder' +import { loadAllWarehouseRecords } from '@/modules/first-leg/utils/warehousePagination' export type ClientWarehouseType = 'DOMESTIC' | 'FBA' | 'OVERSEAS' export type ClientWarehouse = PlatformReceivingWarehouse & { @@ -395,21 +395,13 @@ export const useFirstLegOrdersStore = defineStore('client-first-leg-orders', () function getById(id: string) { return orders.value.find((item) => item.id === id) } async function loadWarehouseType(warehouseType: ClientWarehouseType, force = false) { + if (warehouseType === 'DOMESTIC' && force) warehouseLoadError.value = '' if (!force && loadedWarehouseTypes.value.includes(warehouseType)) return const pending = warehouseTypeRequests.get(warehouseType) if (pending) return pending const request = (async () => { - let page = 1 - const records: MerchantWarehouse[] = [] - let pages = 1 - do { - const response = await getMerchantWarehouses({ warehouseType: warehouseType as MerchantWarehouseType, page, size: 200 }) - records.push(...response.records) - pages = Number(response.pages) || page - page += 1 - } while (page <= pages) - + const records = await loadAllWarehouseRecords(warehouseType, getMerchantWarehouses) return records .filter((item) => item.status === 'ACTIVE' && item.warehouseType === warehouseType) .map(mapWarehouse) diff --git a/src/modules/first-leg/utils/warehousePagination.test.mjs b/src/modules/first-leg/utils/warehousePagination.test.mjs new file mode 100644 index 0000000..5be9cc4 --- /dev/null +++ b/src/modules/first-leg/utils/warehousePagination.test.mjs @@ -0,0 +1,29 @@ +import assert from 'node:assert/strict' +import test from 'node:test' +import { loadAllWarehouseRecords } from './warehousePagination.ts' + +test('loads every page for one warehouse type and preserves the type query', async () => { + const queries = [] + const records = await loadAllWarehouseRecords('DOMESTIC', async (query) => { + queries.push(query) + if (query.page === 1) return { records: [{ id: 'domestic-1' }], pages: '2' } + return { records: [{ id: 'domestic-2' }], pages: '2' } + }) + + assert.deepEqual(queries, [ + { warehouseType: 'DOMESTIC', page: 1, size: 200 }, + { warehouseType: 'DOMESTIC', page: 2, size: 200 }, + ]) + assert.deepEqual(records, [{ id: 'domestic-1' }, { id: 'domestic-2' }]) +}) + +test('stops after the first page when the warehouse type has no later pages', async () => { + let requestCount = 0 + const records = await loadAllWarehouseRecords('FBA', async () => { + requestCount += 1 + return { records: [], pages: 0 } + }) + + assert.equal(requestCount, 1) + assert.deepEqual(records, []) +}) diff --git a/src/modules/first-leg/utils/warehousePagination.ts b/src/modules/first-leg/utils/warehousePagination.ts new file mode 100644 index 0000000..8b56254 --- /dev/null +++ b/src/modules/first-leg/utils/warehousePagination.ts @@ -0,0 +1,28 @@ +export type WarehouseType = 'DOMESTIC' | 'FBA' | 'OVERSEAS' + +export interface WarehousePage { + records: T[] + pages: number | string +} + +export interface WarehousePageQuery { + warehouseType: WarehouseType + page: number + size: number +} + +export async function loadAllWarehouseRecords( + warehouseType: WarehouseType, + fetchPage: (query: WarehousePageQuery) => Promise>, +) { + const records: T[] = [] + let page = 1 + let pages = 1 + do { + const response = await fetchPage({ warehouseType, page, size: 200 }) + records.push(...response.records) + pages = Number(response.pages) || page + page += 1 + } while (page <= pages) + return records +} diff --git a/src/modules/first-leg/views/FirstLegOrdersView.test.mjs b/src/modules/first-leg/views/FirstLegOrdersView.test.mjs index 32358c0..5f594ea 100644 --- a/src/modules/first-leg/views/FirstLegOrdersView.test.mjs +++ b/src/modules/first-leg/views/FirstLegOrdersView.test.mjs @@ -29,7 +29,8 @@ test('exposes real list, draft, submit, and detail workflow entries without canc assert.match(source, /function submitOrder\(\)/) assert.match(source, /async function openDetail\(order: FirstLegOrder\)/) assert.match(source, /Promise\.allSettled\(\[/) - assert.match(source, /Promise\.allSettled\(\[\s*store\.load\(\),\s*companyRequest/) + assert.match(source, /const orderPageRequest = isCreatePage\.value\s*\? store\.loadWarehouseType\('DOMESTIC', true\)\s*:\s*store\.load\(\)/) + assert.match(source, /Promise\.allSettled\(\[\s*orderPageRequest,\s*companyRequest/) assert.match(source, /tenantStore\.loadCompanyContext\(\)/) assert.match(source, /await store\.loadDetail\(order\.id\)/) assert.doesNotMatch(source, /cancelOrder|取消订单/) @@ -127,6 +128,11 @@ test('keeps desktop query actions on the first row and only shows the qualificat assert.doesNotMatch(source, /order-eligibility\.is-approved/) }) +test('does not let order-list loading block the create form', () => { + assert.match(source, /v-loading="isCreatePage && supportingDataLoading"/) + assert.match(source, /const pageDataLoading = computed\(\(\) => supportingDataLoading\.value \|\| \(!isCreatePage\.value && store\.loading\)\)/) +}) + test('does not expose arbitrary status mutation controls to customers', () => { assert.doesNotMatch(source, /setStatusForPreview/) assert.doesNotMatch(source, /状态更新|更新状态/) @@ -160,6 +166,12 @@ test('selects platform destinations by type and caches private addresses per use assert.match(source, /私人地址不能引用平台公共仓库|form\.destinationWarehouseId = ''/) }) +test('shows a recoverable destination warehouse error when list-to-create loading fails', () => { + assert.match(source, /await router\.push\(\{ name: 'first-leg-order-create' \}\)[\s\S]*?try \{[\s\S]*?await loadDestinationWarehouses\(form\.destinationType\)/) + assert.match(source, /supportingDataErrors\.value\.push\(message\)/) + assert.match(source, /目的仓数据加载失败,请点击页面提示重新加载/) +}) + test('auto-fills trusted warehouse and company data instead of asking customers to repeat it', () => { assert.match(source, /async function selectDestinationWarehouse\(id: string\)/) assert.match(source, /await store\.loadWarehouseDetail\(warehouse\.id\)/) diff --git a/src/modules/first-leg/views/FirstLegOrdersView.vue b/src/modules/first-leg/views/FirstLegOrdersView.vue index ccc571c..8cc518c 100644 --- a/src/modules/first-leg/views/FirstLegOrdersView.vue +++ b/src/modules/first-leg/views/FirstLegOrdersView.vue @@ -82,6 +82,7 @@ const inboundBillingError = ref('') const supportingDataLoading = ref(false) const supportingDataErrors = ref([]) const destinationWarehousesLoading = ref(false) +const pageDataLoading = computed(() => supportingDataLoading.value || (!isCreatePage.value && store.loading)) const editorBaseline = ref('') let cargoSequence = 0 let inboundBillingRequestSequence = 0 @@ -296,8 +297,11 @@ async function loadPageData() { const destinationRequest = isCreatePage.value && form.destinationType !== 'PRIVATE' ? loadDestinationWarehouses(form.destinationType) : Promise.resolve() + const orderPageRequest = isCreatePage.value + ? store.loadWarehouseType('DOMESTIC', true) + : store.load() const [ordersResult, companyResult, destinationResult] = await Promise.allSettled([ - store.load(), + orderPageRequest, companyRequest, destinationRequest, ]) @@ -306,7 +310,7 @@ async function loadPageData() { } else if (companyResult.status === 'rejected') { supportingDataErrors.value.push(`企业资料加载失败:${errorMessage(companyResult.reason)}`) } - if (ordersResult.status === 'rejected' && !store.loadError) supportingDataErrors.value.push(`订单加载失败:${errorMessage(ordersResult.reason)}`) + if (!isCreatePage.value && ordersResult.status === 'rejected' && !store.loadError) supportingDataErrors.value.push(`订单加载失败:${errorMessage(ordersResult.reason)}`) if (store.warehouseLoadError) supportingDataErrors.value.push(`公共仓库加载失败:${store.warehouseLoadError}`) if (store.draftLoadError) supportingDataErrors.value.push(`订单草稿加载失败:${store.draftLoadError}`) if (destinationResult.status === 'rejected') supportingDataErrors.value.push(`目的仓加载失败:${errorMessage(destinationResult.reason)}`) @@ -554,8 +558,14 @@ async function openCreate() { replaceForm(emptyDraft()) resetEditorValidation() await router.push({ name: 'first-leg-order-create' }) - await loadDestinationWarehouses(form.destinationType) - await autofillWarehouseSelections() + try { + await loadDestinationWarehouses(form.destinationType) + await autofillWarehouseSelections() + } catch (error) { + const message = `目的仓加载失败:${errorMessage(error)}` + if (!supportingDataErrors.value.includes(message)) supportingDataErrors.value.push(message) + ElMessage.error('目的仓数据加载失败,请点击页面提示重新加载') + } } async function openEdit(order: FirstLegOrder) { @@ -877,12 +887,12 @@ async function searchOrders(model: QueryModel) { - +

发货信息

先选择货物如何送到国内收货仓,并填写本次运输安排。