优化
This commit is contained in:
@@ -16,9 +16,16 @@ vi.mock('@/modules/overseas-master-data', async () => ({
|
||||
vehicleStatusLabels: (await vi.importActual<typeof import('../../overseas-master-data/presentation')>('../../overseas-master-data/presentation')).vehicleStatusLabels,
|
||||
inquiryReturnTarget: (value: Record<string, string>) => value.fleetOfferNo ? { name: 'overseas-master-data-capacity-offer-detail', params: { offerNo: value.fleetOfferNo }, query: { inquiry: value.fleetBookingNo } } : null,
|
||||
}))
|
||||
const options = { global: { plugins: [ElementPlus], stubs: { DataTable: DataTableStub, DispatchWorkflowDialog: true } } }
|
||||
const options = { global: { plugins: [ElementPlus], stubs: { DataTable: DataTableStub, DispatchWorkflowDialog: true, TransportExecutionPanel: true } } }
|
||||
beforeEach(() => { vi.clearAllMocks(); vi.mocked(getDispatch).mockResolvedValue(detail()); can.mockReturnValue(false); push.mockReset(); Object.keys(query).forEach(key => delete query[key]) })
|
||||
describe('whole-dispatch four-tab detail', () => {
|
||||
it.each(['PICKUP', 'LOCAL_TRUCK'] as const)('hosts %s execution in the existing dispatch page', async taskType => {
|
||||
vi.mocked(getDispatch).mockResolvedValueOnce({ ...detail(), taskType })
|
||||
const wrapper = mount(DispatchDetailView, options); await flushPromises()
|
||||
expect(wrapper.get('transport-execution-panel-stub').attributes('dispatchno')).toBe('PC/1')
|
||||
expect(wrapper.findAll('[role="tab"]')).toHaveLength(0)
|
||||
expect(wrapper.find('[aria-label="排车操作"]').exists()).toBe(false); wrapper.unmount()
|
||||
})
|
||||
it('returns to the original appointment date and view from the dispatch detail', async () => {
|
||||
query.appointmentReturn = '/appointment-calendar?date=2026-10-21&mode=week'
|
||||
const wrapper = mount(DispatchDetailView, options); await flushPromises()
|
||||
|
||||
@@ -13,6 +13,7 @@ import { getDispatch } from '../api/dispatches'
|
||||
import type { DispatchAction, DispatchDetail } from '../dispatchContracts'
|
||||
import { stageLabel } from '../dispatchContracts'
|
||||
import { canDispatchAction, dispatchActionLabels, displayDispatchValue as text } from '../dispatchPresentation'
|
||||
import TransportExecutionPanel from '../components/TransportExecutionPanel.vue'
|
||||
import DispatchWorkflowDialog from '../components/DispatchWorkflowDialog.vue'
|
||||
import DispatchEvidenceFiles from '../components/DispatchEvidenceFiles.vue'
|
||||
import OverseasDetailShell from '../components/OverseasDetailShell.vue'
|
||||
@@ -81,8 +82,9 @@ onBeforeUnmount(() => { generation++ })
|
||||
<LoadErrorState v-if="rejectionError" title="排车操作未完成" :message="rejectionError.message" :show-retry="false"><template #details><ApiErrorMeta :details="rejectionError" /></template></LoadErrorState>
|
||||
<LoadErrorState v-if="loadError" title="排车详情加载失败" :message="loadError.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="loadError" /></template></LoadErrorState>
|
||||
<div v-else v-loading="loading" class="dispatch-detail" :aria-busy="loading">
|
||||
<section v-if="detail" class="detail-section" aria-label="排车操作"><div class="density-actions"><el-button v-for="value in visibleActions" :key="value" class="density-action" @click="openAction(value)">{{ dispatchActionLabels[value] }}</el-button><el-button v-if="canDispatchAction(detail?.actions, 'EDIT', access.can)" @click="openAction('EDIT', 'ROUTE')">调整卸货顺序</el-button></div></section>
|
||||
<el-tabs v-model="activeTab" class="density-element-tabs density-contained-tabs">
|
||||
<TransportExecutionPanel v-if="detail?.taskType && detail.taskType !== 'DELIVERY'" :dispatch-no="dispatchNo" @saved="load" />
|
||||
<section v-else-if="detail" class="detail-section" aria-label="排车操作"><div class="density-actions"><el-button v-for="value in visibleActions" :key="value" class="density-action" @click="openAction(value)">{{ dispatchActionLabels[value] }}</el-button><el-button v-if="canDispatchAction(detail?.actions, 'EDIT', access.can)" @click="openAction('EDIT', 'ROUTE')">调整卸货顺序</el-button></div></section>
|
||||
<el-tabs v-if="!detail?.taskType || detail.taskType === 'DELIVERY'" v-model="activeTab" class="density-element-tabs density-contained-tabs">
|
||||
<el-tab-pane name="vehicle"><template #label><span class="density-tab-icon" aria-hidden="true">🚚</span><span>车辆信息</span></template><dl class="detail-fields density-fields"><div v-for="field in vehicleFields" :key="field[0]"><dt>{{ field[0] }}</dt><dd>{{ field[1] }}</dd></div></dl><DispatchEvidenceFiles :model-value="detail?.vehicleInfo.vehicle?.evidenceFileIds || []" :dispatch-no="dispatchNo" kind="VEHICLE" readonly /><DispatchEvidenceFiles :model-value="detail?.vehicleInfo.driver?.evidence?.fileIds || []" :dispatch-no="dispatchNo" kind="DRIVER" readonly /></el-tab-pane>
|
||||
<el-tab-pane name="dispatch"><template #label><span class="density-tab-icon" aria-hidden="true">📋</span><span>排车信息</span></template><dl class="detail-fields density-fields"><div v-for="field in dispatchFields" :key="field[0]"><dt>{{ field[0] }}</dt><dd>{{ field[1] }}</dd></div></dl></el-tab-pane>
|
||||
<el-tab-pane name="documents"><template #label><span class="density-tab-icon" aria-hidden="true">📝</span><span>单据信息</span></template>
|
||||
|
||||
@@ -18,7 +18,7 @@ vi.mock('@/core/auth/auth', () => ({ useAuthStore: () => ({ user: identity }) })
|
||||
vi.mock('@/modules/appointment-calendar', () => ({ appointmentReturnTarget: () => null }))
|
||||
vi.mock('@/modules/overseas-master-data', () => ({ TransportRouteDialog: { template: '<div />' }, routeLocation: (label: string) => ({ label }) }))
|
||||
vi.mock('../api/dispatches', () => ({ listDispatches: vi.fn(), getDispatch: vi.fn(), getDispatchBolPdf: vi.fn() }))
|
||||
const options = { global: { plugins: [createPinia(), ElementPlus], stubs: { SmartDispatchDialog: true, DispatchWorkflowDialog: true } } }
|
||||
const options = { global: { plugins: [createPinia(), ElementPlus], stubs: { SmartDispatchDialog: true, DispatchWorkflowDialog: true, TransportTaskCreateDialog: true } } }
|
||||
beforeEach(() => { vi.clearAllMocks(); identity.id = crypto.randomUUID(); sessionStorage.clear(); can.mockReturnValue(true); vi.stubGlobal('ResizeObserver', class { observe() {} unobserve() {} disconnect() {} }); localStorage.clear(); vi.mocked(listDispatches).mockResolvedValue(page()); vi.mocked(getDispatch).mockResolvedValue(detail()) })
|
||||
it('offers recovery of the original pending command independently of the filtered list rows', async () => {
|
||||
const pending = useDispatchCommandRecovery(() => identity.id); pending.begin('PC-OTHER', 'DEPART', { recordVersion: 0, idempotencyKey: 'original-key' })
|
||||
@@ -66,6 +66,15 @@ it('navigates by the business number without arranging a separate trip', async (
|
||||
const wrapper = mount(DispatchesView, options); await flushPromises(); wrapper.findAllComponents(RowActions).find(entry => entry.props('moreLabel') === '更多操作 · PC/1')!.vm.$emit('action', 'detail'); await flushPromises()
|
||||
expect(push).toHaveBeenCalledWith({ name: 'overseas-dispatch-detail', params: { dispatchNo: 'PC/1' } }); expect(wrapper.text()).not.toContain('安排车次'); wrapper.unmount()
|
||||
})
|
||||
it.each(['PICKUP', 'LOCAL_TRUCK'] as const)('opens %s through the existing dispatch detail and omits warehouse commands', async taskType => {
|
||||
vi.mocked(listDispatches).mockResolvedValueOnce(page([listRow({ taskType })]))
|
||||
const wrapper = mount(DispatchesView, options); await flushPromises()
|
||||
const row = wrapper.findAllComponents(RowActions).find(entry => entry.props('moreLabel') === '更多操作 · PC/1')!
|
||||
expect(row.props('actions')).toEqual([expect.objectContaining({ key: 'detail', label: '运输操作' })])
|
||||
row.vm.$emit('action', 'detail'); await flushPromises()
|
||||
expect(push).toHaveBeenCalledWith({ name: 'overseas-dispatch-detail', params: { dispatchNo: 'PC/1' } })
|
||||
expect(wrapper.text()).toContain('安排到仓取货'); expect(wrapper.text()).toContain('安排本土卡车'); wrapper.unmount()
|
||||
})
|
||||
it('reads one BOL on demand and preserves popup failure handling', async () => {
|
||||
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()
|
||||
})
|
||||
|
||||
@@ -20,6 +20,8 @@ import { getDispatch, getDispatchBolPdf, listDispatches } from '../api/dispatche
|
||||
import { dispatchStages, reviewLabels, stageLabel, type BeforeLoadingSection, type DispatchAction, type DispatchListRow, type DispatchStage, type DispatchStatistics, type DispatchStatus, type ReviewStatus } from '../dispatchContracts'
|
||||
import { canDispatchAction, dispatchActionLabels, dispatchActionPermissions } from '../dispatchPresentation'
|
||||
import { formatOverseasDateTime as time } from '../lastMilePresentation'
|
||||
import TransportTaskCreateDialog from '../components/TransportTaskCreateDialog.vue'
|
||||
import { transportLabels, type TransportType } from '../api/transportTasks'
|
||||
import SmartDispatchDialog from '../components/SmartDispatchDialog.vue'
|
||||
import DispatchWorkflowDialog from '../components/DispatchWorkflowDialog.vue'
|
||||
import { useDispatchCommandRecovery } from '../dispatchCommandRecovery'
|
||||
@@ -29,6 +31,8 @@ const auth = useAuthStore(), recovery = useDispatchCommandRecovery(() => auth.us
|
||||
const rows = ref<DispatchListRow[]>([]), statistics = ref<DispatchStatistics | null>(null)
|
||||
const loading = ref(false), loadError = ref<ApiErrorDetails | null>(null), mutationError = ref<ApiErrorDetails | null>(null)
|
||||
const rejectionError = ref<ApiErrorDetails | null>(null)
|
||||
const transportOpen = ref(false), transportType = ref<TransportType>('PICKUP')
|
||||
function createTransport(type: TransportType) { transportType.value = type; transportOpen.value = true }
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const total = ref(0)
|
||||
@@ -47,6 +51,7 @@ const fields = computed<QueryField[]>(() => [
|
||||
])
|
||||
const columns = computed<DataTableColumn[]>(() => [
|
||||
{ key: 'actions', label: '操作', width: 260, fixed: 'left', slotName: 'actions' },
|
||||
{ key: 'taskType', label: '排车类型', width: 115, slotName: 'taskType' },
|
||||
{ key: 'dispatchNo', prop: 'dispatchNo', label: '排车单号', minWidth: 190 },
|
||||
{ key: 'vehicle', label: '车牌号', minWidth: 130, slotName: 'vehicle' }, { key: 'driver', label: '司机/联系电话', minWidth: 170, slotName: 'driver' },
|
||||
{ key: 'warehouseName', prop: 'warehouseName', label: '起运仓', minWidth: 160 },
|
||||
@@ -74,6 +79,7 @@ function switchSection(value: BeforeLoadingSection) { section.value = value; mod
|
||||
function openDetail(row: DispatchListRow) { void router.push({ name: 'overseas-dispatch-detail', params: { dispatchNo: row.dispatchNo } }) }
|
||||
const rowActionOrder: DispatchAction[] = ['EDIT', 'ADD_CARGO', 'REMOVE_CARGO', 'SUBMIT_REVIEW', 'DISPATCH_REVIEW', 'LOADING_REVIEW', 'RETURN_TO_REVIEW', 'LOADING_COMPLETE', 'ARRANGE_DELIVERY', 'DEPART', 'CANCEL']
|
||||
function rowActions(row: DispatchListRow): RowActionItem[] {
|
||||
if (row.taskType && row.taskType !== 'DELIVERY') return [{ key: 'detail', label: '运输操作', priority: 500 }]
|
||||
return [{ key: 'detail', label: '详情', priority: 500 }, ...rowActionOrder.filter(value => canDispatchAction(row.actions, value, access.can)).map((value, index) => ({ key: value, label: dispatchActionLabels[value], priority: 400 - index * 10, tone: value === 'CANCEL' ? 'danger' as const : undefined })), ...(canDispatchAction(row.actions, 'EDIT', access.can) ? [{ key: 'route-order', label: '调整卸货顺序', priority: 190 }] : []), { key: 'bol', label: '下载BOL', priority: 100, loading: bolLoadingNo.value === row.dispatchNo }]
|
||||
}
|
||||
function handleAction(value: string, row: DispatchListRow) {
|
||||
@@ -117,15 +123,17 @@ onBeforeUnmount(() => { sequence++ })
|
||||
<p v-if="recovery.pending.value" role="status">{{ recovery.pending.value.dispatchNo || '新排车单' }} 的{{ dispatchActionLabels[recovery.pending.value.action] }}{{ recovery.pending.value.result ? '操作已完成,页面资料待刷新。' : '结果尚待核对。' }}<el-button link :disabled="recovery.busy.value" @click="recoverPending">{{ recovery.pending.value.result ? '刷新页面资料' : '核对原请求' }}</el-button></p>
|
||||
<DataTable v-model:page="page" v-model:page-size="pageSize" appearance="integrated" :stripe="false" :data="rows" :columns="columns" :loading="loading" :total="total" :preference-key="preferenceKey" row-key="dispatchNo" empty-text="暂无排车记录" @update:page="load()" @update:page-size="page = 1; load()">
|
||||
<template #toolbar-start><el-button v-if="appointmentReturnTarget(route.query)" link @click="router.push(appointmentReturnTarget(route.query)!)">返回预约工作</el-button><h1>排车列表</h1></template>
|
||||
<template #toolbar-end><el-button v-if="access.can(dispatchActionPermissions.CREATE)" type="primary" @click="createOpen = true">新建排车</el-button></template>
|
||||
<template #toolbar-end><el-button v-if="access.can(dispatchActionPermissions.CREATE)" @click="createTransport('PICKUP')">安排到仓取货</el-button><el-button v-if="access.can(dispatchActionPermissions.CREATE)" @click="createTransport('LOCAL_TRUCK')">安排本土卡车</el-button><el-button v-if="access.can(dispatchActionPermissions.CREATE)" type="primary" @click="createOpen = true">新建排车</el-button></template>
|
||||
<template #filters><div class="integrated-query-group"><div class="density-status-tabs" role="tablist" aria-label="排车执行阶段"><button v-for="item in dispatchStages" :key="item.value" role="tab" :aria-selected="stage === item.value" :class="{ 'is-active': stage === item.value }" @click="switchStage(item.value)">{{ item.label }} <span>{{ statistics?.stageCounts[item.value] ?? '—' }}</span></button></div><div v-if="showSections" class="density-status-tabs" role="tablist" aria-label="装车前审核分区"><button v-for="item in sections" :key="item.value" role="tab" :aria-selected="section === item.value" :class="{ 'is-active': section === item.value }" @click="switchSection(item.value)">{{ item.label }}</button></div><QueryToolbar v-model="model" :fields="fields" :loading="loading" ariaLabel="排车列表筛选" preference-key="overseas-dispatches-query" :search-on-reset="false" @search="search" @reset="search" /></div></template>
|
||||
<template v-if="loadError" #state><LoadErrorState title="排车列表加载失败" :message="loadError.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="loadError" /></template></LoadErrorState></template>
|
||||
<template #actions="{ row }"><RowActions text-actions :actions="rowActions(row as DispatchListRow)" :more-label="`更多操作 · ${row.dispatchNo}`" @action="handleAction($event, row as DispatchListRow)" /><p v-if="!row.actions">操作资格未返回,请刷新核对</p></template>
|
||||
<template #taskType="{ row }">{{ transportLabels[(row.taskType || 'DELIVERY') as keyof typeof transportLabels] }}</template>
|
||||
<template #vehicle="{ row }">{{ row.resources.vehicle?.plate || '—' }}</template><template #driver="{ row }">{{ row.resources.driver?.name || '—' }}<br>{{ row.resources.driver?.phone || '—' }}</template>
|
||||
<template #review="{ row }"><template v-for="review in [section === 'DISPATCH_REVIEW' ? row.reviews.dispatchReview : row.reviews.loadingReview]" :key="review.reviewRound"><div>{{ reviewLabels[review.status as ReviewStatus] }}</div><div v-if="review.reason">{{ review.reason }}</div><div v-if="review.invalidatedReason">{{ review.invalidatedReason }}</div><div>{{ time(review.reviewedAt) }}</div></template></template>
|
||||
<template #counts="{ row }">{{ row.counts.palletCount }}托/{{ row.counts.packageCount }}箱/已拣{{ row.counts.pickedPackageCount }}箱</template><template #planned="{ row }">{{ time(row.plannedDepartureAt) }}</template><template #departure="{ row }">{{ time(row.departedAt) }}</template><template #stage="{ row }">{{ row.status === 'CANCELLED' ? '已取消' : stageLabel(row.stage) }}</template><template #routeMap="{ row }"><el-button link @click="openRoute(row as DispatchListRow)">查看路线</el-button></template>
|
||||
</DataTable>
|
||||
</ListPageLayout></AdminPage>
|
||||
<TransportTaskCreateDialog v-model="transportOpen" :type="transportType" @created="load" />
|
||||
<SmartDispatchDialog v-model="createOpen" :orders="[]" @created="load" />
|
||||
<DispatchWorkflowDialog v-model="actionOpen" :dispatch-no="actionNo" :action="action" :operation="operation" @saved="handleSaved" @rejected="handleRejection" />
|
||||
<TransportRouteDialog v-model="routeOpen" :origin="routeOrigin" :destinations="routeDestinations" />
|
||||
|
||||
Reference in New Issue
Block a user