优化
This commit is contained in:
@@ -22,7 +22,7 @@ const customer = { id: '10001', tenantCode: 'CUS-1', tenantName: '示例客户',
|
||||
const order = { orderId: '20001', orderNo: 'ORD-001', inboundId: '30001', inboundCode: 'IN-001', customerId: '10001', customerName: '示例客户' } as UnifiedOrderResource
|
||||
const secondOrder = { ...order, orderId: '20002', orderNo: 'ORD-002', inboundId: '30002', inboundCode: 'IN-002' } as UnifiedOrderResource
|
||||
const container = { id: '40001', bookingNo: 'BK-001', cabinetNo: 'CAB-001', containerNo: 'MSCU-001' } as ContainerBookingResource
|
||||
const row = { orderId: '20001', orderNo: 'ORD-001', inboundId: '30001', inboundCode: 'IN-001', customerId: '10001', customerName: '示例客户', containerBookingId: '40001', containerNo: 'MSCU-001', dispatchStatus: 'WAITING', version: 1, gate: { passed: true, items: [], checkedAt: '2026-08-11T10:00:00' } } as OutboundDispatchResource
|
||||
const row = { orderId: '20001', orderNo: 'ORD-001', inboundId: '30001', inboundCode: 'IN-001', customerId: '10001', customerName: '示例客户', containerBookingId: '40001', containerNo: 'MSCU-001', oceanBookingStatus: 'RELEASED', truckAppointmentStatus: 'COMPLETED', pickTaskStatus: 'AWAITING_REVIEW', dispatchStatus: 'READY_FOR_DISPATCH', version: 1, gate: { passed: true, items: [], checkedAt: '2026-08-11T10:00:00' } } as OutboundDispatchResource
|
||||
|
||||
enableAutoUnmount(afterEach)
|
||||
|
||||
@@ -65,6 +65,11 @@ describe('OutboundDispatchesView', () => {
|
||||
expect(listOutboundDispatches).toHaveBeenCalledOnce()
|
||||
expect(listOutboundDispatches).toHaveBeenCalledWith({ orderId: '20001', inboundId: '30001', containerBookingId: undefined })
|
||||
expect(wrapper.text()).toContain('ORD-001')
|
||||
expect(wrapper.text()).toContain('已放舱')
|
||||
expect(wrapper.text()).toContain('已完成')
|
||||
expect(wrapper.text()).toContain('待复核')
|
||||
expect(wrapper.text()).toContain('待确认出仓')
|
||||
expect(wrapper.text()).not.toContain('READY_FOR_DISPATCH')
|
||||
})
|
||||
|
||||
it('does not issue a search when reset returns to the initial state', async () => {
|
||||
@@ -132,6 +137,11 @@ describe('OutboundDispatchesView', () => {
|
||||
expect(source).toContain('detail.timeline')
|
||||
expect(source).toContain("'confirm-dispatch'")
|
||||
expect(source).toContain("'reverse-dispatch'")
|
||||
expect(source).toContain("row.dispatchStatus === 'READY_FOR_DISPATCH'")
|
||||
expect(source).toContain("row.dispatchStatus === 'WAITING_FOR_LOADING'")
|
||||
expect(source).toContain('timelineModuleLabel(entry.module)')
|
||||
expect(source).toContain('timelineActionLabel(entry.action)')
|
||||
expect(source).toContain('timelineStatusLabel(entry.module, entry.fromStatus)')
|
||||
expect(source).toContain("error.value?.code === 'A0409'")
|
||||
expect(getOutboundDispatchDetail).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -17,6 +17,7 @@ import { listPlatformUnifiedOrders, type UnifiedOrderResource } from '@/modules/
|
||||
import { listContainers, type ContainerBookingResource } from '@/modules/container-loading'
|
||||
import { changeOutboundDispatch, getOutboundDispatchDetail, listOutboundDispatches } from '../api/platformOutboundDispatch'
|
||||
import type { OutboundDispatchDetail, OutboundDispatchQuery, OutboundDispatchResource } from '../types'
|
||||
import { oceanBookingStatusLabel, outboundDispatchStatusLabel, pickTaskStatusLabel, timelineActionLabel, timelineModuleLabel, timelineStatusLabel, truckStatusLabel } from '../statusLabels'
|
||||
|
||||
type DispatchFilters = QueryModel & { customerId: string; orderId: string; containerBookingId: string }
|
||||
|
||||
@@ -188,15 +189,15 @@ onBeforeUnmount(() => { disposed = true; listRequestId += 1; optionRequestId +=
|
||||
<section v-if="!error" class="surface-panel table-panel">
|
||||
<el-empty v-if="!queried" description="请选择订单 / 入库单或柜次后查询" />
|
||||
<DataTable v-else :data="rows" :columns="columns" :loading="loading" row-key="inboundId" preference-key="outbound-dispatches" empty-text="暂无符合条件的出仓记录">
|
||||
<template #actions="{ row }"><el-button v-if="access.can('platform:firstleg:outbound-dispatch:view')" link type="primary" @click="open(row)"><Eye :size="15" />查看</el-button><el-button v-if="access.can('platform:firstleg:outbound-dispatch:confirm') && row.dispatchStatus !== 'DISPATCHED'" link type="primary" @click="action(row, 'confirm-dispatch')"><Send :size="15" />确认</el-button><el-button v-if="access.can('platform:firstleg:outbound-dispatch:reverse') && row.dispatchStatus === 'DISPATCHED'" link type="danger" @click="action(row, 'reverse-dispatch')"><RotateCcw :size="15" />撤销</el-button></template>
|
||||
<template #booking="{ row }"><StatusBadge :status="row.oceanBookingStatus || '未关联'" /></template><template #truck="{ row }"><StatusBadge :status="row.truckAppointmentStatus || '未关联'" /></template><template #pick="{ row }"><StatusBadge :status="row.pickTaskStatus || '未关联'" /></template><template #status="{ row }"><StatusBadge :status="row.dispatchStatus || '待处理'" /></template><template #gate="{ row }"><el-tag :type="row.gate?.passed ? 'success' : 'danger'">{{ row.gate?.passed ? '通过' : '未通过' }}</el-tag></template>
|
||||
<template #actions="{ row }"><el-button v-if="access.can('platform:firstleg:outbound-dispatch:view')" link type="primary" @click="open(row)"><Eye :size="15" />查看</el-button><el-button v-if="access.can('platform:firstleg:outbound-dispatch:confirm') && row.dispatchStatus === 'READY_FOR_DISPATCH'" link type="primary" @click="action(row, 'confirm-dispatch')"><Send :size="15" />确认</el-button><el-button v-if="access.can('platform:firstleg:outbound-dispatch:reverse') && row.dispatchStatus === 'WAITING_FOR_LOADING'" link type="danger" @click="action(row, 'reverse-dispatch')"><RotateCcw :size="15" />撤销</el-button></template>
|
||||
<template #booking="{ row }"><StatusBadge :status="oceanBookingStatusLabel(row.oceanBookingStatus)" /></template><template #truck="{ row }"><StatusBadge :status="truckStatusLabel(row.truckAppointmentStatus)" /></template><template #pick="{ row }"><StatusBadge :status="pickTaskStatusLabel(row.pickTaskStatus)" /></template><template #status="{ row }"><StatusBadge :status="outboundDispatchStatusLabel(row.dispatchStatus)" /></template><template #gate="{ row }"><el-tag :type="row.gate?.passed ? 'success' : 'danger'">{{ row.gate?.passed ? '通过' : '未通过' }}</el-tag></template>
|
||||
</DataTable>
|
||||
</section>
|
||||
</ListPageLayout>
|
||||
<AppDialog v-model="detailOpen" title="国内出仓详情" width="900px" :show-confirm="false">
|
||||
<LoadErrorState v-if="actionError" title="出仓详情加载失败" :message="actionError.message" :loading="detailLoading" @retry="retryDetail"><template #details><ApiErrorMeta :details="actionError" /></template></LoadErrorState>
|
||||
<div v-else-if="detailLoading" class="detail-loading" aria-busy="true"><el-skeleton :rows="5" animated /></div>
|
||||
<template v-else-if="detail"><el-descriptions :column="2" border><el-descriptions-item label="订单号">{{ detail.aggregate.orderNo }}</el-descriptions-item><el-descriptions-item label="入库单号">{{ detail.aggregate.inboundCode }}</el-descriptions-item><el-descriptions-item label="柜号">{{ detail.aggregate.containerNo || '—' }}</el-descriptions-item><el-descriptions-item label="出仓状态"><StatusBadge :status="detail.aggregate.dispatchStatus || '待处理'" /></el-descriptions-item></el-descriptions><h3>统一门禁</h3><el-table :data="detail.aggregate.gate?.items || []"><el-table-column label="结果" width="80"><template #default="scope"><el-tag :type="scope.row.passed ? 'success' : 'danger'">{{ scope.row.passed ? '通过' : '阻断' }}</el-tag></template></el-table-column><el-table-column prop="name" label="门禁" min-width="140" /><el-table-column prop="actual" label="当前情况" min-width="180" /><el-table-column prop="requirement" label="要求" min-width="180" /></el-table><h3>整体时间线</h3><el-timeline><el-timeline-item v-for="entry in detail.timeline" :key="`${entry.module}-${entry.occurredAt}-${entry.action}`" :timestamp="entry.occurredAt">{{ entry.module }} · {{ entry.action }}:{{ entry.fromStatus || '—' }} → {{ entry.toStatus || '—' }}<small>{{ entry.operatorName || '系统' }} {{ entry.remark || '' }}</small></el-timeline-item></el-timeline></template>
|
||||
<template v-else-if="detail"><el-descriptions :column="2" border><el-descriptions-item label="订单号">{{ detail.aggregate.orderNo }}</el-descriptions-item><el-descriptions-item label="入库单号">{{ detail.aggregate.inboundCode }}</el-descriptions-item><el-descriptions-item label="柜号">{{ detail.aggregate.containerNo || '—' }}</el-descriptions-item><el-descriptions-item label="出仓状态"><StatusBadge :status="outboundDispatchStatusLabel(detail.aggregate.dispatchStatus)" /></el-descriptions-item></el-descriptions><h3>统一门禁</h3><el-table :data="detail.aggregate.gate?.items || []"><el-table-column label="结果" width="80"><template #default="scope"><el-tag :type="scope.row.passed ? 'success' : 'danger'">{{ scope.row.passed ? '通过' : '阻断' }}</el-tag></template></el-table-column><el-table-column prop="name" label="门禁" min-width="140" /><el-table-column prop="actual" label="当前情况" min-width="180" /><el-table-column prop="requirement" label="要求" min-width="180" /></el-table><h3>整体时间线</h3><el-timeline><el-timeline-item v-for="entry in detail.timeline" :key="`${entry.module}-${entry.occurredAt}-${entry.action}`" :timestamp="entry.occurredAt">{{ timelineModuleLabel(entry.module) }} · {{ timelineActionLabel(entry.action) }}:{{ timelineStatusLabel(entry.module, entry.fromStatus) }} → {{ timelineStatusLabel(entry.module, entry.toStatus) }}<small>{{ entry.operatorName || '系统' }} {{ entry.remark || '' }}</small></el-timeline-item></el-timeline></template>
|
||||
</AppDialog>
|
||||
</AdminPage>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { oceanBookingStatusLabel, outboundDispatchStatusLabel, pickTaskStatusLabel, timelineActionLabel, timelineModuleLabel, timelineStatusLabel, truckStatusLabel } from './statusLabels'
|
||||
|
||||
describe('outbound dispatch status labels', () => {
|
||||
it('maps list statuses to business Chinese', () => {
|
||||
expect(outboundDispatchStatusLabel('READY_FOR_DISPATCH')).toBe('待确认出仓')
|
||||
expect(outboundDispatchStatusLabel('WAITING_FOR_LOADING')).toBe('等待装柜')
|
||||
expect(oceanBookingStatusLabel('RELEASED')).toBe('已放舱')
|
||||
expect(truckStatusLabel('PICKUP_IN_PROGRESS')).toBe('提柜中')
|
||||
expect(pickTaskStatusLabel('PENDING')).toBe('待拣货')
|
||||
expect(pickTaskStatusLabel('IN_PROGRESS')).toBe('拣货中')
|
||||
expect(pickTaskStatusLabel('AWAITING_REVIEW')).toBe('待复核')
|
||||
expect(pickTaskStatusLabel('COMPLETED')).toBe('已完成')
|
||||
})
|
||||
|
||||
it('localizes timeline modules, actions and module-specific states', () => {
|
||||
expect(timelineModuleLabel('OUTBOUND_DISPATCH')).toBe('国内出仓')
|
||||
expect(timelineActionLabel('CONFIRM_DISPATCH')).toBe('确认出仓')
|
||||
expect(timelineStatusLabel('OUTBOUND_DISPATCH', 'READY_FOR_DISPATCH')).toBe('待确认出仓')
|
||||
expect(timelineStatusLabel('OCEAN_BOOKING', 'RELEASED')).toBe('已放舱')
|
||||
expect(timelineStatusLabel('TRUCK_APPOINTMENT', 'COMPLETED')).toBe('已完成')
|
||||
expect(timelineStatusLabel('INBOUND', 'INBOUNDED')).toBe('已入库')
|
||||
})
|
||||
|
||||
it('does not expose unknown backend enum values', () => {
|
||||
expect(outboundDispatchStatusLabel('NEW_BACKEND_STATE')).toBe('未知状态')
|
||||
expect(timelineModuleLabel('NEW_MODULE')).toBe('其他业务流程')
|
||||
expect(timelineActionLabel('NEW_ACTION')).toBe('状态更新')
|
||||
expect(timelineStatusLabel('NEW_MODULE', 'NEW_STATE')).toBe('未知状态')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,111 @@
|
||||
import { truckAppointmentStatusLabels } from '@/modules/trucking'
|
||||
import { oceanBookingStatusLabel as sharedOceanBookingStatusLabel } from '@/modules/ocean-bookings'
|
||||
|
||||
const dispatchStatusLabels: Record<string, string> = {
|
||||
READY_FOR_DISPATCH: '待确认出仓',
|
||||
WAITING_FOR_LOADING: '等待装柜',
|
||||
}
|
||||
|
||||
const pickTaskStatusLabels: Record<string, string> = {
|
||||
PENDING: '待拣货',
|
||||
IN_PROGRESS: '拣货中',
|
||||
AWAITING_REVIEW: '待复核',
|
||||
COMPLETED: '已完成',
|
||||
CANCELLED: '已取消',
|
||||
}
|
||||
|
||||
const inboundStatusLabels: Record<string, string> = {
|
||||
WAIT_MEASURE: '待测量',
|
||||
MEASURING: '测量中',
|
||||
MEASURE_PENDING: '待测量审核',
|
||||
MEASURE_REJECTED: '测量已驳回',
|
||||
WAIT_PRICING: '待计费',
|
||||
FEE_CONFIRMING: '费用处理中',
|
||||
INBOUNDING: '入库处理中',
|
||||
INBOUNDED: '已入库',
|
||||
VOIDED: '已作废',
|
||||
}
|
||||
|
||||
const loadingPlanStatusLabels: Record<string, string> = {
|
||||
WAIT_LOADING: '等待装柜',
|
||||
EDITING: '编辑中',
|
||||
PRELOADED: '已预装',
|
||||
CONFIRMED: '已确认',
|
||||
LOADING: '装柜中',
|
||||
COMPLETED: '已完成',
|
||||
CANCELLED: '已取消',
|
||||
}
|
||||
|
||||
const moduleLabels: Record<string, string> = {
|
||||
INBOUND: '入库作业',
|
||||
LOADING_PLAN: '装柜计划',
|
||||
PDA_PICK: 'PDA 拣货',
|
||||
LOADING_EVIDENCE: '装柜证据',
|
||||
OCEAN_BOOKING: '海运订舱',
|
||||
TRUCK_APPOINTMENT: '拖车预约',
|
||||
OUTBOUND_DISPATCH: '国内出仓',
|
||||
}
|
||||
|
||||
const actionLabels: Record<string, string> = {
|
||||
CREATE: '创建',
|
||||
UPDATE: '更新',
|
||||
SUBMIT: '提交',
|
||||
APPROVE: '审核通过',
|
||||
REJECT: '审核驳回',
|
||||
CONFIRM: '确认',
|
||||
RELEASE: '放舱',
|
||||
FAIL: '标记失败',
|
||||
CANCEL: '取消',
|
||||
CHANGE_REQUEST: '申请改船',
|
||||
REQUEST_CHANGE: '申请改船',
|
||||
CONFIRM_CHANGE: '确认改船',
|
||||
FAIL_CHANGE: '驳回改船',
|
||||
CONFIRM_DISPATCH: '确认出仓',
|
||||
REVERSE_DISPATCH: '撤销出仓',
|
||||
UPSERT_EVIDENCE: '登记装柜证据',
|
||||
VOID_EVIDENCE: '作废装柜证据',
|
||||
START: '开始作业',
|
||||
COMPLETE: '完成作业',
|
||||
}
|
||||
|
||||
function mapped(value: string | null | undefined, labels: Record<string, string>, empty: string) {
|
||||
if (!value) return empty
|
||||
return labels[value] || '未知状态'
|
||||
}
|
||||
|
||||
export function outboundDispatchStatusLabel(value: string | null | undefined) {
|
||||
return mapped(value, dispatchStatusLabels, '待确认出仓')
|
||||
}
|
||||
|
||||
export function oceanBookingStatusLabel(value: string | null | undefined) {
|
||||
return sharedOceanBookingStatusLabel(value, '未关联')
|
||||
}
|
||||
|
||||
export function truckStatusLabel(value: string | null | undefined) {
|
||||
return mapped(value, truckAppointmentStatusLabels as Record<string, string>, '未关联')
|
||||
}
|
||||
|
||||
export function pickTaskStatusLabel(value: string | null | undefined) {
|
||||
return mapped(value, pickTaskStatusLabels, '未关联')
|
||||
}
|
||||
|
||||
export function timelineModuleLabel(value: string | null | undefined) {
|
||||
if (!value) return '业务流程'
|
||||
return moduleLabels[value] || '其他业务流程'
|
||||
}
|
||||
|
||||
export function timelineActionLabel(value: string | null | undefined) {
|
||||
if (!value) return '状态更新'
|
||||
return actionLabels[value] || '状态更新'
|
||||
}
|
||||
|
||||
export function timelineStatusLabel(module: string | null | undefined, value: string | null | undefined) {
|
||||
if (!value) return '—'
|
||||
if (module === 'OUTBOUND_DISPATCH') return outboundDispatchStatusLabel(value)
|
||||
if (module === 'OCEAN_BOOKING') return oceanBookingStatusLabel(value)
|
||||
if (module === 'TRUCK_APPOINTMENT') return truckStatusLabel(value)
|
||||
if (module === 'PDA_PICK') return pickTaskStatusLabel(value)
|
||||
if (module === 'INBOUND') return mapped(value, inboundStatusLabels, '—')
|
||||
if (module === 'LOADING_PLAN') return mapped(value, loadingPlanStatusLabels, '—')
|
||||
return '未知状态'
|
||||
}
|
||||
Reference in New Issue
Block a user