style(admin): unify dialog layouts and dense readonly presentation

This commit is contained in:
2026-10-20 10:11:31 +08:00
parent 7ff7c9914f
commit 3267679758
214 changed files with 10931 additions and 2654 deletions
@@ -5,7 +5,7 @@ import SmartDispatchDialog from './SmartDispatchDialog.vue'
import AppDialog from '@/shared/components/AppDialog.vue'
import { listAllPlatformWarehouses } from '@/modules/warehouses'
import * as api from '../api/dispatches'
import { candidate, command, detail } from '../dispatchTestFixtures'
import { candidate, command, detail, sharedPalletCandidates } from '../dispatchTestFixtures'
import type { ConvertDraft, DispatchDraft, Version } from '../dispatchContracts'
import { useDispatchCommandRecovery } from '../dispatchCommandRecovery'
const identity = vi.hoisted(() => ({ id: 'smart-operator' }))
@@ -17,6 +17,29 @@ vi.mock('@/modules/warehouses', () => ({ listAllPlatformWarehouses: vi.fn() }))
vi.mock('./DispatchResourceFields.vue', () => ({ default: { template: '<div />' } }))
vi.mock('../api/dispatches', () => ({ listDispatchDrafts: vi.fn(), listDispatchCandidates: vi.fn(), createDispatchDraft: vi.fn(), createDispatchDraftContainer: vi.fn(), lockDispatchDraftPallet: vi.fn(), unlockDispatchDraftPallet: vi.fn(), deleteDispatchDraftContainer: vi.fn(), releaseDispatchDraft: vi.fn(), convertDispatchDraft: vi.fn(), createDispatch: vi.fn(), getDispatch: vi.fn(), getDispatchCommand: vi.fn() }))
const source = () => candidate(), draft = (): DispatchDraft => ({ draftToken: 'server-token', recordVersion: 0, status: 'ACTIVE', warehouseId: '321', containers: [{ draftContainerId: '100000000000000001', sequenceNo: 1, capacity: 30, sourceKeys: ['source-1'] }], sources: [source()], expiresAt: '2026-12-01T00:00:00Z' })
it('converts a filtered relabel group with both authoritative sources and locks it through one HTTP command', async () => {
const [first, second] = sharedPalletCandidates(), original = draft()
original.sources = [first!]; original.containers[0]!.sourceKeys = [first!.sourceKey]
vi.mocked(api.listDispatchCandidates).mockResolvedValue({ candidates: [first!], complete: true, snapshotAt: '' })
vi.mocked(api.listDispatchDrafts).mockResolvedValue([original]); vi.mocked(api.lockDispatchDraftPallet).mockResolvedValue(original)
vi.mocked(api.convertDispatchDraft).mockImplementation(async request => ({ draftToken: request.draftToken, replayed: false, vehicles: request.groups.map(group => ({ draftContainerId: group.draftContainerId, dispatch: command('CREATE', group.dispatch.idempotencyKey) })) }))
const wrapper = mount(SmartDispatchDialog, options); await flushPromises()
const state = wrapper.vm as unknown as { restoreDraft(token: string): void; draftAction(action: string, row: typeof first, container: string): Promise<void>; submit(): Promise<void> }
state.restoreDraft('server-token'); await flushPromises(); await state.draftAction('LOCK', first, original.containers[0]!.draftContainerId)
expect(api.lockDispatchDraftPallet).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ source: first!.selection }))
await state.submit(); expect(vi.mocked(api.convertDispatchDraft).mock.calls[0]![0].groups[0]!.dispatch.cargo).toEqual([first!.selection, second!.selection])
wrapper.unmount()
})
it('allows one atomic removal of a restored group that is no longer selectable', async () => {
const first = sharedPalletCandidates()[0]!, original = draft()
first.selectionGroup = { ...first.selectionGroup, complete: false, blockReasons: ['来源待核齐'] }
original.sources = [first]; original.containers[0]!.sourceKeys = [first.sourceKey]
vi.mocked(api.listDispatchDrafts).mockResolvedValue([original]); vi.mocked(api.unlockDispatchDraftPallet).mockResolvedValue(original)
const wrapper = mount(SmartDispatchDialog, options); await flushPromises()
const state = wrapper.vm as unknown as { restoreDraft(token: string): void; draftAction(action: string, row: typeof first, container: string): Promise<void> }
state.restoreDraft('server-token'); await flushPromises(); await state.draftAction('UNLOCK', first, original.containers[0]!.draftContainerId)
expect(api.unlockDispatchDraftPallet).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ source: first.selection })); wrapper.unmount()
})
const options = { props: { modelValue: true, orders: [] }, global: { plugins: [ElementPlus] } }
beforeEach(() => { vi.clearAllMocks(); sessionStorage.clear(); identity.id = crypto.randomUUID(); vi.stubGlobal('ResizeObserver', class { observe() {} unobserve() {} disconnect() {} }); vi.mocked(listAllPlatformWarehouses).mockResolvedValue([{ id: '321', warehouseName: '洛杉矶仓' }] as Awaited<ReturnType<typeof listAllPlatformWarehouses>>); vi.mocked(api.listDispatchDrafts).mockResolvedValue([draft()]); vi.mocked(api.listDispatchCandidates).mockResolvedValue({ candidates: [source()], complete: true, snapshotAt: '2026-10-19T00:00:00Z' }); vi.mocked(api.getDispatch).mockResolvedValue(detail({ recordVersion: 1 })) })
it.each([0, '0', '9007199254740993'])('transmits original authoritative batch version %s in complete draft conversion and exact retries', async recordVersion => {
@@ -55,12 +78,12 @@ it('requires an explicit warehouse and allows saving a true empty dispatch with
expect(wrapper.emitted('created')).toHaveLength(1); wrapper.unmount()
})
it('restores an explicit draft and submits complete server source versions in each container', async () => {
const original = draft(); original.sources.push(candidate({ sourceKey: 'source-B', customerId: 'B', customerName: '客户乙' })); original.containers[0]!.sourceKeys.push('source-B')
const original = draft(); original.sources.push(candidate({ sourceKey: 'source-B', customerId: 'B', customerName: '客户乙', selection: { ...source().selection, planPalletId: '22', warehousePalletId: '32' } })); original.containers[0]!.sourceKeys.push('source-B')
vi.mocked(api.listDispatchDrafts).mockResolvedValue([original])
vi.mocked(api.convertDispatchDraft).mockImplementation(async request => ({ draftToken: request.draftToken, replayed: false, vehicles: request.groups.map(group => ({ draftContainerId: group.draftContainerId, dispatch: command('CREATE', group.dispatch.idempotencyKey, 'PC/1') })) }))
const wrapper = mount(SmartDispatchDialog, options); await flushPromises(); const state = wrapper.vm as unknown as { restoreDraft: (token: string) => void; submit: () => Promise<void> }
state.restoreDraft('server-token'); await flushPromises(); await state.submit(); await flushPromises()
expect(api.convertDispatchDraft).toHaveBeenCalledWith(expect.objectContaining({ recordVersion: 0, draftToken: 'server-token', groups: [expect.objectContaining({ draftContainerId: '100000000000000001', dispatch: expect.objectContaining({ cargo: [source().selection, source().selection] }) })] }))
expect(api.convertDispatchDraft).toHaveBeenCalledWith(expect.objectContaining({ recordVersion: 0, draftToken: 'server-token', groups: [expect.objectContaining({ draftContainerId: '100000000000000001', dispatch: expect.objectContaining({ cargo: [source().selection, original.sources[1]!.selection] }) })] }))
expect(wrapper.emitted('created')).toHaveLength(1); wrapper.unmount()
})
it('retains conversion payload and key after an unknown result instead of rebuilding from fresh versions', async () => {