修复订单货柜预约与排车维护界面

This commit is contained in:
2026-10-21 23:20:10 +08:00
parent 93a31b3872
commit 2e26477db5
68 changed files with 602 additions and 239 deletions
@@ -4,12 +4,13 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import DispatchWorkflowDialog from './DispatchWorkflowDialog.vue'
import DispatchEvidenceFiles from './DispatchEvidenceFiles.vue'
import DispatchResourceFields from './DispatchResourceFields.vue'
import { blankVehicleMaintenance } from '../dispatchVehicleMaintenance'
import AppDialog from '@/shared/components/AppDialog.vue'
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import { ApiResponseError } from '@/shared/api/http'
import * as api from '../api/dispatches'
import { command, detail, loadingPreview, resources } from '../dispatchTestFixtures'
import type { DispatchAction, DispatchActions, DispatchEvidenceFile, UpdateDispatch } from '../dispatchContracts'
import type { DispatchAction, DispatchActions, DispatchDetail, DispatchEvidenceFile, UpdateDispatch } from '../dispatchContracts'
const identity = vi.hoisted(() => ({ id: '' }))
const can = vi.hoisted(() => vi.fn((_code: string) => true))
@@ -43,8 +44,8 @@ beforeEach(() => {
vi.mocked(api.getDispatchEvidenceContent).mockResolvedValue(new Blob(['png'], { type: 'image/png' }))
vi.mocked(api.uploadDispatchEvidence).mockResolvedValue(evidence('302'))
})
async function render(action: DispatchAction, operation?: 'ROUTE', files = false) {
const initial = detail({ actions }); if (files) initial.vehicleInfo.vehicle!.evidenceFileIds = ['301']
async function render(action: DispatchAction, operation?: 'ROUTE', files = false, overrides: Partial<DispatchDetail> = {}) {
const initial = detail({ actions, ...overrides }); if (files) initial.vehicleInfo.vehicle!.evidenceFileIds = ['301']
vi.mocked(api.getDispatch).mockResolvedValueOnce(initial)
const wrapper = mount(DispatchWorkflowDialog, { attachTo: document.body, props: { modelValue: true, dispatchNo: 'PC/1', action, operation }, global: { plugins: [ElementPlus] } }); await flushPromises()
return { wrapper, dialog: wrapper.findComponent(AppDialog) }
@@ -52,6 +53,20 @@ async function render(action: DispatchAction, operation?: 'ROUTE', files = false
function field(wrapper: Pick<VueWrapper, 'findAllComponents'>, label: string) { return wrapper.findAllComponents({ name: 'ElFormItem' }).find(item => item.props('label') === label)! }
describe('dispatch workflow native layout', () => {
it('restores saved maintenance facts and shows associated orders without editable departure facts', async () => {
const saved = { ...blankVehicleMaintenance(), plannedDeliveryDate: '2026-11-02', fleetEmail: 'fleet@example.test', appointmentNo: 'APPT-SAVED' }
const { dialog } = await render('EDIT', undefined, false, { vehicleInfo: { ...resources(), maintenance: saved } })
expect((field(dialog, '车队邮箱').get('input').element as HTMLInputElement).value).toBe('fleet@example.test')
expect((field(dialog, '预约号').get('input').element as HTMLInputElement).value).toBe('APPT-SAVED')
expect(dialog.text()).toContain('关联订单 (1)')
const orders = dialog.findAll('[role="tab"]').find(tab => tab.text().includes('关联订单'))!
await orders.trigger('click')
expect(dialog.get('.maintenance-orders').text()).toContain('LM-1')
expect(dialog.get('.maintenance-orders').findAll('button, input')).toHaveLength(0)
dialog.vm.$emit('confirm'); await flushPromises()
expect(api.updateDispatch).toHaveBeenCalledWith('PC/1', expect.objectContaining({ maintenance: saved }))
expect(vi.mocked(api.updateDispatch).mock.calls[0]![1]).not.toHaveProperty('departedAt')
})
it.each(['DISPATCH_REVIEW', 'LOADING_REVIEW'] as const)('blocks %s approval for missing appointment but keeps rejection available', async action => {
vi.mocked(api.getDispatch).mockResolvedValue(detail({ actions: { ...actions, [action]: { allowed: true, blockCode: 'APPROVAL_BLOCKED', blockReason: '预约收货时间尚未确认' } } }))
const wrapper = mount(DispatchWorkflowDialog, { attachTo: document.body, props: { modelValue: true, dispatchNo: 'PC/1', action }, global: { plugins: [ElementPlus] } }); await flushPromises()
@@ -71,11 +86,17 @@ describe('dispatch workflow native layout', () => {
field(dialog, '实际托位容量').getComponent({ name: 'ElInputNumber' }).vm.$emit('update:modelValue', 22)
await field(dialog, '司机姓名').get('input').setValue('新司机')
field(dialog, '计划发车时间').getComponent({ name: 'ElDatePicker' }).vm.$emit('update:modelValue', '2026-11-01T10:00:00Z')
field(dialog, '预派送日期').getComponent({ name: 'ElDatePicker' }).vm.$emit('update:modelValue', '2026-11-02')
for (const [label, value] of [['派送方式', '外配'], ['提货方式', '一提多卸'], ['装车类型', '混装'], ['车辆类型', '厢式货车']]) field(dialog, label!).getComponent({ name: 'ElSelect' }).vm.$emit('update:modelValue', value)
await field(dialog, '车队邮箱').get('input').setValue(' fleet@example.test ')
await field(dialog, '预约账号').get('input').setValue(' fleet-account ')
await field(dialog, '预约号').get('input').setValue(' APPT-NEW ')
expect(field(dialog, '发车时间').get('input').attributes('readonly')).toBeDefined()
expect(dialog.find('textarea[aria-label="本次操作备注"]').exists()).toBe(false)
await dialog.get('button[aria-label="展开全屏"]').trigger('click'); await flushPromises()
dialog.vm.$emit('confirm'); await flushPromises()
const { vehicleStatus: _status, ...vehicle } = resources().vehicle
expect(api.updateDispatch).toHaveBeenCalledExactlyOnceWith('PC/1', { recordVersion: 0, publishLooseboardDemand: true, idempotencyKey: expect.any(String), vehicle: { ...vehicle, plate: 'CA-NEW', capacity: { ...vehicle.capacity, palletSpaces: 22 } }, driver: { ...resources().driver, name: '新司机' }, plannedDepartureAt: '2026-11-01T10:00:00Z', estimatedArrivalAt: null, remark: '真实备注' })
expect(api.updateDispatch).toHaveBeenCalledExactlyOnceWith('PC/1', { recordVersion: 0, publishLooseboardDemand: true, idempotencyKey: expect.any(String), vehicle: { ...vehicle, plate: 'CA-NEW', capacity: { ...vehicle.capacity, palletSpaces: 22 } }, driver: { ...resources().driver, name: '新司机' }, maintenance: { plannedDeliveryDate: '2026-11-02', deliveryMethod: '外配', pickupMethod: '一提多卸', loadType: '混装', fleetEmail: 'fleet@example.test', vehicleType: '厢式货车', appointmentAccount: 'fleet-account', appointmentNo: 'APPT-NEW' }, plannedDepartureAt: '2026-11-01T10:00:00Z', estimatedArrivalAt: null, remark: '真实备注' })
})
it('saves all three resource photos before submitting review with the refreshed version', async () => {
const { dialog } = await render('EDIT')
@@ -179,6 +200,7 @@ describe('dispatch workflow native layout', () => {
it.each(['DISPATCH_REVIEW', 'LOADING_REVIEW'] as const)('shows saved photos and old audit evidence read-only in %s', async action => {
const { wrapper, dialog } = await render(action)
const value = detail({ actions })
value.vehicleInfo.vehicle!.evidenceFileIds = ['401']
value.vehicleInfo.driver!.evidence!.photoFileIds = ['402']
value.reviews.dispatchReview.attachments = { vehicleFileIds: [], driverPhotoFileIds: ['403'], licenseFileIds: [] }
vi.mocked(api.getDispatch).mockResolvedValueOnce(value)
@@ -186,6 +208,9 @@ describe('dispatch workflow native layout', () => {
const fields = dialog.findAllComponents(DispatchEvidenceFiles)
expect(fields.map(item => item.props('label'))).toEqual(['车辆图片', '司机照片', '驾驶证照片'])
expect(fields.every(item => item.props('readonly') && item.props('photoCard'))).toBe(true)
expect(fields[0]!.props('modelValue')).toEqual(['401'])
expect(api.getDispatchEvidence).toHaveBeenCalledWith('PC/1', 'VEHICLE', '401')
expect(api.getDispatchEvidenceContent).toHaveBeenCalledWith('PC/1', 'VEHICLE', '401')
expect(fields[1]!.props('modelValue')).toEqual(['402', '403'])
expect(dialog.findAll('input[type="file"]')).toHaveLength(0)
expect(api.uploadDispatchEvidence).not.toHaveBeenCalled()