This commit is contained in:
2026-08-06 15:32:07 +08:00
parent e33d620d87
commit 9aa79bef6d
8 changed files with 101 additions and 22 deletions
+2 -1
View File
@@ -129,10 +129,11 @@ export interface MerchantUnifiedOrderQuery {
const unifiedOrderBaseUrl = '/merchant/v1/orders'
export function getMerchantUnifiedOrders(params: MerchantUnifiedOrderQuery = {}) {
const { page, size, ...filters } = params
return apiRequest<PageResult<MerchantUnifiedOrderRow>>({
method: 'GET',
url: unifiedOrderBaseUrl,
params: { page: 1, size: 200, ...params },
params: { page: page ?? 1, size: size ?? 200, ...filters },
})
}
@@ -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; }
@@ -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/)
+3 -11
View File
@@ -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)
@@ -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, [])
})
@@ -0,0 +1,28 @@
export type WarehouseType = 'DOMESTIC' | 'FBA' | 'OVERSEAS'
export interface WarehousePage<T> {
records: T[]
pages: number | string
}
export interface WarehousePageQuery {
warehouseType: WarehouseType
page: number
size: number
}
export async function loadAllWarehouseRecords<T>(
warehouseType: WarehouseType,
fetchPage: (query: WarehousePageQuery) => Promise<WarehousePage<T>>,
) {
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
}
@@ -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\)/)
@@ -82,6 +82,7 @@ const inboundBillingError = ref('')
const supportingDataLoading = ref(false)
const supportingDataErrors = ref<string[]>([])
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) {
<div v-if="isCreatePage && supportingDataErrors.length" class="supporting-data-error" role="alert">
<AlertTriangle :size="20" />
<div><strong>下单配套数据加载失败</strong><span v-for="message in supportingDataErrors" :key="message">{{ message }}</span></div>
<el-button :loading="supportingDataLoading || store.loading" @click="loadPageData">重新加载</el-button>
<el-button :loading="pageDataLoading" @click="loadPageData">重新加载</el-button>
</div>
<el-alert v-if="isCreatePage && !supportingDataLoading && availableWarehouses.length === 0" title="当前没有可用的平台收货仓" description="请联系平台维护并启用收货仓后再创建订单。" type="warning" :closable="false" show-icon />
<FirstLegOrderEditorShell v-model="editorOpen" :page="isCreatePage" :title="editingId ? '编辑订单草稿' : '创建订单'" :before-close="requestCloseEditor" @back="closeEditor">
<el-form ref="orderFormRef" v-loading="isCreatePage && (supportingDataLoading || store.loading)" :model="form" :rules="orderFormRules" label-position="top" class="order-form prototype-order-form" scroll-to-error>
<el-form ref="orderFormRef" v-loading="isCreatePage && supportingDataLoading" :model="form" :rules="orderFormRules" label-position="top" class="order-form prototype-order-form" scroll-to-error>
<section class="order-form-section prototype-section">
<div class="order-section-heading"><div><h3>发货信息</h3><p>先选择货物如何送到国内收货仓,并填写本次运输安排。</p></div></div>
<div class="prototype-form-flow shipping-form-flow">