优化
This commit is contained in:
@@ -71,7 +71,9 @@ describe('whole-dispatch four-tab detail', () => {
|
||||
vi.mocked(getDispatch).mockResolvedValueOnce(detail({ actions: { EDIT: { allowed: true, blockCode: null, blockReason: null }, ADD_CARGO: { allowed: true, blockCode: null, blockReason: null }, REMOVE_CARGO: { allowed: true, blockCode: null, blockReason: null } } }))
|
||||
const wrapper = mount(DispatchDetailView, options); await flushPromises()
|
||||
const actions = wrapper.get('[aria-label="排车操作"]').findAll('button').map(button => button.text())
|
||||
expect(actions).toContain('维护车辆信息')
|
||||
expect(actions).toContain('维护车辆')
|
||||
expect(wrapper.findAll('button').map(button => button.text())).not.toContain('调整卸货顺序')
|
||||
expect(wrapper.findAll('button').map(button => button.text())).not.toContain('提交调度审核')
|
||||
expect(actions).not.toContain('添加货物')
|
||||
expect(actions).not.toContain('移除货物')
|
||||
wrapper.unmount()
|
||||
@@ -184,7 +186,7 @@ describe('whole-dispatch four-tab detail', () => {
|
||||
expect(wrapper.text()).toContain('装车中')
|
||||
expect(wrapper.text()).not.toContain('当前阶段或作业事实不满足操作条件')
|
||||
expect(wrapper.text()).not.toContain('当前无此操作权限')
|
||||
expect(wrapper.findAll('button').some(button => button.text() === '维护车辆信息')).toBe(false)
|
||||
expect(wrapper.findAll('button').some(button => button.text() === '维护车辆')).toBe(false)
|
||||
await wrapper.findAll('button').find(button => button.text() === '确认装车完成')!.trigger('click')
|
||||
expect(wrapper.findComponent({ name: 'DispatchWorkflowDialog' }).props()).toMatchObject({ modelValue: true, dispatchNo: 'PC/1', action: 'LOADING_COMPLETE' })
|
||||
wrapper.unmount()
|
||||
@@ -212,17 +214,17 @@ describe('whole-dispatch four-tab detail', () => {
|
||||
})
|
||||
it.each(['saved', 'rejected'] as const)('refreshes detail actions after the workflow %s event and retains a real rejection after closure', async event => {
|
||||
can.mockReturnValue(true)
|
||||
vi.mocked(getDispatch).mockResolvedValueOnce(detail({ actions: { SUBMIT_REVIEW: { allowed: true, blockCode: null, blockReason: null } } }))
|
||||
vi.mocked(getDispatch).mockResolvedValueOnce(detail({ actions: { EDIT: { allowed: true, blockCode: null, blockReason: null } } }))
|
||||
const wrapper = mount(DispatchDetailView, options); await flushPromises()
|
||||
await wrapper.findAll('button').find(button => button.text() === '提交调度审核')!.trigger('click')
|
||||
vi.mocked(getDispatch).mockResolvedValueOnce(detail({ recordVersion: 1, actions: { SUBMIT_REVIEW: { allowed: false, blockCode: 'ALREADY_SUBMITTED', blockReason: '已提交' } } }))
|
||||
await wrapper.findAll('button').find(button => button.text() === '维护车辆')!.trigger('click')
|
||||
vi.mocked(getDispatch).mockResolvedValueOnce(detail({ recordVersion: 1, actions: { EDIT: { allowed: false, blockCode: 'ALREADY_SUBMITTED', blockReason: '已提交' } } }))
|
||||
const dialog = wrapper.getComponent({ name: 'DispatchWorkflowDialog' })
|
||||
if (event === 'rejected') dialog.vm.$emit('rejected', { message: '本轮审核已经提交', code: 'A0409', requestId: 'detail-reject-request' })
|
||||
else dialog.vm.$emit('saved')
|
||||
dialog.vm.$emit('update:modelValue', false)
|
||||
await flushPromises()
|
||||
expect(getDispatch).toHaveBeenCalledTimes(2)
|
||||
expect(wrapper.findAll('button').some(button => button.text() === '提交调度审核')).toBe(false)
|
||||
expect(wrapper.findAll('button').some(button => button.text() === '维护车辆')).toBe(false)
|
||||
expect(wrapper.findAll('[role="tab"]')).toHaveLength(4)
|
||||
if (event === 'rejected') {
|
||||
expect(wrapper.text()).toContain('本轮审核已经提交')
|
||||
|
||||
@@ -29,7 +29,7 @@ const rejectionError = ref<ApiErrorDetails | null>(null)
|
||||
const operation = ref<'ROUTE' | undefined>()
|
||||
const action = ref<DispatchAction>('EDIT'), actionOpen = ref(false)
|
||||
const detailActions: DispatchAction[] = ['EDIT', 'SUBMIT_REVIEW', 'DISPATCH_REVIEW', 'LOADING_REVIEW', 'RETURN_TO_REVIEW', 'LOADING_COMPLETE', 'CANCEL']
|
||||
const visibleActions = computed(() => detailActions.filter(value => !(detail.value?.taskType === 'PICKUP' && value === 'EDIT') && canDispatchAction(detail.value?.actions, value, access.can)))
|
||||
const visibleActions = computed(() => detailActions.filter(value => (value !== 'SUBMIT_REVIEW' || detail.value?.taskType === 'PICKUP') && !(detail.value?.taskType === 'PICKUP' && value === 'EDIT') && canDispatchAction(detail.value?.actions, value, access.can)))
|
||||
const vehicleStatus = computed(() => {
|
||||
const status = detail.value?.vehicleInfo.vehicle?.vehicleStatus
|
||||
return status && Object.hasOwn(vehicleStatusLabels, status) ? vehicleStatusLabels[status as keyof typeof vehicleStatusLabels] : '—'
|
||||
@@ -105,7 +105,6 @@ onBeforeUnmount(() => { generation++ })
|
||||
</el-tabs>
|
||||
<footer v-if="detail && visibleActions.length" class="dispatch-actions density-actions" aria-label="排车操作">
|
||||
<el-button v-for="value in visibleActions" :key="value" :type="value === 'CANCEL' ? 'danger' : undefined" plain @click="openAction(value)">{{ dispatchActionLabels[value] }}</el-button>
|
||||
<el-button v-if="(!detail.taskType || detail.taskType === 'DELIVERY') && canDispatchAction(detail.actions, 'EDIT', access.can)" @click="openAction('EDIT', 'ROUTE')">调整卸货顺序</el-button>
|
||||
</footer>
|
||||
</div>
|
||||
<DispatchWorkflowDialog v-model="actionOpen" :dispatch-no="dispatchNo" :action="action" :operation="operation" @saved="handleSaved" @rejected="handleRejection" />
|
||||
|
||||
@@ -7,7 +7,7 @@ import QueryToolbar from '@/shared/components/QueryToolbar.vue'
|
||||
import DataTable from '@/shared/components/DataTable.vue'
|
||||
import RowActions from '@/shared/components/RowActions.vue'
|
||||
import { getDispatch, getDispatchBolPdf, listDispatches } from '../api/dispatches'
|
||||
import { command, detail, listRow, page } from '../dispatchTestFixtures'
|
||||
import { command, detail, listRow, page, reviews } from '../dispatchTestFixtures'
|
||||
import type { DispatchPage } from '../dispatchContracts'
|
||||
import { useDispatchCommandRecovery } from '../dispatchCommandRecovery'
|
||||
const identity = vi.hoisted(() => ({ id: '' }))
|
||||
@@ -54,17 +54,18 @@ it('centralizes cargo changes in smart dispatch and never exposes row add/remove
|
||||
expect(wrapper.getComponent({ name: 'SmartDispatchDialog' }).props('modelValue')).toBe(true)
|
||||
wrapper.unmount()
|
||||
})
|
||||
it('keeps vehicle maintenance and detail visible while submitting review remains available in the menu', async () => {
|
||||
it('keeps vehicle maintenance but hides standalone review submission and route ordering', async () => {
|
||||
const allowed = { allowed: true, blockCode: null, blockReason: null }
|
||||
vi.mocked(listDispatches).mockResolvedValue(page([listRow({ actions: { EDIT: allowed, SUBMIT_REVIEW: allowed, CANCEL: allowed } })]))
|
||||
const wrapper = mount(DispatchesView, { ...options, attachTo: document.body }); await flushPromises()
|
||||
const wrapper = mount(DispatchesView, options); await flushPromises()
|
||||
const row = wrapper.findAllComponents(RowActions).find(entry => entry.props('moreLabel') === '更多操作 · PC/1')!
|
||||
expect(row.findAll('button').map(button => button.text())).toEqual(['维护车辆信息', '详情', '更多'])
|
||||
await row.get('button[aria-label="更多操作 · PC/1"]').trigger('click'); await flushPromises()
|
||||
const submit = Array.from(document.querySelectorAll<HTMLElement>('.el-dropdown-menu__item')).find(item => item.textContent === '提交调度审核')!
|
||||
expect(submit).toBeDefined()
|
||||
submit.click(); await flushPromises()
|
||||
expect(wrapper.getComponent({ name: 'DispatchWorkflowDialog' }).props()).toMatchObject({ modelValue: true, action: 'SUBMIT_REVIEW', dispatchNo: 'PC/1' })
|
||||
const keys = (row.props('actions') as Array<{ key: string }>).map(item => item.key)
|
||||
expect(keys).toContain('EDIT'); expect(keys).toContain('detail')
|
||||
expect(keys).not.toContain('SUBMIT_REVIEW'); expect(keys).not.toContain('route-order')
|
||||
row.vm.$emit('action', 'SUBMIT_REVIEW'); row.vm.$emit('action', 'route-order'); await flushPromises()
|
||||
expect(wrapper.getComponent({ name: 'DispatchWorkflowDialog' }).props('modelValue')).toBe(false)
|
||||
row.vm.$emit('action', 'EDIT'); await flushPromises()
|
||||
expect(wrapper.getComponent({ name: 'DispatchWorkflowDialog' }).props()).toMatchObject({ modelValue: true, action: 'EDIT', dispatchNo: 'PC/1' })
|
||||
wrapper.unmount()
|
||||
})
|
||||
it.each([['DISPATCH_REVIEW', 1, '调度审核'], ['LOADING_REVIEW', 2, '理货员审核']] as const)('prioritizes the current %s queue action with a short label', async (action, index, label) => {
|
||||
@@ -252,7 +253,7 @@ it('gates every mutation with both its literal service capability and independen
|
||||
const actions = wrapper.findAllComponents(RowActions).find(entry => entry.props('moreLabel') === '更多操作 · PC/1')!.props('actions') as Array<{ key: string }>
|
||||
expect(actions.map(row => row.key)).toContain('EDIT'); expect(actions.map(row => row.key)).not.toContain('DEPART')
|
||||
can.mockReturnValue(false); wrapper.unmount()
|
||||
const readonly = mount(DispatchesView, options); await flushPromises(); expect((readonly.findAllComponents(RowActions).find(entry => entry.props('moreLabel') === '更多操作 · PC/1')!.props('actions') as Array<{ key: string }>).map(row => row.key)).toEqual(['detail', 'bol']); readonly.unmount()
|
||||
const readonly = mount(DispatchesView, options); await flushPromises(); expect((readonly.findAllComponents(RowActions).find(entry => entry.props('moreLabel') === '更多操作 · PC/1')!.props('actions') as Array<{ key: string }>).map(row => row.key)).toEqual(['detail']); readonly.unmount()
|
||||
})
|
||||
it('keeps server errors visible with the integrated query and retry context', async () => {
|
||||
vi.mocked(listDispatches).mockRejectedValueOnce(new Error('service unavailable'))
|
||||
@@ -285,21 +286,38 @@ it('keeps pickup workflow actions but hides route ordering and BOL even when edi
|
||||
wrapper.unmount()
|
||||
})
|
||||
it('reads one BOL on demand and preserves popup failure handling', async () => {
|
||||
const current = reviews()
|
||||
vi.mocked(listDispatches).mockResolvedValue(page([listRow({ reviews: { ...current, dispatchReview: { ...current.dispatchReview, status: 'APPROVED' } } })]))
|
||||
vi.stubGlobal('open', vi.fn().mockReturnValue(null)); const wrapper = mount(DispatchesView, options); await flushPromises(); wrapper.findAllComponents(RowActions).find(entry => entry.props('moreLabel') === '更多操作 · PC/1')!.vm.$emit('action', 'bol'); await flushPromises(); expect(getDispatchBolPdf).not.toHaveBeenCalled(); wrapper.unmount()
|
||||
})
|
||||
it.each(['NOT_SUBMITTED', 'PENDING', 'REJECTED', 'APPROVED'] as const)('offers BOL only after current dispatch review is approved: %s', async status => {
|
||||
const current = reviews()
|
||||
vi.mocked(listDispatches).mockResolvedValue(page([listRow({ reviews: { ...current, dispatchReview: { ...current.dispatchReview, status } } })]))
|
||||
const popup = { close: vi.fn(), location: { href: '' } }
|
||||
vi.stubGlobal('open', vi.fn().mockReturnValue(popup))
|
||||
vi.mocked(getDispatchBolPdf).mockRejectedValue(new Error('print unavailable'))
|
||||
const wrapper = mount(DispatchesView, options); await flushPromises()
|
||||
const actions = wrapper.findAllComponents(RowActions).find(entry => entry.props('moreLabel') === '更多操作 · PC/1')!
|
||||
expect((actions.props('actions') as Array<{ key: string }>).some(item => item.key === 'bol')).toBe(status === 'APPROVED')
|
||||
actions.vm.$emit('action', 'bol'); await flushPromises()
|
||||
expect(getDispatchBolPdf).toHaveBeenCalledTimes(status === 'APPROVED' ? 1 : 0)
|
||||
expect(window.open).toHaveBeenCalledTimes(status === 'APPROVED' ? 1 : 0)
|
||||
if (status === 'APPROVED') expect(popup.close).toHaveBeenCalledOnce()
|
||||
wrapper.unmount()
|
||||
})
|
||||
it.each(['saved', 'rejected'] as const)('refreshes list actions after workflow %s without retaining a stale submit button', async event => {
|
||||
vi.mocked(listDispatches).mockResolvedValueOnce(page([listRow({ actions: { SUBMIT_REVIEW: { allowed: true, blockCode: null, blockReason: null } } })]))
|
||||
vi.mocked(listDispatches).mockResolvedValueOnce(page([listRow({ actions: { EDIT: { allowed: true, blockCode: null, blockReason: null } } })]))
|
||||
const wrapper = mount(DispatchesView, options); await flushPromises()
|
||||
const row = wrapper.findAllComponents(RowActions).find(entry => entry.props('moreLabel') === '更多操作 · PC/1')!
|
||||
expect(row.props('actions')).toContainEqual(expect.objectContaining({ key: 'SUBMIT_REVIEW' }))
|
||||
row.vm.$emit('action', 'SUBMIT_REVIEW'); await flushPromises()
|
||||
vi.mocked(listDispatches).mockResolvedValueOnce(page([listRow({ recordVersion: 1, actions: { SUBMIT_REVIEW: { allowed: false, blockCode: 'ALREADY_SUBMITTED', blockReason: '审核已提交' } } })]))
|
||||
expect(row.props('actions')).toContainEqual(expect.objectContaining({ key: 'EDIT' }))
|
||||
row.vm.$emit('action', 'EDIT'); await flushPromises()
|
||||
vi.mocked(listDispatches).mockResolvedValueOnce(page([listRow({ recordVersion: 1, actions: { EDIT: { allowed: false, blockCode: 'ALREADY_SUBMITTED', blockReason: '审核已提交' } } })]))
|
||||
const dialog = wrapper.getComponent({ name: 'DispatchWorkflowDialog' })
|
||||
if (event === 'rejected') dialog.vm.$emit('rejected', { message: '本轮审核已经提交', code: 'A0409', requestId: 'list-reject-request' })
|
||||
else dialog.vm.$emit('saved')
|
||||
dialog.vm.$emit('update:modelValue', false); await flushPromises()
|
||||
expect(listDispatches).toHaveBeenCalledTimes(2)
|
||||
expect(row.props('actions')).not.toContainEqual(expect.objectContaining({ key: 'SUBMIT_REVIEW' }))
|
||||
expect(row.props('actions')).not.toContainEqual(expect.objectContaining({ key: 'EDIT' }))
|
||||
if (event === 'rejected') {
|
||||
expect(wrapper.text()).toContain('本轮审核已经提交')
|
||||
expect(wrapper.text()).toContain('A0409')
|
||||
|
||||
@@ -113,13 +113,15 @@ function switchSection(value: BeforeLoadingSection) { section.value = value; pag
|
||||
function openDetail(row: DispatchListRow) { void router.push({ name: 'overseas-dispatch-detail', params: { dispatchNo: row.dispatchNo } }) }
|
||||
const rowActionOrder: DispatchAction[] = ['EDIT', 'SUBMIT_REVIEW', 'DISPATCH_REVIEW', 'LOADING_REVIEW', 'RETURN_TO_REVIEW', 'LOADING_COMPLETE', 'MARK_EXCEPTION', 'CANCEL']
|
||||
function canManageDocuments(row: DispatchListRow) { return (row.taskType || 'DELIVERY') === 'DELIVERY' && row.status === 'ACTIVE' && ['LOADING', 'AFTER_LOADING', 'DELIVERING', 'DELIVERED'].includes(row.stage) }
|
||||
// 仅当前调度审核通过的仓库派送排车可下载,退回或审核失效后重新收起入口。
|
||||
function canDownloadBol(row: DispatchListRow) { return (row.taskType || 'DELIVERY') === 'DELIVERY' && row.reviews?.dispatchReview?.status === 'APPROVED' }
|
||||
function rowActions(row: DispatchListRow): RowActionItem[] {
|
||||
if (showSections.value && section.value !== 'VEHICLE_INFO') {
|
||||
const review = section.value
|
||||
return [...(canDispatchAction(row.actions, review, access.can) ? [{ key: review, label: '审核', ariaLabel: dispatchActionLabels[review], priority: 600 }] : []), { key: 'detail', label: '详情', priority: 500 }]
|
||||
}
|
||||
if (row.taskType === 'LOCAL_TRUCK') return [{ key: 'detail', label: '运输操作', priority: 500 }]
|
||||
const available = rowActionOrder.filter(value => canDispatchAction(row.actions, value, access.can))
|
||||
const available = rowActionOrder.filter(value => (value !== 'SUBMIT_REVIEW' || row.taskType === 'PICKUP') && canDispatchAction(row.actions, value, access.can))
|
||||
const primaryAction = available.find(value => value === 'EDIT') ?? available[0]
|
||||
return [...(canManageDocuments(row) ? [{ key: 'documents', label: '管理资料', priority: 700 }] : []), ...available.map((value, index) => ({
|
||||
key: value,
|
||||
@@ -127,7 +129,7 @@ function rowActions(row: DispatchListRow): RowActionItem[] {
|
||||
ariaLabel: dispatchActionLabels[value],
|
||||
priority: value === primaryAction ? 600 : 400 - index * 10,
|
||||
tone: ['CANCEL', 'MARK_EXCEPTION'].includes(value) ? 'danger' as const : undefined,
|
||||
})), { key: 'detail', label: '详情', priority: 500 }, ...(row.taskType === 'PICKUP' ? [] : [...(canDispatchAction(row.actions, 'EDIT', access.can) ? [{ key: 'route-order', label: '调整卸货顺序', priority: 190 }] : []), { key: 'bol', label: '下载BOL', priority: 100, loading: bolLoadingNo.value === row.dispatchNo }])]
|
||||
})), { key: 'detail', label: '详情', priority: 500 }, ...(canDownloadBol(row) ? [{ key: 'bol', label: '下载BOL', priority: 100, loading: bolLoadingNo.value === row.dispatchNo }] : [])]
|
||||
}
|
||||
function handleAction(value: string, row: DispatchListRow) {
|
||||
if (value === 'detail') return openDetail(row)
|
||||
@@ -135,7 +137,7 @@ function handleAction(value: string, row: DispatchListRow) {
|
||||
if (value === 'documents') { if (canManageDocuments(row)) { documentsNo.value = row.dispatchNo; documentsOpen.value = true }; return }
|
||||
if (value === 'bol') return void openBol(row)
|
||||
rejectionError.value = null
|
||||
if (value === 'route-order') { if (!canDispatchAction(row.actions, 'EDIT', access.can)) return; operation.value = 'ROUTE'; action.value = 'EDIT'; actionNo.value = row.dispatchNo; actionOpen.value = true; return }
|
||||
if (value === 'route-order' || value === 'SUBMIT_REVIEW' && row.taskType !== 'PICKUP') return
|
||||
operation.value = undefined
|
||||
if (value === 'ADD_CARGO' || value === 'REMOVE_CARGO') return
|
||||
const requested = value as DispatchAction
|
||||
@@ -152,7 +154,7 @@ function recoverPending() {
|
||||
operation.value = command.operation; action.value = command.action; actionNo.value = command.dispatchNo; actionOpen.value = true
|
||||
}
|
||||
async function openBol(row: DispatchListRow) {
|
||||
if (bolLoadingNo.value) return
|
||||
if (bolLoadingNo.value || !canDownloadBol(row)) return
|
||||
const windowHandle = window.open('', '_blank')
|
||||
if (!windowHandle) { ElMessage.warning('请允许本站打开BOL预览窗口'); return }
|
||||
bolLoadingNo.value = row.dispatchNo
|
||||
|
||||
Reference in New Issue
Block a user