优化
This commit is contained in:
@@ -11,6 +11,11 @@ describe('OrderFulfillmentTreePanel contract', () => {
|
||||
}
|
||||
expect(source).toContain('历史双子单模型')
|
||||
expect(source).toContain('新拆分不会再生成已装子单')
|
||||
expect(source).toContain('inboundCode?: string | null')
|
||||
expect(source).toContain("props.inboundCode || '暂缺'")
|
||||
expect(source).not.toContain('tree.rootOrderNo')
|
||||
expect(source).not.toContain('tree.rootOrderId')
|
||||
expect(source).not.toContain('row.orderNo')
|
||||
})
|
||||
|
||||
it('covers loading, error, empty and responsive states', () => {
|
||||
|
||||
@@ -7,7 +7,7 @@ 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 props = defineProps<{ tenantId: string; orderId: string; inboundCode?: string | null; compact?: boolean }>()
|
||||
const tree = ref<UnifiedOrderFulfillmentTree | null>(null)
|
||||
const loading = ref(false)
|
||||
const error = ref<ApiErrorDetails | null>(null)
|
||||
@@ -31,14 +31,14 @@ onMounted(() => { void load() })
|
||||
<div v-else-if="loading" class="tree-loading" aria-live="polite">正在加载拆分履约…</div>
|
||||
<template v-else-if="tree">
|
||||
<div class="tree-summary">
|
||||
<div><span>根单</span><strong>{{ tree.rootOrderNo || tree.rootOrderId }}</strong></div>
|
||||
<div><span>入仓码</span><strong>{{ props.inboundCode || '暂缺' }}</strong></div>
|
||||
<div><span>原始总量</span><strong>{{ tree.originalPackageCount ?? '—' }} 箱</strong></div>
|
||||
<div><span>累计已装</span><strong>{{ tree.cumulativeLoadedPackageCount ?? '—' }} 箱</strong></div>
|
||||
<div><span>当前余量</span><strong>{{ tree.currentRemainderPackageCount ?? '—' }} 箱</strong></div>
|
||||
</div>
|
||||
<el-alert v-if="tree.historicalTwoChildModel" type="info" :closable="false" show-icon title="该记录使用历史双子单模型,仅兼容展示;新拆分不会再生成已装子单。" />
|
||||
<el-table :data="tree.fragments" row-key="orderId" border stripe 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="分片" min-width="180"><template #default="{ row }"><strong>{{ unifiedOrderFulfillmentRoleLabel(row.splitRole) }}</strong><small v-if="row.splitSequence != null"> · 第 {{ row.splitSequence }} 次</small></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>
|
||||
|
||||
@@ -93,7 +93,7 @@ describe('UnifiedOrdersView contract', () => {
|
||||
expect(source).toContain('v-model:page-size="pageSize"')
|
||||
const columnSource = source.slice(source.indexOf('const columns:'), source.indexOf('function emptyFilters'))
|
||||
const expectedColumnKeys = [
|
||||
'actions', 'orderNo', 'inboundCode', 'mainStatus', 'customerName', 'receivingWarehouseName',
|
||||
'actions', 'inboundCode', 'mainStatus', 'customerName', 'receivingWarehouseName',
|
||||
'destinationWarehouseName', 'totalPackageCount', 'measurementReviewStatus', 'actualGrossWeightKg',
|
||||
'actualVolumeCbm', 'chargeableWeightKg', 'expectedShipDate', 'loadingStatus', 'containerNo', 'cabinetNo',
|
||||
'sailingAt', 'portArrivalAt', 'customerRemark', 'latestInternalNote', 'createdAt', 'updatedAt',
|
||||
@@ -103,8 +103,8 @@ describe('UnifiedOrdersView contract', () => {
|
||||
'internalOrderStatus', 'expectedArrivalAt', 'feeReviewStatus', 'holdCargo', 'companyName',
|
||||
'pickupMethod', 'chargeableVolumeCbm', 'recommendedPalletType', 'locationCodes', 'associatedFiles', 'createdBy',
|
||||
]) expect(columnSource).not.toContain(`key: '${uncontractedKey}'`)
|
||||
expect(columnSource.indexOf("key: 'actions'")).toBeLessThan(columnSource.indexOf("key: 'orderNo'"))
|
||||
expect(columnSource.indexOf("key: 'orderNo'")).toBeLessThan(columnSource.indexOf("key: 'inboundCode'"))
|
||||
expect(columnSource).not.toContain("key: 'orderNo'")
|
||||
expect(columnSource.indexOf("key: 'actions'")).toBeLessThan(columnSource.indexOf("key: 'inboundCode'"))
|
||||
expect(columnSource).toContain("{ key: 'mainStatus', prop: 'mainStatus', label: '订单主状态'")
|
||||
expect(columnSource).toContain("{ key: 'loadingStatus', prop: 'loadingStatus', label: '配载状态', minWidth: 130, slotName: 'field', defaultVisible: false }")
|
||||
expect(columnSource.match(/defaultVisible: false/g)).toHaveLength(8)
|
||||
@@ -229,8 +229,8 @@ describe('UnifiedOrdersView contract', () => {
|
||||
|
||||
it('distinguishes overflow action buttons by row and keeps a stable missing-field fallback', () => {
|
||||
expect(source).toContain('function moreUnifiedOrderActionsLabel(row: UnifiedOrderResource)')
|
||||
expect(source).toContain('[compatibleOrder.inboundCode, compatibleOrder.orderNo, compatibleOrder.orderId, compatibleOrder.id]')
|
||||
expect(source).toContain("identifier || '当前订单'")
|
||||
expect(source).toContain("String(row.inboundCode || '').trim() || '当前入仓单'")
|
||||
expect(source).not.toContain('compatibleOrder.orderNo')
|
||||
expect(source).not.toContain('aria-label="更多订单操作"')
|
||||
expect(source).not.toContain('title="更多操作"')
|
||||
})
|
||||
|
||||
@@ -210,7 +210,6 @@ const queryFields = computed(() => showAdvancedFilters.value
|
||||
|
||||
const columns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 300, fixed: 'left', slotName: 'actions' },
|
||||
{ key: 'orderNo', prop: 'orderNo', label: '订单号', minWidth: 190, slotName: 'order' },
|
||||
{ key: 'inboundCode', prop: 'inboundCode', label: '入仓码', minWidth: 190, slotName: 'inboundCode' },
|
||||
{ key: 'mainStatus', prop: 'mainStatus', label: '订单主状态', minWidth: 130, slotName: 'mainStatus' },
|
||||
{ key: 'customerName', prop: 'customerName', label: '客户', minWidth: 150, slotName: 'field' },
|
||||
@@ -676,11 +675,7 @@ function moreUnifiedOrderActions(row: UnifiedOrderResource) {
|
||||
return unifiedOrderSecondaryActions(row).slice(2)
|
||||
}
|
||||
function moreUnifiedOrderActionsLabel(row: UnifiedOrderResource) {
|
||||
const compatibleOrder = row as UnifiedOrderResource & { id?: string | number | null }
|
||||
const identifier = [compatibleOrder.inboundCode, compatibleOrder.orderNo, compatibleOrder.orderId, compatibleOrder.id]
|
||||
.map((value) => String(value ?? '').trim())
|
||||
.find(Boolean)
|
||||
return `更多订单操作:${identifier || '当前订单'}`
|
||||
return `更多订单操作:${String(row.inboundCode || '').trim() || '当前入仓单'}`
|
||||
}
|
||||
function unifiedOrderActionLabel(action: UnifiedOrderRowAction) {
|
||||
if (action === 'note') return '添加备注'
|
||||
@@ -763,8 +758,7 @@ onMounted(() => {
|
||||
<el-button link class="back-button" @click="closeDetail"><ArrowLeft :size="16" />返回列表</el-button>
|
||||
<span class="order-mark" aria-hidden="true"><Box :size="19" /></span>
|
||||
<div class="order-title">
|
||||
<h1>{{ detail?.summary.inboundCode || (detail?.summary.inboundId ? `入库单 ${detail.summary.inboundId}` : '暂未生成') }}</h1>
|
||||
<p>{{ detail?.summary.orderNo || `订单 ${orderId}` }}</p>
|
||||
<h1>{{ detail?.summary.inboundCode || '入仓码暂缺' }}</h1>
|
||||
</div>
|
||||
<StatusBadge v-if="detail" :status="statusLabel(detail.summary.mainStatus)" />
|
||||
</div>
|
||||
@@ -792,7 +786,7 @@ onMounted(() => {
|
||||
|
||||
<section :id="`order-detail-panel-${activeDetailTab}`" class="detail-panel" role="tabpanel" :aria-labelledby="`order-detail-tab-${activeDetailTab}`" tabindex="0">
|
||||
<template v-if="activeDetailTab === 'basic'">
|
||||
<div class="panel-title"><div><h2>订单基本信息</h2><p>{{ detail.summary.orderNo || `订单 ${detail.summary.orderId}` }}</p></div><Box :size="19" /></div>
|
||||
<div class="panel-title"><div><h2>订单基本信息</h2><p>{{ detail.summary.inboundCode || '入仓码暂缺' }}</p></div><Box :size="19" /></div>
|
||||
<dl class="order-info-grid">
|
||||
<div><dt>公司</dt><dd>{{ customerName(detail.summary) }}</dd></div>
|
||||
<div><dt>运输方式</dt><dd>{{ firstLegTransportModeLabel(detail.order.order?.transportMode) }}</dd></div>
|
||||
@@ -898,7 +892,7 @@ onMounted(() => {
|
||||
|
||||
<template v-else-if="activeDetailTab === 'fulfillment'">
|
||||
<div class="panel-title"><div><h2>拆分履约</h2><p>根单、已完成分片与当前余量</p></div><ListChecks :size="19" /></div>
|
||||
<OrderFulfillmentTreePanel :tenant-id="String(detail.summary.tenantId)" :order-id="String(detail.summary.orderId)" />
|
||||
<OrderFulfillmentTreePanel :tenant-id="String(detail.summary.tenantId)" :order-id="String(detail.summary.orderId)" :inbound-code="detail.summary.inboundCode" />
|
||||
</template>
|
||||
|
||||
<template v-else-if="activeDetailTab === 'workflow'">
|
||||
@@ -1021,7 +1015,6 @@ onMounted(() => {
|
||||
<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>
|
||||
<template #field="{ row, column }"><LineClampText v-if="['customerRemark', 'latestInternalNote'].includes(String(column.property))" :content="displayField(row as UnifiedOrderResource, column.property)" :lines="2" /><template v-else>{{ displayField(row as UnifiedOrderResource, column.property) }}</template></template>
|
||||
<template #order="{ row }"><div class="primary-cell"><strong>{{ (row as UnifiedOrderResource).orderNo || `订单 ${(row as UnifiedOrderResource).orderId}` }}</strong><span v-if="(row as UnifiedOrderResource).inboundCode">入仓码 {{ (row as UnifiedOrderResource).inboundCode }}</span></div></template>
|
||||
<template #mainStatus="{ row }"><StatusBadge :status="statusLabel((row as UnifiedOrderResource).mainStatus)" /></template>
|
||||
<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>
|
||||
@@ -1043,6 +1036,7 @@ onMounted(() => {
|
||||
v-if="relationOrder"
|
||||
:tenant-id="String(relationOrder.tenantId)"
|
||||
:order-id="String(relationOrder.orderId)"
|
||||
:inbound-code="relationOrder.inboundCode"
|
||||
/>
|
||||
</AppDrawer>
|
||||
|
||||
@@ -1056,7 +1050,7 @@ onMounted(() => {
|
||||
|
||||
<el-dialog
|
||||
v-model="measurementDialogOpen"
|
||||
:title="measurementOrder ? `测量 · ${measurementOrder.inboundCode || measurementOrder.orderNo}` : '测量'"
|
||||
:title="measurementOrder?.inboundCode ? `测量 · ${measurementOrder.inboundCode}` : '测量'"
|
||||
width="min(1180px, calc(100vw - 32px))"
|
||||
top="4vh"
|
||||
destroy-on-close
|
||||
|
||||
@@ -65,7 +65,7 @@ const volumeRate = computed(() => utilization(usedVolume.value, container.value?
|
||||
const weightRate = computed(() => utilization(usedWeight.value, container.value?.maxWeightKg))
|
||||
|
||||
const queryFields: QueryField[] = [
|
||||
{ key: 'keyword', type: 'input', label: '候选货物', placeholder: '订单号、入仓码、客户或品名', props: { 'aria-label': '候选货物关键词' } },
|
||||
{ key: 'keyword', type: 'input', label: '候选货物', placeholder: '入仓码、客户或品名', props: { 'aria-label': '候选货物关键词' } },
|
||||
{
|
||||
key: 'eligibility', type: 'select', label: '可装状态', placeholder: '全部状态',
|
||||
options: [{ label: '可加入', value: 'ELIGIBLE' }, { label: '存在冲突', value: 'CONFLICT' }],
|
||||
@@ -138,7 +138,7 @@ const filteredCandidates = computed(() => {
|
||||
const keyword = String(appliedFilters.value.keyword || '').trim().toLowerCase()
|
||||
const eligibility = String(appliedFilters.value.eligibility || '')
|
||||
return candidates.value.filter((candidate) => {
|
||||
const searchable = [candidate.orderNo, candidate.inboundCode, candidate.customerName, candidate.goodsName].join(' ').toLowerCase()
|
||||
const searchable = [candidate.inboundCode, candidate.customerName, candidate.goodsName].join(' ').toLowerCase()
|
||||
if (keyword && !searchable.includes(keyword)) return false
|
||||
const eligible = fittingCount(candidate) > 0
|
||||
return !eligibility || (eligibility === 'ELIGIBLE' ? eligible : !eligible)
|
||||
@@ -309,7 +309,7 @@ onMounted(() => { void load() })
|
||||
<el-button size="small" plain type="primary" :disabled="!canAssign || !editable || Boolean(action)" @click="openAdjust(row as SmartLoadingPlanItem)"><Pencil :size="14" />调整</el-button>
|
||||
<el-button size="small" plain type="danger" :disabled="!canDelete || !editable || Boolean(action)" @click="openRemove(row as SmartLoadingPlanItem)"><Trash2 :size="13" />移除</el-button>
|
||||
</template>
|
||||
<template #order="{ row }"><strong>{{ (row as SmartLoadingPlanItem).orderNo || '—' }}</strong><small>{{ (row as SmartLoadingPlanItem).inboundCode || '—' }}</small></template>
|
||||
<template #order="{ row }"><strong>{{ (row as SmartLoadingPlanItem).inboundCode || '—' }}</strong></template>
|
||||
<template #weight="{ row }">{{ formatNumber((row as SmartLoadingPlanItem).grossWeightKg) }}</template>
|
||||
<template #volume="{ row }">{{ formatNumber((row as SmartLoadingPlanItem).volumeCbm) }}</template>
|
||||
<template #destination="{ row }">{{ (row as SmartLoadingPlanItem).destinationWarehouseName || (row as SmartLoadingPlanItem).destinationWarehouseCode || '—' }}</template>
|
||||
@@ -324,7 +324,7 @@ onMounted(() => { void load() })
|
||||
<DataTable :data="filteredCandidates" :columns="candidateColumns" :loading="loading" empty-text="当前柜次仓库暂无可用候选货物">
|
||||
<template #actions="{ row }"><el-button size="small" plain type="primary" :disabled="!plan || !canAssign || !editable || fittingCount(row as SmartLoadingCandidate) <= 0 || Boolean(action)" @click="openAssign(row as SmartLoadingCandidate)"><Plus :size="14" />加入</el-button></template>
|
||||
<template #priority="{ $index }"><span class="priority">{{ Number($index) + 1 }}</span></template>
|
||||
<template #order="{ row }"><strong>{{ (row as SmartLoadingCandidate).orderNo || '—' }}</strong><small>{{ (row as SmartLoadingCandidate).inboundCode || '—' }}</small></template>
|
||||
<template #order="{ row }"><strong>{{ (row as SmartLoadingCandidate).inboundCode || '—' }}</strong></template>
|
||||
<template #destination="{ row }">{{ (row as SmartLoadingCandidate).destinationWarehouseName || (row as SmartLoadingCandidate).destinationWarehouseCode || '—' }}</template>
|
||||
<template #unitVolume="{ row }">{{ formatNumber((row as SmartLoadingCandidate).unitVolumeCbm) }}</template>
|
||||
<template #unitWeight="{ row }">{{ formatNumber((row as SmartLoadingCandidate).unitWeightKg) }}</template>
|
||||
@@ -336,19 +336,19 @@ onMounted(() => { void load() })
|
||||
</template>
|
||||
|
||||
<AppDialog v-model="assignOpen" title="加入装柜方案" :confirm-loading="action === 'assign'" :confirm-disabled="packageCount < 1 || packageCount > (selectedCandidate ? fittingCount(selectedCandidate) : 0)" confirm-text="确认加入" @confirm="confirmAssign">
|
||||
<p>{{ selectedCandidate?.orderNo || selectedCandidate?.inboundCode }} · 最多可加入 {{ selectedCandidate ? fittingCount(selectedCandidate) : 0 }} 箱</p>
|
||||
<p>{{ selectedCandidate?.inboundCode || '入仓码暂缺' }} · 最多可加入 {{ selectedCandidate ? fittingCount(selectedCandidate) : 0 }} 箱</p>
|
||||
<el-input-number v-model="packageCount" :min="1" :max="selectedCandidate ? fittingCount(selectedCandidate) : 1" controls-position="right" aria-label="加入箱数" />
|
||||
<ApiErrorMeta v-if="actionError" :details="actionError" />
|
||||
</AppDialog>
|
||||
|
||||
<AppDialog v-model="adjustOpen" title="调整装柜箱数" :confirm-loading="action === 'adjust'" :confirm-disabled="packageCount < 1" confirm-text="保存调整" @confirm="confirmAdjust">
|
||||
<p>{{ selectedItem?.orderNo || selectedItem?.inboundCode }} · 当前 {{ selectedItem?.packageCount || 0 }} 箱</p>
|
||||
<p>{{ selectedItem?.inboundCode || '入仓码暂缺' }} · 当前 {{ selectedItem?.packageCount || 0 }} 箱</p>
|
||||
<el-input-number v-model="packageCount" :min="1" controls-position="right" aria-label="调整后箱数" />
|
||||
<ApiErrorMeta v-if="actionError" :details="actionError" />
|
||||
</AppDialog>
|
||||
|
||||
<AppDialog v-model="removeOpen" title="移除装柜明细" confirm-type="danger" confirm-text="确认移除" :confirm-loading="action === 'remove'" @confirm="confirmRemove">
|
||||
<p>将从当前草稿移除 {{ selectedItem?.orderNo || selectedItem?.inboundCode }} 的 {{ selectedItem?.packageCount || 0 }} 箱货物。此操作会写入审计历史。</p>
|
||||
<p>将从当前草稿移除 {{ selectedItem?.inboundCode || '该入仓单' }} 的 {{ selectedItem?.packageCount || 0 }} 箱货物。此操作会写入审计历史。</p>
|
||||
<ApiErrorMeta v-if="actionError" :details="actionError" />
|
||||
</AppDialog>
|
||||
|
||||
|
||||
@@ -548,7 +548,7 @@ onMounted(() => { void refresh() })
|
||||
<el-input v-model="preloadDraft.containerCode" maxlength="64" placeholder="请输入柜号" @input="preloadDraftError = ''" />
|
||||
</el-form-item>
|
||||
<el-form-item label="货物引用" required :error="preloadDraftError === '货物引用不能为空' ? preloadDraftError : ''">
|
||||
<el-input v-model="preloadDraft.cargoReference" maxlength="128" placeholder="订单号、入仓码或业务引用" @input="preloadDraftError = ''" />
|
||||
<el-input v-model="preloadDraft.cargoReference" maxlength="128" placeholder="入仓码或业务引用" @input="preloadDraftError = ''" />
|
||||
</el-form-item>
|
||||
<div class="preload-dialog-grid">
|
||||
<el-form-item label="箱数" required><el-input-number v-model="preloadDraft.packageCount" :min="1" :step="1" step-strictly controls-position="right" @change="preloadDraftError = ''" /></el-form-item>
|
||||
|
||||
@@ -19,7 +19,7 @@ const summary = computed(() => detail.value?.summary || null)
|
||||
|
||||
async function load() {
|
||||
if (!orderId.value || !tenantId.value) {
|
||||
error.value = { message: '订单作业上下文缺少订单号或客户标识' }
|
||||
error.value = { message: '订单作业上下文不完整' }
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
|
||||
Reference in New Issue
Block a user