优化
This commit is contained in:
@@ -45,10 +45,11 @@ beforeEach(() => {
|
||||
vi.mocked(listDrivers).mockResolvedValue({ records: [driverRecord], page: 1, size: 100, total: 1, pages: 1, summary: { total: 1, owned: 1, dispatched: 0, expiringSoon: 0, expired: 0 } })
|
||||
})
|
||||
async function render() { const wrapper = mount(SmartDispatchDialog, { attachTo: document.body, props: { modelValue: true, orders: [] }, global: { plugins: [ElementPlus] } }); await flushPromises(); return { wrapper, dialog: wrapper.findComponent(AppDialog) } }
|
||||
function view(dialog: VueWrapper) { return dialog.getComponent({ name: 'ElDialogContent' }) }
|
||||
function field(dialog: VueWrapper, label: string) { return dialog.findAllComponents({ name: 'ElFormItem' }).find(item => item.props('label') === label)! }
|
||||
async function selectWarehouse(dialog: VueWrapper) { const select = field(dialog, '起运仓').getComponent({ name: 'ElSelect' }); select.vm.$emit('update:modelValue', '321'); select.vm.$emit('change', '321'); await flushPromises() }
|
||||
async function emptyMode(dialog: VueWrapper) { field(dialog, '建单方式').getComponent({ name: 'ElRadioGroup' }).vm.$emit('update:modelValue', 'empty'); await flushPromises() }
|
||||
async function restore(dialog: VueWrapper) { dialog.findAllComponents({ name: 'ElSelect' }).find(select => select.props('placeholder') === '显式恢复已有草稿')!.vm.$emit('change', 'server-token'); await flushPromises() }
|
||||
async function restore(dialog: VueWrapper) { dialog.findAllComponents({ name: 'ElSelect' }).find(select => select.props('placeholder') === '继续已有草稿')!.vm.$emit('change', 'server-token'); await flushPromises() }
|
||||
async function externalResources(dialog: VueWrapper, plate: string, name: string) {
|
||||
for (const label of ['车辆来源', '司机来源']) { const select = field(dialog, label).getComponent({ name: 'ElSelect' }); select.vm.$emit('update:modelValue', 'EXTERNAL'); select.vm.$emit('change', 'EXTERNAL'); await flushPromises() }
|
||||
await field(dialog, '车牌号').get('input').setValue(plate)
|
||||
@@ -62,11 +63,57 @@ async function externalResources(dialog: VueWrapper, plate: string, name: string
|
||||
}
|
||||
|
||||
describe('new dispatch native layout', () => {
|
||||
it('starts cargo with one explicit action, creates the first group and enables submission only after cargo is assigned', async () => {
|
||||
const empty: DispatchDraft = { ...draft(), containers: [], sources: [] }
|
||||
const grouped: DispatchDraft = { ...empty, recordVersion: 1, containers: [{ draftContainerId: 'first-group', sequenceNo: 1, capacity: 30, sourceKeys: [] }] }
|
||||
const planned = candidate({ selection: { ...candidate().selection, warehousePalletId: null } })
|
||||
vi.mocked(api.createDispatchDraft).mockResolvedValue(empty)
|
||||
vi.mocked(api.createDispatchDraftContainer).mockResolvedValue(grouped)
|
||||
vi.mocked(api.listDispatchCandidates).mockResolvedValue({ candidates: [planned], complete: true, snapshotAt: '' })
|
||||
vi.mocked(api.lockDispatchDraftPallet).mockResolvedValue({ ...grouped, recordVersion: 2, sources: [planned], containers: [{ ...grouped.containers[0]!, sourceKeys: [planned.sourceKey] }] })
|
||||
const { dialog } = await render()
|
||||
const start = () => view(dialog).findAll('button').find(button => button.text() === '开始选货')!
|
||||
expect(start().attributes('disabled')).toBeDefined()
|
||||
expect(api.createDispatchDraft).not.toHaveBeenCalled()
|
||||
await selectWarehouse(dialog)
|
||||
expect(view(dialog).text()).toContain('在途计划')
|
||||
const candidateCalls = vi.mocked(api.listDispatchCandidates).mock.calls.length
|
||||
await start().trigger('click'); await flushPromises()
|
||||
expect(api.createDispatchDraft).toHaveBeenCalledExactlyOnceWith({ warehouseId: '321', idempotencyKey: expect.any(String) })
|
||||
expect(api.createDispatchDraftContainer).toHaveBeenCalledExactlyOnceWith({ draftToken: 'server-token', recordVersion: 0, capacity: 30, idempotencyKey: expect.any(String) })
|
||||
expect(api.listDispatchCandidates).toHaveBeenCalledTimes(candidateCalls)
|
||||
expect(dialog.findComponent(DispatchResourceFields).exists()).toBe(true)
|
||||
expect(dialog.props('confirmDisabled')).toBe(true)
|
||||
await view(dialog).findAll('button').find(button => button.text() === '加入本组')!.trigger('click'); await flushPromises()
|
||||
expect(api.lockDispatchDraftPallet).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ draftContainerId: 'first-group', source: planned.selection, recordVersion: 1 }))
|
||||
expect(dialog.props('confirmDisabled')).toBe(false)
|
||||
expect(view(dialog).text()).toContain('1 组有货')
|
||||
expect(view(dialog).findAll('button').find(button => button.text() === '已加入第1组')!.attributes('disabled')).toBeDefined()
|
||||
})
|
||||
it('retains the exact first-group command after an unknown result without creating a second draft', async () => {
|
||||
const empty: DispatchDraft = { ...draft(), containers: [], sources: [] }
|
||||
vi.mocked(api.createDispatchDraft).mockResolvedValue(empty)
|
||||
vi.mocked(api.createDispatchDraftContainer).mockRejectedValueOnce(new Error('network unknown')).mockResolvedValueOnce({ ...empty, recordVersion: 1, containers: [{ draftContainerId: 'first-group', sequenceNo: 1, capacity: 30, sourceKeys: [] }] })
|
||||
const { dialog } = await render(); await selectWarehouse(dialog)
|
||||
await view(dialog).findAll('button').find(button => button.text() === '开始选货')!.trigger('click'); await flushPromises()
|
||||
const original = vi.mocked(api.createDispatchDraftContainer).mock.calls[0]![0]
|
||||
expect(dialog.props('confirmDisabled')).toBe(false)
|
||||
dialog.vm.$emit('confirm'); await flushPromises()
|
||||
expect(api.createDispatchDraft).toHaveBeenCalledTimes(1)
|
||||
expect(api.createDispatchDraftContainer).toHaveBeenNthCalledWith(2, original)
|
||||
expect(dialog.findComponent(DispatchResourceFields).exists()).toBe(true)
|
||||
expect(api.convertDispatchDraft).not.toHaveBeenCalled()
|
||||
})
|
||||
it('requires an active cargo group before accepting per-group settings while keeping setup controls available', async () => {
|
||||
const { dialog } = await render()
|
||||
expect(dialog.text()).toContain('请先选择装车组')
|
||||
expect(dialog.getComponent(DispatchResourceFields).props('disabled')).toBe(true)
|
||||
expect(field(dialog, '备注').get('input').attributes('disabled')).toBeDefined()
|
||||
expect(view(dialog).find('[aria-label="排车设置"] table').exists()).toBe(false)
|
||||
expect(view(dialog).find('[aria-label="排车设置"] .density-dialog-note').exists()).toBe(false)
|
||||
expect(view(dialog).classes()).toContain('admin-density-workspace')
|
||||
expect(dialog.getComponent({ name: 'ElDialog' }).props()).toMatchObject({ appendToBody: true, lockScroll: true })
|
||||
expect(view(dialog).text()).not.toContain('点击“开始选货”建立第一组')
|
||||
expect(view(dialog).text()).not.toContain('选择起运仓后点击')
|
||||
expect(dialog.findComponent(DispatchResourceFields).exists()).toBe(false)
|
||||
expect(dialog.props('confirmDisabled')).toBe(true)
|
||||
expect(field(dialog, '建单方式').getComponent({ name: 'ElRadioGroup' }).props('disabled')).toBe(false)
|
||||
await restore(dialog)
|
||||
expect(dialog.getComponent(DispatchResourceFields).props('disabled')).toBe(false)
|
||||
@@ -81,13 +128,13 @@ describe('new dispatch native layout', () => {
|
||||
field(dialog, '预计到达时间').getComponent({ name: 'ElDatePicker' }).vm.$emit('update:modelValue', '2026-11-02T18:00:00Z')
|
||||
await field(dialog, '备注').get('input').setValue('本趟备注')
|
||||
expect(state.vehicle.plate, 'parent vehicle after dates and remark').toBe('CA-EXTERNAL')
|
||||
await dialog.get('button[aria-label="展开全屏"]').trigger('click'); await flushPromises()
|
||||
await view(dialog).get('button[aria-label="展开全屏"]').trigger('click'); await flushPromises()
|
||||
expect(state.vehicle.plate, 'parent vehicle after fullscreen').toBe('CA-EXTERNAL')
|
||||
expect(dialog.getComponent(DispatchResourceFields).props('continuous')).toBe(true)
|
||||
expect(dialog.findComponent({ name: 'ElDialog' }).props('fullscreen')).toBe(true)
|
||||
expect(dialog.text()).toContain('空单暂不能核平、装车或发车')
|
||||
expect(dialog.find('[aria-label="排车候选货物"]').exists()).toBe(false)
|
||||
await dialog.findAll('button').find(button => button.text() === '保存空排车单')!.trigger('click'); await flushPromises()
|
||||
expect(view(dialog).text()).toContain('空单暂不能核平、装车或发车')
|
||||
expect(view(dialog).find('[aria-label="排车候选货物"]').exists()).toBe(false)
|
||||
await view(dialog).findAll('button').find(button => button.text() === '保存空排车单')!.trigger('click'); await flushPromises()
|
||||
expect(api.createDispatch).toHaveBeenCalledExactlyOnceWith({ idempotencyKey: expect.any(String), warehouseId: '321', cargo: [], batchAllocations: [], plannedDepartureAt: '2026-11-02T10:00:00Z', estimatedArrivalAt: '2026-11-02T18:00:00Z', remark: '本趟备注', vehicle: { mode: 'EXTERNAL', vehicleId: null, plate: 'CA-EXTERNAL', carrierName: '本趟承运公司', capacity: { palletSpaces: 17, maxLoadKg: 15000, maxVolumeCbm: 62 }, evidenceFileIds: [] }, driver: { mode: 'EXTERNAL', driverId: null, name: '临时司机', phone: '555-0102', evidence: { licenseNo: 'EXTERNAL-LICENSE', licenseExpiresOn: '2030-01-01', fileIds: [], expectedTaskEndAt: '2026-11-03T18:00:00Z' } } })
|
||||
})
|
||||
it('queries real master directories and keeps business labels and task-end evidence in the command', async () => {
|
||||
@@ -101,7 +148,7 @@ describe('new dispatch native layout', () => {
|
||||
driverSelect.vm.$emit('update:modelValue', driverRecord.id); driverSelect.vm.$emit('change', driverRecord.id); await flushPromises()
|
||||
expect(listVehicles).toHaveBeenCalledWith({ page: 1, size: 100, keyword: '', region: 'OVERSEAS', status: 'IN_SERVICE' })
|
||||
expect(listDrivers).toHaveBeenCalledWith({ page: 1, size: 100, keyword: '', region: 'OVERSEAS', active: true, reviewStatus: 'APPROVED', expectedEnd: '2026-11-03T18:00:00Z' })
|
||||
expect(dialog.text()).not.toContain(vehicleRecord.id)
|
||||
expect(view(dialog).text()).not.toContain(vehicleRecord.id)
|
||||
dialog.vm.$emit('confirm'); await flushPromises()
|
||||
expect(api.createDispatch).toHaveBeenCalledWith(expect.objectContaining({ vehicle: { mode: 'MASTER', vehicleId: vehicleRecord.id, plate: 'CA-MASTER', carrierName: null, capacity: null, evidenceFileIds: [] }, driver: { mode: 'MASTER', driverId: driverRecord.id, name: '档案司机', phone: '555-0101', evidence: { licenseNo: null, licenseExpiresOn: null, fileIds: [], expectedTaskEndAt: '2026-11-03T18:00:00Z' } } }))
|
||||
})
|
||||
@@ -109,13 +156,13 @@ describe('new dispatch native layout', () => {
|
||||
vi.mocked(api.getDispatch).mockRejectedValueOnce(new Error('details not ready')).mockImplementation(async no => detail({ dispatchNo: no, recordVersion: 1 }))
|
||||
const { wrapper, dialog } = await render(); await restore(dialog)
|
||||
await externalResources(dialog, 'CA-A', '司机甲'); await field(dialog, '备注').get('input').setValue('第一组')
|
||||
const radios = dialog.get('[aria-label="排车装车组"]').findAllComponents({ name: 'ElRadio' })
|
||||
const radios = view(dialog).get('[aria-label="排车装车组"]').findAllComponents({ name: 'ElRadio' })
|
||||
radios[1]!.vm.$emit('update:modelValue', 'container-2'); await flushPromises()
|
||||
await externalResources(dialog, 'CA-B', '司机乙'); await field(dialog, '备注').get('input').setValue('第二组')
|
||||
radios[0]!.vm.$emit('update:modelValue', 'container-1'); await flushPromises()
|
||||
expect((field(dialog, '车牌号').get('input').element as HTMLInputElement).value).toBe('CA-A')
|
||||
expect((field(dialog, '备注').get('input').element as HTMLInputElement).value).toBe('第一组')
|
||||
await dialog.get('button[aria-label="展开全屏"]').trigger('click'); await flushPromises()
|
||||
await view(dialog).get('button[aria-label="展开全屏"]').trigger('click'); await flushPromises()
|
||||
dialog.vm.$emit('confirm'); await flushPromises()
|
||||
expect(api.convertDispatchDraft).toHaveBeenCalledTimes(1)
|
||||
const request = vi.mocked(api.convertDispatchDraft).mock.calls[0]![0]
|
||||
@@ -123,7 +170,7 @@ describe('new dispatch native layout', () => {
|
||||
expect(request.groups.map(group => [group.dispatch.vehicle?.plate, group.dispatch.driver?.name, group.dispatch.remark])).toEqual([['CA-A', '司机甲', '第一组'], ['CA-B', '司机乙', '第二组']])
|
||||
expect(request.groups.map(group => group.dispatch.vehicle?.capacity?.palletSpaces)).toEqual([17, 17])
|
||||
expect(wrapper.emitted('created')).toBeUndefined()
|
||||
expect(dialog.text()).toContain('草稿操作结果未确认,原请求已保留')
|
||||
expect(view(dialog).text()).toContain('草稿操作结果未确认,原请求已保留')
|
||||
dialog.findComponent(LoadErrorState).vm.$emit('retry'); await flushPromises()
|
||||
expect(api.convertDispatchDraft).toHaveBeenCalledTimes(1)
|
||||
expect(wrapper.emitted('created')).toHaveLength(1)
|
||||
@@ -138,7 +185,7 @@ describe('new dispatch native layout', () => {
|
||||
expect(dialog.findComponent({ name: 'ElDialog' }).props()).toMatchObject({ showClose: false, closeOnPressEscape: false })
|
||||
expect(field(dialog, '车牌号').get('input').attributes('disabled')).toBeDefined()
|
||||
reject(new ApiResponseError('转换结果尚未确认', 'A0503', 503, null, 'new-dispatch-request')); await flushPromises()
|
||||
expect(dialog.text()).toContain('new-dispatch-request')
|
||||
expect(view(dialog).text()).toContain('new-dispatch-request')
|
||||
expect(dialog.getComponent(DispatchResourceFields).props('disabled')).toBe(true)
|
||||
await wrapper.setProps({ modelValue: false }); await wrapper.setProps({ modelValue: true }); await flushPromises()
|
||||
dialog.vm.$emit('confirm'); await flushPromises()
|
||||
@@ -149,10 +196,11 @@ describe('new dispatch native layout', () => {
|
||||
rows[0] = sharedPalletCandidates()[0]!
|
||||
vi.mocked(api.listDispatchCandidates).mockResolvedValue({ candidates: rows, complete: true, snapshotAt: '' })
|
||||
const { dialog } = await render(); await selectWarehouse(dialog)
|
||||
const table = dialog.get('[aria-label="排车候选货物"]')
|
||||
const table = view(dialog).get('[aria-label="排车候选货物"]')
|
||||
expect(table.findAll('.el-table__body-wrapper .el-table__row')).toHaveLength(25)
|
||||
expect(table.text()).toContain('PALLET-24')
|
||||
expect(dialog.findComponent({ name: 'ElTable' }).props('height')).toBeUndefined()
|
||||
expect(dialog.findComponent({ name: 'ElTable' }).props('maxHeight')).toBeUndefined()
|
||||
expect(dialog.findAllComponents(LineClampText).length).toBeGreaterThanOrEqual(50)
|
||||
const sourceGroup = table.findAll('details')[0]!
|
||||
await sourceGroup.get('summary').trigger('click'); await flushPromises()
|
||||
@@ -162,22 +210,22 @@ describe('new dispatch native layout', () => {
|
||||
expect(sourceGroup.text()).toContain('客户B')
|
||||
expect(api.lockDispatchDraftPallet).not.toHaveBeenCalled()
|
||||
})
|
||||
it('keeps query conditions and regional tabs on the original candidate transport', async () => {
|
||||
it.each(['PLAN', 'RELABEL'])('keeps %s query conditions and regional tabs on the original candidate transport', async sourceStage => {
|
||||
const { dialog } = await render(); await selectWarehouse(dialog)
|
||||
await dialog.get('input[placeholder="订单号/入仓码"]').setValue('IN-QUERY')
|
||||
await dialog.get('input[placeholder="目的仓码"]').setValue('ONT8')
|
||||
dialog.findAllComponents({ name: 'ElSelect' }).find(select => select.props('placeholder') === '全部来源')!.vm.$emit('update:modelValue', 'RELABEL'); await flushPromises()
|
||||
await dialog.findAll('button').find(button => button.text() === '查询')!.trigger('click'); await flushPromises()
|
||||
expect(api.listDispatchCandidates).toHaveBeenLastCalledWith({ warehouseId: '321', orderNo: 'IN-QUERY', destinationWarehouseCode: 'ONT8', sourceStage: 'RELABEL', draftToken: undefined })
|
||||
await view(dialog).get('input[placeholder="订单号/入仓码"]').setValue('IN-QUERY')
|
||||
await view(dialog).get('input[placeholder="目的仓码"]').setValue('ONT8')
|
||||
dialog.findAllComponents({ name: 'ElSelect' }).find(select => select.props('placeholder') === '全部来源')!.vm.$emit('update:modelValue', sourceStage); await flushPromises()
|
||||
await view(dialog).findAll('button').find(button => button.text() === '查询')!.trigger('click'); await flushPromises()
|
||||
expect(api.listDispatchCandidates).toHaveBeenLastCalledWith({ warehouseId: '321', orderNo: 'IN-QUERY', destinationWarehouseCode: 'ONT8', sourceStage, draftToken: undefined })
|
||||
const calls = vi.mocked(api.listDispatchCandidates).mock.calls.length
|
||||
await dialog.get('[aria-label="候选仓库展示"]').findAll('button').find(button => button.text() === '纽约')!.trigger('click'); await flushPromises()
|
||||
expect(dialog.get('[aria-label="候选仓库展示"]').findAll('button').find(button => button.text() === '纽约')!.attributes('aria-selected')).toBe('true')
|
||||
await view(dialog).get('[aria-label="候选仓库展示"]').findAll('button').find(button => button.text() === '纽约')!.trigger('click'); await flushPromises()
|
||||
expect(view(dialog).get('[aria-label="候选仓库展示"]').findAll('button').find(button => button.text() === '纽约')!.attributes('aria-selected')).toBe('true')
|
||||
expect(api.listDispatchCandidates).toHaveBeenCalledTimes(calls)
|
||||
})
|
||||
it('shows loading and initialization diagnostics without issuing create commands', async () => {
|
||||
vi.mocked(api.listDispatchDrafts).mockRejectedValueOnce(new ApiResponseError('草稿加载失败', 'A0503', 503, null, 'draft-load-request')).mockResolvedValueOnce([draft()])
|
||||
const { dialog } = await render()
|
||||
expect(dialog.text()).toContain('draft-load-request')
|
||||
expect(view(dialog).text()).toContain('draft-load-request')
|
||||
dialog.findComponent(LoadErrorState).vm.$emit('retry'); await flushPromises()
|
||||
expect(api.listDispatchDrafts).toHaveBeenCalledTimes(2)
|
||||
expect(api.createDispatch).not.toHaveBeenCalled(); expect(api.createDispatchDraft).not.toHaveBeenCalled()
|
||||
@@ -185,13 +233,13 @@ describe('new dispatch native layout', () => {
|
||||
it('requires the explicit recheck action after a definite rejection without automatically resubmitting', async () => {
|
||||
vi.mocked(api.createDispatchDraft).mockRejectedValue(new ApiResponseError('草稿条件已变化', 'A0409', 409, null, 'draft-rejected-request'))
|
||||
const { dialog } = await render(); await selectWarehouse(dialog)
|
||||
await dialog.findAll('button').find(button => button.text() === '新建草稿')!.trigger('click'); await flushPromises()
|
||||
expect(dialog.text()).toContain('draft-rejected-request')
|
||||
expect(dialog.getComponent(DispatchResourceFields).props('disabled')).toBe(true)
|
||||
await dialog.findAll('button').find(button => button.text() === '按明确驳回结果重新核对')!.trigger('click'); await flushPromises()
|
||||
await view(dialog).findAll('button').find(button => button.text() === '开始选货')!.trigger('click'); await flushPromises()
|
||||
expect(view(dialog).text()).toContain('draft-rejected-request')
|
||||
expect(dialog.findComponent(DispatchResourceFields).exists()).toBe(false)
|
||||
await view(dialog).findAll('button').find(button => button.text() === '按明确驳回结果重新核对')!.trigger('click'); await flushPromises()
|
||||
expect(api.createDispatchDraft).toHaveBeenCalledTimes(1)
|
||||
expect(field(dialog, '建单方式').getComponent({ name: 'ElRadioGroup' }).props('disabled')).toBe(false)
|
||||
expect(dialog.getComponent(DispatchResourceFields).props('disabled')).toBe(true)
|
||||
expect(dialog.findComponent(DispatchResourceFields).exists()).toBe(false)
|
||||
expect(api.listDispatchDrafts).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
it('keeps creation unavailable without its write permission', async () => {
|
||||
|
||||
Reference in New Issue
Block a user