This commit is contained in:
2026-08-11 19:35:28 +08:00
parent 886c7d3529
commit c690a30422
48 changed files with 935 additions and 171 deletions
@@ -14,6 +14,7 @@ import { InboundMeasurementPanel } from '../measurement'
import { WeightOcrPanel } from '../weight'
import { MeasurementReviewPanel } from '../review'
import { InboundFeePanel } from '../fee'
import { inboundProcessStatusLabels, inboundStatusLabel } from '../status'
const props = defineProps<{
customerId: string
@@ -35,20 +36,7 @@ let disposed = false
const title = computed(() => detail.value?.summary.inboundCode || props.inboundId)
const canViewMeasurement = computed(() => access.can('platform:firstleg:inbound:measure:view'))
const statusLabel = computed(() => {
const status = detail.value?.summary.processStatus || ''
return ({
WAIT_MEASURE: '待测量',
MEASURING: '测量中',
MEASURE_PENDING: '待审核',
MEASURE_REJECTED: '测量已驳回',
WAIT_PRICING: '待计费',
FEE_CONFIRMING: '费用确认中',
INBOUNDING: '入库处理中',
INBOUNDED: '已入库',
VOIDED: '已作废',
} as Record<string, string>)[status] || status || '未知状态'
})
const statusLabel = computed(() => inboundStatusLabel(detail.value?.summary.processStatus, inboundProcessStatusLabels, '未知状态'))
async function loadDetail() {
const serial = ++requestSerial
@@ -1,5 +1,6 @@
<script setup lang="ts">
import type { InboundFileSummary, InboundInternalNote, InboundPalletLocation, InboundStatusHistory } from './types'
import { inboundHistoryActionLabel, inboundProcessStatusLabels, inboundStatusLabel } from '../status'
defineProps<{
palletLocations: InboundPalletLocation[]
@@ -43,7 +44,7 @@ function date(value: string | null) {
</section>
<section class="history-section">
<div class="section-heading"><h3>状态历史</h3><span>{{ statusHistory.length }} 条</span></div>
<ol v-if="statusHistory.length" class="timeline"><li v-for="(history, index) in statusHistory" :key="`${history.occurredAt}-${index}`"><span class="timeline-dot" /><div><strong>{{ value(history.action) }}</strong><span>{{ value(history.fromStatus) }} → {{ value(history.toStatus) }} · {{ value(history.operatorName || history.operatorId) }}</span><small>{{ date(history.occurredAt) }}<template v-if="history.reason"> · {{ history.reason }}</template></small></div></li></ol>
<ol v-if="statusHistory.length" class="timeline"><li v-for="(history, index) in statusHistory" :key="`${history.occurredAt}-${index}`"><span class="timeline-dot" /><div><strong>{{ inboundHistoryActionLabel(history.action) }}</strong><span>{{ inboundStatusLabel(history.fromStatus, inboundProcessStatusLabels) }} → {{ inboundStatusLabel(history.toStatus, inboundProcessStatusLabels) }} · {{ value(history.operatorName || history.operatorId) }}</span><small>{{ date(history.occurredAt) }}<template v-if="history.reason"> · {{ history.reason }}</template></small></div></li></ol>
<p v-else class="muted-empty">暂无状态历史</p>
</section>
<section v-if="publicRemark" class="history-section remark-section"><div class="section-heading"><h3>公开备注</h3></div><p>{{ publicRemark }}</p></section>
@@ -23,4 +23,14 @@ describe('InboundSummaryPanel', () => {
expect(source).not.toContain('接收审核')
expect(source).not.toContain('客户确认费用')
})
it('renders aggregate statuses through the inbound translation contract', () => {
expect(source).toContain('inboundOrderStatusLabels')
expect(source).toContain('inboundLoadingStatusLabels')
expect(source).toContain('inboundReviewStatusLabels')
expect(source).not.toContain('{{ value(summary.orderStatusSnapshot) }}')
expect(source).toContain('firstLegBusinessTypeLabel(summary.businessType)')
expect(source).toContain('firstLegTaxModeLabel(summary.taxMode)')
expect(source).toContain('firstLegDestinationTypeLabel(summary.destinationType)')
})
})
@@ -1,15 +1,12 @@
<script setup lang="ts">
import { Box, MapPin, Scale, Warehouse } from '@lucide/vue'
import { firstLegBusinessTypeLabel, firstLegContainerModeLabel, firstLegDestinationTypeLabel, firstLegTaxModeLabel } from '@/shared/utils/firstLegBusinessLabels'
import type { InboundOrderSummary } from './types'
import { inboundFeeStatusLabels, inboundLoadingStatusLabels, inboundNotificationStatusLabels, inboundOrderStatusLabels, inboundProcessStatusLabels, inboundReviewStatusLabels, inboundStatusLabel } from '../status'
withDefaults(defineProps<{ summary: InboundOrderSummary; publicRemark?: string | null }>(), { publicRemark: null })
const processLabels: Record<string, string> = {
WAIT_MEASURE: '待测量', MEASURING: '测量中', MEASURE_PENDING: '待审核', MEASURE_REJECTED: '测量驳回',
WAIT_PRICING: '待计费', FEE_CONFIRMING: '待发送费用', INBOUNDING: '入库处理中', INBOUNDED: '已入库', VOIDED: '已作废',
}
function processLabel(status: string) { return processLabels[status] || status || '未知状态' }
function processLabel(status: string) { return inboundStatusLabel(status, inboundProcessStatusLabels, '未知状态') }
function processType(status: string) {
if (status === 'INBOUNDED') return 'success'
if (status === 'MEASURE_REJECTED' || status === 'VOIDED') return 'danger'
@@ -24,10 +21,7 @@ function date(value: string | null) {
return Number.isNaN(parsed.getTime()) ? value : parsed.toLocaleString('zh-CN', { hour12: false })
}
function yesNo(value: boolean | null) { return value === null ? '—' : value ? '是' : '否' }
const measureReviewLabels: Record<string, string> = { PENDING: '待审核', APPROVED: '审核通过', REJECTED: '审核驳回' }
const feeLabels: Record<string, string> = { UNPRICED: '未计费', CALCULATED: '已试算', CONFIRMED: '已确认', SENT: '已发送', VOIDED: '已作废' }
const notificationLabels: Record<string, string> = { NOT_CREATED: '未创建', PENDING: '待发送', SUCCESS: '发送成功', FAILED: '发送失败' }
function label(value: string | null, labels: Record<string, string>) { return value ? labels[value] || value : '—' }
function label(value: string | null, labels: Record<string, string>) { return inboundStatusLabel(value, labels) }
</script>
<template>
@@ -46,13 +40,13 @@ function label(value: string | null, labels: Record<string, string>) { return va
<h4 id="inbound-basic-heading">基本信息</h4>
<dl class="summary-grid">
<div><dt>头程订单号</dt><dd>{{ value(summary.orderNo) }}</dd></div>
<div><dt>订单状态</dt><dd>{{ value(summary.orderStatusSnapshot) }}</dd></div>
<div><dt>订单状态</dt><dd>{{ label(summary.orderStatusSnapshot, inboundOrderStatusLabels) }}</dd></div>
<div><dt>客户</dt><dd>{{ value(summary.customerName) }}</dd></div>
<div><dt>公司</dt><dd>{{ value(summary.companyName) }}</dd></div>
<div><dt>业务类型</dt><dd>{{ value(summary.businessType) }}</dd></div>
<div><dt>业务类型</dt><dd>{{ firstLegBusinessTypeLabel(summary.businessType) }}</dd></div>
<div><dt>渠道</dt><dd>{{ value(summary.channelName || summary.channelCode) }}</dd></div>
<div><dt>买单 / 退税</dt><dd>{{ value(summary.taxMode) }}</dd></div>
<div><dt>是否整柜</dt><dd>{{ value(summary.containerMode) }}</dd></div>
<div><dt>买单 / 退税</dt><dd>{{ firstLegTaxModeLabel(summary.taxMode) }}</dd></div>
<div><dt>是否整柜</dt><dd>{{ firstLegContainerModeLabel(summary.containerMode) }}</dd></div>
<div><dt>商检验</dt><dd>{{ yesNo(summary.customsInspection) }}</dd></div>
<div><dt>备注</dt><dd>{{ value(publicRemark) }}</dd></div>
<div><dt>创建时间</dt><dd>{{ date(summary.createdAt) }}</dd></div>
@@ -65,11 +59,11 @@ function label(value: string | null, labels: Record<string, string>) { return va
<div><dt><Warehouse :size="14" />收货仓</dt><dd>{{ value(summary.warehouseName) }}</dd></div>
<div><dt>交货仓</dt><dd>{{ value(summary.deliveryWarehouseName) }}</dd></div>
<div><dt><MapPin :size="14" />目的仓</dt><dd>{{ value(summary.destinationName || summary.destinationCode) }}</dd></div>
<div><dt>目的仓类型</dt><dd>{{ value(summary.destinationType) }}</dd></div>
<div><dt>目的仓类型</dt><dd>{{ firstLegDestinationTypeLabel(summary.destinationType) }}</dd></div>
<div><dt>集装箱号</dt><dd>{{ value(summary.containerNo) }}</dd></div>
<div><dt>柜号</dt><dd>{{ value(summary.cabinetNo) }}</dd></div>
<div><dt>船名 / 航次</dt><dd>{{ value(summary.vesselName) }} / {{ value(summary.voyageNo) }}</dd></div>
<div><dt>配载状态</dt><dd>{{ value(summary.loadingStatus) }}</dd></div>
<div><dt>配载状态</dt><dd>{{ label(summary.loadingStatus, inboundLoadingStatusLabels) }}</dd></div>
<div><dt>预计到达</dt><dd>{{ date(summary.expectedArrivalAt) }}</dd></div>
<div><dt>开船 / 到港</dt><dd>{{ date(summary.sailingAt) }} / {{ date(summary.portArrivalAt) }}</dd></div>
<div><dt>快递公司</dt><dd>{{ value(summary.expressCompanyName || summary.expressCompanyCode) }}</dd></div>
@@ -87,11 +81,11 @@ function label(value: string | null, labels: Record<string, string>) { return va
<div><dt>总材积重</dt><dd>{{ number(summary.volumeWeightKg) }} kg</dd></div>
<div><dt>计费毛重</dt><dd>{{ number(summary.chargeableWeightKg) }} kg</dd></div>
<div><dt>计费体积</dt><dd>{{ number(summary.chargeableVolumeCbm) }} CBM</dd></div>
<div><dt>测量审核</dt><dd>{{ label(summary.measureReviewStatus, measureReviewLabels) }}</dd></div>
<div><dt>费用状态</dt><dd>{{ label(summary.feeStatus, feeLabels) }}</dd></div>
<div><dt>费用审核状态</dt><dd>{{ value(summary.feeReviewStatus) }}</dd></div>
<div><dt>测量审核</dt><dd>{{ label(summary.measureReviewStatus, inboundReviewStatusLabels) }}</dd></div>
<div><dt>费用状态</dt><dd>{{ label(summary.feeStatus, inboundFeeStatusLabels) }}</dd></div>
<div><dt>费用审核状态</dt><dd>{{ label(summary.feeReviewStatus, inboundReviewStatusLabels) }}</dd></div>
<div><dt>测量完成时间</dt><dd>{{ date(summary.measureCompletedAt) }}</dd></div>
<div><dt>通知状态</dt><dd>{{ label(summary.notificationStatus, notificationLabels) }}</dd></div>
<div><dt>通知状态</dt><dd>{{ label(summary.notificationStatus, inboundNotificationStatusLabels) }}</dd></div>
</dl>
</section>
@@ -79,6 +79,7 @@ describe('InboundFeePanel', () => {
expect(document.body.textContent).toContain('附加费 ¥100.00')
expect(document.body.textContent).toContain('133.6 KG')
expect(document.body.textContent).toContain('按重量计费')
expect(document.body.textContent).toContain('FBA 海运')
expect(document.body.textContent).toContain('¥8.50 / KG')
expect(document.body.textContent).toContain('RULE-FBA-KG · V3')
expect(document.body.textContent).toContain('RATE-LAX · V5')
+5 -4
View File
@@ -2,6 +2,7 @@
import { computed, reactive, ref, watch } from 'vue'
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
import { CircleDollarSign, PencilLine, RefreshCw } from '@lucide/vue'
import { firstLegBusinessTypeLabel, firstLegTaxModeLabel } from '@/shared/utils/firstLegBusinessLabels'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import AppDialog from '@/shared/components/AppDialog.vue'
@@ -74,7 +75,7 @@ function feeStatusInfo(status?: string) {
SENT: { label: '已发布', type: 'success' },
SUPERSEDED: { label: '历史版本', type: 'info' },
}
return map[status || ''] || { label: status || '未知状态', type: 'info' as const }
return map[status || ''] || { label: '未知状态', type: 'info' as const }
}
function formatNumber(value: number | null | undefined, digits = 3) {
@@ -104,7 +105,7 @@ function formatDateTime(value: string | null | undefined) {
}
function taxModeLabel(value: string) {
return value === 'TAX_INCLUDED' ? '包税' : value === 'TAX_EXCLUDED' ? '不包税' : value
return firstLegTaxModeLabel(value)
}
function roundingModeLabel(value: string) {
@@ -112,7 +113,7 @@ function roundingModeLabel(value: string) {
UP: '远离零方向', DOWN: '向零方向', CEILING: '向上', FLOOR: '向下',
HALF_UP: '四舍五入', HALF_DOWN: '五舍六入', HALF_EVEN: '银行家舍入',
}
return labels[value] || value
return labels[value] || '未知规则'
}
async function loadFee(options: { preserveAdjustment?: boolean } = {}) {
@@ -314,7 +315,7 @@ defineExpose({ reload: loadFee })
<div class="section-title"><div><h4 id="rule-title">客户计费规则</h4><p>{{ snapshot.ruleCode }} · V{{ snapshot.chargeRuleVersion }}</p></div></div>
<dl>
<div><dt>客户</dt><dd>{{ snapshot.customerName }}</dd></div>
<div><dt>业务类型</dt><dd>{{ snapshot.businessType }}</dd></div>
<div><dt>业务类型</dt><dd>{{ firstLegBusinessTypeLabel(snapshot.businessType) }}</dd></div>
<div><dt>计费口径</dt><dd>{{ chargeModeLabel }}</dd></div>
<div><dt>舍入规则</dt><dd>保留 {{ snapshot.roundingScale }} 位,{{ roundingModeLabel(snapshot.roundingMode) }}</dd></div>
</dl>
@@ -8,6 +8,7 @@ import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import StatusBadge from '@/shared/components/StatusBadge.vue'
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
import { inboundProcessStatusLabels, inboundStatusLabel } from '../status'
import { downloadInboundMeasurementImage, getInboundMeasurement, submitInboundMeasurement, updateConfirmedPackageCount } from './api'
import type {
InboundMeasurementDetail,
@@ -314,10 +315,7 @@ async function previewImage(fileId: string | null) {
}
function processStatusLabel(status: string | undefined) {
return ({
WAIT_MEASURE: '待测量', MEASURING: '测量中', MEASURE_PENDING: '待审核', MEASURE_REJECTED: '审核驳回',
WAIT_PRICING: '待计费', FEE_CONFIRMING: '费用确认中', INBOUNDING: '入库中', INBOUNDED: '已入库', VOIDED: '已作废',
} as Record<string, string>)[status || ''] || status || '—'
return inboundStatusLabel(status, inboundProcessStatusLabels)
}
watch(
@@ -13,12 +13,14 @@ import FormSection from '@/shared/components/FormSection.vue'
import ListPageLayout from '@/shared/components/ListPageLayout.vue'
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
import { firstLegBusinessTypeLabel, firstLegContainerModeLabel, firstLegDestinationTypeLabel, firstLegTaxModeLabel } from '@/shared/utils/firstLegBusinessLabels'
import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
import { listPlatformWarehouses } from '@/modules/warehouses'
import { downloadInboundCartonLabels, getPlatformInbound, listPlatformInbounds } from '../core/api'
import InboundHistoryPanel from '../core/InboundHistoryPanel.vue'
import InboundSummaryPanel from '../core/InboundSummaryPanel.vue'
import type { InboundListTab, InboundOrderDetail, InboundOrderSummary } from '../core/types'
import { inboundFeeStatusLabels, inboundLoadingStatusLabels, inboundOrderStatusLabels, inboundProcessStatusLabels, inboundReviewStatusLabels, inboundStatusLabel } from '../status'
type FilterModel = QueryModel & {
customerId: string
@@ -100,7 +102,7 @@ const allQueryFields = computed<QueryField[]>(() => [
{ label: '待计费', value: 'WAIT_PRICING' }, { label: '费用处理中', value: 'FEE_CONFIRMING' },
{ label: '入库处理中', value: 'INBOUNDING' }, { label: '已入库', value: 'INBOUNDED' }, { label: '已作废', value: 'VOIDED' },
] },
{ key: 'orderStatus', type: 'input', width: 200, label: '订单状态码', placeholder: '订单状态码' },
{ key: 'orderStatus', type: 'select', width: 180, label: '订单状态', placeholder: '全部订单状态', options: Object.entries(inboundOrderStatusLabels).map(([value, label]) => ({ value, label })) },
{ key: 'measureReviewStatus', type: 'select', width: 160, label: '测量审核状态', placeholder: '测量审核状态', options: [
{ label: '待审核', value: 'PENDING' }, { label: '审核通过', value: 'APPROVED' }, { label: '审核驳回', value: 'REJECTED' },
] },
@@ -108,7 +110,7 @@ const allQueryFields = computed<QueryField[]>(() => [
{ label: '未计费', value: 'UNPRICED' }, { label: '已试算', value: 'CALCULATED' },
{ label: '已确认', value: 'CONFIRMED' }, { label: '已发送', value: 'SENT' }, { label: '已作废', value: 'VOIDED' },
] },
{ key: 'feeReviewStatus', type: 'input', width: 200, label: '费用审核状态码', placeholder: '费用审核状态码' },
{ key: 'feeReviewStatus', type: 'select', width: 180, label: '费用审核状态', placeholder: '全部审核状态', options: Object.entries(inboundReviewStatusLabels).map(([value, label]) => ({ value, label })) },
{ key: 'customerName', type: 'input', width: 260, label: '客户名称', placeholder: '客户名称' },
{ key: 'destinationCode', type: 'input', width: 220, label: '目的仓编码', placeholder: '目的仓编码' },
{ key: 'holdFlag', type: 'select', width: 160, label: '挂起状态', placeholder: '挂起状态', options: [{ label: '已挂起', value: 'true' }, { label: '未挂起', value: 'false' }] },
@@ -348,7 +350,7 @@ function formatDate(value: string | null) {
}
function text(value: string | number | null | undefined) { return value === null || value === undefined || value === '' ? '—' : String(value) }
function statusLabel(status: string) {
return ({ WAIT_MEASURE: '待测量', MEASURING: '测量中', MEASURE_PENDING: '待审核', MEASURE_REJECTED: '测量驳回', WAIT_PRICING: '待计费', FEE_CONFIRMING: '待发送费用', INBOUNDING: '入库处理中', INBOUNDED: '已入库', VOIDED: '已作废' } as Record<string, string>)[status] || status || '未知状态'
return inboundStatusLabel(status, inboundProcessStatusLabels, '未知状态')
}
function statusType(status: string) {
if (status === 'INBOUNDED') return 'success'
@@ -356,13 +358,10 @@ function statusType(status: string) {
if (status === 'MEASURING' || status === 'INBOUNDING') return 'info'
return 'warning'
}
function codeLabel(status: string | null | undefined, labels: Record<string, string>) { return status ? labels[status] || status : '—' }
const measureReviewLabels: Record<string, string> = { PENDING: '待审核', APPROVED: '审核通过', REJECTED: '审核驳回' }
const feeStatusLabels: Record<string, string> = { UNPRICED: '未计费', CALCULATED: '已试算', CONFIRMED: '已确认', SENT: '已发送', VOIDED: '已作废' }
const loadingStatusLabels: Record<string, string> = { NOT_LOADED: '未配载', LOADING: '配载中', LOADED: '已配载' }
const taxModeLabels: Record<string, string> = { TAX_INCLUDED: '包税', TAX_EXCLUDED: '不包税', DDP: '包税', DDU: '不包税' }
const containerModeLabels: Record<string, string> = { FCL: '整柜', LCL: '拼箱' }
function codeLabel(status: string | null | undefined, labels: Record<string, string>) { return inboundStatusLabel(status, labels) }
const measureReviewLabels = inboundReviewStatusLabels
const feeStatusLabels = inboundFeeStatusLabels
const loadingStatusLabels = inboundLoadingStatusLabels
function resetFilters() {
selectedCustomerId.value = ''
showAdvancedFilters.value = false
@@ -453,10 +452,10 @@ onBeforeUnmount(() => { disposed = true; loadRequestId += 1; detailRequestId +=
<template #pagination-left>当前显示 {{ currentPageCount }} 条 · 共 {{ result.total }} 条</template>
<template #actions="{ row }"><div class="row-actions"><el-button link type="primary" @click="openWorkspace(row as InboundOrderSummary)"><ClipboardCheck :size="15" />处理</el-button><el-button link :loading="printingInboundId === (row as InboundOrderSummary).id" :disabled="Boolean(printingInboundId)" @click="printCartonLabels(row as InboundOrderSummary)"><Printer :size="15" />打印箱唛</el-button><el-button link @click="openDetail(row as InboundOrderSummary)"><Eye :size="15" />查看</el-button></div></template>
<template #inbound="{ row }"><div class="primary-cell"><strong>{{ text((row as InboundOrderSummary).inboundCode) }}</strong><span>订单 {{ text((row as InboundOrderSummary).orderNo) }}</span></div></template>
<template #orderStatus="{ row }"><div class="status-stack"><el-tag :type="statusType((row as InboundOrderSummary).processStatus)" effect="light">{{ statusLabel((row as InboundOrderSummary).processStatus) }}</el-tag><span>订单:{{ text((row as InboundOrderSummary).orderStatusSnapshot) }}</span></div></template>
<template #orderStatus="{ row }"><div class="status-stack"><el-tag :type="statusType((row as InboundOrderSummary).processStatus)" effect="light">{{ statusLabel((row as InboundOrderSummary).processStatus) }}</el-tag><span>订单:{{ codeLabel((row as InboundOrderSummary).orderStatusSnapshot, inboundOrderStatusLabels) }}</span></div></template>
<template #measureReviewStatus="{ row }">{{ codeLabel((row as InboundOrderSummary).measureReviewStatus, measureReviewLabels) }}</template>
<template #expectedArrivalAt="{ row }">{{ formatDate((row as InboundOrderSummary).expectedArrivalAt) }}</template>
<template #feeStatus="{ row }"><div class="status-stack"><span>{{ codeLabel((row as InboundOrderSummary).feeStatus, feeStatusLabels) }}</span><small v-if="(row as InboundOrderSummary).feeReviewStatus">审核:{{ text((row as InboundOrderSummary).feeReviewStatus) }}</small></div></template>
<template #feeStatus="{ row }"><div class="status-stack"><span>{{ codeLabel((row as InboundOrderSummary).feeStatus, feeStatusLabels) }}</span><small v-if="(row as InboundOrderSummary).feeReviewStatus">审核:{{ codeLabel((row as InboundOrderSummary).feeReviewStatus, inboundReviewStatusLabels) }}</small></div></template>
<template #customerName="{ row }">{{ text((row as InboundOrderSummary).customerName) }}</template>
<template #warehouseName="{ row }">{{ text((row as InboundOrderSummary).warehouseName) }}</template>
<template #deliveryWarehouseName="{ row }">{{ text((row as InboundOrderSummary).deliveryWarehouseName) }}</template>
@@ -466,10 +465,10 @@ onBeforeUnmount(() => { disposed = true; loadRequestId += 1; detailRequestId +=
<template #containerNo="{ row }">{{ text((row as InboundOrderSummary).containerNo) }}</template>
<template #cabinetNo="{ row }">{{ text((row as InboundOrderSummary).cabinetNo) }}</template>
<template #companyName="{ row }">{{ text((row as InboundOrderSummary).companyName) }}</template>
<template #businessType="{ row }">{{ text((row as InboundOrderSummary).businessType) }}</template>
<template #businessType="{ row }">{{ firstLegBusinessTypeLabel((row as InboundOrderSummary).businessType) }}</template>
<template #channelName="{ row }"><div class="primary-cell"><strong>{{ text((row as InboundOrderSummary).channelName) }}</strong><span>{{ text((row as InboundOrderSummary).channelCode) }}</span></div></template>
<template #taxMode="{ row }">{{ codeLabel((row as InboundOrderSummary).taxMode, taxModeLabels) }}</template>
<template #containerMode="{ row }">{{ codeLabel((row as InboundOrderSummary).containerMode, containerModeLabels) }}</template>
<template #taxMode="{ row }">{{ firstLegTaxModeLabel((row as InboundOrderSummary).taxMode) }}</template>
<template #containerMode="{ row }">{{ firstLegContainerModeLabel((row as InboundOrderSummary).containerMode) }}</template>
<template #customsInspection="{ row }">{{ (row as InboundOrderSummary).customsInspection === null ? '—' : (row as InboundOrderSummary).customsInspection ? '是' : '否' }}</template>
<template #sailingAt="{ row }">{{ formatDate((row as InboundOrderSummary).sailingAt) }}</template>
<template #portArrivalAt="{ row }">{{ formatDate((row as InboundOrderSummary).portArrivalAt) }}</template>
@@ -482,7 +481,7 @@ onBeforeUnmount(() => { disposed = true; loadRequestId += 1; detailRequestId +=
<template #locationCodes="{ row }">{{ text((row as InboundOrderSummary).locationCodes) }}</template>
<template #volumeWeightKg="{ row }">{{ formatNumber((row as InboundOrderSummary).volumeWeightKg) }}</template>
<template #actualNetWeightKg="{ row }">{{ formatNumber((row as InboundOrderSummary).actualNetWeightKg) }}</template>
<template #destinationType="{ row }">{{ text((row as InboundOrderSummary).destinationType) }}</template>
<template #destinationType="{ row }">{{ firstLegDestinationTypeLabel((row as InboundOrderSummary).destinationType) }}</template>
<template #destinationName="{ row }"><div class="primary-cell"><strong>{{ text((row as InboundOrderSummary).destinationName) }}</strong><span>{{ text((row as InboundOrderSummary).destinationCode) }}</span></div></template>
<template #measureCompletedAt="{ row }">{{ formatDate((row as InboundOrderSummary).measureCompletedAt) }}</template>
<template #inboundedAt="{ row }">{{ formatDate((row as InboundOrderSummary).inboundedAt) }}</template>
@@ -46,6 +46,8 @@ describe('MeasurementReviewPanel', () => {
expect(wrapper.text()).toContain('原单申报')
expect(wrapper.text()).toContain('设备实测')
expect(wrapper.text()).toContain('设备采集')
expect(wrapper.text()).not.toContain('DEVICE')
expect(wrapper.text()).toContain('称重 OCR')
expect(wrapper.text()).toContain('实测长 × 宽 × 高(cm)')
expect(wrapper.text()).toContain('周长超过渠道限制')
@@ -7,6 +7,7 @@ import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
import StatusBadge from '@/shared/components/StatusBadge.vue'
import { inboundReviewStatusLabels, inboundStatusLabel, weightRecognitionStatusLabels } from '../status'
import {
approveMeasurement,
getMeasurementReviewDetail,
@@ -49,12 +50,7 @@ const customerId = computed(() => props.customerId || props.tenantId || '')
const isPending = computed(() => detail.value?.measureReviewStatus === 'PENDING' && detail.value?.processStatus === 'MEASURE_PENDING')
const isRejected = computed(() => detail.value?.measureReviewStatus === 'REJECTED' && detail.value?.processStatus === 'MEASURE_REJECTED')
const confirmedWeights = computed(() => detail.value?.weightResults.filter((item) => item.confirmedAt && item.finalWeightKg !== null).length ?? 0)
const reviewStatusLabel = computed(() => {
if (isPending.value) return '待审核'
if (isRejected.value) return '已驳回'
if (detail.value?.measureReviewStatus === 'APPROVED') return '已通过'
return detail.value?.measureReviewStatus || '未提交'
})
const reviewStatusLabel = computed(() => inboundStatusLabel(detail.value?.measureReviewStatus, inboundReviewStatusLabels, '未提交'))
const measurementColumns: DataTableColumn[] = [
{ key: 'boxCode', label: '扫描箱号', minWidth: 190, slotName: 'boxCode' },
@@ -83,7 +79,16 @@ function formatDate(value: string | null | undefined) {
function resultLabel(value: string | null | undefined) {
if (value === 'APPROVED') return '审核通过'
if (value === 'REJECTED') return '审核驳回'
return value || '未知结果'
return '未知结果'
}
function recognitionStatusLabel(value: string | null | undefined) {
return inboundStatusLabel(value, weightRecognitionStatusLabels, '待确认')
}
function measurementSourceLabel(value: string | null | undefined) {
if (!value) return '-'
return ({ DEVICE: '设备采集', THIRD_PARTY: '第三方设备', MANUAL: '人工录入' } as Record<string, string>)[value] || '其他来源'
}
function isConflict(error: unknown) {
@@ -247,7 +252,7 @@ onMounted(() => {
<strong>{{ formatNumber(detail.actualGrossWeightKg, ' kg') }}</strong>
<dl>
<div><dt>已确认图片</dt><dd>{{ confirmedWeights }} / {{ detail.weightResults.length }}</dd></div>
<div><dt>测量来源</dt><dd>{{ detail.sourceType || '-' }}</dd></div>
<div><dt>测量来源</dt><dd>{{ measurementSourceLabel(detail.sourceType) }}</dd></div>
</dl>
</article>
</div>
@@ -281,7 +286,7 @@ onMounted(() => {
<div class="section-heading"><div><h3>称重图片识别结果</h3><p>原始 OCR 值不可覆盖,只有已确认且有效的结果会计入实际重量。</p></div><span class="section-count">{{ detail.weightResults.length }} 张</span></div>
<div class="weight-list">
<div v-for="item in detail.weightResults" :key="item.id" class="weight-row">
<div><strong>{{ item.fileId || '未关联文件' }}</strong><span>{{ item.ocrProvider || 'OCR' }} · {{ item.recognitionStatus || '待确认' }}</span><p v-if="item.ocrRawText">OCR 原文:{{ item.ocrRawText }}</p><p v-if="item.adjustmentReason">调整原因:{{ item.adjustmentReason }}</p></div>
<div><strong>{{ item.fileId || '未关联文件' }}</strong><span>{{ item.ocrProvider || 'OCR' }} · {{ recognitionStatusLabel(item.recognitionStatus) }}</span><p v-if="item.ocrRawText">OCR 原文:{{ item.ocrRawText }}</p><p v-if="item.adjustmentReason">调整原因:{{ item.adjustmentReason }}</p></div>
<div><span>原始</span><strong>{{ formatNumber(item.ocrWeightValue, ` ${item.ocrWeightUnit || ''}`) }}</strong></div>
<div><span>调整后</span><strong>{{ formatNumber(item.adjustedWeightValue, ` ${item.adjustedWeightUnit || ''}`) }}</strong></div>
<div><span>最终</span><strong>{{ formatNumber(item.finalWeightKg, ' kg') }}</strong></div>
+14
View File
@@ -0,0 +1,14 @@
import { describe, expect, it } from 'vitest'
import { inboundHistoryActionLabel, inboundOrderStatusLabels, inboundProcessStatusLabels, inboundStatusLabel } from './status'
describe('inbound status presentation', () => {
it('translates process and order statuses', () => {
expect(inboundStatusLabel('MEASURE_PENDING', inboundProcessStatusLabels)).toBe('待测量审核')
expect(inboundStatusLabel('WAIT_SPLIT', inboundOrderStatusLabels)).toBe('待分柜')
})
it('does not expose unknown status and action codes', () => {
expect(inboundStatusLabel('NEW_STATUS', inboundProcessStatusLabels)).toBe('未知状态')
expect(inboundHistoryActionLabel('NEW_ACTION')).toBe('状态更新')
})
})
+47
View File
@@ -0,0 +1,47 @@
export const inboundProcessStatusLabels: Record<string, string> = {
WAIT_MEASURE: '待测量', MEASURING: '测量中', MEASURE_PENDING: '待测量审核', MEASURE_REJECTED: '测量已驳回',
WAIT_PRICING: '待计费', FEE_CONFIRMING: '费用处理中', INBOUNDING: '入库处理中', INBOUNDED: '已入库', VOIDED: '已作废',
}
export const inboundOrderStatusLabels: Record<string, string> = {
DRAFT: '草稿', SUBMITTED: '待审核', APPROVED: '已审核', ACCEPTED: '已受理', REJECTED: '已驳回',
PROCESSING: '处理中', COMPLETED: '已完成', CANCELLED: '已取消', PENDING_REVIEW: '待审核',
WAIT_MEASURE: '待测量', INBOUNDED: '已入库', WAIT_SPLIT: '待分柜', LOADING: '装柜中',
LOADED: '装柜完成', DELIVERED: '派送完成',
}
export const inboundReviewStatusLabels: Record<string, string> = {
NOT_SUBMITTED: '未提交', UNRECORDED: '未录入', PENDING: '待审核', APPROVED: '审核通过', REJECTED: '审核驳回', CONFIRMED: '已确认',
}
export const inboundFeeStatusLabels: Record<string, string> = {
UNPRICED: '未计费', CALCULATED: '已试算', CONFIRMED: '已确认', SENT: '已发送', VOIDED: '已作废',
}
export const inboundLoadingStatusLabels: Record<string, string> = {
NOT_LOADED: '未配载', WAIT_LOADING: '待配载', EDITING: '预装柜中', LOADING: '配载中', LOADED: '已配载', COMPLETED: '装柜完成',
}
export const inboundNotificationStatusLabels: Record<string, string> = {
NOT_CREATED: '未创建', PENDING: '待发送', SUCCESS: '发送成功', FAILED: '发送失败',
}
export const weightRecognitionStatusLabels: Record<string, string> = {
PENDING: '待识别', PENDING_CONFIRM: '待确认', NEEDS_MANUAL: '需人工录入', CONFIRMED: '已确认',
}
const inboundHistoryActionLabels: Record<string, string> = {
CREATE: '创建入库单', STATUS_UPDATE: '状态更新', MEASURE_SUBMIT: '提交测量', MEASURE_APPROVE: '测量审核通过',
MEASURE_REJECT: '测量审核驳回', FEE_CALCULATE: '费用试算', FEE_CONFIRM: '费用确认',
INBOUND_BEGIN: '开始入库', INBOUND_COMPLETE: '完成入库', VOID: '作废入库单',
}
export function inboundStatusLabel(value: string | null | undefined, labels: Record<string, string>, emptyLabel = '—') {
if (!value) return emptyLabel
return labels[value] || '未知状态'
}
export function inboundHistoryActionLabel(value: string | null | undefined) {
if (!value) return '状态更新'
return inboundHistoryActionLabels[value] || '状态更新'
}
@@ -22,6 +22,7 @@ import {
} from '../api/weightOcr'
import type { WeightResultResource, WeightWorkspaceResource } from '../types'
import WeightImageThumbnail from './WeightImageThumbnail.vue'
import { inboundProcessStatusLabels, inboundStatusLabel, weightRecognitionStatusLabels } from '../../status'
const props = defineProps<{ customerId?: string; tenantId?: string; inboundId: string }>()
const emit = defineEmits<{ changed: [] }>()
@@ -73,11 +74,7 @@ function formatConfidence(value: number | null) {
}
function recognitionLabel(status: string) {
return ({
PENDING_CONFIRM: '待确认',
NEEDS_MANUAL: '需人工录入',
CONFIRMED: '已确认',
} as Record<string, string>)[status] || status
return inboundStatusLabel(status, weightRecognitionStatusLabels, '未知状态')
}
function requiresEvidenceReview(row: WeightResultResource) {
@@ -384,7 +381,7 @@ defineExpose({ reload: loadWorkspace })
<template v-else>
<el-alert v-if="workspace && !editable" type="info" :closable="false" show-icon class="weight-state-note">
当前入库状态为 {{ workspace.processStatus }},称重数据仅供查看。
当前入库状态为 {{ inboundStatusLabel(workspace.processStatus, inboundProcessStatusLabels, '未知状态') }},称重数据仅供查看。
</el-alert>
<LoadErrorState