Przeglądaj źródła

fix(s8): show order chain duration days

S8-ORDER-EXECUTION-DURATION-AND-MULTI-AGG-VERIFY-1

链路全景主节点矩阵单订单态"实际达成"从 actualReachedAt 日期改为
stage.actualDays 天数,与多订单态字段语义对齐。

- ChainOverviewList.vue:
  - 删除 ColumnAgg.actualReachedAt 字段(DURATION-ALIGN 错误遗留)
  - buildSingleOrderColumn 命中分支填 achievementDaysText 为 actualDays 天数
  - 模板 v-if=isSingleOrder 分支渲染 achievementDaysText 而非 actualReachedAt
  - 单订单态达标情况继续读 stage.status + STAGE_STATUS_LABEL
- stage-meta.ts(限定并入):5 阶段元数据抽取 + 部门字典
- 多订单态 aggregateColumn 与 L2/L3 aggregateByIndex 公式零侵入
- baseline 态 ChainBaselineMatrix 零侵入

Web version bump 2.4.156 -> 2.4.157
YY968XX 1 miesiąc temu
rodzic
commit
3c293092ca

+ 1 - 1
Web/package.json

@@ -1,7 +1,7 @@
 {
 	"name": "admin.net",
 	"type": "module",
-	"version": "2.4.156",
+	"version": "2.4.157",
 	"packageManager": "pnpm@10.32.1",
 	"lastBuildTime": "2026.03.15",
 	"description": "Admin.NET 站在巨人肩膀上的 .NET 通用权限开发框架",

+ 73 - 21
Web/src/views/aidop/s8/monitoring/components/order-execution/ChainOverviewList.vue

@@ -7,6 +7,8 @@ import type {
 	SalesOrderExecution,
 	StageSnapshot,
 } from '/@/views/aidop/s8/monitoring/data/order-execution/types';
+import { ORDER_CHAIN_STAGE_ORDER } from '/@/views/aidop/s8/monitoring/data/order-execution/stage-meta';
+import { STAGE_STATUS_LABEL } from './statusMapping';
 
 interface Props {
 	orders: SalesOrderExecution[];
@@ -20,14 +22,6 @@ const emit = defineEmits<{
 	'stage-click': [key: OrderNodeKey];
 }>();
 
-const STAGE_ORDER: ReadonlyArray<{ key: OrderNodeKey; name: string; ownerDept: string }> = [
-	{ key: 'order_review', name: '订单评审', ownerDept: '市场部' },
-	{ key: 'product_design', name: '产品设计', ownerDept: '研发中心' },
-	{ key: 'material_procurement', name: '材料采购', ownerDept: '供应链部' },
-	{ key: 'body_production', name: '本体生产', ownerDept: '生产部' },
-	{ key: 'final_assembly_shipping', name: '总装发货', ownerDept: '生产部' },
-];
-
 interface ColumnAgg {
 	key: OrderNodeKey;
 	name: string;
@@ -37,6 +31,7 @@ interface ColumnAgg {
 	achievementRateText: string;
 	statusTone: 'green' | 'yellow' | 'red' | 'pending';
 	statusLabel: string;
+	isSingleOrder: boolean;
 }
 
 function pickStage(order: SalesOrderExecution, key: OrderNodeKey): StageSnapshot | undefined {
@@ -81,6 +76,7 @@ function aggregateColumn(
 			achievementRateText: '--',
 			statusTone: 'pending',
 			statusLabel: STATUS_LABEL.pending,
+			isSingleOrder: false,
 		};
 	}
 
@@ -124,18 +120,69 @@ function aggregateColumn(
 		achievementRateText: onTimeRate != null ? `${onTimeRate}%` : '--',
 		statusTone,
 		statusLabel: STATUS_LABEL[statusTone],
+		isSingleOrder: false,
 	};
 }
 
 const visibleStages = computed(() => {
 	const filterSet = new Set(props.selectedStageKeys);
 	const useAll = props.selectedStageKeys.length === 0;
-	return STAGE_ORDER.filter((s) => useAll || filterSet.has(s.key));
+	return ORDER_CHAIN_STAGE_ORDER.filter((s) => useAll || filterSet.has(s.key));
 });
 
-const columns = computed<ColumnAgg[]>(() =>
-	visibleStages.value.map((s) => aggregateColumn(s.key, s.name, s.ownerDept, props.orders)),
-);
+const isSingleOrder = computed(() => props.orders.length === 1);
+const singleOrder = computed<SalesOrderExecution | null>(() => props.orders[0] ?? null);
+
+// 单订单态:实际达成显示 stage.actualDays 天数(与多订单态同字段同语义,不再渲染日期);
+// 达标情况直接读 stage.status;文案沿用 statusMapping.STAGE_STATUS_LABEL。
+function buildSingleOrderColumn(
+	key: OrderNodeKey,
+	name: string,
+	ownerDept: string,
+	order: SalesOrderExecution,
+): ColumnAgg {
+	const stage = pickStage(order, key);
+	if (!stage) {
+		return {
+			key,
+			name,
+			ownerDept,
+			kpiText: '--',
+			achievementDaysText: '--',
+			achievementRateText: '--',
+			statusTone: 'pending',
+			statusLabel: STAGE_STATUS_LABEL.pending,
+			isSingleOrder: true,
+		};
+	}
+	const kpi =
+		stage.actualDays != null && stage.nodeVarianceDays != null
+			? Number((stage.actualDays - stage.nodeVarianceDays).toFixed(2))
+			: null;
+	const tone: ColumnAgg['statusTone'] =
+		stage.status === 'green' || stage.status === 'yellow' || stage.status === 'red'
+			? stage.status
+			: 'pending';
+	return {
+		key,
+		name,
+		ownerDept,
+		kpiText: kpi != null ? `${kpi.toFixed(1)} 天` : '--',
+		achievementDaysText: stage.actualDays != null ? `${stage.actualDays.toFixed(1)} 天` : '--',
+		achievementRateText: '--',
+		statusTone: tone,
+		statusLabel: STAGE_STATUS_LABEL[tone],
+		isSingleOrder: true,
+	};
+}
+
+const columns = computed<ColumnAgg[]>(() => {
+	if (isSingleOrder.value && singleOrder.value) {
+		const order = singleOrder.value;
+		return visibleStages.value.map((s) => buildSingleOrderColumn(s.key, s.name, s.ownerDept, order));
+	}
+	return visibleStages.value.map((s) => aggregateColumn(s.key, s.name, s.ownerDept, props.orders));
+});
 
 const sampleSoNo = computed(() =>
 	props.orders.length === 1 ? props.orders[0].soNo : null,
@@ -209,16 +256,21 @@ function onClick(key: OrderNodeKey) {
 						:key="`a-${col.key}`"
 						:class="{ 'chain-matrix__cell--active': activeStageKey === col.key }"
 					>
-						<div class="chain-matrix__achievement">
+						<template v-if="col.isSingleOrder">
 							<span class="chain-matrix__achievement-days">{{ col.achievementDaysText }}</span>
-							<span class="chain-matrix__achievement-sep">/</span>
-							<span
-								class="chain-matrix__achievement-rate"
-								:class="`chain-matrix__achievement-rate--${col.statusTone}`"
-							>
-								{{ col.achievementRateText }}
-							</span>
-						</div>
+						</template>
+						<template v-else>
+							<div class="chain-matrix__achievement">
+								<span class="chain-matrix__achievement-days">{{ col.achievementDaysText }}</span>
+								<span class="chain-matrix__achievement-sep">/</span>
+								<span
+									class="chain-matrix__achievement-rate"
+									:class="`chain-matrix__achievement-rate--${col.statusTone}`"
+								>
+									{{ col.achievementRateText }}
+								</span>
+							</div>
+						</template>
 					</td>
 				</tr>
 				<tr>

+ 17 - 0
Web/src/views/aidop/s8/monitoring/data/order-execution/stage-meta.ts

@@ -0,0 +1,17 @@
+import type { OrderNodeKey } from './types';
+
+export const ORDER_CHAIN_OWNER_DEPT_BY_NODE_KEY: Record<OrderNodeKey, string> = {
+	order_review: '市场部',
+	product_design: '研发中心',
+	material_procurement: '供应链部',
+	body_production: '生产部',
+	final_assembly_shipping: '生产部',
+};
+
+export const ORDER_CHAIN_STAGE_ORDER: ReadonlyArray<{ key: OrderNodeKey; name: string; ownerDept: string }> = [
+	{ key: 'order_review', name: '订单评审', ownerDept: ORDER_CHAIN_OWNER_DEPT_BY_NODE_KEY.order_review },
+	{ key: 'product_design', name: '产品设计', ownerDept: ORDER_CHAIN_OWNER_DEPT_BY_NODE_KEY.product_design },
+	{ key: 'material_procurement', name: '材料采购', ownerDept: ORDER_CHAIN_OWNER_DEPT_BY_NODE_KEY.material_procurement },
+	{ key: 'body_production', name: '本体生产', ownerDept: ORDER_CHAIN_OWNER_DEPT_BY_NODE_KEY.body_production },
+	{ key: 'final_assembly_shipping', name: '总装发货', ownerDept: ORDER_CHAIN_OWNER_DEPT_BY_NODE_KEY.final_assembly_shipping },
+];