diff --git a/src/modules/overseas-master-data/index.ts b/src/modules/overseas-master-data/index.ts index 0be6075b..30f63890 100644 --- a/src/modules/overseas-master-data/index.ts +++ b/src/modules/overseas-master-data/index.ts @@ -1,5 +1,6 @@ export { overseasMasterDataRoutes } from './routes' export { inquiryReturnTarget } from './inquiryNavigation' +export { vehicleStatusLabels } from './presentation' export { overseasMasterDataPermissions } from './permissions' export type * from './types' export { default as TransportRouteDialog } from './components/TransportRouteDialog.vue' diff --git a/src/modules/overseas-warehouse/pages/DispatchDetailView.test.ts b/src/modules/overseas-warehouse/pages/DispatchDetailView.test.ts index 23e58ab1..2979b773 100644 --- a/src/modules/overseas-warehouse/pages/DispatchDetailView.test.ts +++ b/src/modules/overseas-warehouse/pages/DispatchDetailView.test.ts @@ -1,7 +1,8 @@ import ElementPlus from 'element-plus' import { flushPromises, mount } from '@vue/test-utils' import { beforeEach, describe, expect, it, vi } from 'vitest' -import { getDispatch } from '../api/dispatches' +import { ApiResponseError } from '@/shared/api/http' +import { getDispatch, getDispatchEvidence } from '../api/dispatches' import DispatchDetailView from './DispatchDetailView.vue' import { detail, destination } from '../dispatchTestFixtures' import { DataTableStub } from './phase5TestStubs' @@ -10,8 +11,11 @@ const push = vi.hoisted(() => vi.fn()) const can = vi.hoisted(() => vi.fn(() => false)) vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can }) })) vi.mock('vue-router', () => ({ useRoute: () => ({ params: { dispatchNo: 'PC/1' }, query }), useRouter: () => ({ push }) })) -vi.mock('../api/dispatches', () => ({ getDispatch: vi.fn() })) -vi.mock('@/modules/overseas-master-data', () => ({ inquiryReturnTarget: (value: Record) => value.fleetOfferNo ? { name: 'overseas-master-data-capacity-offer-detail', params: { offerNo: value.fleetOfferNo }, query: { inquiry: value.fleetBookingNo } } : null })) +vi.mock('../api/dispatches', () => ({ getDispatch: vi.fn(), getDispatchEvidence: vi.fn() })) +vi.mock('@/modules/overseas-master-data', async () => ({ + vehicleStatusLabels: (await vi.importActual('../../overseas-master-data/presentation')).vehicleStatusLabels, + inquiryReturnTarget: (value: Record) => 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 } } } beforeEach(() => { vi.mocked(getDispatch).mockResolvedValue(detail()); can.mockReturnValue(false); push.mockReset(); Object.keys(query).forEach(key => delete query[key]) }) describe('whole-dispatch four-tab detail', () => { @@ -39,6 +43,18 @@ describe('whole-dispatch four-tab detail', () => { expect(wrapper.getComponent(DataTableStub).vm.$attrs.columns).toContainEqual(expect.objectContaining({ prop: 'sourceBusinessNo', label: '来源业务号' })) expect(value.orderDetails[0]!.grossWeightKg).toBeNull(); wrapper.unmount() }) + it.each([ + ['IN_SERVICE', '在役'], ['DISPATCHED', '外派中'], ['MAINTENANCE', '维修中'], ['INACTIVE', '停用'], ['RETURNED', '已归还'], + ['FUTURE_STATUS', '—'], ['toString', '—'], [null, '—'], + ])('presents vehicle status %s through the existing master-data labels or an explicit placeholder', async (status, label) => { + const value = detail(); value.vehicleInfo.vehicle!.vehicleStatus = status + vi.mocked(getDispatch).mockResolvedValueOnce(value) + const wrapper = mount(DispatchDetailView, options); await flushPromises() + const field = wrapper.findAll('.detail-fields > div').find(row => row.find('dt').text() === '车辆状态')! + expect(field.get('dd').text()).toBe(label) + if (status) expect(field.text()).not.toContain(status) + wrapper.unmount() + }) it('renders all customer delivery references and real update time/remark without leaking internal IDs', async () => { const value = detail(); value.documents.deliveries = ['DEL-A', 'DEL-B'].map((deliveryNo, index) => ({ deliveryNo, customerId: `C${index}`, customerName: `客户${index}`, outboundNo: `OUT-${index}`, groupKey: `group-${index}`, destination: destination(), status: 'WAIT_DELIVERY', packageCount: 1, palletCount: 1 })) vi.mocked(getDispatch).mockResolvedValueOnce(value) @@ -49,7 +65,36 @@ describe('whole-dispatch four-tab detail', () => { it('requires a literal service allowance and permission for a write action', async () => { can.mockReturnValue(true); vi.mocked(getDispatch).mockResolvedValueOnce(detail({ actions: { DEPART: { allowed: false, blockCode: 'NOT_ARRANGED', blockReason: '尚未安排派送' } } })) const wrapper = mount(DispatchDetailView, options); await flushPromises() - expect(wrapper.text()).toContain('尚未安排派送'); expect(wrapper.findAll('button').some(button => button.text() === '确认发车')).toBe(false); wrapper.unmount() + expect(wrapper.text()).not.toContain('尚未安排派送'); expect(wrapper.findAll('button').some(button => button.text() === '确认发车')).toBe(false); wrapper.unmount() + }) + it('keeps the allowed Admin action without presenting unrelated stage or PDA permission refusals as page warnings', async () => { + can.mockReturnValue(true) + vi.mocked(getDispatch).mockResolvedValueOnce(detail({ stage: 'AFTER_LOADING', actions: { + ARRANGE_DELIVERY: { allowed: true, blockCode: null, blockReason: null }, + EDIT: { allowed: false, blockCode: 'STAGE', blockReason: '当前阶段或作业事实不满足操作条件' }, + PICK: { allowed: false, blockCode: 'PERMISSION', blockReason: '当前无此操作权限' }, + } })) + const wrapper = mount(DispatchDetailView, options); await flushPromises() + expect(wrapper.text()).toContain('装车后') + expect(wrapper.text()).not.toContain('当前阶段或作业事实不满足操作条件') + expect(wrapper.text()).not.toContain('当前无此操作权限') + 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: 'ARRANGE_DELIVERY' }) + wrapper.unmount() + }) + it('retains a real attachment A0404 failure and its request diagnostics in the vehicle tab', async () => { + const value = detail(); value.vehicleInfo.vehicle!.evidenceFileIds = ['301'] + vi.mocked(getDispatch).mockResolvedValueOnce(value) + vi.mocked(getDispatchEvidence).mockRejectedValueOnce(new ApiResponseError('附件不存在', 'A0404', 404, undefined, 'evidence-request', 'evidence-trace')) + const wrapper = mount(DispatchDetailView, options); await flushPromises() + expect(getDispatchEvidence).toHaveBeenCalledWith('PC/1', 'VEHICLE', '301') + expect(wrapper.text()).toContain('本趟附件操作未完成') + expect(wrapper.text()).toContain('A0404') + expect(wrapper.text()).toContain('evidence-request') + expect(wrapper.text()).toContain('evidence-trace') + expect(value.vehicleInfo.vehicle!.evidenceFileIds).toEqual(['301']) + wrapper.unmount() }) it('retains a real API failure and exposes retry rather than rendering a successful empty detail', async () => { vi.mocked(getDispatch).mockRejectedValueOnce(new Error('permission denied')) diff --git a/src/modules/overseas-warehouse/pages/DispatchDetailView.vue b/src/modules/overseas-warehouse/pages/DispatchDetailView.vue index 3049a957..c6a7d058 100644 --- a/src/modules/overseas-warehouse/pages/DispatchDetailView.vue +++ b/src/modules/overseas-warehouse/pages/DispatchDetailView.vue @@ -2,7 +2,7 @@ import '@/styles/adminDensity.css' import { computed, onBeforeUnmount, ref, watch } from 'vue' import { useRoute, useRouter } from 'vue-router' -import { inquiryReturnTarget } from '@/modules/overseas-master-data' +import { inquiryReturnTarget, vehicleStatusLabels } from '@/modules/overseas-master-data' import { useAccessPreviewStore } from '@/core/access/accessPreview' import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http' import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue' @@ -25,10 +25,13 @@ const operation = ref<'ROUTE' | undefined>() const action = ref('EDIT'), actionOpen = ref(false) const detailActions: DispatchAction[] = ['EDIT', 'ADD_CARGO', 'REMOVE_CARGO', 'SUBMIT_REVIEW', 'DISPATCH_REVIEW', 'LOADING_REVIEW', 'RETURN_TO_REVIEW', 'LOADING_COMPLETE', 'ARRANGE_DELIVERY', 'DEPART', 'CANCEL'] const visibleActions = computed(() => detailActions.filter(value => canDispatchAction(detail.value?.actions, value, access.can))) -const actionBlockReasons = computed(() => [...new Set(Object.values(detail.value?.actions || {}).map(gate => gate?.allowed === true ? '' : gate?.blockReason).filter(Boolean))]) +const vehicleStatus = computed(() => { + const status = detail.value?.vehicleInfo.vehicle?.vehicleStatus + return status && Object.hasOwn(vehicleStatusLabels, status) ? vehicleStatusLabels[status as keyof typeof vehicleStatusLabels] : '—' +}) const vehicleFields = computed(() => [ ['车牌号', text(detail.value?.vehicleInfo.vehicle?.plate)], ['司机', text(detail.value?.vehicleInfo.driver?.name)], - ['联系电话', text(detail.value?.vehicleInfo.driver?.phone)], ['拖车公司名称', text(detail.value?.vehicleInfo.vehicle?.carrierName)], ['车辆状态', text(detail.value?.vehicleInfo.vehicle?.vehicleStatus)], + ['联系电话', text(detail.value?.vehicleInfo.driver?.phone)], ['拖车公司名称', text(detail.value?.vehicleInfo.vehicle?.carrierName)], ['车辆状态', vehicleStatus.value], ]) const dispatchFields = computed(() => [ ['路线', text(detail.value?.dispatchInfo.route)], ['发车时间', time(detail.value?.dispatchInfo.departedAt)], @@ -71,7 +74,7 @@ onBeforeUnmount(() => { generation++ })
-

{{ reason }}

{{ dispatchActionLabels[value] }}调整卸货顺序
+
{{ dispatchActionLabels[value] }}调整卸货顺序
{{ field[0] }}
{{ field[1] }}
{{ field[0] }}
{{ field[1] }}