This commit is contained in:
2026-08-18 11:33:01 +08:00
parent 834382e04b
commit 114d5bacc7
34 changed files with 522 additions and 570 deletions
@@ -98,6 +98,23 @@ describe('unified orders API contract', () => {
expect(http.post).toHaveBeenCalledWith('/platform/v1/orders/first-leg/90001/approve', undefined, { params: { tenantId: '208' } })
})
it('keeps split rows bound to their own current-container schedule and preserves effective-time semantics', async () => {
vi.mocked(http.get).mockResolvedValue({ data: {
records: [
{ orderId: '90001', tenantId: '208', inboundId: '701', mainStatus: 'LOADED', relation: { role: 'PARENT', rootOrderId: '90001', parentOrderId: null, splitSequence: null, childCount: 1 }, transport: { cabinetNo: 'OLD-CAB', estimatedSailingAt: '2026-08-20T08:00:00', actualSailingAt: '2026-08-21T09:00:00', effectiveSailingAt: '2026-08-21T09:00:00', effectiveSailingAtEstimated: false, estimatedPortArrivalAt: '2026-09-01T08:00:00', actualPortArrivalAt: null, effectivePortArrivalAt: '2026-09-01T08:00:00', effectivePortArrivalAtEstimated: true } },
{ orderId: '90002', tenantId: '208', inboundId: '702', mainStatus: 'LOADED', relation: { role: 'CHILD', rootOrderId: '90001', parentOrderId: '90001', splitSequence: 1, childCount: 0 }, transport: { cabinetNo: 'NEW-CAB', estimatedSailingAt: '2026-08-25T08:00:00', actualSailingAt: null, effectiveSailingAt: '2026-08-25T08:00:00', effectiveSailingAtEstimated: true, estimatedPortArrivalAt: null, actualPortArrivalAt: null, effectivePortArrivalAt: null, effectivePortArrivalAtEstimated: null } },
],
page: 1, size: 20, total: 2, pages: 1,
} })
const result = await listPlatformUnifiedOrders({ page: 1, size: 20 })
expect(result.records.map((row) => ({ inboundId: row.inboundId, transport: row.transport }))).toMatchObject([
{ inboundId: '701', transport: { cabinetNo: 'OLD-CAB', effectiveSailingAt: '2026-08-21T09:00:00', effectiveSailingAtEstimated: false, effectivePortArrivalAt: '2026-09-01T08:00:00', effectivePortArrivalAtEstimated: true } },
{ inboundId: '702', transport: { cabinetNo: 'NEW-CAB', effectiveSailingAt: '2026-08-25T08:00:00', effectiveSailingAtEstimated: true, effectivePortArrivalAt: null, effectivePortArrivalAtEstimated: null } },
])
})
it('uses the tenant-scoped batch approval contract and preserves long IDs', async () => {
vi.mocked(http.post).mockResolvedValue({ data: {
total: 2, successCount: 1, failureCount: 1,
@@ -57,8 +57,14 @@ export interface UnifiedOrderTransportSummary {
cabinetNo: string | null
vesselName: string | null
voyageNo: string | null
sailingAt: string | null
portArrivalAt: string | null
estimatedSailingAt: string | null
actualSailingAt: string | null
effectiveSailingAt: string | null
effectiveSailingAtEstimated: boolean | null
estimatedPortArrivalAt: string | null
actualPortArrivalAt: string | null
effectivePortArrivalAt: string | null
effectivePortArrivalAtEstimated: boolean | null
}
export interface UnifiedOrderRelationSummary {
@@ -100,7 +100,7 @@ describe('UnifiedOrdersView contract', () => {
'actualVolumeCbm', 'totalNetWeightKg', 'chargeableWeightKg', 'chargeableVolumeCbm', 'expectedShipDate',
'expectedArrivalAt', 'feeReviewStatus', 'skuNos', 'totalQuantity', 'expressCompany', 'expressTrackingNo',
'shipmentId', 'referenceId', 'containerNo', 'cabinetNo',
'sailingAt', 'portArrivalAt', 'customerRemark', 'latestInternalNote', 'createdAt', 'updatedAt',
'effectiveSailingAt', 'effectivePortArrivalAt', 'customerRemark', 'latestInternalNote', 'createdAt', 'updatedAt',
]
for (const key of expectedColumnKeys) expect(columnSource).toContain(`key: '${key}'`)
for (const uncontractedKey of [
@@ -115,10 +115,18 @@ describe('UnifiedOrdersView contract', () => {
expect(columnSource).not.toContain("label: '配载状态'")
expect(columnSource).not.toContain("key: 'recommendedPalletType'")
expect(columnSource).toContain("key: 'requestedPickupAt', prop: 'requestedPickupAt', label: '预约提货时间'")
expect(columnSource.match(/defaultVisible: false/g)).toHaveLength(16)
expect(columnSource.match(/defaultVisible: false/g)).toHaveLength(14)
expect(columnSource).toContain("key: 'effectiveSailingAt', prop: 'effectiveSailingAt', label: '开船时间', minWidth: 180, slotName: 'field' }")
expect(columnSource).toContain("key: 'effectivePortArrivalAt', prop: 'effectivePortArrivalAt', label: '到港时间', minWidth: 180, slotName: 'field' }")
expect(source).toContain('preference-key="unified-orders-table-v2"')
expect(source).toContain("property === 'requestedPickupAt' && (row.pickupMethod !== 'PICKUP' || !value)")
expect(source).toContain("property === 'skuNos' ? value.join('、')")
expect(source).not.toContain('<dt>推荐托盘类型</dt>')
expect(source).toContain("estimated === true ? '预计 ' : ''")
expect(source).toContain('row.transport?.effectiveSailingAtEstimated')
expect(source).toContain('row.transport?.effectivePortArrivalAtEstimated')
expect(source).not.toContain("transport.sailingAt")
expect(source).not.toContain("transport.portArrivalAt")
expect(source).toContain('.table-card { overflow: hidden; }')
expect(source).toContain('slotName: \'measurementReviewStatus\'')
expect(source).toContain('unifiedOrderReviewStatusLabel(value, \'未录入\')')
@@ -266,8 +266,8 @@ const columns = computed<DataTableColumn[]>(() => [
{ key: 'referenceId', prop: 'referenceId', label: 'Reference ID', minWidth: 160, slotName: 'field', defaultVisible: false },
{ key: 'containerNo', prop: 'containerNo', label: '真实集装箱号', minWidth: 160, slotName: 'field', defaultVisible: false },
{ key: 'cabinetNo', prop: 'cabinetNo', label: '内部柜次号', minWidth: 150, slotName: 'field', defaultVisible: false },
{ key: 'sailingAt', prop: 'sailingAt', label: '开船时间', minWidth: 150, slotName: 'field', defaultVisible: false },
{ key: 'portArrivalAt', prop: 'portArrivalAt', label: '到港时间', minWidth: 150, slotName: 'field', defaultVisible: false },
{ key: 'effectiveSailingAt', prop: 'effectiveSailingAt', label: '开船时间', minWidth: 180, slotName: 'field' },
{ key: 'effectivePortArrivalAt', prop: 'effectivePortArrivalAt', label: '到港时间', minWidth: 180, slotName: 'field' },
{ key: 'customerRemark', prop: 'customerRemark', label: '客户备注', minWidth: 180, slotName: 'field', defaultVisible: false },
{ key: 'latestInternalNote', prop: 'latestInternalNote', label: '内部备注', minWidth: 180, slotName: 'field', defaultVisible: false },
{ key: 'createdAt', prop: 'createdAt', label: '创建时间', minWidth: 150, slotName: 'field', defaultVisible: false },
@@ -370,13 +370,18 @@ function displayBoolean(value: boolean | null | undefined) {
return value === null || value === undefined ? '—' : value ? '是' : '否'
}
function formatEffectiveTransportTime(value: string | null | undefined, estimated: boolean | null | undefined) {
if (!value) return '—'
return `${estimated === true ? '预计 ' : ''}${formatDate(value)}`
}
function displayField(row: UnifiedOrderResource, property: string | undefined) {
if (!property) return '—'
const source = row as unknown as Record<string, unknown>
const nested: Record<string, string> = {
measurementReviewStatus: 'measurement.reviewStatus', expectedArrivalAt: 'expectedArrivalAt',
containerNo: 'transport.containerNo', cabinetNo: 'transport.cabinetNo',
sailingAt: 'transport.sailingAt', portArrivalAt: 'transport.portArrivalAt',
effectiveSailingAt: 'transport.effectiveSailingAt', effectivePortArrivalAt: 'transport.effectivePortArrivalAt',
chargeableWeightKg: 'measurement.chargeableWeightKg', measurementCompletedAt: 'measurement.completedAt',
expressTrackingNo: 'delivery.trackingNo',
}
@@ -386,6 +391,8 @@ function displayField(row: UnifiedOrderResource, property: string | undefined) {
), source)
if ((value === null || value === undefined || value === '') && property === 'internalOrderStatus') value = source.mainStatus
if (property === 'requestedPickupAt' && (row.pickupMethod !== 'PICKUP' || !value)) return '—'
if (property === 'effectiveSailingAt') return formatEffectiveTransportTime(row.transport?.effectiveSailingAt, row.transport?.effectiveSailingAtEstimated)
if (property === 'effectivePortArrivalAt') return formatEffectiveTransportTime(row.transport?.effectivePortArrivalAt, row.transport?.effectivePortArrivalAtEstimated)
if (value === null || value === undefined || value === '') return '—'
if (typeof value === 'boolean') return value ? '是' : '否'
if (Array.isArray(value)) return value.length ? (property === 'skuNos' ? value.join('、') : `${value.length} 个文件`) : '—'
@@ -942,8 +949,8 @@ onBeforeUnmount(() => {
<div><dt>真实集装箱号</dt><dd>{{ detail.summary.transport?.containerNo || '—' }}</dd></div>
<div><dt>内部柜次号</dt><dd>{{ detail.summary.transport?.cabinetNo || '—' }}</dd></div>
<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>{{ formatEffectiveTransportTime(detail.summary.transport?.effectiveSailingAt, detail.summary.transport?.effectiveSailingAtEstimated) }}</dd></div>
<div><dt>到港时间</dt><dd>{{ formatEffectiveTransportTime(detail.summary.transport?.effectivePortArrivalAt, detail.summary.transport?.effectivePortArrivalAtEstimated) }}</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>
@@ -1168,7 +1175,7 @@ onBeforeUnmount(() => {
<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" border stripe empty-text="暂无符合条件的订单" aria-label="统一订单列表" @selection-change="handleApprovalSelectionChange">
<DataTable v-model:page="page" v-model:page-size="pageSize" :data="records" :columns="columns" preference-key="unified-orders-table-v2" :loading="loading" :total="total" :page-sizes="[20, 50, 100]" row-key="orderId" border stripe empty-text="暂无符合条件的订单" aria-label="统一订单列表" @selection-change="handleApprovalSelectionChange">
<template #toolbar-actions>
<el-button v-if="can('platform:order:approve')" type="success" :loading="batchApprovalSaving" :disabled="loading || !selectedApprovalOrders.length" @click="batchApproveOrders"><CircleCheck :size="16" />批量审核通过<span v-if="selectedApprovalOrders.length">({{ selectedApprovalOrders.length }})</span></el-button>
<el-button v-if="can('platform:firstleg:smart-loading:list')" type="primary" @click="smartLoadingOpen = true"><PackageOpen :size="16" />智能装柜</el-button>