Merge remote-tracking branch 'origin/release/test' into dev

# Conflicts:
#	src/modules/container-loading/pages/ContainersView.vue
This commit is contained in:
2026-09-09 15:42:42 +08:00
22 changed files with 677 additions and 123 deletions
@@ -232,6 +232,37 @@ describe('FinanceCustomerStatementsView', () => {
expect(vm.filteredRows).toHaveLength(0)
})
it('does not issue one detail request per statement for an ordinary query', async () => {
vi.mocked(listFinanceStatements).mockResolvedValue(Array.from({ length: 60 }, (_, index) => ({
...statement,
id: String(501 + index),
billNo: `REC-${501 + index}`,
})))
const { wrapper } = await mountPage()
vi.mocked(listFinanceStatements).mockClear()
vi.mocked(getReconciliationStatement).mockClear()
await (wrapper.vm as unknown as { load(model: Record<string, unknown>): Promise<void> }).load({ status: 'ISSUED' })
await flushPromises()
expect(listFinanceStatements).toHaveBeenCalledTimes(1)
expect(getReconciliationStatement).not.toHaveBeenCalled()
})
it('uses only the statement list request for an ordinary search', async () => {
const { wrapper } = await mountPage()
vi.mocked(listFinanceStatements).mockClear()
vi.mocked(getReconciliationStatement).mockClear()
await (wrapper.vm as unknown as { load(model: Record<string, unknown>): Promise<void> }).load({
status: 'ISSUED', billNo: '501', customerId: '', containerNo: '',
periodStartRange: [], periodEndRange: [], settled: '', lastPaymentRange: [], createdRange: [],
})
expect(listFinanceStatements).toHaveBeenCalledTimes(1)
expect(getReconciliationStatement).not.toHaveBeenCalled()
})
it('keeps statement status filter options aligned with the list status column', async () => {
const { wrapper } = await mountPage()
const vm = wrapper.vm as unknown as {
@@ -355,10 +386,10 @@ describe('FinanceCustomerStatementsView', () => {
expect(document.body.textContent).not.toContain('hash-81')
})
it('surfaces detail hydration failures instead of silently returning an empty filtered list', async () => {
it('surfaces container-filter detail hydration failures instead of silently returning an empty filtered list', async () => {
const { wrapper } = await mountPage()
vi.mocked(getReconciliationStatement).mockRejectedValueOnce(new Error('detail unavailable'))
await (wrapper.vm as unknown as { load(model: Record<string, unknown>): Promise<void> }).load({ status: 'ISSUED' })
await (wrapper.vm as unknown as { load(model: Record<string, unknown>): Promise<void> }).load({ status: 'ISSUED', containerNo: 'CONT-31' })
await flushPromises()
expect(document.body.textContent).toContain('筛选字段加载失败')
})
@@ -246,7 +246,10 @@ async function loadCustomers() {
}
async function load(model?: QueryModel) {
const hydrateDetails = model !== undefined
// The list response already contains every normal filter field. Only the
// container-number filter still depends on statement lines from the detail API.
// Avoid loading every statement detail for an ordinary search (N+1 requests).
const hydrateDetails = Boolean(String(model?.containerNo || '').trim())
if (model) {
filters.value = { ...model }
appliedFilters.value = { ...model }
@@ -647,7 +650,7 @@ watch(detailRouteId, (statementId) => {
</el-tabs>
</DetailPageLayout>
<ListPageLayout v-else aria-label="客户对账单">
<template #toolbar><QueryToolbar v-model="filters" class="surface-panel" ariaLabel="客户对账单筛选" :fields="fields" preference-key="finance-statements-query" :search-on-clear="false" @search="load" /></template>
<template #toolbar><QueryToolbar v-model="filters" class="surface-panel" ariaLabel="客户对账单筛选" :fields="fields" :loading="loading" preference-key="finance-statements-query" :search-on-clear="false" @search="load" /></template>
<template v-if="customerError || error" #state><LoadErrorState title="客户对账单加载失败" :message="(customerError || error)?.message || ''" :loading="loading" @retry="customerError ? loadCustomers() : load()"><template #details><ApiErrorMeta :details="(customerError || error)!" /></template></LoadErrorState></template>
<template v-if="!customerError && !error">
<section class="surface-panel table-panel">