feat(admin): show single-remainder fulfillment aggregate
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
const source = readFileSync(resolve(process.cwd(), 'src/modules/unified-orders/components/OrderFulfillmentTreePanel.vue'), 'utf8')
|
||||
|
||||
describe('OrderFulfillmentTreePanel contract', () => {
|
||||
it('shows root conservation fields and fragment picking quantities', () => {
|
||||
for (const label of ['原始总量', '累计已装', '当前余量', '本单计划', '已拣', '未拣']) {
|
||||
expect(source).toContain(label)
|
||||
}
|
||||
expect(source).toContain('历史双子单模型')
|
||||
expect(source).toContain('新拆分不会再生成已装子单')
|
||||
})
|
||||
|
||||
it('covers loading, error, empty and responsive states', () => {
|
||||
expect(source).toContain('正在加载拆分履约')
|
||||
expect(source).toContain('拆分履约暂时无法加载')
|
||||
expect(source).toContain('<ApiErrorMeta')
|
||||
expect(source).toContain('暂无拆分分片')
|
||||
expect(source).toContain('@media (max-width: 720px)')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,68 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||||
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'
|
||||
|
||||
const props = defineProps<{ tenantId: string; orderId: string; compact?: boolean }>()
|
||||
const tree = ref<UnifiedOrderFulfillmentTree | null>(null)
|
||||
const loading = ref(false)
|
||||
const error = ref<ApiErrorDetails | null>(null)
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try { tree.value = await getPlatformOrderFulfillmentTree(props.tenantId, props.orderId) }
|
||||
catch (cause) { error.value = getApiErrorDetails(cause) }
|
||||
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>
|
||||
|
||||
<template>
|
||||
<div class="fulfillment-tree-panel" :class="{ compact }" aria-label="拆分履约聚合">
|
||||
<LoadErrorState v-if="error" title="拆分履约暂时无法加载" :message="error.message" :loading="loading" @retry="load">
|
||||
<ApiErrorMeta :details="error" />
|
||||
</LoadErrorState>
|
||||
<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>{{ 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" 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="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="柜次" min-width="150"><template #default="{ row }">{{ row.containerBookingId || '—' }}</template></el-table-column>
|
||||
</el-table>
|
||||
</template>
|
||||
<el-empty v-else description="暂无拆分履约数据" :image-size="64" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.fulfillment-tree-panel { display: grid; gap: 16px; }
|
||||
.tree-summary { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; padding: 14px; border: 1px solid var(--cc-border); border-radius: 8px; background: var(--cc-surface-muted); }
|
||||
.tree-summary div { display: grid; gap: 4px; min-width: 0; }
|
||||
.tree-summary span { color: var(--cc-muted); font-size: 12px; }
|
||||
.tree-summary strong { overflow: hidden; color: var(--cc-text); text-overflow: ellipsis; white-space: nowrap; }
|
||||
.tree-loading { min-height: 120px; display: grid; place-items: center; color: var(--cc-muted); }
|
||||
small { color: var(--cc-muted); font-weight: 400; }
|
||||
@media (max-width: 720px) { .tree-summary { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
||||
</style>
|
||||
Reference in New Issue
Block a user