This commit is contained in:
2026-08-12 09:25:55 +08:00
parent ffb2116741
commit fb542e0b6f
24 changed files with 983 additions and 75 deletions
@@ -69,11 +69,11 @@ describe('unified orders API contract', () => {
it('normalizes C02 row IDs and keeps inboundId for measurement routing', async () => {
vi.mocked(http.get).mockResolvedValue({ data: {
records: [{ orderId: '90001', tenantId: '208', inboundId: '77', inboundVersion: 3, mainStatus: 'WAIT_MEASURE', recommendedPalletType: 'STANDARD', locationCodes: 'A0-01, B2-03', customerRemark: '客户备注', latestInternalNote: '最新内部备注', measurement: null, preload: null, loading: null, delivery: null, transport: null }],
records: [{ orderId: '90001', tenantId: '208', inboundId: '77', inboundVersion: 3, mainStatus: 'WAIT_MEASURE', recommendedPalletType: 'STANDARD', locationCodes: 'A0-01, B2-03', customerRemark: '客户备注', latestInternalNote: '最新内部备注', relation: { role: 'PARENT', rootOrderId: 90001, parentOrderId: null, splitSequence: null, childCount: '2' }, measurement: null, preload: null, loading: null, delivery: null, transport: null }],
page: 1, size: 20, total: 1, pages: 1,
} })
await expect(listPlatformUnifiedOrders({ page: 1, size: 20 })).resolves.toMatchObject({
records: [{ orderId: '90001', tenantId: '208', inboundId: '77', inboundVersion: 3, recommendedPalletType: 'STANDARD', locationCodes: 'A0-01, B2-03', customerRemark: '客户备注', latestInternalNote: '最新内部备注' }],
records: [{ orderId: '90001', tenantId: '208', inboundId: '77', inboundVersion: 3, recommendedPalletType: 'STANDARD', locationCodes: 'A0-01, B2-03', customerRemark: '客户备注', latestInternalNote: '最新内部备注', relation: { role: 'PARENT', rootOrderId: '90001', childCount: 2 } }],
})
})
@@ -63,12 +63,21 @@ export interface UnifiedOrderTransportSummary {
portArrivalAt: string | null
}
export interface UnifiedOrderRelationSummary {
role: 'PARENT' | 'CHILD'
rootOrderId: number | string | null
parentOrderId: number | string | null
splitSequence: number | null
childCount: number
}
export interface UnifiedOrderResource {
orderId: number | string
tenantId: number | string
orderNo: string | null
mainStatus: UnifiedOrderMainStatus
nextAction: string | null
relation: UnifiedOrderRelationSummary | null
internalOrderStatus: string | null
customerId: number | string | null
customerName: string | null
@@ -319,6 +328,13 @@ function normalizeRow(raw: UnifiedOrderResource): UnifiedOrderResource {
destinationWarehouseId: id(raw.destinationWarehouseId),
inboundId: id(raw.inboundId),
inboundVersion: nullableInteger(raw.inboundVersion),
relation: raw.relation ? {
...raw.relation,
rootOrderId: id(raw.relation.rootOrderId),
parentOrderId: id(raw.relation.parentOrderId),
splitSequence: nullableInteger(raw.relation.splitSequence),
childCount: integer(raw.relation.childCount, 0),
} : null,
totalPackageCount: nullableInteger(raw.totalPackageCount),
totalPalletCount: nullableInteger(raw.totalPalletCount),
recommendedPalletType: raw.recommendedPalletType ?? null,
@@ -162,9 +162,13 @@ describe('UnifiedOrdersView contract', () => {
expect(source).not.toContain("value.replaceAll('_', ' ')")
})
it('exposes the single-remainder fulfillment aggregate in row expansion and detail', () => {
expect(source).toContain("{ key: 'fulfillment', type: 'expand', width: 52, fixed: 'left'")
expect(source).toContain('<template #fulfillment="{ row }">')
it('shows parent-child markers only for related rows and keeps the aggregate in detail', () => {
expect(source).not.toContain("type: 'expand'")
expect(source).toContain('v-if="(row as UnifiedOrderResource).relation"')
expect(source).toContain('父单 · ${row.relation.childCount} 个子单')
expect(source).toContain('子单 · 第 ${row.relation.splitSequence} 次')
expect(source).toContain('title="父子订单关联"')
expect(source).toContain('@click="openOrderRelation(row as UnifiedOrderResource)"')
expect(source).toContain("{ key: 'fulfillment', label: '拆分履约', icon: ListChecks }")
expect(source).toContain("activeDetailTab === 'fulfillment'")
expect(source).toContain('根单、已完成分片与当前余量')
@@ -184,7 +188,6 @@ describe('UnifiedOrdersView contract', () => {
expect(source).toContain('aria-label="订单详情模块"')
expect(source).toContain("{ key: 'basic', label: '基本信息'")
expect(source).toContain("{ key: 'measurement', label: '测量数据'")
expect(source).not.toContain('<AppDrawer')
expect(source).not.toContain('detailOpen')
expect(source).toContain('watch(detailRouteKey,')
expect(source).toContain('requestKey !== detailRouteKey.value')
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { computed, nextTick, onMounted, reactive, ref, watch, type Component } from 'vue'
import { ElMessage } from 'element-plus'
import { ArrowLeft, Box, ChevronDown, ChevronUp, CircleCheck, Eye, History, ListChecks, MoreHorizontal, PackageOpen, Ruler, SlidersHorizontal, StickyNote, WalletCards } from '@lucide/vue'
import { ArrowLeft, Box, ChevronDown, ChevronUp, CircleCheck, Eye, GitBranch, History, ListChecks, MoreHorizontal, PackageOpen, Ruler, SlidersHorizontal, StickyNote, WalletCards } from '@lucide/vue'
import { useRoute, useRouter } from 'vue-router'
import {
getPlatformUnifiedOrder,
@@ -23,6 +23,7 @@ import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
import AdminPage from '@/shared/components/AdminPage.vue'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import AppDialog from '@/shared/components/AppDialog.vue'
import AppDrawer from '@/shared/components/AppDrawer.vue'
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
import DetailPageLayout from '@/shared/components/DetailPageLayout.vue'
import ListPageLayout from '@/shared/components/ListPageLayout.vue'
@@ -97,6 +98,8 @@ const noteOpen = ref(false)
const noteSaving = ref(false)
const noteContent = ref('')
const noteOrder = ref<UnifiedOrderResource | null>(null)
const relationOpen = ref(false)
const relationOrder = ref<UnifiedOrderResource | null>(null)
const feeDetails = ref<UnifiedOrderFeeDetailResource[]>([])
const feeLoading = ref(false)
const feeLoadedKey = ref('')
@@ -197,8 +200,7 @@ const queryFields = computed(() => showAdvancedFilters.value
const columns: DataTableColumn[] = [
{ key: 'actions', label: '操作', width: 300, fixed: 'left', slotName: 'actions' },
{ key: 'fulfillment', type: 'expand', width: 52, fixed: 'left', slotName: 'fulfillment', hideable: false, movable: false },
{ key: 'inboundCode', prop: 'inboundCode', label: '入仓码', minWidth: 140, slotName: 'field' },
{ key: 'inboundCode', prop: 'inboundCode', label: '入仓码', minWidth: 190, slotName: 'inboundCode' },
{ key: 'internalOrderStatus', prop: 'internalOrderStatus', label: '订单状态', minWidth: 130, slotName: 'orderStatus' },
{ key: 'measurementReviewStatus', prop: 'measurementReviewStatus', label: '测量数据审核状态', minWidth: 170, slotName: 'measurementReviewStatus' },
{ key: 'expectedArrivalAt', prop: 'expectedArrivalAt', label: '预计到达时间', minWidth: 150, slotName: 'field' },
@@ -303,6 +305,19 @@ function customerName(row: UnifiedOrderResource) {
return row.customerName?.trim() || customers.value.find((item) => String(item.id) === String(row.tenantId))?.tenantName || `客户 ${row.tenantId}`
}
function orderRelationLabel(row: UnifiedOrderResource) {
if (row.relation?.role === 'PARENT') return `父单 · ${row.relation.childCount} 个子单`
if (row.relation?.role === 'CHILD' && row.relation.splitSequence !== null) {
return `子单 · 第 ${row.relation.splitSequence} 次`
}
return '子单'
}
function openOrderRelation(row: UnifiedOrderResource) {
relationOrder.value = row
relationOpen.value = true
}
function formatDate(value: string | null | undefined) {
return value ? value.replace('T', ' ').slice(0, 16) : '—'
}
@@ -1074,12 +1089,18 @@ onMounted(() => {
</el-dropdown>
</div>
</template>
<template #fulfillment="{ row }">
<OrderFulfillmentTreePanel
:tenant-id="String((row as UnifiedOrderResource).tenantId)"
:order-id="String((row as UnifiedOrderResource).orderId)"
compact
/>
<template #inboundCode="{ row }">
<div class="inbound-relation-cell">
<span>{{ (row as UnifiedOrderResource).inboundCode || '—' }}</span>
<el-button
v-if="(row as UnifiedOrderResource).relation"
class="order-relation-link"
link
type="primary"
:aria-label="`查看${orderRelationLabel(row as UnifiedOrderResource)}关联`"
@click="openOrderRelation(row as UnifiedOrderResource)"
><GitBranch :size="14" />{{ orderRelationLabel(row as UnifiedOrderResource) }}</el-button>
</div>
</template>
<template #orderStatus="{ row }"><StatusBadge :status="orderStatusLabel((row as UnifiedOrderResource).internalOrderStatus || (row as UnifiedOrderResource).mainStatus)" /></template>
<template #measurementReviewStatus="{ row }"><StatusBadge :status="measurementStatusLabel((row as UnifiedOrderResource).measurement?.reviewStatus)" /></template>
@@ -1100,6 +1121,14 @@ onMounted(() => {
</ListPageLayout>
</template>
<AppDrawer v-model="relationOpen" title="父子订单关联" size="min(860px, 94vw)" :show-footer="false" destroy-on-close>
<OrderFulfillmentTreePanel
v-if="relationOrder"
:tenant-id="String(relationOrder.tenantId)"
:order-id="String(relationOrder.orderId)"
/>
</AppDrawer>
<AppDialog v-model="noteOpen" title="添加内部备注" confirm-text="保存备注" :confirm-loading="noteSaving" @confirm="saveNote">
<el-input v-model="noteContent" type="textarea" :rows="5" maxlength="2000" show-word-limit placeholder="请输入仅平台内部可见的备注" aria-label="内部备注内容" />
</AppDialog>
@@ -1130,6 +1159,10 @@ onMounted(() => {
.table-card { overflow: hidden; }
.row-actions { display: flex; align-items: center; gap: 4px; white-space: nowrap; }
.row-actions :deep(.el-button) { margin-left: 0; }
.inbound-relation-cell { display: grid; min-width: 0; gap: 5px; }
.inbound-relation-cell > span { overflow-wrap: anywhere; }
.order-relation-link { width: fit-content; max-width: 100%; height: auto; justify-content: flex-start; padding: 0; font-size: 12px; }
.order-relation-link :deep(span) { min-width: 0; white-space: normal; text-align: left; }
.primary-cell { display: grid; min-width: 0; gap: 4px; }
.primary-cell strong { overflow-wrap: anywhere; }
.primary-cell span { color: var(--cc-muted); font-size: 12px; overflow-wrap: anywhere; }