Merge branch 'release/test' of http://8.163.30.128:10082/html/admin-web into release/test

This commit is contained in:
2026-08-11 20:11:01 +08:00
103 changed files with 3854 additions and 622 deletions
@@ -214,6 +214,8 @@ export interface UnifiedOrderDetailResource {
export interface UnifiedOrderFeeDetailResource {
id: number | string
sourceFeeId?: number | string | null
sourceFeeVersion?: number | null
inboundId: number | string
inboundCode: string | null
reviewStatus: string | null
@@ -389,5 +391,5 @@ export async function listPlatformUnifiedOrderFeeDetails(accountId: string, inbo
}
export function unifiedOrderStatusLabel(status: UnifiedOrderMainStatus) {
return unifiedOrderStatusOptions.find((item) => item.value === status)?.label || status
return unifiedOrderStatusOptions.find((item) => item.value === status)?.label || '未知状态'
}
@@ -20,4 +20,11 @@ describe('OrderFulfillmentTreePanel contract', () => {
expect(source).toContain('暂无拆分分片')
expect(source).toContain('@media (max-width: 720px)')
})
it('translates fragment roles and statuses instead of exposing backend enums', () => {
expect(source).toContain('unifiedOrderFulfillmentRoleLabel(row.splitRole)')
expect(source).toContain('unifiedOrderFulfillmentStatusLabel(row.status, row.splitRole)')
expect(source).not.toContain(':status="row.status')
expect(source).not.toContain('return role ||')
})
})
@@ -5,6 +5,7 @@ import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import StatusBadge from '@/shared/components/StatusBadge.vue'
import { getPlatformOrderFulfillmentTree, type UnifiedOrderFulfillmentTree } from '../api/orderFulfillment'
import { unifiedOrderFulfillmentRoleLabel, unifiedOrderFulfillmentStatusLabel } from '../status'
const props = defineProps<{ tenantId: string; orderId: string; compact?: boolean }>()
const tree = ref<UnifiedOrderFulfillmentTree | null>(null)
@@ -19,12 +20,6 @@ async function load() {
finally { loading.value = false }
}
function roleLabel(role: string | null) {
if (role === 'REMAINDER' || role === 'ACTIVE_REMAINDER') return '当前余量'
if (role === 'LOADED' || role === 'COMPLETED_FRAGMENT' || role === 'SOURCE_COMPLETED') return '已完成分片'
return role || '分片'
}
onMounted(() => { void load() })
</script>
@@ -44,11 +39,11 @@ onMounted(() => { void load() })
<el-alert v-if="tree.historicalTwoChildModel" type="info" :closable="false" show-icon title="该记录使用历史双子单模型,仅兼容展示;新拆分不会再生成已装子单。" />
<el-table :data="tree.fragments" row-key="orderId" empty-text="暂无拆分分片">
<el-table-column label="分片订单" min-width="180"><template #default="{ row }"><strong>{{ row.orderNo || row.orderId }}</strong><small v-if="row.splitSequence != null"> · 第 {{ row.splitSequence }} 次</small></template></el-table-column>
<el-table-column label="类型" width="120"><template #default="{ row }">{{ roleLabel(row.splitRole) }}</template></el-table-column>
<el-table-column label="类型" width="120"><template #default="{ row }">{{ unifiedOrderFulfillmentRoleLabel(row.splitRole) }}</template></el-table-column>
<el-table-column label="本单计划" width="100"><template #default="{ row }">{{ row.plannedPackageCount ?? '—' }} 箱</template></el-table-column>
<el-table-column label="已拣" width="90"><template #default="{ row }">{{ row.pickedPackageCount ?? '—' }}</template></el-table-column>
<el-table-column label="未拣" width="90"><template #default="{ row }">{{ row.unpickedPackageCount ?? '—' }}</template></el-table-column>
<el-table-column label="状态" width="130"><template #default="{ row }"><StatusBadge :status="row.status || roleLabel(row.splitRole)" /></template></el-table-column>
<el-table-column label="状态" width="130"><template #default="{ row }"><StatusBadge :status="unifiedOrderFulfillmentStatusLabel(row.status, row.splitRole)" /></template></el-table-column>
<el-table-column label="柜次" min-width="150"><template #default="{ row }">{{ row.containerBookingId || '—' }}</template></el-table-column>
</el-table>
</template>
+15
View File
@@ -0,0 +1,15 @@
export {
listPlatformUnifiedOrderFeeDetails,
listPlatformUnifiedOrders,
type UnifiedOrderFeeDetailResource,
type UnifiedOrderResource,
} from './api/unifiedOrders'
export {
unifiedOrderFulfillmentRoleLabel,
unifiedOrderFulfillmentStatusLabel,
unifiedOrderHistoryActionLabel,
unifiedOrderLoadingStatusLabel,
unifiedOrderReviewStatusLabel,
unifiedOrderStatusLabel,
} from './status'
@@ -0,0 +1,118 @@
import { flushPromises, shallowMount } from '@vue/test-utils'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { listPlatformUnifiedOrders } from '@/modules/unified-orders/api/unifiedOrders'
import UnifiedOrdersView from './UnifiedOrdersView.vue'
vi.mock('vue-router', () => ({
useRoute: () => ({ params: {}, query: {} }),
useRouter: () => ({ push: vi.fn() }),
}))
vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: () => true }) }))
vi.mock('@/modules/iam', () => ({ listPlatformTenants: vi.fn().mockResolvedValue({ records: [], page: 1, size: 200, total: 0, pages: 0 }) }))
vi.mock('@/modules/warehouses', () => ({ listAllPlatformWarehouses: vi.fn().mockResolvedValue([]) }))
vi.mock('@/modules/inbounds', () => ({ downloadWeightImage: vi.fn(), getInboundMeasurement: vi.fn() }))
vi.mock('@/modules/unified-orders/workflows', () => ({ UnifiedOrderWorkflowPanel: { template: '<div />' } }))
vi.mock('@/modules/unified-orders/api/unifiedOrders', () => ({
addPlatformUnifiedOrderNote: vi.fn(),
approvePlatformUnifiedOrder: vi.fn(),
getPlatformUnifiedOrder: vi.fn(),
listPlatformUnifiedOrderFeeDetails: vi.fn(),
listPlatformUnifiedOrders: vi.fn(),
unifiedOrderStatusOptions: [
{ value: 'PENDING_REVIEW', label: '待审核' },
{ value: 'WAIT_MEASURE', label: '待测量数据' },
{ value: 'INBOUNDED', label: '已入库' },
{ value: 'WAIT_SPLIT', label: '待分柜' },
{ value: 'LOADING', label: '装柜中' },
{ value: 'LOADED', label: '装柜完成' },
{ value: 'DELIVERED', label: '派送完成' },
],
}))
function emptyPage() {
return { records: [], page: 1, size: 20, total: 0, pages: 0 }
}
function mountView() {
return shallowMount(UnifiedOrdersView, {
global: {
directives: { loading: () => undefined },
stubs: {
AdminPage: { template: '<main><slot /></main>' },
ListPageLayout: { template: '<section><slot name="toolbar" /><slot name="state" /><slot /></section>' },
QueryToolbar: { name: 'QueryToolbar', props: ['modelValue'], emits: ['update:modelValue', 'search', 'reset'], template: '<div data-testid="query-toolbar" />' },
DataTable: { name: 'DataTable', props: ['page', 'pageSize'], emits: ['update:page', 'update:pageSize'], template: '<div data-testid="data-table" />' },
'el-button': { template: '<button><slot /></button>' },
},
},
})
}
describe('UnifiedOrdersView list request coordination', () => {
beforeEach(() => {
vi.mocked(listPlatformUnifiedOrders).mockReset().mockResolvedValue(emptyPage())
})
it('loads once initially and once for query, reset, status tab and pagination', async () => {
const wrapper = mountView()
await flushPromises()
expect(listPlatformUnifiedOrders).toHaveBeenCalledTimes(1)
const toolbar = wrapper.getComponent({ name: 'QueryToolbar' })
toolbar.vm.$emit('search')
await flushPromises()
expect(listPlatformUnifiedOrders).toHaveBeenCalledTimes(2)
toolbar.vm.$emit('reset')
await flushPromises()
expect(listPlatformUnifiedOrders).toHaveBeenCalledTimes(3)
await wrapper.findAll('.status-tab')[1]!.trigger('click')
await flushPromises()
expect(listPlatformUnifiedOrders).toHaveBeenCalledTimes(4)
wrapper.getComponent({ name: 'DataTable' }).vm.$emit('update:page', 2)
await flushPromises()
expect(listPlatformUnifiedOrders).toHaveBeenCalledTimes(5)
})
it('does not apply draft filters to pagination until query is submitted', async () => {
const wrapper = mountView()
await flushPromises()
const toolbar = wrapper.getComponent({ name: 'QueryToolbar' })
const draft = { ...toolbar.props('modelValue'), inboundCode: ' DRAFT-CODE ' }
toolbar.vm.$emit('update:modelValue', draft)
await wrapper.vm.$nextTick()
wrapper.getComponent({ name: 'DataTable' }).vm.$emit('update:page', 2)
await flushPromises()
expect(listPlatformUnifiedOrders).toHaveBeenLastCalledWith(expect.objectContaining({ page: 2, inboundCode: undefined }))
toolbar.vm.$emit('search')
await flushPromises()
expect(listPlatformUnifiedOrders).toHaveBeenCalledTimes(3)
expect(listPlatformUnifiedOrders).toHaveBeenLastCalledWith(expect.objectContaining({ page: 1, inboundCode: 'DRAFT-CODE' }))
})
it('resets a later page and changes page size with one request per interaction', async () => {
const wrapper = mountView()
await flushPromises()
const table = wrapper.getComponent({ name: 'DataTable' })
const toolbar = wrapper.getComponent({ name: 'QueryToolbar' })
table.vm.$emit('update:page', 3)
await flushPromises()
expect(listPlatformUnifiedOrders).toHaveBeenCalledTimes(2)
toolbar.vm.$emit('reset')
await flushPromises()
expect(listPlatformUnifiedOrders).toHaveBeenCalledTimes(3)
expect(listPlatformUnifiedOrders).toHaveBeenLastCalledWith(expect.objectContaining({ page: 1 }))
table.vm.$emit('update:pageSize', 50)
table.vm.$emit('update:page', 1)
await flushPromises()
expect(listPlatformUnifiedOrders).toHaveBeenCalledTimes(4)
expect(listPlatformUnifiedOrders).toHaveBeenLastCalledWith(expect.objectContaining({ page: 1, size: 50 }))
})
})
@@ -19,7 +19,7 @@ describe('UnifiedOrdersView contract', () => {
it('renders accessible current-status tabs with keyboard navigation', () => {
expect(source).toContain('role="tablist"')
expect(source).toContain('role="tab"')
expect(source).toContain(':aria-selected="filters.mainStatus === tab.value"')
expect(source).toContain(':aria-selected="selectedStatus === tab.value"')
expect(source).toContain("event.key === 'ArrowRight'")
expect(source).toContain("event.key === 'ArrowLeft'")
expect(source).toContain("event.key === 'Home'")
@@ -38,8 +38,8 @@ describe('UnifiedOrdersView contract', () => {
expect(source).toContain('listAllPlatformWarehouses')
expect(source).toContain("{ status: 'ACTIVE' }")
expect(source).toContain("warehouse.warehouseType === 'DOMESTIC'")
expect(source).toContain("loaded: optionalBoolean(filters.value.loaded)")
expect(source).toContain("fullContainer: optionalBoolean(filters.value.fullContainer)")
expect(source).toContain("loaded: optionalBoolean(activeFilters.loaded)")
expect(source).toContain("fullContainer: optionalBoolean(activeFilters.fullContainer)")
expect(source).toContain("{ value: 'UNRECORDED', label: '未录入' }")
expect(source).not.toContain("{ value: 'DRAFT', label: '草稿' }")
expect(source).not.toContain("{ value: 'CANCELLED', label: '已取消' }")
@@ -59,7 +59,7 @@ describe('UnifiedOrdersView contract', () => {
})
it('uses tabs as the only order-status filter and keeps advanced controls inside QueryToolbar', () => {
expect(source).toContain('filters.value.mainStatus || undefined')
expect(source).toContain('mainStatus: selectedStatus.value || undefined')
expect(source).toContain('<template #actions>')
expect(source).toContain(':aria-expanded="showAdvancedFilters"')
expect(source).not.toContain('class="filter-mode-actions"')
@@ -69,9 +69,9 @@ describe('UnifiedOrdersView contract', () => {
it('covers real list states and keeps operation column first and left-fixed', () => {
expect(source).toContain("{ key: 'actions', label: '操作', width: 300, fixed: 'left'")
expect(source).toContain("{ key: 'selection', type: 'selection', width: 52, fixed: 'left'")
expect(source).toContain('批量审核接口待接入')
expect(source).toContain('@selection-change="handleSelectionChange"')
expect(source).not.toContain("{ key: 'selection', type: 'selection'")
expect(source).not.toContain('批量审核接口待接入')
expect(source).not.toContain('@selection-change="handleSelectionChange"')
expect(source).toContain('title="订单列表暂时无法加载"')
expect(source).toContain('empty-text="暂无符合条件的订单"')
expect(source).toContain(':loading="loading"')
@@ -79,7 +79,7 @@ describe('UnifiedOrdersView contract', () => {
expect(source).toContain('v-model:page-size="pageSize"')
expect(source).toContain('slotName: \'orderStatus\'')
expect(source).toContain('slotName: \'measurementReviewStatus\'')
expect(source).toContain("NOT_SUBMITTED: '未提交'")
expect(source).toContain('unifiedOrderReviewStatusLabel(value, \'未录入\')')
expect(source).toContain("{ key: 'customerRemark', prop: 'customerRemark', label: '备注'")
expect(source).toContain("{ key: 'latestInternalNote', prop: 'latestInternalNote', label: '客服内部备注'")
expect(source.match(/<LineClampText v-if="column\.key === 'remark'"/g)).toHaveLength(3)
@@ -102,6 +102,14 @@ describe('UnifiedOrdersView contract', () => {
expect(source).toContain('v-if="moreUnifiedOrderActions(row as UnifiedOrderResource).length"')
})
it('keeps editable query values separate from the applied list request', () => {
expect(source).toContain('const appliedFilters = ref<FilterModel>(emptyFilters())')
expect(source).toContain('const activeFilters = appliedFilters.value')
expect(source).toContain('appliedFilters.value = cloneFilters(filters.value)')
expect(source).toContain('appliedFilters.value = emptyFilters()')
expect(source).not.toContain('filters.value.mainStatus')
})
it('keeps only confirmed business actions as explicit boundaries', () => {
expect(source).toContain('nextActionBoundary')
expect(source).not.toContain('class="action-boundary"')
@@ -148,6 +156,10 @@ describe('UnifiedOrdersView contract', () => {
expect(source).toContain(':title="moreUnifiedOrderActionsLabel(row as UnifiedOrderResource)"')
expect(source).toContain("can('platform:order:note')")
expect(source).toContain('await loadOrders()')
expect(source).toContain('orderStatusLabel(detail.summary.internalOrderStatus || detail.summary.mainStatus)')
expect(source).toContain('feeReviewLabel(detail.summary.feeReviewStatus)')
expect(source).toContain('unifiedOrderLoadingStatusLabel(detail.summary.transport?.loadingStatus)')
expect(source).not.toContain("value.replaceAll('_', ' ')")
})
it('exposes the single-remainder fulfillment aggregate in row expansion and detail', () => {
@@ -188,6 +200,9 @@ describe('UnifiedOrdersView contract', () => {
expect(source).not.toContain("partyValue(detail.order.parties, 'CONSIGNEE'")
expect(source).toContain("cargoTotal(detail.order.cargoes, 'netWeightKg')")
expect(source).toContain('暂无费用数据')
expect(source).toContain('firstLegTransportModeLabel(detail.order.order?.transportMode)')
expect(source).toContain('firstLegPickupMethodLabel(detail.order.order?.pickupMethod)')
expect(source).toContain('firstLegDestinationTypeLabel(detail.order.order?.destinationType)')
})
it('mounts downstream progress inside the existing order detail without opening unsafe writes', () => {
@@ -30,7 +30,22 @@ import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import LineClampText from '@/shared/components/LineClampText.vue'
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
import StatusBadge from '@/shared/components/StatusBadge.vue'
import {
firstLegBusinessTypeLabel,
firstLegContainerModeLabel,
firstLegDestinationTypeLabel,
firstLegPickupMethodLabel,
firstLegTaxModeLabel,
firstLegTradeTermLabel,
firstLegTransportModeLabel,
} from '@/shared/utils/firstLegBusinessLabels'
import { UnifiedOrderWorkflowPanel } from '@/modules/unified-orders/workflows'
import {
unifiedOrderHistoryActionLabel,
unifiedOrderLoadingStatusLabel,
unifiedOrderReviewStatusLabel,
unifiedOrderStatusLabel,
} from '@/modules/unified-orders/status'
import {
downloadWeightImage,
getInboundMeasurement,
@@ -39,7 +54,6 @@ import {
type FilterModel = QueryModel & {
tenantId: string
mainStatus: UnifiedOrderMainStatus | ''
receivingWarehouseId: string
destinationWarehouseId: string
inboundCode: string
@@ -60,7 +74,6 @@ const route = useRoute()
const router = useRouter()
const access = useAccessPreviewStore()
const records = ref<UnifiedOrderResource[]>([])
const selectedOrders = ref<UnifiedOrderResource[]>([])
const customers = ref<PlatformTenantResource[]>([])
const warehouses = ref<PlatformWarehouseSummaryResource[]>([])
const page = ref(1)
@@ -96,6 +109,8 @@ let detailLoadingKey = ''
let detailRequestSerial = 0
const showAdvancedFilters = ref(false)
const filters = ref<FilterModel>(emptyFilters())
const appliedFilters = ref<FilterModel>(emptyFilters())
const selectedStatus = ref<UnifiedOrderMainStatus | ''>('')
const orderId = computed(() => typeof route.params.orderId === 'string' ? route.params.orderId : '')
const detailTenantId = computed(() => typeof route.query.tenantId === 'string' ? route.query.tenantId : '')
const detailRouteKey = computed(() => orderId.value ? `${detailTenantId.value}:${orderId.value}` : '')
@@ -182,7 +197,6 @@ const queryFields = computed(() => showAdvancedFilters.value
const columns: DataTableColumn[] = [
{ key: 'actions', label: '操作', width: 300, fixed: 'left', slotName: 'actions' },
{ key: 'selection', type: 'selection', width: 52, fixed: 'left', align: 'center', selectable: canBulkReview },
{ key: 'fulfillment', type: 'expand', width: 52, fixed: 'left', slotName: 'fulfillment', hideable: false, movable: false },
{ key: 'inboundCode', prop: 'inboundCode', label: '入仓码', minWidth: 140, slotName: 'field' },
{ key: 'internalOrderStatus', prop: 'internalOrderStatus', label: '订单状态', minWidth: 130, slotName: 'orderStatus' },
@@ -240,13 +254,17 @@ const columns: DataTableColumn[] = [
function emptyFilters(): FilterModel {
return {
tenantId: '', mainStatus: '', receivingWarehouseId: '', destinationWarehouseId: '',
tenantId: '', receivingWarehouseId: '', destinationWarehouseId: '',
inboundCode: '', expectedArrivalDate: '', feeReviewStatus: '', channelName: '',
containerNo: '', cabinetNo: '', sailingDate: '', loaded: '', fullContainer: '', measurementReviewStatus: '',
fbaCode: '', createdRange: [],
}
}
function cloneFilters(source: FilterModel): FilterModel {
return { ...source, createdRange: [...source.createdRange] }
}
function trimmed(value: string) {
return value.trim() || undefined
}
@@ -256,8 +274,10 @@ function optionalBoolean(value: string) {
}
function selectStatus(status: UnifiedOrderMainStatus | '') {
filters.value = { ...filters.value, mainStatus: status }
search()
if (selectedStatus.value === status) return
selectedStatus.value = status
if (page.value === 1) void loadOrders()
else page.value = 1
}
function handleTabKeydown(event: KeyboardEvent, index: number) {
@@ -338,34 +358,29 @@ function displayField(row: UnifiedOrderResource, property: string | undefined) {
if (property.endsWith('At') || property.endsWith('Date')) return formatDate(String(value))
if (property === 'measurementReviewStatus') return measurementStatusLabel(String(value))
if (property === 'internalOrderStatus') return orderStatusLabel(String(value))
if (property === 'feeReviewStatus') return feeReviewLabel(String(value))
if (property === 'loadingStatus') return unifiedOrderLoadingStatusLabel(String(value))
if (property === 'businessType') return firstLegBusinessTypeLabel(String(value))
if (property === 'pickupMethod') return firstLegPickupMethodLabel(String(value))
if (property === 'taxMode') return firstLegTaxModeLabel(String(value))
if (property === 'destinationType') return firstLegDestinationTypeLabel(String(value))
return String(value)
}
function statusLabel(value: UnifiedOrderMainStatus | string | null) {
return unifiedOrderStatusOptions.find((item) => item.value === value)?.label || orderStatusLabel(value) || '状态更新'
return unifiedOrderStatusLabel(value, '状态更新')
}
function historyActionLabel(value: string | null | undefined) {
return ({
CREATE: '创建订单', CREATED: '创建订单', SUBMITTED: '提交订单', APPROVED: '审核通过',
ORDER_APPROVED: '订单审核通过', REJECTED: '审核驳回', MEASURE_APPROVE: '测量审核通过',
MEASURE_REJECT: '测量审核驳回', INBOUND_BEGIN: '开始入库', INBOUND_COMPLETE: '入库完成',
UI_FIXTURE_MEASURE_APPROVE: '测量数据审核通过', UI_FIXTURE_INBOUND_BEGIN: '开始入库',
STATUS_UPDATE: '状态更新',
} as Record<string, string>)[value || ''] || (value ? value.replaceAll('_', ' ') : '状态更新')
return unifiedOrderHistoryActionLabel(value)
}
function measurementStatusLabel(value: string | null | undefined) {
return ({ NOT_SUBMITTED: '未提交', UNRECORDED: '未录入', PENDING: '待审核', APPROVED: '审核通过', REJECTED: '审核驳回' } as Record<string, string>)[value || ''] || value || '未录入'
return unifiedOrderReviewStatusLabel(value, '未录入')
}
function orderStatusLabel(value: string | null | undefined) {
return ({
DRAFT: '草稿', SUBMITTED: '待审核', APPROVED: '已审核', ACCEPTED: '已受理', REJECTED: '已驳回',
PROCESSING: '处理中', COMPLETED: '已完成', CANCELLED: '已取消', PENDING_REVIEW: '待审核',
WAIT_MEASURE: '待测量数据', INBOUNDED: '已入库', WAIT_SPLIT: '待分柜', LOADING: '装柜中',
LOADED: '装柜完成', DELIVERED: '派送完成',
} as Record<string, string>)[value || ''] || value || '—'
return unifiedOrderStatusLabel(value)
}
function cargoValue(cargo: Record<string, unknown>, key: string) {
@@ -398,11 +413,12 @@ function feeNumber(value: number | null | undefined) {
}
function feeReviewLabel(value: string | null | undefined) {
return ({ PENDING: '待审核', APPROVED: '审核通过', REJECTED: '审核驳回', NOT_SUBMITTED: '未提交' } as Record<string, string>)[value || ''] || value || '—'
return unifiedOrderReviewStatusLabel(value)
}
function feeDirectionLabel(value: string | null | undefined) {
return ({ RECEIVABLE: '应收', PAYABLE: '应付' } as Record<string, string>)[value || ''] || value || '—'
if (!value) return '—'
return ({ RECEIVABLE: '应收', PAYABLE: '应付' } as Record<string, string>)[value] || '未知方向'
}
async function loadFeeDetails() {
@@ -508,19 +524,6 @@ function canApprove(row: UnifiedOrderResource) {
return row.mainStatus === 'PENDING_REVIEW' && row.nextAction === 'APPROVE_ORDER' && can('platform:order:approve')
}
function canBulkReview(row: UnifiedOrderResource) {
return canApprove(row)
}
function handleSelectionChange(rows: UnifiedOrderResource[]) {
selectedOrders.value = rows
}
function openBulkReviewPlaceholder() {
if (selectedOrders.value.length === 0) return
ElMessage.info('批量审核接口待接入,请先逐笔审核')
}
function openApproval(row: UnifiedOrderResource) {
if (!canApprove(row)) return
approvalOrder.value = row
@@ -625,30 +628,30 @@ async function loadOrders() {
loading.value = true
loadError.value = null
try {
const range = filters.value.createdRange
const activeFilters = appliedFilters.value
const range = activeFilters.createdRange
const result = await listPlatformUnifiedOrders({
tenantId: filters.value.tenantId || undefined,
mainStatus: filters.value.mainStatus || undefined,
receivingWarehouseId: filters.value.receivingWarehouseId || undefined,
destinationWarehouseId: filters.value.destinationWarehouseId || undefined,
inboundCode: trimmed(filters.value.inboundCode),
expectedArrivalDate: filters.value.expectedArrivalDate || undefined,
feeReviewStatus: filters.value.feeReviewStatus || undefined,
channelName: trimmed(filters.value.channelName),
containerNo: trimmed(filters.value.containerNo),
cabinetNo: trimmed(filters.value.cabinetNo),
sailingDate: filters.value.sailingDate || undefined,
loaded: optionalBoolean(filters.value.loaded),
fullContainer: optionalBoolean(filters.value.fullContainer),
measurementReviewStatus: filters.value.measurementReviewStatus || undefined,
fbaCode: trimmed(filters.value.fbaCode),
tenantId: activeFilters.tenantId || undefined,
mainStatus: selectedStatus.value || undefined,
receivingWarehouseId: activeFilters.receivingWarehouseId || undefined,
destinationWarehouseId: activeFilters.destinationWarehouseId || undefined,
inboundCode: trimmed(activeFilters.inboundCode),
expectedArrivalDate: activeFilters.expectedArrivalDate || undefined,
feeReviewStatus: activeFilters.feeReviewStatus || undefined,
channelName: trimmed(activeFilters.channelName),
containerNo: trimmed(activeFilters.containerNo),
cabinetNo: trimmed(activeFilters.cabinetNo),
sailingDate: activeFilters.sailingDate || undefined,
loaded: optionalBoolean(activeFilters.loaded),
fullContainer: optionalBoolean(activeFilters.fullContainer),
measurementReviewStatus: activeFilters.measurementReviewStatus || undefined,
fbaCode: trimmed(activeFilters.fbaCode),
createdFrom: range.length === 2 ? `${range[0]}T00:00:00` : undefined,
createdTo: range.length === 2 ? `${range[1]}T23:59:59` : undefined,
page: page.value,
size: pageSize.value,
})
records.value = result.records
selectedOrders.value = []
total.value = result.total
} catch (error) {
records.value = []
@@ -660,6 +663,7 @@ async function loadOrders() {
}
function search() {
appliedFilters.value = cloneFilters(filters.value)
if (page.value !== 1) {
page.value = 1
return
@@ -669,6 +673,7 @@ function search() {
function reset() {
filters.value = emptyFilters()
appliedFilters.value = emptyFilters()
if (page.value === 1) void loadOrders()
else page.value = 1
}
@@ -838,9 +843,9 @@ onMounted(() => {
<div class="panel-title"><div><h2>订单基本信息</h2><p>{{ detail.summary.orderNo || `订单 ${detail.summary.orderId}` }}</p></div><Box :size="19" /></div>
<dl class="order-info-grid">
<div><dt>公司</dt><dd>{{ customerName(detail.summary) }}</dd></div>
<div><dt>运输方式</dt><dd>{{ detail.order.order?.transportMode || '—' }}</dd></div>
<div><dt>运输方式</dt><dd>{{ firstLegTransportModeLabel(detail.order.order?.transportMode) }}</dd></div>
<div><dt>当前状态</dt><dd><StatusBadge :status="statusLabel(detail.summary.mainStatus)" /></dd></div>
<div><dt>内部订单状态</dt><dd>{{ detail.summary.internalOrderStatus || '—' }}</dd></div>
<div><dt>内部订单状态</dt><dd>{{ orderStatusLabel(detail.summary.internalOrderStatus || detail.summary.mainStatus) }}</dd></div>
<div><dt>平台收货仓</dt><dd>{{ detail.summary.receivingWarehouseName || '—' }}</dd></div>
<div><dt>收货仓地址</dt><dd>{{ detail.order.order?.receivingWarehouseAddress || '—' }}</dd></div>
<div><dt>目的仓</dt><dd>{{ detail.summary.destinationWarehouseName || '—' }}</dd></div>
@@ -850,13 +855,13 @@ onMounted(() => {
<div><dt>目的仓联系人</dt><dd>{{ detail.summary.destinationContact || '—' }}</dd></div>
<div><dt>目的仓联系人电话</dt><dd>{{ detail.summary.destinationContactPhone || '—' }}</dd></div>
<div><dt>渠道</dt><dd>{{ detail.summary.channelName || '—' }}</dd></div>
<div><dt>业务类型</dt><dd>{{ detail.summary.businessType || '—' }}</dd></div>
<div><dt>贸易条款</dt><dd>{{ detail.order.order?.tradeTerm || '—' }}</dd></div>
<div><dt>买单 / 退税</dt><dd>{{ detail.order.order?.taxMode || '—' }}</dd></div>
<div><dt>费用审核状态</dt><dd>{{ detail.summary.feeReviewStatus || '—' }}</dd></div>
<div><dt>业务类型</dt><dd>{{ firstLegBusinessTypeLabel(detail.summary.businessType) }}</dd></div>
<div><dt>贸易条款</dt><dd>{{ firstLegTradeTermLabel(detail.order.order?.tradeTerm) }}</dd></div>
<div><dt>买单 / 退税</dt><dd>{{ firstLegTaxModeLabel(detail.order.order?.taxMode) }}</dd></div>
<div><dt>费用审核状态</dt><dd>{{ feeReviewLabel(detail.summary.feeReviewStatus) }}</dd></div>
<div><dt>是否扣货</dt><dd>{{ displayBoolean(detail.summary.holdCargo) }}</dd></div>
<div><dt>扣货理由</dt><dd>{{ detail.summary.holdReason || '—' }}</dd></div>
<div><dt>提货方式</dt><dd>{{ detail.order.order?.pickupMethod || '—' }}</dd></div>
<div><dt>提货方式</dt><dd>{{ firstLegPickupMethodLabel(detail.order.order?.pickupMethod) }}</dd></div>
<div><dt>提货时间</dt><dd>{{ formatDate(detail.summary.pickupAt) }}</dd></div>
<div><dt>到仓时间</dt><dd>{{ detail.order.order?.receivingTime || '—' }}</dd></div>
<div><dt>货物到仓时间</dt><dd>{{ formatDate(detail.summary.expectedArrivalAt || detail.summary.inboundCompletedAt) }}</dd></div>
@@ -867,9 +872,9 @@ onMounted(() => {
<div><dt>船名 / 航次</dt><dd>{{ detail.summary.transport?.vesselName || '—' }} / {{ detail.summary.transport?.voyageNo || '—' }}</dd></div>
<div><dt>开船时间</dt><dd>{{ formatDate(detail.summary.transport?.sailingAt) }}</dd></div>
<div><dt>到港时间</dt><dd>{{ formatDate(detail.summary.transport?.portArrivalAt) }}</dd></div>
<div><dt>配载状态</dt><dd>{{ detail.summary.transport?.loadingStatus || '—' }}</dd></div>
<div><dt>目的仓类型</dt><dd>{{ detail.order.order?.destinationType || '—' }}</dd></div>
<div><dt>是否整柜</dt><dd>{{ detail.order.order?.containerMode || '—' }}</dd></div>
<div><dt>配载状态</dt><dd>{{ unifiedOrderLoadingStatusLabel(detail.summary.transport?.loadingStatus) }}</dd></div>
<div><dt>目的仓类型</dt><dd>{{ firstLegDestinationTypeLabel(detail.order.order?.destinationType) }}</dd></div>
<div><dt>是否整柜</dt><dd>{{ firstLegContainerModeLabel(detail.order.order?.containerMode) }}</dd></div>
<div><dt>商检验</dt><dd>{{ displayBoolean(detail.order.order?.customsInspection) }}</dd></div>
<div><dt>总箱数</dt><dd>{{ detail.summary.totalPackageCount ?? '—' }} 箱</dd></div>
<div><dt>推荐托盘类型</dt><dd>{{ detail.summary.recommendedPalletType || '—' }}</dd></div>
@@ -1026,10 +1031,10 @@ onMounted(() => {
:key="tab.value || 'ALL'"
type="button"
role="tab"
:aria-selected="filters.mainStatus === tab.value"
:aria-selected="selectedStatus === tab.value"
aria-controls="unified-orders-results"
:tabindex="filters.mainStatus === tab.value ? 0 : -1"
:class="['status-tab', { 'is-active': filters.mainStatus === tab.value }]"
:tabindex="selectedStatus === tab.value ? 0 : -1"
:class="['status-tab', { 'is-active': selectedStatus === tab.value }]"
@click="selectStatus(tab.value)"
@keydown="handleTabKeydown($event, index)"
>{{ tab.label }}</button>
@@ -1054,10 +1059,7 @@ onMounted(() => {
<template #default>
<div v-if="!loadError" id="unified-orders-results" class="surface-panel table-panel table-card" role="tabpanel">
<DataTable v-model:page="page" v-model:page-size="pageSize" :data="records" :columns="columns" preference-key="unified-orders-table" :loading="loading" :total="total" :page-sizes="[20, 50, 100]" row-key="orderId" empty-text="暂无符合条件的订单" aria-label="统一订单列表" @selection-change="handleSelectionChange">
<template #toolbar-actions>
<el-button type="primary" plain :disabled="selectedOrders.length === 0" title="批量审核接口待接入" @click="openBulkReviewPlaceholder"><ListChecks :size="16" />批量审核<span v-if="selectedOrders.length">({{ selectedOrders.length }})</span></el-button>
</template>
<DataTable v-model:page="page" v-model:page-size="pageSize" :data="records" :columns="columns" preference-key="unified-orders-table" :loading="loading" :total="total" :page-sizes="[20, 50, 100]" row-key="orderId" empty-text="暂无符合条件的订单" aria-label="统一订单列表">
<template #actions="{ row }">
<div class="row-actions">
<el-button link type="primary" @click="openDetail(row as UnifiedOrderResource)"><Eye :size="15" />详情</el-button>
@@ -1087,7 +1089,7 @@ onMounted(() => {
<template #customer="{ row }"><div class="primary-cell"><strong>{{ customerName(row as UnifiedOrderResource) }}</strong><span>客户 ID {{ (row as UnifiedOrderResource).tenantId }}</span></div></template>
<template #receivingWarehouseName="{ row }">{{ (row as UnifiedOrderResource).receivingWarehouseName || '—' }}</template>
<template #destinationWarehouseName="{ row }">{{ (row as UnifiedOrderResource).destinationWarehouseName || '—' }}</template>
<template #transport="{ row }"><div class="primary-cell"><strong>{{ (row as UnifiedOrderResource).transport?.containerNo || (row as UnifiedOrderResource).transport?.loadingStatus || '—' }}</strong><span v-if="(row as UnifiedOrderResource).transport?.vesselName">{{ (row as UnifiedOrderResource).transport?.vesselName }} · {{ (row as UnifiedOrderResource).transport?.voyageNo || '—' }}</span></div></template>
<template #transport="{ row }"><div class="primary-cell"><strong>{{ (row as UnifiedOrderResource).transport?.containerNo || unifiedOrderLoadingStatusLabel((row as UnifiedOrderResource).transport?.loadingStatus) }}</strong><span v-if="(row as UnifiedOrderResource).transport?.vesselName">{{ (row as UnifiedOrderResource).transport?.vesselName }} · {{ (row as UnifiedOrderResource).transport?.voyageNo || '—' }}</span></div></template>
<template #cargo="{ row }"><span>{{ (row as UnifiedOrderResource).totalPackageCount ?? '—' }} 箱</span><span v-if="(row as UnifiedOrderResource).measurement?.actualGrossWeightKg !== null && (row as UnifiedOrderResource).measurement?.actualGrossWeightKg !== undefined"> · {{ formatNumber((row as UnifiedOrderResource).measurement?.actualGrossWeightKg) }} kg</span><span v-if="(row as UnifiedOrderResource).measurement?.actualVolumeCbm !== null && (row as UnifiedOrderResource).measurement?.actualVolumeCbm !== undefined"> · {{ formatNumber((row as UnifiedOrderResource).measurement?.actualVolumeCbm, 3) }} m³</span></template>
<template #expectedShipDate="{ row }">{{ formatDate((row as UnifiedOrderResource).expectedShipDate) }}</template>
<template #updatedAt="{ row }">{{ formatDate((row as UnifiedOrderResource).updatedAt) }}</template>
+31
View File
@@ -0,0 +1,31 @@
import { describe, expect, it } from 'vitest'
import {
unifiedOrderFulfillmentRoleLabel,
unifiedOrderFulfillmentStatusLabel,
unifiedOrderHistoryActionLabel,
unifiedOrderLoadingStatusLabel,
unifiedOrderReviewStatusLabel,
unifiedOrderStatusLabel,
} from './status'
describe('unified order status labels', () => {
it('maps order, review and loading statuses to domain-specific Chinese', () => {
expect(unifiedOrderStatusLabel('WAIT_SPLIT')).toBe('待分柜')
expect(unifiedOrderReviewStatusLabel('APPROVED')).toBe('审核通过')
expect(unifiedOrderLoadingStatusLabel('LOADED')).toBe('已配载')
})
it('never exposes unknown backend enums', () => {
expect(unifiedOrderStatusLabel('NEW_ORDER_STATE')).toBe('未知状态')
expect(unifiedOrderReviewStatusLabel('NEW_REVIEW_STATE')).toBe('未知状态')
expect(unifiedOrderLoadingStatusLabel('NEW_LOADING_STATE')).toBe('未知状态')
expect(unifiedOrderHistoryActionLabel('NEW_ACTION')).toBe('状态更新')
expect(unifiedOrderFulfillmentRoleLabel('NEW_ROLE')).toBe('未知类型')
expect(unifiedOrderFulfillmentStatusLabel('NEW_FRAGMENT_STATE', 'REMAINDER')).toBe('未知状态')
})
it('uses the fragment role when a fulfillment status is absent', () => {
expect(unifiedOrderFulfillmentStatusLabel(null, 'ACTIVE_REMAINDER')).toBe('当前余量')
expect(unifiedOrderFulfillmentStatusLabel(undefined, 'COMPLETED_FRAGMENT')).toBe('已完成分片')
})
})
+98
View File
@@ -0,0 +1,98 @@
const orderStatusLabels: Record<string, string> = {
DRAFT: '草稿',
SUBMITTED: '待审核',
APPROVED: '已审核',
ACCEPTED: '已受理',
REJECTED: '已驳回',
PROCESSING: '处理中',
COMPLETED: '已完成',
CANCELLED: '已取消',
PENDING_REVIEW: '待审核',
WAIT_MEASURE: '待测量数据',
INBOUNDED: '已入库',
WAIT_SPLIT: '待分柜',
LOADING: '装柜中',
LOADED: '装柜完成',
DELIVERED: '派送完成',
}
const reviewStatusLabels: Record<string, string> = {
NOT_SUBMITTED: '未提交',
UNRECORDED: '未录入',
PENDING: '待审核',
APPROVED: '审核通过',
REJECTED: '审核驳回',
CONFIRMED: '已确认',
}
const loadingStatusLabels: Record<string, string> = {
NOT_LOADED: '未配载',
WAIT_LOADING: '待配载',
EDITING: '预装柜中',
LOADING: '配载中',
LOADED: '已配载',
COMPLETED: '装柜完成',
}
const historyActionLabels: Record<string, string> = {
CREATE: '创建订单',
CREATED: '创建订单',
SUBMITTED: '提交订单',
APPROVED: '审核通过',
ORDER_APPROVED: '订单审核通过',
REJECTED: '审核驳回',
MEASURE_APPROVE: '测量审核通过',
MEASURE_REJECT: '测量审核驳回',
INBOUND_BEGIN: '开始入库',
INBOUND_COMPLETE: '入库完成',
UI_FIXTURE_MEASURE_APPROVE: '测量数据审核通过',
UI_FIXTURE_INBOUND_BEGIN: '开始入库',
STATUS_UPDATE: '状态更新',
}
const fulfillmentRoleLabels: Record<string, string> = {
REMAINDER: '当前余量',
ACTIVE_REMAINDER: '当前余量',
LOADED: '已完成分片',
COMPLETED_FRAGMENT: '已完成分片',
SOURCE_COMPLETED: '已完成分片',
}
function normalized(value: string | null | undefined) {
return value?.trim().toUpperCase() || ''
}
function statusLabel(value: string | null | undefined, labels: Record<string, string>, emptyLabel = '—') {
const status = normalized(value)
if (!status) return emptyLabel
return labels[status] || '未知状态'
}
export function unifiedOrderStatusLabel(value: string | null | undefined, emptyLabel = '—') {
return statusLabel(value, orderStatusLabels, emptyLabel)
}
export function unifiedOrderReviewStatusLabel(value: string | null | undefined, emptyLabel = '—') {
return statusLabel(value, reviewStatusLabels, emptyLabel)
}
export function unifiedOrderLoadingStatusLabel(value: string | null | undefined, emptyLabel = '—') {
return statusLabel(value, loadingStatusLabels, emptyLabel)
}
export function unifiedOrderHistoryActionLabel(value: string | null | undefined) {
const action = normalized(value)
if (!action) return '状态更新'
return historyActionLabels[action] || '状态更新'
}
export function unifiedOrderFulfillmentRoleLabel(value: string | null | undefined) {
const role = normalized(value)
if (!role) return '分片'
return fulfillmentRoleLabels[role] || '未知类型'
}
export function unifiedOrderFulfillmentStatusLabel(status: string | null | undefined, role: string | null | undefined) {
if (!status) return unifiedOrderFulfillmentRoleLabel(role)
return unifiedOrderStatusLabel(status)
}
@@ -106,6 +106,11 @@ function formatNumber(value: number, precision = 3) {
return Number.isFinite(value) ? value.toLocaleString('zh-CN', { maximumFractionDigits: precision }) : '—'
}
function planStatusLabel(status?: string | null) {
return ({ EDITING: '预装柜中', LOADING: '拣货装柜中', COMPLETED: '装柜完成' } as Record<string, string>)[status || '']
|| (status ? '未知状态' : '尚未创建')
}
function routeMatches(candidate: SmartLoadingCandidate) {
const target = container.value?.destinationWarehouseCode
return !target || candidate.destinationWarehouseCode === target
@@ -291,7 +296,7 @@ onMounted(() => { void load() })
</el-alert>
<div v-if="container" class="smart-loading__summary">
<div><span>计划状态</span><strong>{{ plan?.status || '尚未创建' }}</strong></div>
<div><span>计划状态</span><strong>{{ planStatusLabel(plan?.status) }}</strong></div>
<div><span>已装箱数</span><strong>{{ totalPackages }}</strong></div>
<div><span>容积</span><strong>{{ formatNumber(usedVolume) }} / {{ formatNumber(container.capacityCbm || 0) }} m³</strong><small>{{ volumeRate == null ? '容量待配置' : `${volumeRate}%` }}</small></div>
<div><span>重量</span><strong>{{ formatNumber(usedWeight) }} / {{ formatNumber(container.maxWeightKg || 0) }} kg</strong><small>{{ weightRate == null ? '承重待配置' : `${weightRate}%` }}</small></div>
@@ -1,4 +1,4 @@
import ElementPlus from 'element-plus'
import ElementPlus, { ElMessageBox } from 'element-plus'
import { createPinia, setActivePinia } from 'pinia'
import { flushPromises, mount } from '@vue/test-utils'
import { beforeEach, describe, expect, it, vi } from 'vitest'
@@ -7,8 +7,10 @@ import UnifiedOrderWorkflowPanel from './UnifiedOrderWorkflowPanel.vue'
import {
getEvidence,
getOutboundDispatch,
getPicking,
getPreload,
smartSplit,
voidEvidence,
} from '../api'
vi.mock('../api', async (importOriginal) => {
@@ -17,8 +19,10 @@ vi.mock('../api', async (importOriginal) => {
...original,
getEvidence: vi.fn(),
getOutboundDispatch: vi.fn(),
getPicking: vi.fn(),
getPreload: vi.fn(),
smartSplit: vi.fn(),
voidEvidence: vi.fn(),
}
})
@@ -48,8 +52,10 @@ describe('UnifiedOrderWorkflowPanel preload editor', () => {
beforeEach(() => {
vi.mocked(getOutboundDispatch).mockReset().mockResolvedValue(null as never)
vi.mocked(getEvidence).mockReset().mockResolvedValue({ inboundId: '2086085512165634051', evidences: [] })
vi.mocked(getPicking).mockReset().mockResolvedValue({ id: '51', inboundId: '2086085512165634051', status: 'PICKING', version: 2, expectedCount: 10, pickedCount: 4, remark: null })
vi.mocked(getPreload).mockReset().mockResolvedValue(preload('REF-OLD'))
vi.mocked(smartSplit).mockReset()
vi.mocked(voidEvidence).mockReset()
})
it('renders the formal preload table and keeps the operation column first', async () => {
@@ -116,4 +122,74 @@ describe('UnifiedOrderWorkflowPanel preload editor', () => {
expect(wrapper.text()).not.toContain('完成装柜')
expect(wrapper.findAll('button').some((button) => button.text() === '作废')).toBe(false)
})
it('uses a validated dialog when voiding evidence', async () => {
vi.mocked(getEvidence).mockResolvedValue({
inboundId: '2086085512165634051',
evidences: [
{ id: '1', inboundId: '2086085512165634051', tenantId: '2086085512165634052', evidenceType: 'PHOTO_STEP_1', fileId: '11', fileName: 'wrong.jpg', sealNo: null, remark: null, version: 2, status: 'ACTIVE', createdAt: null, updatedAt: null },
],
})
vi.mocked(voidEvidence).mockResolvedValue({} as never)
const prompt = vi.spyOn(ElMessageBox, 'prompt').mockResolvedValue({ value: '照片上传错误' } as never)
const wrapper = mountPanel(['platform:firstleg:inbound:loading:view', 'platform:firstleg:inbound:loading:edit'], 'LOADING')
await flushPromises()
const voidButton = wrapper.findAll('button').find((button) => button.text() === '作废')
expect(voidButton).toBeTruthy()
await voidButton!.trigger('click')
await flushPromises()
expect(prompt).toHaveBeenCalledWith(
'作废后照片仍会保留在审计记录中。',
'作废证据照片',
expect.objectContaining({
inputType: 'textarea',
inputPlaceholder: '请输入作废原因',
type: 'warning',
}),
)
const options = prompt.mock.calls[0]?.[2]
expect(options?.inputValidator?.(' ')).toBe('作废原因不能为空')
expect(voidEvidence).toHaveBeenCalledWith(
'2086085512165634052',
'2086085512165634051',
'1',
{ expectedVersion: 2, reason: '照片上传错误' },
)
prompt.mockRestore()
})
it('renders booking, trucking and picking backend statuses as business Chinese', async () => {
vi.mocked(getOutboundDispatch).mockResolvedValue({
orderId: '1', orderNo: 'ORD-1', inboundId: '2', inboundCode: 'IN-2', containerBookingId: '3', containerNo: 'CONT-3',
loadingPlanId: '4', loadingPlanStatus: 'LOADING', oceanBookingId: '5', oceanBookingStatus: 'RELEASED',
truckAppointmentId: '6', truckAppointmentStatus: 'PICKUP_IN_PROGRESS', gate: { passed: false, items: [] },
})
const wrapper = mountPanel(['platform:firstleg:inbound:picking:view'], 'LOADING')
await flushPromises()
expect(wrapper.text()).toContain('已放舱')
expect(wrapper.text()).toContain('提柜中')
expect(wrapper.text()).toContain('拣货中')
expect(wrapper.text()).not.toContain('RELEASED')
expect(wrapper.text()).not.toContain('PICKUP_IN_PROGRESS')
expect(wrapper.text()).not.toContain('PICKING')
})
it('does not expose unknown backend enum values', async () => {
vi.mocked(getOutboundDispatch).mockResolvedValue({
orderId: '1', orderNo: 'ORD-1', inboundId: '2', inboundCode: 'IN-2', containerBookingId: null, containerNo: null,
loadingPlanId: null, loadingPlanStatus: null, oceanBookingId: '5', oceanBookingStatus: 'NEW_BOOKING_STATE',
truckAppointmentId: '6', truckAppointmentStatus: 'NEW_TRUCK_STATE', gate: { passed: false, items: [] },
})
vi.mocked(getPicking).mockResolvedValue({ id: '51', inboundId: '2', status: 'NEW_PICK_STATE', version: 2, expectedCount: null, pickedCount: null, remark: null })
const wrapper = mountPanel(['platform:firstleg:inbound:picking:view'], 'LOADING')
await flushPromises()
expect(wrapper.text()).toContain('未知状态')
expect(wrapper.text()).not.toContain('NEW_BOOKING_STATE')
expect(wrapper.text()).not.toContain('NEW_TRUCK_STATE')
expect(wrapper.text()).not.toContain('NEW_PICK_STATE')
})
})
@@ -1,5 +1,6 @@
<script setup lang="ts">
import { computed, onMounted, ref, watch } from 'vue'
import { ElMessageBox } from 'element-plus'
import { RefreshCw, Save, Check, Play, Camera, Eye, Plus, Pencil, Trash2 } from '@lucide/vue'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { ApiResponseError, getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
@@ -8,6 +9,8 @@ import AppDialog from '@/shared/components/AppDialog.vue'
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import SmartLoadingWorkbench from '@/modules/unified-orders/workflows/components/SmartLoadingWorkbench.vue'
import { oceanBookingStatusLabel } from '@/modules/ocean-bookings'
import { truckAppointmentStatusLabel } from '@/modules/trucking'
import { sanitizePreloadItem, validatePreloadItem, validatePreloadItems } from '@/modules/unified-orders/workflows/preloadForm'
import {
completeLoading,
@@ -145,7 +148,19 @@ function removeItem(index: number) {
}
function preloadStatusLabel(status?: string | null) {
return ({ PENDING: '待提交', CONFIRMING: '确认中', CONFIRMED: '已确认' } as Record<string, string>)[status || ''] || '尚未生成'
return workflowStatusLabel(status, { PENDING: '待提交', CONFIRMING: '确认中', CONFIRMED: '已确认' }, '尚未生成')
}
function workflowStatusLabel(value: string | null | undefined, labels: Record<string, string>, emptyLabel: string) {
if (!value) return emptyLabel
return labels[value] || '未知状态'
}
function pickingStatusLabel(status?: string | null) {
return workflowStatusLabel(status, {
PENDING: '待拣货', PICKING: '拣货中', IN_PROGRESS: '拣货中', AWAITING_REVIEW: '待复核',
COMPLETED: '已完成', CANCELLED: '已取消',
}, '暂无数据')
}
function formatVolume(value: number | null) {
@@ -345,8 +360,21 @@ async function uploadAndSaveEvidence(type: EvidenceType, event: Event) {
async function markEvidenceVoided(item: Evidence) {
if (!canEvidence.value || !item.id || String(item.id).startsWith('draft-')) return
const reason = window.prompt('请输入照片作废原因')?.trim()
if (!reason) return
let reason = ''
try {
const result = await ElMessageBox.prompt('作废后照片仍会保留在审计记录中。', '作废证据照片', {
inputType: 'textarea',
inputPlaceholder: '请输入作废原因',
inputValidator: (value) => Boolean(value?.trim()) || '作废原因不能为空',
confirmButtonText: '确认作废',
cancelButtonText: '取消',
type: 'warning',
})
reason = result.value.trim()
} catch (cause) {
if (cause === 'cancel' || cause === 'close') return
throw cause
}
const result = await run(`evidence-void-${item.id}`, () => voidEvidence(props.tenantId, props.inboundId, item.id, {
expectedVersion: item.version,
reason,
@@ -409,8 +437,8 @@ onMounted(() => { void refresh() })
<el-card v-if="dispatch" shadow="never" class="workflow-card">
<template #header><div class="card-title"><span>子流程与门禁摘要</span><el-tag :type="dispatch.gate?.passed ? 'success' : 'warning'">{{ dispatch.gate?.passed ? '已通过' : '待补齐' }}</el-tag></div></template>
<div class="gate-grid">
<div><span>订舱</span><strong>{{ dispatch.oceanBookingStatus || '暂无数据' }}</strong></div>
<div><span>拖车</span><strong>{{ dispatch.truckAppointmentStatus || '暂无数据' }}</strong></div>
<div><span>订舱</span><strong>{{ oceanBookingStatusLabel(dispatch.oceanBookingStatus, '未关联') }}</strong></div>
<div><span>拖车</span><strong>{{ truckAppointmentStatusLabel(dispatch.truckAppointmentStatus, '未关联') }}</strong></div>
<div><span>柜号</span><strong>{{ dispatch.containerNo || '暂无数据' }}</strong></div>
<div v-for="item in dispatch.gate?.items || []" :key="item.code"><span>{{ gateItemName(item.code, item.name) }}</span><el-tag size="small" :type="item.passed ? 'success' : 'danger'">{{ item.passed ? '已满足' : '待补齐' }}</el-tag></div>
</div>
@@ -459,7 +487,7 @@ onMounted(() => { void refresh() })
<el-card shadow="never" class="workflow-card">
<template #header><div class="card-title"><span>拣货与装柜</span><el-tag type="warning">装柜中</el-tag></div></template>
<el-descriptions v-if="picking" :column="2" border>
<el-descriptions-item label="拣货状态">{{ picking.status || '—' }}</el-descriptions-item>
<el-descriptions-item label="拣货状态">{{ pickingStatusLabel(picking.status) }}</el-descriptions-item>
<el-descriptions-item label="拣货版本">{{ picking.version }}</el-descriptions-item>
<el-descriptions-item label="应拣件数">{{ picking.expectedCount ?? '—' }}</el-descriptions-item>
<el-descriptions-item label="已拣件数">{{ picking.pickedCount ?? '—' }}</el-descriptions-item>