优化优化

This commit is contained in:
2026-08-11 18:06:35 +08:00
parent 8a8c5a3faa
commit 03e1cd0c80
40 changed files with 2092 additions and 354 deletions
@@ -0,0 +1,118 @@
import { flushPromises, shallowMount } from '@vue/test-utils'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { listPlatformUnifiedOrders } from '@/modules/unified-orders/api/unifiedOrders'
import UnifiedOrdersView from './UnifiedOrdersView.vue'
vi.mock('vue-router', () => ({
useRoute: () => ({ params: {}, query: {} }),
useRouter: () => ({ push: vi.fn() }),
}))
vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: () => true }) }))
vi.mock('@/modules/iam', () => ({ listPlatformTenants: vi.fn().mockResolvedValue({ records: [], page: 1, size: 200, total: 0, pages: 0 }) }))
vi.mock('@/modules/warehouses', () => ({ listAllPlatformWarehouses: vi.fn().mockResolvedValue([]) }))
vi.mock('@/modules/inbounds', () => ({ downloadWeightImage: vi.fn(), getInboundMeasurement: vi.fn() }))
vi.mock('@/modules/unified-orders/workflows', () => ({ UnifiedOrderWorkflowPanel: { template: '<div />' } }))
vi.mock('@/modules/unified-orders/api/unifiedOrders', () => ({
addPlatformUnifiedOrderNote: vi.fn(),
approvePlatformUnifiedOrder: vi.fn(),
getPlatformUnifiedOrder: vi.fn(),
listPlatformUnifiedOrderFeeDetails: vi.fn(),
listPlatformUnifiedOrders: vi.fn(),
unifiedOrderStatusOptions: [
{ value: 'PENDING_REVIEW', label: '待审核' },
{ value: 'WAIT_MEASURE', label: '待测量数据' },
{ value: 'INBOUNDED', label: '已入库' },
{ value: 'WAIT_SPLIT', label: '待分柜' },
{ value: 'LOADING', label: '装柜中' },
{ value: 'LOADED', label: '装柜完成' },
{ value: 'DELIVERED', label: '派送完成' },
],
}))
function emptyPage() {
return { records: [], page: 1, size: 20, total: 0, pages: 0 }
}
function mountView() {
return shallowMount(UnifiedOrdersView, {
global: {
directives: { loading: () => undefined },
stubs: {
AdminPage: { template: '<main><slot /></main>' },
ListPageLayout: { template: '<section><slot name="toolbar" /><slot name="state" /><slot /></section>' },
QueryToolbar: { name: 'QueryToolbar', props: ['modelValue'], emits: ['update:modelValue', 'search', 'reset'], template: '<div data-testid="query-toolbar" />' },
DataTable: { name: 'DataTable', props: ['page', 'pageSize'], emits: ['update:page', 'update:pageSize'], template: '<div data-testid="data-table" />' },
'el-button': { template: '<button><slot /></button>' },
},
},
})
}
describe('UnifiedOrdersView list request coordination', () => {
beforeEach(() => {
vi.mocked(listPlatformUnifiedOrders).mockReset().mockResolvedValue(emptyPage())
})
it('loads once initially and once for query, reset, status tab and pagination', async () => {
const wrapper = mountView()
await flushPromises()
expect(listPlatformUnifiedOrders).toHaveBeenCalledTimes(1)
const toolbar = wrapper.getComponent({ name: 'QueryToolbar' })
toolbar.vm.$emit('search')
await flushPromises()
expect(listPlatformUnifiedOrders).toHaveBeenCalledTimes(2)
toolbar.vm.$emit('reset')
await flushPromises()
expect(listPlatformUnifiedOrders).toHaveBeenCalledTimes(3)
await wrapper.findAll('.status-tab')[1]!.trigger('click')
await flushPromises()
expect(listPlatformUnifiedOrders).toHaveBeenCalledTimes(4)
wrapper.getComponent({ name: 'DataTable' }).vm.$emit('update:page', 2)
await flushPromises()
expect(listPlatformUnifiedOrders).toHaveBeenCalledTimes(5)
})
it('does not apply draft filters to pagination until query is submitted', async () => {
const wrapper = mountView()
await flushPromises()
const toolbar = wrapper.getComponent({ name: 'QueryToolbar' })
const draft = { ...toolbar.props('modelValue'), inboundCode: ' DRAFT-CODE ' }
toolbar.vm.$emit('update:modelValue', draft)
await wrapper.vm.$nextTick()
wrapper.getComponent({ name: 'DataTable' }).vm.$emit('update:page', 2)
await flushPromises()
expect(listPlatformUnifiedOrders).toHaveBeenLastCalledWith(expect.objectContaining({ page: 2, inboundCode: undefined }))
toolbar.vm.$emit('search')
await flushPromises()
expect(listPlatformUnifiedOrders).toHaveBeenCalledTimes(3)
expect(listPlatformUnifiedOrders).toHaveBeenLastCalledWith(expect.objectContaining({ page: 1, inboundCode: 'DRAFT-CODE' }))
})
it('resets a later page and changes page size with one request per interaction', async () => {
const wrapper = mountView()
await flushPromises()
const table = wrapper.getComponent({ name: 'DataTable' })
const toolbar = wrapper.getComponent({ name: 'QueryToolbar' })
table.vm.$emit('update:page', 3)
await flushPromises()
expect(listPlatformUnifiedOrders).toHaveBeenCalledTimes(2)
toolbar.vm.$emit('reset')
await flushPromises()
expect(listPlatformUnifiedOrders).toHaveBeenCalledTimes(3)
expect(listPlatformUnifiedOrders).toHaveBeenLastCalledWith(expect.objectContaining({ page: 1 }))
table.vm.$emit('update:pageSize', 50)
table.vm.$emit('update:page', 1)
await flushPromises()
expect(listPlatformUnifiedOrders).toHaveBeenCalledTimes(4)
expect(listPlatformUnifiedOrders).toHaveBeenLastCalledWith(expect.objectContaining({ page: 1, size: 50 }))
})
})
@@ -19,7 +19,7 @@ describe('UnifiedOrdersView contract', () => {
it('renders accessible current-status tabs with keyboard navigation', () => {
expect(source).toContain('role="tablist"')
expect(source).toContain('role="tab"')
expect(source).toContain(':aria-selected="filters.mainStatus === tab.value"')
expect(source).toContain(':aria-selected="selectedStatus === tab.value"')
expect(source).toContain("event.key === 'ArrowRight'")
expect(source).toContain("event.key === 'ArrowLeft'")
expect(source).toContain("event.key === 'Home'")
@@ -38,8 +38,8 @@ describe('UnifiedOrdersView contract', () => {
expect(source).toContain('listAllPlatformWarehouses')
expect(source).toContain("{ status: 'ACTIVE' }")
expect(source).toContain("warehouse.warehouseType === 'DOMESTIC'")
expect(source).toContain("loaded: optionalBoolean(filters.value.loaded)")
expect(source).toContain("fullContainer: optionalBoolean(filters.value.fullContainer)")
expect(source).toContain("loaded: optionalBoolean(activeFilters.loaded)")
expect(source).toContain("fullContainer: optionalBoolean(activeFilters.fullContainer)")
expect(source).toContain("{ value: 'UNRECORDED', label: '未录入' }")
expect(source).not.toContain("{ value: 'DRAFT', label: '草稿' }")
expect(source).not.toContain("{ value: 'CANCELLED', label: '已取消' }")
@@ -59,7 +59,7 @@ describe('UnifiedOrdersView contract', () => {
})
it('uses tabs as the only order-status filter and keeps advanced controls inside QueryToolbar', () => {
expect(source).toContain('filters.value.mainStatus || undefined')
expect(source).toContain('mainStatus: selectedStatus.value || undefined')
expect(source).toContain('<template #actions>')
expect(source).toContain(':aria-expanded="showAdvancedFilters"')
expect(source).not.toContain('class="filter-mode-actions"')
@@ -102,6 +102,14 @@ describe('UnifiedOrdersView contract', () => {
expect(source).toContain('v-if="moreUnifiedOrderActions(row as UnifiedOrderResource).length"')
})
it('keeps editable query values separate from the applied list request', () => {
expect(source).toContain('const appliedFilters = ref<FilterModel>(emptyFilters())')
expect(source).toContain('const activeFilters = appliedFilters.value')
expect(source).toContain('appliedFilters.value = cloneFilters(filters.value)')
expect(source).toContain('appliedFilters.value = emptyFilters()')
expect(source).not.toContain('filters.value.mainStatus')
})
it('keeps only confirmed business actions as explicit boundaries', () => {
expect(source).toContain('nextActionBoundary')
expect(source).not.toContain('class="action-boundary"')
@@ -39,7 +39,6 @@ import {
type FilterModel = QueryModel & {
tenantId: string
mainStatus: UnifiedOrderMainStatus | ''
receivingWarehouseId: string
destinationWarehouseId: string
inboundCode: string
@@ -96,6 +95,8 @@ let detailLoadingKey = ''
let detailRequestSerial = 0
const showAdvancedFilters = ref(false)
const filters = ref<FilterModel>(emptyFilters())
const appliedFilters = ref<FilterModel>(emptyFilters())
const selectedStatus = ref<UnifiedOrderMainStatus | ''>('')
const orderId = computed(() => typeof route.params.orderId === 'string' ? route.params.orderId : '')
const detailTenantId = computed(() => typeof route.query.tenantId === 'string' ? route.query.tenantId : '')
const detailRouteKey = computed(() => orderId.value ? `${detailTenantId.value}:${orderId.value}` : '')
@@ -240,13 +241,17 @@ const columns: DataTableColumn[] = [
function emptyFilters(): FilterModel {
return {
tenantId: '', mainStatus: '', receivingWarehouseId: '', destinationWarehouseId: '',
tenantId: '', receivingWarehouseId: '', destinationWarehouseId: '',
inboundCode: '', expectedArrivalDate: '', feeReviewStatus: '', channelName: '',
containerNo: '', cabinetNo: '', sailingDate: '', loaded: '', fullContainer: '', measurementReviewStatus: '',
fbaCode: '', createdRange: [],
}
}
function cloneFilters(source: FilterModel): FilterModel {
return { ...source, createdRange: [...source.createdRange] }
}
function trimmed(value: string) {
return value.trim() || undefined
}
@@ -256,8 +261,10 @@ function optionalBoolean(value: string) {
}
function selectStatus(status: UnifiedOrderMainStatus | '') {
filters.value = { ...filters.value, mainStatus: status }
search()
if (selectedStatus.value === status) return
selectedStatus.value = status
if (page.value === 1) void loadOrders()
else page.value = 1
}
function handleTabKeydown(event: KeyboardEvent, index: number) {
@@ -625,23 +632,24 @@ async function loadOrders() {
loading.value = true
loadError.value = null
try {
const range = filters.value.createdRange
const activeFilters = appliedFilters.value
const range = activeFilters.createdRange
const result = await listPlatformUnifiedOrders({
tenantId: filters.value.tenantId || undefined,
mainStatus: filters.value.mainStatus || undefined,
receivingWarehouseId: filters.value.receivingWarehouseId || undefined,
destinationWarehouseId: filters.value.destinationWarehouseId || undefined,
inboundCode: trimmed(filters.value.inboundCode),
expectedArrivalDate: filters.value.expectedArrivalDate || undefined,
feeReviewStatus: filters.value.feeReviewStatus || undefined,
channelName: trimmed(filters.value.channelName),
containerNo: trimmed(filters.value.containerNo),
cabinetNo: trimmed(filters.value.cabinetNo),
sailingDate: filters.value.sailingDate || undefined,
loaded: optionalBoolean(filters.value.loaded),
fullContainer: optionalBoolean(filters.value.fullContainer),
measurementReviewStatus: filters.value.measurementReviewStatus || undefined,
fbaCode: trimmed(filters.value.fbaCode),
tenantId: activeFilters.tenantId || undefined,
mainStatus: selectedStatus.value || undefined,
receivingWarehouseId: activeFilters.receivingWarehouseId || undefined,
destinationWarehouseId: activeFilters.destinationWarehouseId || undefined,
inboundCode: trimmed(activeFilters.inboundCode),
expectedArrivalDate: activeFilters.expectedArrivalDate || undefined,
feeReviewStatus: activeFilters.feeReviewStatus || undefined,
channelName: trimmed(activeFilters.channelName),
containerNo: trimmed(activeFilters.containerNo),
cabinetNo: trimmed(activeFilters.cabinetNo),
sailingDate: activeFilters.sailingDate || undefined,
loaded: optionalBoolean(activeFilters.loaded),
fullContainer: optionalBoolean(activeFilters.fullContainer),
measurementReviewStatus: activeFilters.measurementReviewStatus || undefined,
fbaCode: trimmed(activeFilters.fbaCode),
createdFrom: range.length === 2 ? `${range[0]}T00:00:00` : undefined,
createdTo: range.length === 2 ? `${range[1]}T23:59:59` : undefined,
page: page.value,
@@ -660,6 +668,7 @@ async function loadOrders() {
}
function search() {
appliedFilters.value = cloneFilters(filters.value)
if (page.value !== 1) {
page.value = 1
return
@@ -669,6 +678,7 @@ function search() {
function reset() {
filters.value = emptyFilters()
appliedFilters.value = emptyFilters()
if (page.value === 1) void loadOrders()
else page.value = 1
}
@@ -1026,10 +1036,10 @@ onMounted(() => {
:key="tab.value || 'ALL'"
type="button"
role="tab"
:aria-selected="filters.mainStatus === tab.value"
:aria-selected="selectedStatus === tab.value"
aria-controls="unified-orders-results"
:tabindex="filters.mainStatus === tab.value ? 0 : -1"
:class="['status-tab', { 'is-active': filters.mainStatus === tab.value }]"
:tabindex="selectedStatus === tab.value ? 0 : -1"
:class="['status-tab', { 'is-active': selectedStatus === tab.value }]"
@click="selectStatus(tab.value)"
@keydown="handleTabKeydown($event, index)"
>{{ tab.label }}</button>