优化优化

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
@@ -1,5 +1,138 @@
import { describe, expect, it } from 'vitest'
import ElementPlus from 'element-plus'
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
const source = readFileSync(resolve(process.cwd(), 'src/modules/outbound-dispatch/pages/OutboundDispatchesView.vue'), 'utf8')
describe('OutboundDispatchesView', () => { it('uses standard list components and left actions', () => { expect(source).toContain('QueryToolbar'); expect(source).toContain('DataTable'); expect(source).toContain("fixed: 'left'") }); it('waits for a business id before querying', () => { expect(source).not.toContain('onMounted(load)'); expect(source).toContain("String(filters.orderId || '').trim() || undefined"); expect(source).toContain('if (!params.orderId && !params.inboundId && !params.containerBookingId)'); expect(source).toContain('请输入订单 ID、入库单 ID 或柜次 ID 后查询'); expect(source).toContain('@reset="resetSearch"') }); it('shows gate, timeline and guarded dispatch actions', () => { expect(source).toContain('aggregate.gate?.items'); expect(source).toContain('detail.timeline'); expect(source).toContain("'confirm-dispatch'"); expect(source).toContain("'reverse-dispatch'"); expect(source).toContain("error.value?.code === 'A0409'") }) })
import { createPinia, setActivePinia } from 'pinia'
import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import QueryToolbar from '@/shared/components/QueryToolbar.vue'
import DataTable from '@/shared/components/DataTable.vue'
import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
import { listPlatformUnifiedOrders, type UnifiedOrderResource } from '@/modules/unified-orders'
import { listContainers, type ContainerBookingResource } from '@/modules/container-loading'
import { getOutboundDispatchDetail, listOutboundDispatches } from '../api/platformOutboundDispatch'
import type { OutboundDispatchResource } from '../types'
import OutboundDispatchesView from './OutboundDispatchesView.vue'
vi.mock('@/modules/iam', () => ({ listPlatformTenants: vi.fn() }))
vi.mock('@/modules/unified-orders', () => ({ listPlatformUnifiedOrders: vi.fn() }))
vi.mock('@/modules/container-loading', () => ({ listContainers: vi.fn() }))
vi.mock('../api/platformOutboundDispatch', () => ({ listOutboundDispatches: vi.fn(), getOutboundDispatchDetail: vi.fn(), changeOutboundDispatch: vi.fn() }))
const customer = { id: '10001', tenantCode: 'CUS-1', tenantName: '示例客户', legalName: '示例客户有限公司', status: 'ACTIVE' } as PlatformTenantResource
const order = { orderId: '20001', orderNo: 'ORD-001', inboundId: '30001', inboundCode: 'IN-001', customerId: '10001', customerName: '示例客户' } as UnifiedOrderResource
const secondOrder = { ...order, orderId: '20002', orderNo: 'ORD-002', inboundId: '30002', inboundCode: 'IN-002' } as UnifiedOrderResource
const container = { id: '40001', bookingNo: 'BK-001', cabinetNo: 'CAB-001', containerNo: 'MSCU-001' } as ContainerBookingResource
const row = { orderId: '20001', orderNo: 'ORD-001', inboundId: '30001', inboundCode: 'IN-001', customerId: '10001', customerName: '示例客户', containerBookingId: '40001', containerNo: 'MSCU-001', dispatchStatus: 'WAITING', version: 1, gate: { passed: true, items: [], checkedAt: '2026-08-11T10:00:00' } } as OutboundDispatchResource
enableAutoUnmount(afterEach)
function deferred<T>() {
let resolve!: (value: T) => void
let reject!: (reason?: unknown) => void
const promise = new Promise<T>((resolvePromise, rejectPromise) => { resolve = resolvePromise; reject = rejectPromise })
return { promise, resolve, reject }
}
function mountPage() {
return mount(OutboundDispatchesView, { attachTo: document.body, global: { plugins: [ElementPlus, createPinia()] } })
}
async function setFilters(wrapper: ReturnType<typeof mountPage>, values: Record<string, string>) {
const toolbar = wrapper.findComponent(QueryToolbar)
toolbar.vm.$emit('update:modelValue', { ...toolbar.props('modelValue'), ...values })
await flushPromises()
return toolbar
}
describe('OutboundDispatchesView', () => {
beforeEach(() => {
vi.clearAllMocks()
setActivePinia(createPinia())
vi.mocked(listPlatformTenants).mockResolvedValue({ records: [customer], page: 1, size: 200, total: 1, pages: 1 })
vi.mocked(listPlatformUnifiedOrders).mockResolvedValue({ records: [order, secondOrder], page: 1, size: 200, total: 2, pages: 1 })
vi.mocked(listContainers).mockResolvedValue({ records: [container], page: 1, size: 200, total: 1, pages: 1 })
vi.mocked(listOutboundDispatches).mockResolvedValue([row])
})
it('starts idle and queries real ids selected through business labels', async () => {
const wrapper = mountPage()
await flushPromises()
expect(listOutboundDispatches).not.toHaveBeenCalled()
expect(wrapper.text()).toContain('请选择订单 / 入库单或柜次后查询')
const toolbar = await setFilters(wrapper, { orderId: '20001' })
toolbar.vm.$emit('search')
await flushPromises()
expect(listOutboundDispatches).toHaveBeenCalledOnce()
expect(listOutboundDispatches).toHaveBeenCalledWith({ orderId: '20001', inboundId: '30001', containerBookingId: undefined })
expect(wrapper.text()).toContain('ORD-001')
})
it('does not issue a search when reset returns to the initial state', async () => {
const wrapper = mountPage()
await flushPromises()
const toolbar = await setFilters(wrapper, { containerBookingId: '40001' })
toolbar.vm.$emit('search')
await flushPromises()
expect(listOutboundDispatches).toHaveBeenCalledOnce()
const resetButton = wrapper.findAll('button').find((button) => button.text().includes('重置'))
await resetButton?.trigger('click')
await flushPromises()
expect(listOutboundDispatches).toHaveBeenCalledOnce()
expect(wrapper.text()).toContain('请选择订单 / 入库单或柜次后查询')
})
it('shows loading, empty and recoverable error states', async () => {
const pending = deferred<OutboundDispatchResource[]>()
vi.mocked(listOutboundDispatches).mockReturnValueOnce(pending.promise)
const wrapper = mountPage()
await flushPromises()
const toolbar = await setFilters(wrapper, { containerBookingId: '40001' })
toolbar.vm.$emit('search')
await flushPromises()
expect(wrapper.findComponent(DataTable).props('loading')).toBe(true)
pending.resolve([])
await flushPromises()
expect(wrapper.text()).toContain('暂无符合条件的出仓记录')
vi.mocked(listOutboundDispatches).mockRejectedValueOnce(new Error('network down')).mockResolvedValueOnce([row])
toolbar.vm.$emit('search')
await flushPromises()
expect(wrapper.text()).toContain('国内出仓加载失败')
const retry = wrapper.findAll('button').find((button) => button.text().includes('重新加载'))
await retry?.trigger('click')
await flushPromises()
expect(listOutboundDispatches).toHaveBeenCalledTimes(3)
expect(wrapper.text()).toContain('ORD-001')
})
it('ignores stale list responses when a newer query wins', async () => {
const first = deferred<OutboundDispatchResource[]>()
const second = deferred<OutboundDispatchResource[]>()
vi.mocked(listOutboundDispatches).mockReturnValueOnce(first.promise).mockReturnValueOnce(second.promise)
const wrapper = mountPage()
await flushPromises()
let toolbar = await setFilters(wrapper, { orderId: '20001' })
toolbar.vm.$emit('search')
toolbar = await setFilters(wrapper, { orderId: '20002' })
toolbar.vm.$emit('search')
second.resolve([{ ...row, orderId: '20002', orderNo: 'ORD-002', inboundId: '30002', inboundCode: 'IN-002' }])
await flushPromises()
first.resolve([row])
await flushPromises()
expect(wrapper.text()).toContain('ORD-002')
expect(wrapper.text()).not.toContain('ORD-001')
})
it('keeps the operation column first and guarded workflow actions', () => {
const source = readFileSync(resolve(process.cwd(), 'src/modules/outbound-dispatch/pages/OutboundDispatchesView.vue'), 'utf8')
const columnSource = source.slice(source.indexOf('const columns'))
expect(columnSource.indexOf("key: 'actions'")).toBeLessThan(columnSource.indexOf("prop: 'orderNo'"))
expect(columnSource).toContain("fixed: 'left'")
expect(source).toContain('aggregate.gate?.items')
expect(source).toContain('detail.timeline')
expect(source).toContain("'confirm-dispatch'")
expect(source).toContain("'reverse-dispatch'")
expect(source).toContain("error.value?.code === 'A0409'")
expect(getOutboundDispatchDetail).not.toHaveBeenCalled()
})
})