Jelajahi Sumber

feat(s8): align order execution demo and chain drilldown

将订单执行档案 + chain 链路全景页(含三态、L2/L3 横向矩阵、采购供应商×线材透视表、
本体生产 PPT 真值对齐)整合为可演示的完整链路。

主要变更:
- 后端:新增 ado_demo_order_flow / snapshot / stage 三表 + Service + Controller + Seed
  作为 chain 演示数据集;与生产业务表解耦
- 前端 store/api:orderExecution.ts + s8OrderFlowApi.ts 接入后端 demo API;
  store.mapOrder 接回前端派生 order_review.subSteps 供 L2/L3 三态消费
- chain 三态恢复:baseline / 多订单聚合 / 单订单 三态独立 computed;
  selectedStageKeys 仅控列、不影响样本集合
- L1 矩阵:ChainBaselineMatrix + ChainOverviewList 达标情况由「分布多色 chip」
  统一为「单圆点 + KPI 阈值(≤PI=green / +20%=yellow / 否则=red)」
- L2/L3 横向矩阵:ReviewSubStepsCard / ReviewBreakdownCard 重写为 4 行 × N 列
  对齐源项目 ecc-sandbox 第 2 页风格;状态用彩色发光圆点 + a11y title/aria-label
- L2/L3 三态数据流:baseline → PPT_REVIEW_SUBSTEPS / PPT_OPINION_DRILLDOWN 常量;
  multi → filteredOrders 逐位 avg + 重判 status;single → 该订单自身 subSteps/breakdown
- 第 2 页删除:NodeDetailCard、当前节点详情、子节点 N、五子节点、四方明细、
  PPT 全量基线 等冗余文案
- 第 4 页采购:ProcurementDetailPanel 新增供应商 × 线材周期透视表
  (XX/YY/ZZ 三套 × 4 行 × 4 列,YY = PPT 真值,XX/ZZ = 源项目演示 fixture);
  默认材料 YY;切换材料同步一维表 + 透视表
- 第 5 页本体生产:ManufacturingDetailPanel 工序明细单位 H → 天,影响台次/达标颜色
  对齐 PPT;操作员表现 ↔ 损失因素位置调换;作业效率损失行整行红色高亮
- 入口/退出收口:watch store.selectedOrderNo 同步 selectedOrderNos,避免路由复用时
  入口 1/20 失效;isSingleOrderMode computed 守门顶部 summary,避免关闭 SO tag 后
  残留旧订单标题
- 主题色 token:全项 #ffb4ab → #ff4d4f(rgba 同比例迁移),影响损失/严重/P1
  等所有红色语义点;与 PPT 视觉一致

排除文件(保留本地):
- server/Admin.NET.Application/Configuration/Database.json(AllowPublicKeyRetrieval
  本地连接补丁,与功能无关)
- server/Plugins/Admin.NET.Plugin.AiDOP/Service/S8/S8ConfigDraftService.cs
  (mes_morder 软删字段补充,属 S8 配置中心 SQL 生成器,与 chain 无关)
YY968XX 3 bulan lalu
induk
melakukan
27e521a6c1
35 mengubah file dengan 2663 tambahan dan 441 penghapusan
  1. 126 36
      Web/src/stores/orderExecution.ts
  2. 83 0
      Web/src/views/aidop/s8/api/s8OrderFlowApi.ts
  3. 154 24
      Web/src/views/aidop/s8/monitoring/OrderChainOverviewPage.vue
  4. 1 1
      Web/src/views/aidop/s8/monitoring/S8MonitoringDeliveryPage.vue
  5. 4 4
      Web/src/views/aidop/s8/monitoring/S8MonitoringOverviewPage.vue
  6. 3 3
      Web/src/views/aidop/s8/monitoring/S8MonitoringProductionPage.vue
  7. 1 1
      Web/src/views/aidop/s8/monitoring/S8MonitoringSupplyPage.vue
  8. 25 6
      Web/src/views/aidop/s8/monitoring/SoOrderExecutionDashboardPage.vue
  9. 1 1
      Web/src/views/aidop/s8/monitoring/components/S8MonitoringCategoryCard.vue
  10. 1 1
      Web/src/views/aidop/s8/monitoring/components/S8MonitoringStageCard.vue
  11. 1 1
      Web/src/views/aidop/s8/monitoring/components/S8MonitoringSummaryCard.vue
  12. 1 1
      Web/src/views/aidop/s8/monitoring/components/S9QdcSummaryCard.vue
  13. 214 0
      Web/src/views/aidop/s8/monitoring/components/order-execution/ChainBaselineMatrix.vue
  14. 34 127
      Web/src/views/aidop/s8/monitoring/components/order-execution/ChainOverviewList.vue
  15. 93 0
      Web/src/views/aidop/s8/monitoring/components/order-execution/DesignDetailPanel.vue
  16. 102 0
      Web/src/views/aidop/s8/monitoring/components/order-execution/ManufacturingDetailPanel.vue
  17. 3 3
      Web/src/views/aidop/s8/monitoring/components/order-execution/NodeDetailCard.vue
  18. 3 3
      Web/src/views/aidop/s8/monitoring/components/order-execution/OrderExecutionKpiCards.vue
  19. 2 2
      Web/src/views/aidop/s8/monitoring/components/order-execution/OrderExecutionOrderCard.vue
  20. 327 15
      Web/src/views/aidop/s8/monitoring/components/order-execution/OrderExecutionOrderList.vue
  21. 142 0
      Web/src/views/aidop/s8/monitoring/components/order-execution/ProcurementDetailPanel.vue
  22. 137 121
      Web/src/views/aidop/s8/monitoring/components/order-execution/ReviewBreakdownCard.vue
  23. 128 85
      Web/src/views/aidop/s8/monitoring/components/order-execution/ReviewSubStepsCard.vue
  24. 3 3
      Web/src/views/aidop/s8/monitoring/components/order-execution/StageMatrixCard.vue
  25. 3 3
      Web/src/views/aidop/s8/monitoring/data/order-execution/builder.ts
  26. 24 0
      Web/src/views/aidop/s8/monitoring/data/order-execution/demo-step-config.ts
  27. 22 0
      Web/src/views/aidop/s8/monitoring/data/order-execution/ppt-baseline.ts
  28. 196 0
      Web/src/views/aidop/s8/monitoring/data/order-execution/stage-detail.ts
  29. 44 0
      server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S8/AdoS8DemoOrderFlowController.cs
  30. 87 0
      server/Plugins/Admin.NET.Plugin.AiDOP/Dto/S8/Demo/AdoDemoOrderFlowDtos.cs
  31. 118 0
      server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S8/Demo/AdoDemoOrderFlow.cs
  32. 59 0
      server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S8/Demo/AdoDemoOrderFlowSnapshot.cs
  33. 68 0
      server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S8/Demo/AdoDemoOrderFlowStage.cs
  34. 318 0
      server/Plugins/Admin.NET.Plugin.AiDOP/SeedData/S8DemoOrderFlowSeedData.cs
  35. 135 0
      server/Plugins/Admin.NET.Plugin.AiDOP/Service/S8/Demo/S8DemoOrderFlowService.cs

+ 126 - 36
Web/src/stores/orderExecution.ts

@@ -1,27 +1,31 @@
-// ORDER-FLOW-FIXTURE-STORE-1:订单执行档案前端 fixture store(不调后端)。
+// ORDER-FLOW-BE-DEMO-DATASET-RESET-1:主数据源切换为后端 demo API。
+// 旧 fixture(customers.ts / builder.ts)保留代码作为 dev fallback 工具,本 store 不再静默回退。
 import { defineStore } from 'pinia';
 import { Session } from '/@/utils/storage';
-import {
-	EXECUTION_MOCK_CUSTOMERS,
-	EXECUTION_OBSERVED_AT,
-} from '/@/views/aidop/s8/monitoring/data/order-execution/customers';
-import {
-	OBSERVED_AT_DATE,
-	buildSalesOrderExecution,
-} from '/@/views/aidop/s8/monitoring/data/order-execution/builder';
+// EXECUTION_OBSERVED_AT 仍来自旧 fixture 模块,仅用作 UI 顶部"当前观测时间"展示文案;
+// 后端接入后该值与明细订单时间不再绑定,UI 视为基线观测时刻。
+import { EXECUTION_OBSERVED_AT } from '/@/views/aidop/s8/monitoring/data/order-execution/customers';
 import {
 	computeOrderExecutionKpis,
 	createDefaultFilters,
 	filterOrders,
-	flattenCustomerOrders,
 } from '/@/views/aidop/s8/monitoring/data/order-execution/aggregation';
+// ORDER-FLOW-CHAIN-PAGE2-ORIGINAL-LOGIC-RESTORE-1:order_review 子节点数据由前端 fixture 派生(与原项目同源),后端 DTO 不返。
+import { buildReviewSubSteps } from '/@/views/aidop/s8/monitoring/data/order-execution/builder';
 import type {
 	CustomerGroup,
 	OrderExecutionFilters,
 	OrderExecutionKpiAverages,
 	OrderNodeKey,
 	SalesOrderExecution,
+	StageSnapshot,
 } from '/@/views/aidop/s8/monitoring/data/order-execution/types';
+import {
+	s8OrderFlowApi,
+	type S8OrderFlowAggregateSnapshotDto,
+	type S8OrderFlowDto,
+	type S8OrderFlowStageDto,
+} from '/@/views/aidop/s8/api/s8OrderFlowApi';
 
 export interface OrderExecutionFilterOption<V = string> {
 	value: V;
@@ -53,23 +57,88 @@ const dedupe = <T>(values: Iterable<T>): T[] => Array.from(new Set(values));
 
 const NAV_STATE_KEY = 's8_order_chain_nav_state';
 
+// 后端时间字符串:'YYYY-MM-DD HH:mm' → 前端展示 'MM-DD HH:mm';'YYYY-MM-DD' → 'MM-DD'。
+function toMonthDayTime(s: string | null | undefined): string {
+	if (!s) return '';
+	// 形如 2026-04-15 09:00 → 04-15 09:00
+	const m = s.match(/^\d{4}-(\d{2}-\d{2}( \d{2}:\d{2})?)/);
+	return m ? m[1] : s;
+}
+
+function mapStage(s: S8OrderFlowStageDto): StageSnapshot {
+	return {
+		key: s.stageKey as OrderNodeKey,
+		name: s.stageName,
+		targetDate: toMonthDayTime(s.expectedDate),
+		actualReachedAt: s.actualEndAt ? toMonthDayTime(s.actualEndAt) : null,
+		actualDays: s.actualDays,
+		nodeVarianceDays: s.nodeVarianceDays,
+		cumulativeVarianceDays: s.cumulativeVarianceDays,
+		status: s.status,
+	};
+}
+
+function mapOrder(dto: S8OrderFlowDto): SalesOrderExecution {
+	return {
+		soNo: dto.soNo,
+		orderId: dto.soNo.replace('SO', 'ORD'),
+		productName: dto.productName,
+		productLine: dto.productLine,
+		customerName: dto.customerName,
+		customerCode: dto.customerCode,
+		customerType: dto.customerType,
+		region: dto.region,
+		priority: dto.priority,
+		workflowStatus: dto.workflowStatus,
+		currentNode: dto.currentNodeLabel,
+		currentNodeKey: dto.currentNodeKey as OrderNodeKey,
+		focusNodeKey: dto.focusNodeKey as OrderNodeKey,
+		nodeStatus: dto.nodeStatus,
+		observedAt: EXECUTION_OBSERVED_AT,
+		orderStartDate: toMonthDayTime(dto.releaseAt),
+		targetCycleDays: dto.targetCycleDays,
+		actualCycleDays: dto.actualCycleDays,
+		currentCycleDays: dto.currentCycleDays,
+		nodeVarianceDays: dto.nodeVarianceDays,
+		cumulativeVarianceDays: dto.cumulativeVarianceDays,
+		exceptionCount: dto.exceptionCount,
+		responseDisplayMinutes: dto.responseMinutes,
+		responseTime: dto.responseMinutes,
+		processingTime: dto.processingMinutes,
+		currentProcessingMinutes: dto.processingMinutes,
+		totalLossTime: dto.totalLossMinutes,
+		exceptionStatus: dto.exceptionStatus,
+		lifecycle: dto.lifecycle.map(mapStage).map((stage) =>
+			stage.key === 'order_review'
+				? { ...stage, subSteps: buildReviewSubSteps(dto.soNo, stage.actualDays ?? null) }
+				: stage,
+		),
+	};
+}
+
 interface OrderExecutionState {
-	customers: CustomerGroup[];
+	orders: SalesOrderExecution[];
+	aggregateSnapshot: S8OrderFlowAggregateSnapshotDto | null;
 	filters: OrderExecutionFilters;
 	selectedOrderNo: string | null;
 	initialized: boolean;
+	loading: boolean;
+	loadError: string | null;
 }
 
 export const useOrderExecutionStore = defineStore('orderExecution', {
 	state: (): OrderExecutionState => ({
-		customers: [],
+		orders: [],
+		aggregateSnapshot: null,
 		filters: createDefaultFilters(),
 		selectedOrderNo: null,
 		initialized: false,
+		loading: false,
+		loadError: null,
 	}),
 	getters: {
 		allOrders(state): SalesOrderExecution[] {
-			return flattenCustomerOrders(state.customers);
+			return state.orders;
 		},
 		filteredOrders(): SalesOrderExecution[] {
 			return filterOrders(this.allOrders, this.filters);
@@ -78,26 +147,32 @@ export const useOrderExecutionStore = defineStore('orderExecution', {
 			return computeOrderExecutionKpis(this.filteredOrders);
 		},
 		customerGroups(state): CustomerGroup[] {
-			return state.customers;
+			const buckets = new Map<string, SalesOrderExecution[]>();
+			for (const o of state.orders) {
+				const key = o.customerCode || o.customerName;
+				(buckets.get(key) ?? buckets.set(key, []).get(key)!).push(o);
+			}
+			return Array.from(buckets.entries()).map(([key, orders]) => ({
+				id: key,
+				name: orders[0]?.customerName ?? key,
+				type: orders[0]?.customerType ?? 'KA',
+				orders,
+			}));
 		},
-		filteredCustomerGroups(state): CustomerGroup[] {
+		filteredCustomerGroups(): CustomerGroup[] {
 			const filtered = this.filteredOrders;
 			if (filtered.length === 0) return [];
 			const buckets = new Map<string, SalesOrderExecution[]>();
 			for (const order of filtered) {
 				const key = order.customerCode || order.customerName;
-				const bucket = buckets.get(key) ?? [];
-				bucket.push(order);
-				buckets.set(key, bucket);
+				(buckets.get(key) ?? buckets.set(key, []).get(key)!).push(order);
 			}
-			return state.customers
-				.map((customer) => {
-					const sample = customer.orders[0];
-					const key = sample?.customerCode || sample?.customerName || customer.id;
-					const orders = buckets.get(key) ?? [];
-					return { id: customer.id, name: customer.name, type: customer.type, orders };
-				})
-				.filter((customer) => customer.orders.length > 0);
+			return Array.from(buckets.entries()).map(([key, orders]) => ({
+				id: key,
+				name: orders[0]?.customerName ?? key,
+				type: orders[0]?.customerType ?? 'KA',
+				orders,
+			}));
 		},
 		filterOptions(): OrderExecutionFilterOptions {
 			const orders = this.allOrders;
@@ -145,21 +220,36 @@ export const useOrderExecutionStore = defineStore('orderExecution', {
 		},
 		selectedOrder(state): SalesOrderExecution | null {
 			if (!state.selectedOrderNo) return null;
-			return this.allOrders.find((o) => o.soNo === state.selectedOrderNo) ?? null;
+			return state.orders.find((o) => o.soNo === state.selectedOrderNo) ?? null;
 		},
 	},
 	actions: {
+		async loadFromBackend() {
+			if (this.loading) return;
+			this.loading = true;
+			this.loadError = null;
+			try {
+				const [orders, snapshot] = await Promise.all([
+					s8OrderFlowApi.listOrders(),
+					s8OrderFlowApi.aggregateSnapshot(),
+				]);
+				this.orders = orders.map(mapOrder);
+				this.aggregateSnapshot = snapshot;
+				this.initialized = true;
+			} catch (err: unknown) {
+				// fail-closed:清空数据,不静默回退到旧 fixture
+				this.orders = [];
+				this.aggregateSnapshot = null;
+				this.initialized = true;
+				this.loadError = err instanceof Error ? err.message : String(err);
+			} finally {
+				this.loading = false;
+			}
+		},
+		// 兼容现有 3 个页面 onMounted 调用名;行为已改为后端取数。
 		loadFixture() {
 			if (this.initialized) return;
-			this.customers = EXECUTION_MOCK_CUSTOMERS.map((customer) => ({
-				id: customer.id,
-				name: customer.name,
-				type: customer.type,
-				orders: customer.orders.map((order) =>
-					buildSalesOrderExecution(order, customer.type, OBSERVED_AT_DATE),
-				),
-			}));
-			this.initialized = true;
+			void this.loadFromBackend();
 		},
 		setFilters(partial: Partial<OrderExecutionFilters>) {
 			this.filters = { ...this.filters, ...partial };

+ 83 - 0
Web/src/views/aidop/s8/api/s8OrderFlowApi.ts

@@ -0,0 +1,83 @@
+// ORDER-FLOW-BE-DEMO-DATASET-RESET-1:S8 demo 订单执行档案 API client。
+import service from '/@/utils/request';
+
+function unwrap<T>(res: { data: T }): T {
+	return res.data;
+}
+
+export interface S8OrderFlowStageDto {
+	stageKey: string;
+	stageName: string;
+	sortNo: number;
+	plannedDays: number;
+	actualDays: number | null;
+	expectedDate: string; // YYYY-MM-DD
+	actualStartAt: string | null; // YYYY-MM-DD HH:mm
+	actualEndAt: string | null; // YYYY-MM-DD HH:mm
+	status: 'green' | 'yellow' | 'red' | 'pending';
+	nodeVarianceDays: number | null;
+	cumulativeVarianceDays: number | null;
+}
+
+export interface S8OrderFlowDto {
+	soNo: string;
+	productName: string;
+	productLine: string;
+	customerName: string;
+	customerCode: string;
+	customerType: 'KA' | 'SMB' | 'MICRO';
+	region: string;
+	priority: 'P1' | 'P2' | 'P3';
+	materialCode: 'XX' | 'YY' | 'ZZ';
+	supplierGroup: 'A' | 'B' | 'C';
+	workflowStatus: 'completed' | 'in_progress';
+	currentNodeKey: string;
+	focusNodeKey: string;
+	currentNodeLabel: string;
+	nodeStatus: 'green' | 'yellow' | 'red';
+	releaseAt: string; // YYYY-MM-DD HH:mm
+	targetCycleDays: number;
+	currentCycleDays: number | null;
+	actualCycleDays: number | null;
+	nodeVarianceDays: number | null;
+	cumulativeVarianceDays: number | null;
+	exceptionCount: number;
+	responseMinutes: number | null;
+	processingMinutes: number | null;
+	totalLossMinutes: number | null;
+	exceptionStatus: '待响应' | '处理中' | '已闭环';
+	lifecycle: S8OrderFlowStageDto[];
+}
+
+export interface S8OrderFlowStageSnapshotItem {
+	stageKey: string;
+	stageName: string;
+	ownerDept: string;
+	kpiAvgDays: number;
+	actualAvgDays: number;
+	onTimeRate: number;
+	green: number;
+	yellow: number;
+	red: number;
+	pending: number;
+}
+
+export interface S8OrderFlowAggregateSnapshotDto {
+	snapshotCode: string;
+	snapshotLabel: string;
+	totalOrders: number;
+	totalCustomers: number;
+	avgResponseMinutes: number;
+	avgProcessingMinutes: number;
+	avgLossMinutes: number;
+	stageSnapshots: S8OrderFlowStageSnapshotItem[];
+	remark: string | null;
+}
+
+const BASE = '/api/aidop/s8/demo/order-flow';
+
+export const s8OrderFlowApi = {
+	listOrders: () => service.get<S8OrderFlowDto[]>(`${BASE}/orders`).then(unwrap),
+	getOrder: (soNo: string) => service.get<S8OrderFlowDto>(`${BASE}/orders/${encodeURIComponent(soNo)}`).then(unwrap),
+	aggregateSnapshot: () => service.get<S8OrderFlowAggregateSnapshotDto>(`${BASE}/aggregate-snapshot`).then(unwrap),
+};

+ 154 - 24
Web/src/views/aidop/s8/monitoring/OrderChainOverviewPage.vue

@@ -9,12 +9,26 @@ import { useOrderExecutionStore } from '/@/stores/orderExecution';
 import type {
 	OrderNodeKey,
 	SalesOrderExecution,
+	SubStepDetail,
 } from '/@/views/aidop/s8/monitoring/data/order-execution/types';
 import ChainStageFilterBar from './components/order-execution/ChainStageFilterBar.vue';
 import ChainOverviewList from './components/order-execution/ChainOverviewList.vue';
-import NodeDetailCard from './components/order-execution/NodeDetailCard.vue';
+import ChainBaselineMatrix from './components/order-execution/ChainBaselineMatrix.vue';
 import ReviewSubStepsCard from './components/order-execution/ReviewSubStepsCard.vue';
 import ReviewBreakdownCard from './components/order-execution/ReviewBreakdownCard.vue';
+import DesignDetailPanel from './components/order-execution/DesignDetailPanel.vue';
+import ProcurementDetailPanel from './components/order-execution/ProcurementDetailPanel.vue';
+import ManufacturingDetailPanel from './components/order-execution/ManufacturingDetailPanel.vue';
+import {
+	DESIGN_DETAIL_FIXTURE,
+	PROCUREMENT_DETAIL_FIXTURE,
+	MANUFACTURING_DETAIL_FIXTURE,
+} from '/@/views/aidop/s8/monitoring/data/order-execution/stage-detail';
+// ORDER-FLOW-CHAIN-PAGE2-ORIGINAL-LOGIC-RESTORE-1:baseline (isUnfiltered) 态下 L2/L3 使用 PPT 常量。
+import {
+	PPT_REVIEW_SUBSTEPS,
+	PPT_OPINION_DRILLDOWN,
+} from '/@/views/aidop/s8/monitoring/data/order-execution/ppt-baseline';
 
 const ALL_STAGE_KEYS: OrderNodeKey[] = [
 	'order_review',
@@ -30,8 +44,23 @@ const router = useRouter();
 onMounted(() => {
 	store.loadFixture();
 	store.restoreChainSelection();
+	// ORDER-FLOW-CHAIN-ENTRY-FOCUS-1:从订单行进入时默认收敛到该订单视角,避免一进来就是 105/105 聚合。
+	const focused = store.selectedOrderNo;
+	if (focused && !selectedOrderNos.value.includes(focused)) {
+		selectedOrderNos.value = [focused];
+	}
 });
 
+// ORDER-FLOW-CHAIN-ENTRY-AND-CLEANUP-1:路由复用场景下 onMounted 不会重跑,需 watch 同步入口订单。
+watch(
+	() => store.selectedOrderNo,
+	(newVal) => {
+		if (newVal && !selectedOrderNos.value.includes(newVal)) {
+			selectedOrderNos.value = [newVal];
+		}
+	},
+);
+
 const selectedOrder = computed(() => store.selectedOrder);
 const allOrders = computed(() => store.allOrders);
 
@@ -72,27 +101,73 @@ const detailOrder = computed<SalesOrderExecution | null>(() => {
 	return filteredChainOrders.value[0] ?? selectedOrder.value ?? null;
 });
 
-const activeStage = computed(() => {
-	const o = detailOrder.value;
-	const key = activeStageKey.value;
-	if (!o || !key) return null;
-	return o.lifecycle.find((s) => s.key === key) ?? null;
+// ORDER-FLOW-CHAIN-PAGE2-ORIGINAL-LOGIC-RESTORE-1:同页三态判定(不纳入 selectedStageKeys)。
+const isUnfiltered = computed(
+	() => filteredChainOrders.value.length === allOrders.value.length,
+);
+// ORDER-FLOW-CHAIN-ENTRY-AND-CLEANUP-1:单订单态守门,避免关闭 SO tag 后顶部 summary 残留旧订单。
+const isSingleOrderMode = computed(() => filteredChainOrders.value.length === 1);
+
+// 多订单聚合时按 PI 阈值重判 status(与 builder.ts 内同口径:≤PI=green / +20%=yellow / 否则 red)。
+function classifyStatus(actual: number, pi: number): SubStepDetail['status'] {
+	if (actual <= pi) return 'green';
+	return (actual - pi) / pi <= 0.2 ? 'yellow' : 'red';
+}
+
+function aggregateByIndex(samples: SubStepDetail[][]): SubStepDetail[] {
+	if (samples.length === 0) return [];
+	const template = samples[0];
+	return template.map((entry, i) => {
+		const hours = samples
+			.map((arr) => arr[i]?.actualHours)
+			.filter((v): v is number => typeof v === 'number');
+		if (hours.length === 0) {
+			return { name: entry.name, piHours: entry.piHours, actualHours: null, status: 'pending' };
+		}
+		const avg = Number((hours.reduce((a, b) => a + b, 0) / hours.length).toFixed(1));
+		return {
+			name: entry.name,
+			piHours: entry.piHours,
+			actualHours: avg,
+			status: classifyStatus(avg, entry.piHours),
+		};
+	});
+}
+
+// L2 三态:baseline → PPT 常量;单订单 → 该订单自身;多订单 → filteredChainOrders 逐位 avg。
+const reviewSubSteps = computed<SubStepDetail[]>(() => {
+	if (activeStageKey.value !== 'order_review') return [];
+	if (isUnfiltered.value) return PPT_REVIEW_SUBSTEPS;
+	const ordersSubSteps = filteredChainOrders.value
+		.map((o) => o.lifecycle.find((s) => s.key === 'order_review')?.subSteps)
+		.filter((x): x is SubStepDetail[] => Array.isArray(x) && x.length > 0);
+	if (ordersSubSteps.length === 0) return [];
+	if (ordersSubSteps.length === 1) return ordersSubSteps[0];
+	return aggregateByIndex(ordersSubSteps);
 });
 
-const reviewStage = computed(
-	() => detailOrder.value?.lifecycle.find((s) => s.key === 'order_review') ?? null,
-);
-const reviewSubSteps = computed(() => reviewStage.value?.subSteps ?? null);
-const opinionSubStep = computed(
-	() => reviewSubSteps.value?.find((s) => s.name === '意见评审') ?? null,
-);
-const opinionBreakdown = computed(() => opinionSubStep.value?.breakdown ?? null);
+// L3 三态:baseline → PPT 常量;单订单 → 该订单自身意见评审 breakdown;多订单 → filteredChainOrders 逐位 avg。
+const opinionBreakdown = computed<SubStepDetail[]>(() => {
+	if (activeStageKey.value !== 'order_review') return [];
+	if (isUnfiltered.value) return PPT_OPINION_DRILLDOWN;
+	const ordersBreakdowns = filteredChainOrders.value
+		.map(
+			(o) =>
+				o.lifecycle
+					.find((s) => s.key === 'order_review')
+					?.subSteps?.find((ss) => ss.name === '意见评审')?.breakdown,
+		)
+		.filter((x): x is SubStepDetail[] => Array.isArray(x) && x.length > 0);
+	if (ordersBreakdowns.length === 0) return [];
+	if (ordersBreakdowns.length === 1) return ordersBreakdowns[0];
+	return aggregateByIndex(ordersBreakdowns);
+});
 
 const showReviewSubSteps = computed(
-	() => activeStageKey.value === 'order_review' && (reviewSubSteps.value?.length ?? 0) > 0,
+	() => activeStageKey.value === 'order_review' && reviewSubSteps.value.length > 0,
 );
 const showBreakdown = computed(
-	() => activeStageKey.value === 'order_review' && (opinionBreakdown.value?.length ?? 0) > 0,
+	() => activeStageKey.value === 'order_review' && opinionBreakdown.value.length > 0,
 );
 
 watch(
@@ -138,13 +213,28 @@ const STAGE_NAME_MAP: Record<OrderNodeKey, string> = {
 	order_review: '订单评审',
 	product_design: '产品设计',
 	material_procurement: '材料采购',
-	body_production: '本体生产',
+	// chain 上下文沿用业务侧拍板:内部 key=body_production,展示语境改用「设备制造」
+	body_production: '设备制造',
 	final_assembly_shipping: '总装发货',
 };
 
+const showDesignDetail = computed(() => activeStageKey.value === 'product_design');
+const showProcurementDetail = computed(() => activeStageKey.value === 'material_procurement');
+const showManufacturingDetail = computed(() => activeStageKey.value === 'body_production');
+
 const activeStageName = computed(() =>
 	activeStageKey.value ? STAGE_NAME_MAP[activeStageKey.value] : '--',
 );
+
+// ORDER-FLOW-BE-DEMO-DATASET-RESET-1:默认无筛选时显示后端 105 单基线快照矩阵;任一筛选触发实时聚合。
+const aggregateSnapshot = computed(() => store.aggregateSnapshot);
+const loadError = computed(() => store.loadError);
+// baseline 仅看订单样本数(isUnfiltered);selectedStageKeys 只控制列显示,不影响 baseline 切换。
+const showBaseline = computed(() => isUnfiltered.value && !!aggregateSnapshot.value);
+
+function onRetry() {
+	void store.loadFromBackend();
+}
 </script>
 
 <template>
@@ -158,14 +248,15 @@ const activeStageName = computed(() =>
 				</p>
 				<h1 class="oe-chain__title-main">订单链路全景</h1>
 				<p class="oe-chain__title-sub">
-					<template v-if="detailOrder">
+					<template v-if="isSingleOrderMode && detailOrder">
 						<span class="oe-chain__so">{{ detailOrder.soNo }}</span>
 						<span class="oe-chain__sep">·</span>
 						<span>{{ detailOrder.productName }}</span>
 						<span class="oe-chain__sep">·</span>
 						<span>当前环节 {{ activeStageName }}</span>
 					</template>
-					<template v-else>尚未选择订单</template>
+					<template v-else-if="isUnfiltered">基线聚合 · 全部 {{ allOrders.length }} 单</template>
+					<template v-else>多订单聚合 · 命中 {{ filteredChainOrders.length }} 单</template>
 				</p>
 			</div>
 			<el-button class="oe-chain__back" @click="goBack">‹ 返回总览</el-button>
@@ -190,8 +281,43 @@ const activeStageName = computed(() =>
 			@reset="onResetFilters"
 		/>
 
-		<template v-if="detailOrder">
-			<section class="oe-chain__summary">
+		<el-alert
+			v-if="loadError"
+			class="oe-chain__error"
+			type="error"
+			:closable="false"
+			show-icon
+			title="订单执行档案数据加载失败"
+			:description="loadError"
+		>
+			<template #default>
+				<el-button size="small" type="primary" plain @click="onRetry">重试</el-button>
+			</template>
+		</el-alert>
+
+		<template v-if="showBaseline">
+			<ChainBaselineMatrix
+				:snapshot="aggregateSnapshot!"
+				:active-stage-key="activeStageKey"
+				:visible-stage-keys="visibleStageKeys"
+				@stage-click="onStageClick"
+			/>
+			<ReviewSubStepsCard
+				v-if="showReviewSubSteps"
+				:sub-steps="reviewSubSteps"
+				title="节点详情:评审 / 排产 / 测算"
+			/>
+			<ReviewBreakdownCard
+				v-if="showBreakdown"
+				:breakdown="opinionBreakdown"
+				title="意见评审 — 详细执行明细"
+			/>
+			<DesignDetailPanel v-if="showDesignDetail" :detail="DESIGN_DETAIL_FIXTURE" />
+			<ProcurementDetailPanel v-if="showProcurementDetail" :detail="PROCUREMENT_DETAIL_FIXTURE" />
+			<ManufacturingDetailPanel v-if="showManufacturingDetail" :detail="MANUFACTURING_DETAIL_FIXTURE" />
+		</template>
+		<template v-else-if="detailOrder">
+			<section v-if="isSingleOrderMode" class="oe-chain__summary">
 				<div class="oe-chain__summary-cell">
 					<span class="oe-chain__summary-label">客户</span>
 					<span class="oe-chain__summary-value">{{ detailOrder.customerName }}</span>
@@ -224,17 +350,19 @@ const activeStageName = computed(() =>
 				:active-stage-key="activeStageKey"
 				@stage-click="onStageClick"
 			/>
-			<NodeDetailCard :stage="activeStage" />
 			<ReviewSubStepsCard
 				v-if="showReviewSubSteps"
 				:sub-steps="reviewSubSteps"
-				title="订单评审 · 五子节点"
+				title="节点详情:评审 / 排产 / 测算"
 			/>
 			<ReviewBreakdownCard
 				v-if="showBreakdown"
 				:breakdown="opinionBreakdown"
-				title="意见评审 · 四方明细"
+				title="意见评审 — 详细执行明细"
 			/>
+			<DesignDetailPanel v-if="showDesignDetail" :detail="DESIGN_DETAIL_FIXTURE" />
+			<ProcurementDetailPanel v-if="showProcurementDetail" :detail="PROCUREMENT_DETAIL_FIXTURE" />
+			<ManufacturingDetailPanel v-if="showManufacturingDetail" :detail="MANUFACTURING_DETAIL_FIXTURE" />
 		</template>
 
 		<section v-else class="oe-chain__empty">
@@ -380,6 +508,8 @@ const activeStageName = computed(() =>
 	color: #ffc107;
 }
 
+.oe-chain__error { margin: 0; }
+
 .oe-chain__empty {
 	display: flex;
 	flex-direction: column;

+ 1 - 1
Web/src/views/aidop/s8/monitoring/S8MonitoringDeliveryPage.vue

@@ -568,7 +568,7 @@ onMounted(async () => {
 	border: 1px solid rgba(144, 144, 151, 0.12);
 }
 
-.anomaly-type-card--danger { border-color: rgba(255, 180, 171, 0.2); }
+.anomaly-type-card--danger { border-color: rgba(255, 77, 79, 0.2); }
 
 .anomaly-type-card__head {
 	display: flex;

+ 4 - 4
Web/src/views/aidop/s8/monitoring/S8MonitoringOverviewPage.vue

@@ -927,7 +927,7 @@ watch(deptCards, (depts) => {
 }
 
 .dept-card--danger {
-	border-color: rgba(255, 180, 171, 0.2);
+	border-color: rgba(255, 77, 79, 0.2);
 }
 
 .dept-card[draggable="true"] { cursor: grab; }
@@ -966,8 +966,8 @@ watch(deptCards, (depts) => {
 }
 
 .dept-card__badge--danger {
-	background: rgba(255, 180, 171, 0.12);
-	color: #ffb4ab;
+	background: rgba(255, 77, 79, 0.12);
+	color: #ff4d4f;
 }
 
 .dept-card__metrics {
@@ -1025,7 +1025,7 @@ watch(deptCards, (depts) => {
 }
 
 .s8-monitor__efficiency-label--danger {
-	color: #ffb4ab;
+	color: #ff4d4f;
 }
 
 .s8-monitor__efficiency-track {

+ 3 - 3
Web/src/views/aidop/s8/monitoring/S8MonitoringProductionPage.vue

@@ -410,7 +410,7 @@ onMounted(async () => {
 	color: var(--text-primary);
 	background:
 		radial-gradient(circle at top left, rgba(255, 193, 7, 0.06), transparent 30%),
-		radial-gradient(circle at bottom right, rgba(255, 180, 171, 0.05), transparent 26%),
+		radial-gradient(circle at bottom right, rgba(255, 77, 79, 0.05), transparent 26%),
 		linear-gradient(180deg, #10131a 0%, #161b25 100%);
 	overflow-x: hidden;
 	overflow-y: auto;
@@ -559,7 +559,7 @@ onMounted(async () => {
 	border: 1px solid rgba(144, 144, 151, 0.12);
 }
 
-.anomaly-type-card--danger { border-color: rgba(255, 180, 171, 0.2); }
+.anomaly-type-card--danger { border-color: rgba(255, 77, 79, 0.2); }
 
 .anomaly-type-card__head {
 	display: flex;
@@ -634,7 +634,7 @@ onMounted(async () => {
 	background: #ffc107;
 }
 
-.section-title__bar--accent { background: #ffb4ab; }
+.section-title__bar--accent { background: #ff4d4f; }
 
 .section-title__text {
 	margin: 0;

+ 1 - 1
Web/src/views/aidop/s8/monitoring/S8MonitoringSupplyPage.vue

@@ -571,7 +571,7 @@ onMounted(async () => {
 	border: 1px solid rgba(144, 144, 151, 0.12);
 }
 
-.anomaly-type-card--danger { border-color: rgba(255, 180, 171, 0.2); }
+.anomaly-type-card--danger { border-color: rgba(255, 77, 79, 0.2); }
 
 .anomaly-type-card__head {
 	display: flex;

+ 25 - 6
Web/src/views/aidop/s8/monitoring/SoOrderExecutionDashboardPage.vue

@@ -26,12 +26,16 @@ const filterOptions = computed(() => store.filterOptions);
 const filteredOrders = computed(() => store.filteredOrders);
 const filteredCustomers = computed(() => store.filteredCustomerGroups);
 const kpi = computed(() => store.kpiAverages);
-const observedAt = computed(() => store.currentObservationTime);
 const ready = computed(() => store.initialized && filteredOrders.value.length > 0);
 const orderCount = computed(() => filteredOrders.value.length);
 const customerCount = computed(() => filteredCustomers.value.length);
 const totalOrderCount = computed(() => store.allOrders.length);
 const isEmptyResult = computed(() => store.initialized && filteredOrders.value.length === 0);
+const loadError = computed(() => store.loadError);
+
+function onRetry() {
+	void store.loadFromBackend();
+}
 
 function onFiltersUpdate(next: OrderExecutionFilters) {
 	store.setFilters(next);
@@ -56,12 +60,22 @@ const activeTab = ref<'overview' | 'execution' | 'source-dept' | 'handle-dept'>(
 				<h1 class="oe-dashboard__title-main">SO 订单执行档案总览</h1>
 				<p class="oe-dashboard__title-sub">基于订单全流程维度的客户订单执行监控</p>
 			</div>
-			<div class="oe-dashboard__observation">
-				<span class="oe-dashboard__observation-label">当前观测时间</span>
-				<span class="oe-dashboard__observation-value">{{ observedAt }}</span>
-			</div>
 		</header>
 
+		<el-alert
+			v-if="loadError"
+			class="oe-dashboard__error"
+			type="error"
+			:closable="false"
+			show-icon
+			title="订单执行档案数据加载失败"
+			:description="loadError"
+		>
+			<template #default>
+				<el-button size="small" type="primary" plain @click="onRetry">重试</el-button>
+			</template>
+		</el-alert>
+
 		<el-tabs v-model="activeTab" class="oe-dashboard__tabs">
 			<el-tab-pane label="订单/客户总览" name="overview">
 				<section class="oe-dashboard__section">
@@ -100,7 +114,8 @@ const activeTab = ref<'overview' | 'execution' | 'source-dept' | 'handle-dept'>(
 						@open-chain="onOpenChain"
 					/>
 					<div v-else class="oe-dashboard__empty">
-						当前筛选无匹配订单,可点击「重置」恢复全部。
+						<template v-if="loadError">订单数据未能加载,请点击上方「重试」。</template>
+						<template v-else>当前筛选无匹配订单,可点击「重置」恢复全部。</template>
 					</div>
 				</section>
 			</el-tab-pane>
@@ -300,6 +315,10 @@ const activeTab = ref<'overview' | 'execution' | 'source-dept' | 'handle-dept'>(
 	font-weight: 700;
 }
 
+.oe-dashboard__error {
+	margin: 0 0 12px;
+}
+
 .oe-dashboard__placeholder {
 	display: flex;
 	align-items: center;

+ 1 - 1
Web/src/views/aidop/s8/monitoring/components/S8MonitoringCategoryCard.vue

@@ -158,6 +158,6 @@ const cardStyle = computed(() => {
 }
 
 .category-card--danger .category-card__rate {
-	color: #ffb4ab;
+	color: #ff4d4f;
 }
 </style>

+ 1 - 1
Web/src/views/aidop/s8/monitoring/components/S8MonitoringStageCard.vue

@@ -421,7 +421,7 @@ const yellowBarWidth = computed(() => {
 }
 
 .stage-card__seg--red {
-	background: linear-gradient(90deg, #ffb4ab 0%, #93000a 100%);
+	background: linear-gradient(90deg, #ff4d4f 0%, #93000a 100%);
 }
 
 .stage-card__wide-meter {

+ 1 - 1
Web/src/views/aidop/s8/monitoring/components/S8MonitoringSummaryCard.vue

@@ -73,7 +73,7 @@ const cardStyle = computed(() => ({
 
 .summary-card--good  { border-color: rgba(109, 224, 57, 0.22); }
 .summary-card--warn  { border-color: rgba(255, 193, 7, 0.22); }
-.summary-card--danger { border-color: rgba(255, 180, 171, 0.22); }
+.summary-card--danger { border-color: rgba(255, 77, 79, 0.22); }
 
 .summary-card__glow {
   position: absolute;

+ 1 - 1
Web/src/views/aidop/s8/monitoring/components/S9QdcSummaryCard.vue

@@ -203,7 +203,7 @@ function fmtTarget(item: S8ResultKpiItem): string {
 }
 
 .s9-qdc-card__placeholder--err {
-	color: #ffb4ab;
+	color: #ff4d4f;
 }
 
 .s9-qdc-card__badge {

+ 214 - 0
Web/src/views/aidop/s8/monitoring/components/order-execution/ChainBaselineMatrix.vue

@@ -0,0 +1,214 @@
+<script setup lang="ts" name="ChainBaselineMatrix">
+// ORDER-FLOW-BE-DEMO-DATASET-RESET-1:chain 默认全选基线快照矩阵。
+// 渲染独立 105 单聚合快照(与 20 条明细不同源)。仅在用户未做任何筛选时出现。
+import { computed } from 'vue';
+import type { OrderNodeKey } from '/@/views/aidop/s8/monitoring/data/order-execution/types';
+import type { S8OrderFlowAggregateSnapshotDto } from '/@/views/aidop/s8/api/s8OrderFlowApi';
+
+interface Props {
+	snapshot: S8OrderFlowAggregateSnapshotDto;
+	activeStageKey: OrderNodeKey | null;
+	// ORDER-FLOW-CHAIN-PAGE2-ORIGINAL-LOGIC-RESTORE-1:baseline 矩阵列按"关键环节"开关过滤;
+	// 不传或空数组时显示全部 stage 列(向后兼容)。
+	visibleStageKeys?: OrderNodeKey[];
+}
+
+const props = withDefaults(defineProps<Props>(), {
+	visibleStageKeys: () => [],
+});
+const emit = defineEmits<{
+	'stage-click': [key: OrderNodeKey];
+}>();
+
+interface Col {
+	key: OrderNodeKey;
+	name: string;
+	ownerDept: string;
+	kpiText: string;
+	achievementDaysText: string;
+	achievementRateText: string;
+	statusTone: 'green' | 'yellow' | 'red' | 'pending';
+	statusLabel: string;
+}
+
+// 与 L2/L3 sub-step 同口径:≤KPI=green / +20%=yellow / 否则=red
+function classifyByKpi(actualDays: number, kpiDays: number): Col['statusTone'] {
+	if (kpiDays <= 0) return 'pending';
+	if (actualDays <= kpiDays) return 'green';
+	return (actualDays - kpiDays) / kpiDays <= 0.2 ? 'yellow' : 'red';
+}
+
+const STATUS_LABEL: Record<Col['statusTone'], string> = {
+	green: '达标',
+	yellow: '关注',
+	red: '严重',
+	pending: '未开始',
+};
+
+const columns = computed<Col[]>(() => {
+	const visibleSet = new Set<OrderNodeKey>(props.visibleStageKeys ?? []);
+	const sourceStages =
+		visibleSet.size > 0
+			? props.snapshot.stageSnapshots.filter((s) => visibleSet.has(s.stageKey as OrderNodeKey))
+			: props.snapshot.stageSnapshots;
+	return sourceStages.map((s) => {
+		const tone = classifyByKpi(s.actualAvgDays, s.kpiAvgDays);
+		return {
+			key: s.stageKey as OrderNodeKey,
+			name: s.stageName,
+			ownerDept: s.ownerDept,
+			kpiText: `${s.kpiAvgDays.toFixed(1)} 天`,
+			achievementDaysText: `${s.actualAvgDays.toFixed(1)} 天`,
+			achievementRateText: `${s.onTimeRate}%`,
+			statusTone: tone,
+			statusLabel: STATUS_LABEL[tone],
+		};
+	});
+});
+
+function onClick(key: OrderNodeKey) {
+	emit('stage-click', key);
+}
+</script>
+
+<template>
+	<section class="chain-matrix">
+		<header class="chain-matrix__head">
+			<span class="chain-matrix__bar" />
+			<h2 class="chain-matrix__title">关键环节进度矩阵</h2>
+		</header>
+		<table class="chain-matrix__table">
+			<thead>
+				<tr>
+					<th class="chain-matrix__row-head">关键环节</th>
+					<th
+						v-for="col in columns"
+						:key="col.key"
+						class="chain-matrix__col-head"
+						:class="{
+							'chain-matrix__col-head--active': activeStageKey === col.key,
+							[`chain-matrix__col-head--${col.statusTone}`]: true,
+						}"
+						role="button"
+						tabindex="0"
+						@click="onClick(col.key)"
+						@keydown.enter.prevent="onClick(col.key)"
+						@keydown.space.prevent="onClick(col.key)"
+					>
+						{{ col.name }}
+						<span v-if="activeStageKey === col.key" class="chain-matrix__active-pin">当前</span>
+					</th>
+				</tr>
+			</thead>
+			<tbody>
+				<tr>
+					<td class="chain-matrix__row-label">负责部门</td>
+					<td v-for="col in columns" :key="`d-${col.key}`">{{ col.ownerDept }}</td>
+				</tr>
+				<tr>
+					<td class="chain-matrix__row-label">KPI</td>
+					<td v-for="col in columns" :key="`k-${col.key}`">{{ col.kpiText }}</td>
+				</tr>
+				<tr>
+					<td class="chain-matrix__row-label">实际达成</td>
+					<td v-for="col in columns" :key="`a-${col.key}`">
+						<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>
+					</td>
+				</tr>
+				<tr>
+					<td class="chain-matrix__row-label">达标情况</td>
+					<td v-for="col in columns" :key="`s-${col.key}`">
+						<span
+							class="chain-matrix__dot"
+							:class="`chain-matrix__dot--${col.statusTone}`"
+							role="img"
+							:title="col.statusLabel"
+							:aria-label="col.statusLabel"
+						/>
+					</td>
+				</tr>
+			</tbody>
+		</table>
+	</section>
+</template>
+
+<style scoped>
+.chain-matrix { display: flex; flex-direction: column; gap: 10px; }
+.chain-matrix__head { display: flex; align-items: center; gap: 10px; }
+.chain-matrix__bar { width: 5px; height: 18px; border-radius: 999px; background: var(--order-accent, #7bd0ff); }
+.chain-matrix__title { margin: 0; font-size: 16px; font-weight: 700; }
+.chain-matrix__table {
+	width: 100%;
+	border-collapse: collapse;
+	background: rgba(25, 28, 34, 0.55);
+	border: 1px solid rgba(69, 70, 77, 0.28);
+	border-radius: 10px;
+	overflow: hidden;
+	font-size: 12px;
+	table-layout: fixed;
+}
+.chain-matrix__table th, .chain-matrix__table td {
+	padding: 10px;
+	text-align: center;
+	border-bottom: 1px solid rgba(69, 70, 77, 0.18);
+	border-right: 1px solid rgba(69, 70, 77, 0.18);
+	color: var(--order-text-primary, #e1e2eb);
+	font-family: 'JetBrains Mono', 'Cascadia Code', Menlo, monospace;
+}
+.chain-matrix__table th:last-child, .chain-matrix__table td:last-child { border-right: none; }
+.chain-matrix__table tr:last-child td { border-bottom: none; }
+.chain-matrix__row-head, .chain-matrix__row-label {
+	width: 140px; text-align: left;
+	color: var(--order-text-muted, #909097);
+	background: rgba(15, 19, 26, 0.55);
+	font-family: 'PingFang SC', sans-serif; font-weight: 600;
+}
+.chain-matrix__col-head {
+	cursor: pointer; user-select: none;
+	background: rgba(15, 19, 26, 0.7);
+	color: var(--order-text-primary, #e1e2eb);
+	font-weight: 700; font-family: 'PingFang SC', sans-serif;
+	transition: background 120ms ease, color 120ms ease, box-shadow 120ms ease;
+}
+.chain-matrix__col-head:hover { background: rgba(123, 208, 255, 0.08); color: #7bd0ff; }
+.chain-matrix__col-head--green { box-shadow: inset 0 3px 0 #88fd54; }
+.chain-matrix__col-head--yellow { box-shadow: inset 0 3px 0 #ffc107; }
+.chain-matrix__col-head--red { box-shadow: inset 0 3px 0 #ff4d4f; }
+.chain-matrix__col-head--pending { box-shadow: inset 0 3px 0 rgba(144, 144, 151, 0.45); }
+.chain-matrix__col-head--active {
+	background: rgba(123, 208, 255, 0.16); color: #ffffff;
+	box-shadow: inset 0 0 0 1px rgba(123, 208, 255, 0.7);
+}
+.chain-matrix__active-pin {
+	margin-left: 6px; padding: 1px 6px; border-radius: 999px;
+	font-size: 10px; letter-spacing: 0.06em; color: #10131a; background: #7bd0ff;
+}
+.chain-matrix__achievement {
+	display: inline-flex; align-items: baseline; gap: 6px;
+	font-family: 'JetBrains Mono', 'Cascadia Code', Menlo, monospace;
+}
+.chain-matrix__achievement-days { font-size: 13px; color: var(--order-text-primary, #e1e2eb); font-weight: 600; }
+.chain-matrix__achievement-sep { color: var(--order-text-muted, #909097); }
+.chain-matrix__achievement-rate { font-size: 13px; font-weight: 700; }
+.chain-matrix__achievement-rate--green { color: #88fd54; }
+.chain-matrix__achievement-rate--yellow { color: #ffc107; }
+.chain-matrix__achievement-rate--red { color: #ff4d4f; }
+.chain-matrix__achievement-rate--pending { color: var(--order-text-muted, #909097); }
+.chain-matrix__dot {
+	display: inline-block;
+	width: 18px; height: 18px;
+	border-radius: 50%;
+	vertical-align: middle;
+}
+.chain-matrix__dot--green { background: #88fd54; box-shadow: 0 0 10px #88fd54; }
+.chain-matrix__dot--yellow { background: #ffc107; box-shadow: 0 0 10px #ffc107; }
+.chain-matrix__dot--red { background: #ff4d4f; box-shadow: 0 0 10px #ff4d4f; }
+.chain-matrix__dot--pending { background: rgba(144, 144, 151, 0.45); }
+</style>

+ 34 - 127
Web/src/views/aidop/s8/monitoring/components/order-execution/ChainOverviewList.vue

@@ -36,10 +36,7 @@ interface ColumnAgg {
 	achievementDaysText: string;
 	achievementRateText: string;
 	statusTone: 'green' | 'yellow' | 'red' | 'pending';
-	isSingleOrder: boolean;
-	singleStatusLabel: string;
-	statusCounts: { green: number; yellow: number; red: number; pending: number };
-	statusTooltip: string;
+	statusLabel: string;
 }
 
 function pickStage(order: SalesOrderExecution, key: OrderNodeKey): StageSnapshot | undefined {
@@ -52,22 +49,27 @@ function avg(values: number[]): number | null {
 	return sum / values.length;
 }
 
-const STATUS_LABEL: Record<StageSnapshot['status'], string> = {
-	green: '正常',
+const STATUS_LABEL: Record<'green' | 'yellow' | 'red' | 'pending', string> = {
+	green: '达标',
 	yellow: '关注',
 	red: '严重',
 	pending: '未开始',
 };
 
+// 与 ChainBaselineMatrix / L2 / L3 同口径:≤KPI=green / +20%=yellow / 否则=red
+function classifyByKpi(actualDays: number, kpiDays: number): 'green' | 'yellow' | 'red' {
+	if (kpiDays <= 0) return 'green';
+	if (actualDays <= kpiDays) return 'green';
+	return (actualDays - kpiDays) / kpiDays <= 0.2 ? 'yellow' : 'red';
+}
+
 function aggregateColumn(
 	key: OrderNodeKey,
 	name: string,
 	ownerDept: string,
 	orders: SalesOrderExecution[],
 ): ColumnAgg {
-	const isSingleOrder = orders.length === 1;
 	const stages = orders.map((o) => pickStage(o, key)).filter((s): s is StageSnapshot => !!s);
-	const emptyCounts = { green: 0, yellow: 0, red: 0, pending: 0 };
 
 	if (stages.length === 0) {
 		return {
@@ -78,10 +80,7 @@ function aggregateColumn(
 			achievementDaysText: '--',
 			achievementRateText: '--',
 			statusTone: 'pending',
-			isSingleOrder,
-			singleStatusLabel: '暂无',
-			statusCounts: emptyCounts,
-			statusTooltip: '暂无样本',
+			statusLabel: STATUS_LABEL.pending,
 		};
 	}
 
@@ -108,28 +107,14 @@ function aggregateColumn(
 					(validKpiPairs.filter((p) => p.actual <= p.kpi).length / validKpiPairs.length) * 100,
 				);
 
-	const statusCounts = { ...emptyCounts };
-	for (const s of stages) statusCounts[s.status]++;
-
+	// 仅按 KPI 单维判定(与 ChainBaselineMatrix / L2 / L3 一致):actualAvg ≤ kpiAvg ⇒ green / +20% ⇒ yellow / 否则 red
 	let statusTone: ColumnAgg['statusTone'];
-	if (isSingleOrder) {
-		statusTone = stages[0].status;
-	} else if (onTimeRate == null) {
+	if (kpiAvg == null || actualAvg == null) {
 		statusTone = 'pending';
-	} else if (onTimeRate >= 95) {
-		statusTone = 'green';
-	} else if (onTimeRate >= 85) {
-		statusTone = 'yellow';
 	} else {
-		statusTone = 'red';
+		statusTone = classifyByKpi(actualAvg, kpiAvg);
 	}
 
-	const tooltipParts: string[] = [];
-	if (statusCounts.green > 0) tooltipParts.push(`达成 ${statusCounts.green}`);
-	if (statusCounts.yellow > 0) tooltipParts.push(`预警 ${statusCounts.yellow}`);
-	if (statusCounts.red > 0) tooltipParts.push(`风险 ${statusCounts.red}`);
-	if (statusCounts.pending > 0) tooltipParts.push(`待启 ${statusCounts.pending}`);
-
 	return {
 		key,
 		name,
@@ -138,10 +123,7 @@ function aggregateColumn(
 		achievementDaysText: actualAvg != null ? `${actualAvg.toFixed(1)} 天` : '--',
 		achievementRateText: onTimeRate != null ? `${onTimeRate}%` : '--',
 		statusTone,
-		isSingleOrder,
-		singleStatusLabel: STATUS_LABEL[stages[0].status],
-		statusCounts,
-		statusTooltip: tooltipParts.join(' / ') || '暂无',
+		statusLabel: STATUS_LABEL[statusTone],
 	};
 }
 
@@ -246,57 +228,13 @@ function onClick(key: OrderNodeKey) {
 						:key="`s-${col.key}`"
 						:class="{ 'chain-matrix__cell--active': activeStageKey === col.key }"
 					>
-						<div
-							v-if="col.isSingleOrder"
-							class="chain-matrix__status chain-matrix__status--single"
-							:class="`chain-matrix__status--${col.statusTone}`"
-							:title="col.statusTooltip"
-						>
-							<span class="chain-matrix__status-dot" />
-							<span>{{ col.singleStatusLabel }}</span>
-						</div>
-						<div v-else class="chain-matrix__dist" :title="col.statusTooltip">
-							<span
-								v-if="col.statusCounts.green > 0"
-								class="chain-matrix__dist-item chain-matrix__dist-item--green"
-							>
-								<span class="chain-matrix__status-dot" />
-								<span>{{ col.statusCounts.green }}</span>
-							</span>
-							<span
-								v-if="col.statusCounts.yellow > 0"
-								class="chain-matrix__dist-item chain-matrix__dist-item--yellow"
-							>
-								<span class="chain-matrix__status-dot" />
-								<span>{{ col.statusCounts.yellow }}</span>
-							</span>
-							<span
-								v-if="col.statusCounts.red > 0"
-								class="chain-matrix__dist-item chain-matrix__dist-item--red"
-							>
-								<span class="chain-matrix__status-dot" />
-								<span>{{ col.statusCounts.red }}</span>
-							</span>
-							<span
-								v-if="col.statusCounts.pending > 0"
-								class="chain-matrix__dist-item chain-matrix__dist-item--pending"
-							>
-								<span class="chain-matrix__status-dot" />
-								<span>{{ col.statusCounts.pending }}</span>
-							</span>
-							<span
-								v-if="
-									col.statusCounts.green === 0 &&
-									col.statusCounts.yellow === 0 &&
-									col.statusCounts.red === 0 &&
-									col.statusCounts.pending === 0
-								"
-								class="chain-matrix__dist-item chain-matrix__dist-item--pending"
-							>
-								<span class="chain-matrix__status-dot" />
-								<span>--</span>
-							</span>
-						</div>
+						<span
+							class="chain-matrix__dot"
+							:class="`chain-matrix__dot--${col.statusTone}`"
+							role="img"
+							:title="col.statusLabel"
+							:aria-label="col.statusLabel"
+						/>
 					</td>
 				</tr>
 			</tbody>
@@ -412,7 +350,7 @@ function onClick(key: OrderNodeKey) {
 
 .chain-matrix__col-head--green { box-shadow: inset 0 3px 0 #88fd54; }
 .chain-matrix__col-head--yellow { box-shadow: inset 0 3px 0 #ffc107; }
-.chain-matrix__col-head--red { box-shadow: inset 0 3px 0 #ffb4ab; }
+.chain-matrix__col-head--red { box-shadow: inset 0 3px 0 #ff4d4f; }
 .chain-matrix__col-head--mixed { box-shadow: inset 0 3px 0 rgba(123, 208, 255, 0.5); }
 .chain-matrix__col-head--pending { box-shadow: inset 0 3px 0 rgba(144, 144, 151, 0.45); }
 
@@ -436,28 +374,18 @@ function onClick(key: OrderNodeKey) {
 	background: rgba(123, 208, 255, 0.06);
 }
 
-.chain-matrix__status {
-	display: inline-flex;
-	align-items: center;
-	gap: 5px;
-	padding: 2px 10px;
-	border-radius: 999px;
-	font-size: 11px;
-	font-family: 'PingFang SC', sans-serif;
-	background: rgba(50, 53, 60, 0.6);
-}
-
-.chain-matrix__status-dot {
-	width: 7px;
-	height: 7px;
-	border-radius: 999px;
-	background: currentColor;
+.chain-matrix__dot {
+	display: inline-block;
+	width: 18px;
+	height: 18px;
+	border-radius: 50%;
+	vertical-align: middle;
 }
 
-.chain-matrix__status--green { color: #88fd54; }
-.chain-matrix__status--yellow { color: #ffc107; }
-.chain-matrix__status--red { color: #ffb4ab; }
-.chain-matrix__status--pending { color: var(--order-text-muted, #909097); }
+.chain-matrix__dot--green { background: #88fd54; box-shadow: 0 0 10px #88fd54; }
+.chain-matrix__dot--yellow { background: #ffc107; box-shadow: 0 0 10px #ffc107; }
+.chain-matrix__dot--red { background: #ff4d4f; box-shadow: 0 0 10px #ff4d4f; }
+.chain-matrix__dot--pending { background: rgba(144, 144, 151, 0.45); }
 
 .chain-matrix__achievement {
 	display: inline-flex;
@@ -483,28 +411,7 @@ function onClick(key: OrderNodeKey) {
 
 .chain-matrix__achievement-rate--green { color: #88fd54; }
 .chain-matrix__achievement-rate--yellow { color: #ffc107; }
-.chain-matrix__achievement-rate--red { color: #ffb4ab; }
+.chain-matrix__achievement-rate--red { color: #ff4d4f; }
 .chain-matrix__achievement-rate--pending { color: var(--order-text-muted, #909097); }
 
-.chain-matrix__dist {
-	display: inline-flex;
-	flex-wrap: wrap;
-	justify-content: center;
-	gap: 8px;
-	font-family: 'JetBrains Mono', 'Cascadia Code', Menlo, monospace;
-	cursor: help;
-}
-
-.chain-matrix__dist-item {
-	display: inline-flex;
-	align-items: center;
-	gap: 4px;
-	font-size: 12px;
-	font-weight: 700;
-}
-
-.chain-matrix__dist-item--green { color: #88fd54; }
-.chain-matrix__dist-item--yellow { color: #ffc107; }
-.chain-matrix__dist-item--red { color: #ffb4ab; }
-.chain-matrix__dist-item--pending { color: var(--order-text-muted, #909097); }
 </style>

+ 93 - 0
Web/src/views/aidop/s8/monitoring/components/order-execution/DesignDetailPanel.vue

@@ -0,0 +1,93 @@
+<script setup lang="ts" name="DesignDetailPanel">
+// ORDER-FLOW-CHAIN-STAGE-DETAIL-MIGRATE-1:产品设计阶段结构化详情。fixture 来源 ecc-sandbox 硬编码;非真实数据。
+import type { DesignDetail, StageDetailStatus } from '/@/views/aidop/s8/monitoring/data/order-execution/stage-detail';
+
+defineProps<{ detail: DesignDetail }>();
+
+function statusColor(s: StageDetailStatus | null): string {
+	if (s === 'red') return '#ff4d4f';
+	if (s === 'yellow') return '#ffc107';
+	if (s === 'green') return '#88fd54';
+	return 'var(--order-text-muted, #909097)';
+}
+</script>
+
+<template>
+	<section class="design-panel">
+		<header class="design-panel__head">
+			<span class="design-panel__bar" />
+			<h2 class="design-panel__title">产品设计 · 阶段详情</h2>
+			<span class="design-panel__demo">演示数据</span>
+		</header>
+
+		<div class="design-panel__block">
+			<div class="design-panel__caption">设计分类汇总</div>
+			<table class="design-panel__table">
+				<thead>
+					<tr>
+						<th>产品类型</th><th>台数</th><th>占比</th><th>KPI</th><th>平均设计周期</th><th>达标</th><th>图号数量</th>
+					</tr>
+				</thead>
+				<tbody>
+					<tr v-for="row in detail.categoryRows" :key="row.type">
+						<td>{{ row.type }}</td>
+						<td>{{ row.count }}</td>
+						<td>{{ row.ratio }}</td>
+						<td>{{ row.kpi }}</td>
+						<td :style="{ color: statusColor(row.status) }">{{ row.avgCycle }}</td>
+						<td>
+							<span v-if="row.status" class="design-panel__dot" :style="{ background: statusColor(row.status) }" />
+							<span v-else>—</span>
+						</td>
+						<td>{{ row.drawingCount }}</td>
+					</tr>
+				</tbody>
+			</table>
+		</div>
+
+		<div class="design-panel__block">
+			<div class="design-panel__caption">图号执行明细(含两类样例)</div>
+			<table class="design-panel__table">
+				<thead>
+					<tr>
+						<th>负责人</th><th>图号</th><th>类别</th><th>计划开始</th><th>计划结束</th><th>实际开始</th><th>实际结束</th><th>设计周期</th><th>达标</th>
+					</tr>
+				</thead>
+				<tbody>
+					<tr v-for="row in detail.drawings" :key="row.id">
+						<td>{{ row.owner }}</td>
+						<td>{{ row.id }}</td>
+						<td>{{ row.category }}</td>
+						<td>{{ row.planStart }}</td>
+						<td>{{ row.planEnd }}</td>
+						<td>{{ row.actualStart }}</td>
+						<td>{{ row.actualEnd }}</td>
+						<td :style="{ color: statusColor(row.status) }">{{ row.cycle }}</td>
+						<td><span class="design-panel__dot" :style="{ background: statusColor(row.status) }" /></td>
+					</tr>
+				</tbody>
+			</table>
+		</div>
+	</section>
+</template>
+
+<style scoped>
+.design-panel { display: flex; flex-direction: column; gap: 12px; }
+.design-panel__head { display: flex; align-items: center; gap: 10px; }
+.design-panel__bar { width: 5px; height: 18px; border-radius: 999px; background: var(--order-accent, #7bd0ff); }
+.design-panel__title { margin: 0; font-size: 16px; font-weight: 700; }
+.design-panel__demo {
+	margin-left: 6px; padding: 2px 10px; border-radius: 999px; font-size: 11px;
+	color: #ffc107; background: rgba(255,193,7,0.1); border: 1px solid rgba(255,193,7,0.32);
+}
+.design-panel__block { background: rgba(25, 28, 34, 0.55); border: 1px solid rgba(69, 70, 77, 0.28); border-radius: 10px; padding: 12px; overflow-x: auto; }
+.design-panel__caption { font-size: 12px; color: var(--order-text-muted, #909097); margin-bottom: 8px; }
+.design-panel__table { width: 100%; border-collapse: collapse; font-size: 12px; color: var(--order-text-primary, #e1e2eb); }
+.design-panel__table th, .design-panel__table td {
+	padding: 8px 10px; text-align: center;
+	border: 1px solid rgba(69, 70, 77, 0.18);
+	font-family: 'JetBrains Mono', 'Cascadia Code', Menlo, monospace;
+}
+.design-panel__table th { background: rgba(15, 19, 26, 0.7); font-weight: 600; font-family: 'PingFang SC', sans-serif; }
+.design-panel__dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; }
+</style>

+ 102 - 0
Web/src/views/aidop/s8/monitoring/components/order-execution/ManufacturingDetailPanel.vue

@@ -0,0 +1,102 @@
+<script setup lang="ts" name="ManufacturingDetailPanel">
+// ORDER-FLOW-CHAIN-STAGE-DETAIL-MIGRATE-1:设备制造(本体生产)阶段结构化详情。fixture 来源 ecc-sandbox 硬编码;非真实数据。
+import type {
+	ManufacturingDetail,
+	StageDetailStatus,
+} from '/@/views/aidop/s8/monitoring/data/order-execution/stage-detail';
+
+defineProps<{ detail: ManufacturingDetail }>();
+
+function statusColor(s: StageDetailStatus): string {
+	if (s === 'red') return '#ff4d4f';
+	if (s === 'yellow') return '#ffc107';
+	return '#88fd54';
+}
+</script>
+
+<template>
+	<section class="mfg-panel">
+		<header class="mfg-panel__head">
+			<span class="mfg-panel__bar" />
+			<h2 class="mfg-panel__title">设备制造(本体生产) · 阶段详情</h2>
+			<span class="mfg-panel__demo">演示数据</span>
+		</header>
+
+		<div class="mfg-panel__block">
+			<div class="mfg-panel__caption">工序明细</div>
+			<table class="mfg-panel__table">
+				<thead>
+					<tr><th>工序</th><th>PI</th><th>平均周期</th><th>周期状态</th><th>影响台次</th><th>计划达成率</th><th>计划状态</th></tr>
+				</thead>
+				<tbody>
+					<tr v-for="row in detail.processRows" :key="row.process">
+						<td>{{ row.process }}</td>
+						<td>{{ row.pi }}</td>
+						<td :style="{ color: statusColor(row.cycleStatus), fontWeight: 600 }">{{ row.avgCycle }}</td>
+						<td><span class="mfg-panel__dot" :style="{ background: statusColor(row.cycleStatus) }" /></td>
+						<td>{{ row.impactCount ?? '—' }}</td>
+						<td :style="{ color: statusColor(row.planRateStatus), fontWeight: 600 }">{{ row.planRate }}</td>
+						<td><span class="mfg-panel__dot" :style="{ background: statusColor(row.planRateStatus) }" /></td>
+					</tr>
+				</tbody>
+			</table>
+		</div>
+
+		<div class="mfg-panel__split">
+			<div class="mfg-panel__block mfg-panel__block--half">
+				<div class="mfg-panel__caption">10 工序 · 操作员表现</div>
+				<table class="mfg-panel__table">
+					<thead><tr><th>操作员</th><th>平均工时</th><th>达标</th></tr></thead>
+					<tbody>
+						<tr v-for="row in detail.operators" :key="row.name">
+							<td>{{ row.name }}</td>
+							<td :style="{ color: statusColor(row.status), fontWeight: 600 }">{{ row.avgHours }}</td>
+							<td><span class="mfg-panel__dot" :style="{ background: statusColor(row.status) }" /></td>
+						</tr>
+					</tbody>
+				</table>
+			</div>
+			<div class="mfg-panel__block mfg-panel__block--half">
+				<div class="mfg-panel__caption">10 工序 · 损失因素</div>
+				<table class="mfg-panel__table">
+					<thead><tr><th>因素</th><th>件数</th><th>占比</th><th>损失工时</th></tr></thead>
+					<tbody>
+						<tr
+							v-for="row in detail.lossFactors"
+							:key="row.factor"
+							:style="row.factor === '作业效率损失' ? { color: '#ff4d4f', fontWeight: 700 } : undefined"
+						>
+							<td>{{ row.factor }}</td>
+							<td>{{ row.count }}</td>
+							<td>{{ row.ratio }}</td>
+							<td>{{ row.lossHours }}</td>
+						</tr>
+					</tbody>
+				</table>
+			</div>
+		</div>
+	</section>
+</template>
+
+<style scoped>
+.mfg-panel { display: flex; flex-direction: column; gap: 12px; }
+.mfg-panel__head { display: flex; align-items: center; gap: 10px; }
+.mfg-panel__bar { width: 5px; height: 18px; border-radius: 999px; background: var(--order-accent, #7bd0ff); }
+.mfg-panel__title { margin: 0; font-size: 16px; font-weight: 700; }
+.mfg-panel__demo {
+	margin-left: 6px; padding: 2px 10px; border-radius: 999px; font-size: 11px;
+	color: #ffc107; background: rgba(255,193,7,0.1); border: 1px solid rgba(255,193,7,0.32);
+}
+.mfg-panel__block { background: rgba(25, 28, 34, 0.55); border: 1px solid rgba(69, 70, 77, 0.28); border-radius: 10px; padding: 12px; overflow-x: auto; }
+.mfg-panel__split { display: flex; gap: 12px; }
+.mfg-panel__block--half { flex: 1; min-width: 0; }
+.mfg-panel__caption { font-size: 12px; color: var(--order-text-muted, #909097); margin-bottom: 8px; }
+.mfg-panel__table { width: 100%; border-collapse: collapse; font-size: 12px; color: var(--order-text-primary, #e1e2eb); }
+.mfg-panel__table th, .mfg-panel__table td {
+	padding: 8px 10px; text-align: center;
+	border: 1px solid rgba(69, 70, 77, 0.18);
+	font-family: 'JetBrains Mono', 'Cascadia Code', Menlo, monospace;
+}
+.mfg-panel__table th { background: rgba(15, 19, 26, 0.7); font-weight: 600; font-family: 'PingFang SC', sans-serif; }
+.mfg-panel__dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; }
+</style>

+ 3 - 3
Web/src/views/aidop/s8/monitoring/components/order-execution/NodeDetailCard.vue

@@ -131,7 +131,7 @@ const detail = computed(() => {
 
 .oe-node-detail--green { border-left-color: #88fd54; }
 .oe-node-detail--yellow { border-left-color: #ffc107; }
-.oe-node-detail--red { border-left-color: #ffb4ab; }
+.oe-node-detail--red { border-left-color: #ff4d4f; }
 .oe-node-detail--pending { border-left-color: rgba(144, 144, 151, 0.35); }
 
 .oe-node-detail__name {
@@ -162,8 +162,8 @@ const detail = computed(() => {
 }
 
 .oe-node-detail__chip--red {
-	background: rgba(255, 180, 171, 0.12);
-	color: #ffb4ab;
+	background: rgba(255, 77, 79, 0.12);
+	color: #ff4d4f;
 }
 
 .oe-node-detail__chip--pending {

+ 3 - 3
Web/src/views/aidop/s8/monitoring/components/order-execution/OrderExecutionKpiCards.vue

@@ -67,7 +67,7 @@ const cards = computed(() => [
 
 .oe-kpi-card--accent::before { background: #7bd0ff; }
 .oe-kpi-card--warn::before { background: #ffc107; }
-.oe-kpi-card--danger::before { background: #ffb4ab; }
+.oe-kpi-card--danger::before { background: #ff4d4f; }
 
 .oe-kpi-card__head {
 	display: flex;
@@ -85,7 +85,7 @@ const cards = computed(() => [
 
 .oe-kpi-card--accent .oe-kpi-card__dot { background: #7bd0ff; color: rgba(123, 208, 255, 0.6); }
 .oe-kpi-card--warn .oe-kpi-card__dot { background: #ffc107; color: rgba(255, 193, 7, 0.6); }
-.oe-kpi-card--danger .oe-kpi-card__dot { background: #ffb4ab; color: rgba(255, 180, 171, 0.6); }
+.oe-kpi-card--danger .oe-kpi-card__dot { background: #ff4d4f; color: rgba(255, 77, 79, 0.6); }
 
 .oe-kpi-card__label {
 	font-size: 12px;
@@ -114,7 +114,7 @@ const cards = computed(() => [
 
 .oe-kpi-card--accent .oe-kpi-card__num { color: #7bd0ff; }
 .oe-kpi-card--warn .oe-kpi-card__num { color: #ffc107; }
-.oe-kpi-card--danger .oe-kpi-card__num { color: #ffb4ab; }
+.oe-kpi-card--danger .oe-kpi-card__num { color: #ff4d4f; }
 
 @media (max-width: 768px) {
 	.oe-kpi-cards { grid-template-columns: 1fr; }

+ 2 - 2
Web/src/views/aidop/s8/monitoring/components/order-execution/OrderExecutionOrderCard.vue

@@ -153,7 +153,7 @@ function onChain() {
 	font-weight: 600;
 }
 
-.oe-order-card__priority--p1 { background: rgba(255, 180, 171, 0.16); color: #ffb4ab; }
+.oe-order-card__priority--p1 { background: rgba(255, 77, 79, 0.16); color: #ff4d4f; }
 .oe-order-card__priority--p2 { background: rgba(255, 193, 7, 0.16); color: #ffc107; }
 .oe-order-card__priority--p3 { background: rgba(123, 208, 255, 0.16); color: #7bd0ff; }
 
@@ -213,7 +213,7 @@ function onChain() {
 .oe-order-card__chip--region { background: rgba(255, 193, 7, 0.08); color: #ffc107; }
 .oe-order-card__chip--green { background: rgba(109, 224, 57, 0.12); color: #88fd54; }
 .oe-order-card__chip--yellow { background: rgba(255, 193, 7, 0.12); color: #ffc107; }
-.oe-order-card__chip--red { background: rgba(255, 180, 171, 0.14); color: #ffb4ab; }
+.oe-order-card__chip--red { background: rgba(255, 77, 79, 0.14); color: #ff4d4f; }
 .oe-order-card__chip--exception { background: rgba(255, 193, 7, 0.08); color: #ffc107; }
 
 .oe-order-card__metrics {

+ 327 - 15
Web/src/views/aidop/s8/monitoring/components/order-execution/OrderExecutionOrderList.vue

@@ -6,6 +6,12 @@ import type {
 	SalesOrderExecution,
 	StageSnapshot,
 } from '/@/views/aidop/s8/monitoring/data/order-execution/types';
+import {
+	DEMO_MAX_STEP,
+	DEMO_STEPS,
+	DEMO_TARGET_SO_NO,
+	DEMO_TOP_TITLE,
+} from '/@/views/aidop/s8/monitoring/data/order-execution/demo-step-config';
 
 interface Props {
 	orders: SalesOrderExecution[];
@@ -19,8 +25,82 @@ const emit = defineEmits<{
 
 const expandedSoNo = ref<string | null>(null);
 
+// ORDER-FLOW-OVERVIEW-DEMO-PLAYBACK-1:演示模式仅作用于 SO-2026-001 当前展开行。
+const demoModeSoNo = ref<string | null>(null);
+const demoStep = ref<number>(0);
+
+function isDemoOn(soNo: string): boolean {
+	return demoModeSoNo.value === soNo;
+}
+
+function enterDemo(soNo: string) {
+	demoModeSoNo.value = soNo;
+	demoStep.value = 0;
+}
+
+function exitDemo() {
+	demoModeSoNo.value = null;
+	demoStep.value = 0;
+}
+
+function demoPrev() {
+	if (demoStep.value > 0) demoStep.value -= 1;
+}
+
+function demoNext() {
+	if (demoStep.value < DEMO_MAX_STEP) demoStep.value += 1;
+}
+
+function demoReset() {
+	demoStep.value = 0;
+}
+
+const currentDemoStep = computed(() => DEMO_STEPS[demoStep.value] ?? DEMO_STEPS[0]);
+
+function findStageByKey(stages: StageSnapshot[], key: string | null | undefined): StageSnapshot | null {
+	if (!key) return null;
+	return stages.find((s) => s.key === key) ?? null;
+}
+
+// 演示模式下 step N 仅揭示前 N 个阶段的 4 个结果字段(完成时间/节点状态/当前节点偏差/累计偏差)。
+// 非演示模式返回 null,表示所有阶段全可见(不做隐藏)。
+function visibleStageKeySet(soNo: string, lifecycle: StageSnapshot[]): Set<string> | null {
+	if (!isDemoOn(soNo)) return null;
+	const n = demoStep.value;
+	if (n <= 0) return new Set<string>();
+	return new Set(lifecycle.slice(0, n).map((s) => s.key));
+}
+
+// 工作台快照:演示模式按 currentDemoStep.stageKey 取(step 0 → null),否则取 currentNodeKey 对应。
+function workbenchStage(soNo: string, order: SalesOrderExecution): StageSnapshot | null {
+	if (isDemoOn(soNo)) {
+		return findStageByKey(order.lifecycle, currentDemoStep.value.stageKey);
+	}
+	return findStageByKey(order.lifecycle, order.currentNodeKey);
+}
+
+// 演示模式下某阶段是否已揭示(仅作用于 完成时间/节点状态/当前节点偏差/累计偏差 4 行)。
+// 非演示模式恒为 true;演示模式按 lifecycle 顺序前 N 个阶段可见。
+function isStageRevealed(soNo: string, lifecycle: StageSnapshot[], stageKey: string): boolean {
+	const visible = visibleStageKeySet(soNo, lifecycle);
+	if (visible === null) return true;
+	return visible.has(stageKey);
+}
+
+function arrowToneColor(status: StageSnapshot['status']): string {
+	switch (status) {
+		case 'green': return '#88fd54';
+		case 'yellow': return '#ffc107';
+		case 'red': return '#ff4d4f';
+		default: return '#909097';
+	}
+}
+
 function toggleExpand(soNo: string) {
-	expandedSoNo.value = expandedSoNo.value === soNo ? null : soNo;
+	const next = expandedSoNo.value === soNo ? null : soNo;
+	expandedSoNo.value = next;
+	// 切换/折叠订单时退出演示,避免跨订单状态污染
+	if (next !== demoModeSoNo.value) exitDemo();
 }
 
 function onChain(order: SalesOrderExecution) {
@@ -250,12 +330,52 @@ const rows = computed(() =>
 							<header class="oe-detail-section-head">
 								<span class="oe-detail-section-bar" />
 								<h3 class="oe-detail-section-title">五阶段对照</h3>
-								<span class="oe-detail-matrix__legend">
+								<span v-if="!isDemoOn(row.so)" class="oe-detail-matrix__legend">
 									<span class="oe-detail-matrix__legend-item"><span class="oe-detail-matrix__legend-dot oe-detail-matrix__legend-dot--green" />达成规划</span>
 									<span class="oe-detail-matrix__legend-item"><span class="oe-detail-matrix__legend-dot oe-detail-matrix__legend-dot--yellow" />未达成规划,对客户无影响</span>
 									<span class="oe-detail-matrix__legend-item"><span class="oe-detail-matrix__legend-dot oe-detail-matrix__legend-dot--red" />未达成规划,对客户有影响</span>
 								</span>
+								<div v-if="row.so === DEMO_TARGET_SO_NO" class="oe-demo-controls">
+									<button
+										v-if="!isDemoOn(row.so)"
+										type="button"
+										class="oe-demo-btn oe-demo-btn--primary"
+										@click="enterDemo(row.so)"
+									>▶ 演示模式</button>
+									<template v-else>
+										<span class="oe-demo-controls__indicator">第 {{ demoStep }} / {{ DEMO_MAX_STEP }} 步</span>
+										<button type="button" class="oe-demo-btn" :disabled="demoStep === 0" @click="demoPrev">‹ 上一步</button>
+										<button type="button" class="oe-demo-btn" :disabled="demoStep === DEMO_MAX_STEP" @click="demoNext">下一步 ›</button>
+										<button type="button" class="oe-demo-btn" @click="demoReset">重置</button>
+										<button type="button" class="oe-demo-btn oe-demo-btn--ghost" @click="exitDemo">退出演示</button>
+									</template>
+								</div>
 							</header>
+
+							<div v-if="isDemoOn(row.so)" class="oe-demo-narrative">
+								<p class="oe-demo-narrative__title">{{ DEMO_TOP_TITLE }}</p>
+								<p v-if="currentDemoStep.topNarrative" class="oe-demo-narrative__step">{{ currentDemoStep.topNarrative }}</p>
+							</div>
+
+							<div
+								v-if="isDemoOn(row.so) && demoStep > 0 && currentDemoStep.stageKey"
+								class="oe-demo-arrows"
+								:style="{ '--demo-arrow-left': `calc(140px + (100% - 140px - 160px) * ${(demoStep - 0.5) / 5})` }"
+							>
+								<svg class="oe-demo-arrow oe-demo-arrow--down" viewBox="0 0 16 28" aria-hidden="true">
+									<line x1="8" y1="2" x2="8" y2="20" stroke="#0070c0" stroke-width="2" />
+									<polygon points="3,18 13,18 8,26" fill="#0070c0" />
+								</svg>
+								<svg
+									v-if="findStageByKey(row.raw.lifecycle, currentDemoStep.stageKey)"
+									class="oe-demo-arrow oe-demo-arrow--up"
+									viewBox="0 0 16 28"
+									aria-hidden="true"
+								>
+									<line x1="8" y1="26" x2="8" y2="8" :stroke="arrowToneColor(findStageByKey(row.raw.lifecycle, currentDemoStep.stageKey)!.status)" stroke-width="2" />
+									<polygon points="3,10 13,10 8,2" :fill="arrowToneColor(findStageByKey(row.raw.lifecycle, currentDemoStep.stageKey)!.status)" />
+								</svg>
+							</div>
 							<table class="oe-detail-matrix-table">
 								<thead>
 									<tr>
@@ -268,6 +388,7 @@ const rows = computed(() =>
 										>
 											{{ col.name }}
 										</th>
+										<th class="oe-detail-matrix-table__workbench-head">项目经理工作台</th>
 									</tr>
 								</thead>
 								<tbody>
@@ -276,6 +397,48 @@ const rows = computed(() =>
 										<td v-for="col in buildStageColumns(row.raw.lifecycle)" :key="col.key">
 											{{ col.kpiText }}
 										</td>
+										<td class="oe-detail-matrix-table__workbench-cell" rowspan="6">
+											<div class="oe-workbench">
+												<div class="oe-workbench__title">项目经理工作台</div>
+												<template v-if="workbenchStage(row.so, row.raw)">
+													<div class="oe-workbench__row">
+												<span class="oe-workbench__label">状态</span>
+												<span
+													class="oe-detail-matrix-table__status"
+													:class="`oe-detail-matrix-table__status--${workbenchStage(row.so, row.raw)!.status}`"
+												>
+													<span class="oe-detail-matrix-table__status-dot" />
+													{{ STAGE_STATUS_LABEL[workbenchStage(row.so, row.raw)!.status] }}
+												</span>
+													</div>
+													<div class="oe-workbench__row">
+												<span class="oe-workbench__label">节点</span>
+												<span class="oe-workbench__value">{{ workbenchStage(row.so, row.raw)!.name }}</span>
+													</div>
+													<div class="oe-workbench__row">
+												<span class="oe-workbench__label">期量差</span>
+												<span
+													class="oe-workbench__value"
+													:class="`oe-var--${varTone(workbenchStage(row.so, row.raw)!.cumulativeVarianceDays)}`"
+												>{{ fmtVar(workbenchStage(row.so, row.raw)!.cumulativeVarianceDays) }} 天</span>
+													</div>
+												</template>
+												<template v-else>
+													<div class="oe-workbench__row">
+												<span class="oe-workbench__label">状态</span>
+												<span class="oe-workbench__value oe-workbench__value--empty">--</span>
+													</div>
+													<div class="oe-workbench__row">
+												<span class="oe-workbench__label">节点</span>
+												<span class="oe-workbench__value oe-workbench__value--empty">--</span>
+													</div>
+													<div class="oe-workbench__row">
+												<span class="oe-workbench__label">期量差</span>
+												<span class="oe-workbench__value oe-workbench__value--empty">--</span>
+													</div>
+												</template>
+											</div>
+										</td>
 									</tr>
 									<tr>
 										<td class="oe-detail-matrix-table__metric">目标时间</td>
@@ -286,19 +449,21 @@ const rows = computed(() =>
 									<tr>
 										<td class="oe-detail-matrix-table__metric">完成时间</td>
 										<td v-for="col in buildStageColumns(row.raw.lifecycle)" :key="col.key">
-											{{ col.actualReachedAt }}
+											{{ isStageRevealed(row.so, row.raw.lifecycle, col.key) ? col.actualReachedAt : '--' }}
 										</td>
 									</tr>
 									<tr>
 										<td class="oe-detail-matrix-table__metric">节点状态</td>
 										<td v-for="col in buildStageColumns(row.raw.lifecycle)" :key="col.key">
 											<span
+												v-if="isStageRevealed(row.so, row.raw.lifecycle, col.key)"
 												class="oe-detail-matrix-table__status"
 												:class="`oe-detail-matrix-table__status--${col.status}`"
 											>
 												<span class="oe-detail-matrix-table__status-dot" />
 												{{ col.statusLabel }}
 											</span>
+											<span v-else>--</span>
 										</td>
 									</tr>
 									<tr>
@@ -306,9 +471,9 @@ const rows = computed(() =>
 										<td
 											v-for="col in buildStageColumns(row.raw.lifecycle)"
 											:key="col.key"
-											:class="`oe-var--${col.nodeVarTone}`"
+											:class="isStageRevealed(row.so, row.raw.lifecycle, col.key) ? `oe-var--${col.nodeVarTone}` : undefined"
 										>
-											{{ col.nodeVarText }}
+											{{ isStageRevealed(row.so, row.raw.lifecycle, col.key) ? col.nodeVarText : '--' }}
 										</td>
 									</tr>
 									<tr>
@@ -316,9 +481,9 @@ const rows = computed(() =>
 										<td
 											v-for="col in buildStageColumns(row.raw.lifecycle)"
 											:key="col.key"
-											:class="`oe-var--${col.cumVarTone}`"
+											:class="isStageRevealed(row.so, row.raw.lifecycle, col.key) ? `oe-var--${col.cumVarTone}` : undefined"
 										>
-											{{ col.cumVarText }}
+											{{ isStageRevealed(row.so, row.raw.lifecycle, col.key) ? col.cumVarText : '--' }}
 										</td>
 									</tr>
 								</tbody>
@@ -450,14 +615,14 @@ const rows = computed(() =>
 
 .oe-order-row__chip--line { background: rgba(123, 208, 255, 0.08); color: #7bd0ff; }
 .oe-order-row__chip--region { background: rgba(255, 193, 7, 0.08); color: #ffc107; }
-.oe-order-row__chip--priority-p1 { background: rgba(255, 180, 171, 0.16); color: #ffb4ab; font-weight: 600; }
+.oe-order-row__chip--priority-p1 { background: rgba(255, 77, 79, 0.16); color: #ff4d4f; font-weight: 600; }
 .oe-order-row__chip--priority-p2 { background: rgba(255, 193, 7, 0.16); color: #ffc107; font-weight: 600; }
 .oe-order-row__chip--priority-p3 { background: rgba(123, 208, 255, 0.16); color: #7bd0ff; font-weight: 600; }
 .oe-order-row__chip--workflow-progress { background: rgba(123, 208, 255, 0.12); color: #7bd0ff; }
 .oe-order-row__chip--workflow-done { background: rgba(109, 224, 57, 0.12); color: #88fd54; }
 .oe-order-row__chip--green { background: rgba(109, 224, 57, 0.12); color: #88fd54; }
 .oe-order-row__chip--yellow { background: rgba(255, 193, 7, 0.12); color: #ffc107; }
-.oe-order-row__chip--red { background: rgba(255, 180, 171, 0.14); color: #ffb4ab; }
+.oe-order-row__chip--red { background: rgba(255, 77, 79, 0.14); color: #ff4d4f; }
 
 .oe-order-row__metric {
 	display: inline-flex;
@@ -539,7 +704,7 @@ const rows = computed(() =>
 }
 
 .oe-detail-kpi__value--accent { color: #ffc107; }
-.oe-detail-kpi__value--danger { color: #ffb4ab; }
+.oe-detail-kpi__value--danger { color: #ff4d4f; }
 
 .oe-detail-section-head {
 	display: flex;
@@ -598,7 +763,7 @@ const rows = computed(() =>
 	border-bottom: none;
 }
 
-.oe-var--over { color: #ffb4ab; }
+.oe-var--over { color: #ff4d4f; }
 .oe-var--under { color: #88fd54; }
 .oe-var--neutral { color: var(--order-text-muted, #909097); }
 
@@ -622,7 +787,7 @@ const rows = computed(() =>
 
 .oe-detail-status--green { color: #88fd54; }
 .oe-detail-status--yellow { color: #ffc107; }
-.oe-detail-status--red { color: #ffb4ab; }
+.oe-detail-status--red { color: #ff4d4f; }
 
 .oe-detail-matrix {
 	display: flex;
@@ -654,7 +819,7 @@ const rows = computed(() =>
 
 .oe-detail-matrix__legend-dot--green { background: #88fd54; }
 .oe-detail-matrix__legend-dot--yellow { background: #ffc107; }
-.oe-detail-matrix__legend-dot--red { background: #ffb4ab; }
+.oe-detail-matrix__legend-dot--red { background: #ff4d4f; }
 
 .oe-detail-matrix-table {
 	width: 100%;
@@ -708,7 +873,7 @@ const rows = computed(() =>
 
 .oe-detail-matrix-table__stage-head--green { box-shadow: inset 3px 0 0 #88fd54; }
 .oe-detail-matrix-table__stage-head--yellow { box-shadow: inset 3px 0 0 #ffc107; }
-.oe-detail-matrix-table__stage-head--red { box-shadow: inset 3px 0 0 #ffb4ab; }
+.oe-detail-matrix-table__stage-head--red { box-shadow: inset 3px 0 0 #ff4d4f; }
 .oe-detail-matrix-table__stage-head--pending { box-shadow: inset 3px 0 0 rgba(144, 144, 151, 0.45); }
 
 .oe-detail-matrix-table__status {
@@ -731,9 +896,156 @@ const rows = computed(() =>
 
 .oe-detail-matrix-table__status--green { color: #88fd54; }
 .oe-detail-matrix-table__status--yellow { color: #ffc107; }
-.oe-detail-matrix-table__status--red { color: #ffb4ab; }
+.oe-detail-matrix-table__status--red { color: #ff4d4f; }
 .oe-detail-matrix-table__status--pending { color: var(--order-text-muted, #909097); }
 
+.oe-detail-matrix-table__workbench-head {
+	width: 160px;
+	background: rgba(15, 19, 26, 0.78) !important;
+	box-shadow: inset 4px 0 0 rgba(108, 140, 255, 0.55);
+	color: var(--order-text-primary, #e1e2eb) !important;
+}
+
+.oe-detail-matrix-table__workbench-cell {
+	width: 160px;
+	background: rgba(15, 19, 26, 0.55);
+	vertical-align: top;
+	padding: 10px 12px !important;
+	text-align: left !important;
+	box-shadow: inset 4px 0 0 rgba(108, 140, 255, 0.35);
+}
+
+.oe-workbench {
+	display: flex;
+	flex-direction: column;
+	gap: 8px;
+	font-family: 'PingFang SC', sans-serif;
+}
+
+.oe-workbench__title {
+	font-size: 11px;
+	color: var(--order-text-muted, #909097);
+	letter-spacing: 0.5px;
+}
+
+.oe-workbench__row {
+	display: flex;
+	align-items: center;
+	gap: 8px;
+	font-size: 12px;
+}
+
+.oe-workbench__label {
+	flex: 0 0 36px;
+	color: var(--order-text-muted, #909097);
+	font-size: 11px;
+}
+
+.oe-workbench__value {
+	color: var(--order-text-primary, #e1e2eb);
+	font-family: 'JetBrains Mono', 'Cascadia Code', Menlo, monospace;
+}
+
+.oe-workbench__value--empty {
+	color: var(--order-text-muted, #909097);
+}
+
+.oe-demo-controls {
+	display: flex;
+	align-items: center;
+	gap: 8px;
+	margin-left: auto;
+	font-family: 'PingFang SC', sans-serif;
+}
+
+.oe-demo-controls__indicator {
+	font-size: 11px;
+	color: var(--order-text-muted, #909097);
+	padding: 0 4px;
+}
+
+.oe-demo-btn {
+	padding: 4px 10px;
+	border-radius: 999px;
+	border: 1px solid rgba(108, 140, 255, 0.35);
+	background: rgba(15, 19, 26, 0.55);
+	color: var(--order-text-primary, #e1e2eb);
+	font-size: 11px;
+	line-height: 1.4;
+	cursor: pointer;
+	transition: background 120ms ease, border-color 120ms ease;
+}
+
+.oe-demo-btn:hover:not(:disabled) {
+	background: rgba(108, 140, 255, 0.18);
+	border-color: rgba(108, 140, 255, 0.65);
+}
+
+.oe-demo-btn:disabled {
+	opacity: 0.4;
+	cursor: not-allowed;
+}
+
+.oe-demo-btn--primary {
+	background: rgba(108, 140, 255, 0.22);
+	border-color: rgba(108, 140, 255, 0.65);
+}
+
+.oe-demo-btn--ghost {
+	border-color: rgba(144, 144, 151, 0.35);
+	color: var(--order-text-muted, #909097);
+}
+
+.oe-demo-narrative {
+	margin: 8px 0 6px;
+	padding: 10px 14px;
+	border-radius: 10px;
+	background: linear-gradient(90deg, rgba(108, 140, 255, 0.12), rgba(15, 19, 26, 0.55));
+	border: 1px solid rgba(108, 140, 255, 0.25);
+	display: flex;
+	flex-direction: column;
+	gap: 4px;
+	font-family: 'PingFang SC', sans-serif;
+}
+
+.oe-demo-narrative__title {
+	margin: 0;
+	font-size: 12px;
+	color: rgba(108, 140, 255, 0.9);
+	letter-spacing: 0.5px;
+}
+
+.oe-demo-narrative__step {
+	margin: 0;
+	font-size: 13px;
+	color: var(--order-text-primary, #e1e2eb);
+	font-weight: 600;
+}
+
+.oe-demo-arrows {
+	position: relative;
+	height: 38px;
+	margin-top: 2px;
+	pointer-events: none;
+}
+
+.oe-demo-arrow {
+	position: absolute;
+	width: 16px;
+	height: 28px;
+	left: var(--demo-arrow-left, 50%);
+	transform: translateX(-50%);
+	top: 5px;
+}
+
+.oe-demo-arrow--down {
+	left: calc(var(--demo-arrow-left, 50%) - 14px);
+}
+
+.oe-demo-arrow--up {
+	left: calc(var(--demo-arrow-left, 50%) + 14px);
+}
+
 .oe-expand-enter-active,
 .oe-expand-leave-active {
 	transition: max-height 200ms ease, opacity 180ms ease;

+ 142 - 0
Web/src/views/aidop/s8/monitoring/components/order-execution/ProcurementDetailPanel.vue

@@ -0,0 +1,142 @@
+<script setup lang="ts" name="ProcurementDetailPanel">
+// ORDER-FLOW-CHAIN-STAGE-DETAIL-MIGRATE-1:材料采购阶段结构化详情。fixture 来源 ecc-sandbox 硬编码;非真实数据。
+import { computed, ref } from 'vue';
+import {
+	PROCUREMENT_MATRIX_COLUMNS,
+	type ProcurementDetail,
+	type StageDetailStatus,
+} from '/@/views/aidop/s8/monitoring/data/order-execution/stage-detail';
+
+const props = defineProps<{ detail: ProcurementDetail }>();
+
+// ORDER-FLOW-CHAIN-PROCUREMENT-PIVOT-1:默认 YY(PPT 主案例 + 源项目默认)。
+const selectedMaterial = ref<'XX' | 'YY' | 'ZZ'>('YY');
+
+const MATRIX_COLUMNS = PROCUREMENT_MATRIX_COLUMNS;
+
+const supplierRows = computed(() => props.detail.supplierBreakdown.filter((r) => r.material === selectedMaterial.value));
+const specRows = computed(() => props.detail.specBreakdown.filter((r) => r.material === selectedMaterial.value));
+const currentMatrixRows = computed(() => props.detail.matrixByMaterial[selectedMaterial.value]);
+
+function statusColor(s: StageDetailStatus): string {
+	if (s === 'red') return '#ff4d4f';
+	if (s === 'yellow') return '#ffc107';
+	return '#88fd54';
+}
+</script>
+
+<template>
+	<section class="proc-panel">
+		<header class="proc-panel__head">
+			<span class="proc-panel__bar" />
+			<h2 class="proc-panel__title">材料采购 · 阶段详情</h2>
+			<span class="proc-panel__demo">演示数据</span>
+		</header>
+
+		<div class="proc-panel__block">
+			<div class="proc-panel__caption">关键材料总览(点击切换下钻)</div>
+			<table class="proc-panel__table">
+				<thead>
+					<tr><th>关键材料</th><th>平均周期</th><th>周期状态</th><th>影响台次</th><th>准时齐套率</th><th>齐套状态</th></tr>
+				</thead>
+				<tbody>
+					<tr
+						v-for="row in detail.keyMaterials"
+						:key="row.material"
+						:class="{ 'proc-panel__row--active': row.material !== '合计' && row.material === selectedMaterial }"
+						@click="row.material !== '合计' && (selectedMaterial = row.material as 'XX' | 'YY' | 'ZZ')"
+					>
+						<td>{{ row.material }}</td>
+						<td :style="{ color: statusColor(row.cycleStatus), fontWeight: 600 }">{{ row.avgCycle }}</td>
+						<td><span class="proc-panel__dot" :style="{ background: statusColor(row.cycleStatus) }" /></td>
+						<td>{{ row.impactCount }}</td>
+						<td>{{ row.kitRate }}</td>
+						<td><span class="proc-panel__dot" :style="{ background: statusColor(row.resultStatus) }" /></td>
+					</tr>
+				</tbody>
+			</table>
+		</div>
+
+		<div class="proc-panel__split">
+			<div class="proc-panel__block proc-panel__block--half">
+				<div class="proc-panel__caption">{{ selectedMaterial }} · 供应商表现</div>
+				<table class="proc-panel__table">
+					<thead><tr><th>供应商</th><th>平均周期</th><th>达标</th></tr></thead>
+					<tbody>
+						<tr v-for="row in supplierRows" :key="`sup-${row.supplier}`">
+							<td>{{ row.supplier }}</td>
+							<td :style="{ color: statusColor(row.status), fontWeight: 600 }">{{ row.avgCycle }}</td>
+							<td><span class="proc-panel__dot" :style="{ background: statusColor(row.status) }" /></td>
+						</tr>
+					</tbody>
+				</table>
+			</div>
+
+			<div class="proc-panel__block proc-panel__block--half">
+				<div class="proc-panel__caption">{{ selectedMaterial }} · 规格表现</div>
+				<table class="proc-panel__table">
+					<thead><tr><th>规格</th><th>平均周期</th><th>达标</th></tr></thead>
+					<tbody>
+						<tr v-for="row in specRows" :key="`spec-${row.spec}`">
+							<td>{{ row.spec }}</td>
+							<td :style="{ color: statusColor(row.status), fontWeight: 600 }">{{ row.avgCycle }}</td>
+							<td><span class="proc-panel__dot" :style="{ background: statusColor(row.status) }" /></td>
+						</tr>
+					</tbody>
+				</table>
+			</div>
+		</div>
+
+		<div class="proc-panel__block">
+			<div class="proc-panel__caption">{{ selectedMaterial }} 供应商 × 线材周期矩阵</div>
+			<table class="proc-panel__table">
+				<thead>
+					<tr>
+						<th>供应商 / 线材</th>
+						<th v-for="col in MATRIX_COLUMNS" :key="`h-${col}`">{{ col }}</th>
+					</tr>
+				</thead>
+				<tbody>
+					<tr v-for="row in currentMatrixRows" :key="row.supplier">
+						<td :style="{ fontWeight: row.supplier === '合计' ? 700 : 600 }">{{ row.supplier }}</td>
+						<td
+							v-for="col in MATRIX_COLUMNS"
+							:key="`${row.supplier}-${col}`"
+							:style="{
+								color: statusColor(row.values[col].status),
+								fontWeight: row.supplier === '合计' ? 700 : 500,
+							}"
+						>
+							{{ row.values[col].cycle }}
+						</td>
+					</tr>
+				</tbody>
+			</table>
+		</div>
+	</section>
+</template>
+
+<style scoped>
+.proc-panel { display: flex; flex-direction: column; gap: 12px; }
+.proc-panel__head { display: flex; align-items: center; gap: 10px; }
+.proc-panel__bar { width: 5px; height: 18px; border-radius: 999px; background: var(--order-accent, #7bd0ff); }
+.proc-panel__title { margin: 0; font-size: 16px; font-weight: 700; }
+.proc-panel__demo {
+	margin-left: 6px; padding: 2px 10px; border-radius: 999px; font-size: 11px;
+	color: #ffc107; background: rgba(255,193,7,0.1); border: 1px solid rgba(255,193,7,0.32);
+}
+.proc-panel__block { background: rgba(25, 28, 34, 0.55); border: 1px solid rgba(69, 70, 77, 0.28); border-radius: 10px; padding: 12px; overflow-x: auto; }
+.proc-panel__split { display: flex; gap: 12px; }
+.proc-panel__block--half { flex: 1; min-width: 0; }
+.proc-panel__caption { font-size: 12px; color: var(--order-text-muted, #909097); margin-bottom: 8px; }
+.proc-panel__table { width: 100%; border-collapse: collapse; font-size: 12px; color: var(--order-text-primary, #e1e2eb); }
+.proc-panel__table th, .proc-panel__table td {
+	padding: 8px 10px; text-align: center;
+	border: 1px solid rgba(69, 70, 77, 0.18);
+	font-family: 'JetBrains Mono', 'Cascadia Code', Menlo, monospace;
+}
+.proc-panel__table th { background: rgba(15, 19, 26, 0.7); font-weight: 600; font-family: 'PingFang SC', sans-serif; }
+.proc-panel__row--active { background: rgba(123, 208, 255, 0.12); cursor: pointer; }
+.proc-panel__table tbody tr { cursor: pointer; }
+.proc-panel__dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; }
+</style>

+ 137 - 121
Web/src/views/aidop/s8/monitoring/components/order-execution/ReviewBreakdownCard.vue

@@ -1,6 +1,7 @@
 <script setup lang="ts" name="ReviewBreakdownCard">
-// ORDER-FLOW-CHAIN-DETAILS-1:意见评审四方明细。
-// fixture 中 order_review.subSteps[0](意见评审)的 breakdown 为 4 个部门记录。
+// ORDER-FLOW-CHAIN-PAGE2-DETAIL-LAYOUT-ALIGN-1:L3 横向矩阵(4 行 × N 列),
+// 与源项目 ecc-sandbox/OrderChainOverviewPage L1263-1360 结构对齐。
+// 紧贴 L2 ReviewSubStepsCard 之下,作为意见评审下钻明细矩阵。
 import { computed } from 'vue';
 import type { SubStepDetail } from '/@/views/aidop/s8/monitoring/data/order-execution/types';
 
@@ -10,7 +11,7 @@ interface Props {
 }
 
 const props = withDefaults(defineProps<Props>(), {
-	title: '意见评审四方明细',
+	title: '意见评审 — 详细执行明细',
 });
 
 const STATUS_LABEL: Record<SubStepDetail['status'], string> = {
@@ -20,30 +21,14 @@ const STATUS_LABEL: Record<SubStepDetail['status'], string> = {
 	pending: '未开始',
 };
 
-const varianceArrow = (v: number | null) => {
-	if (v == null) return '';
-	if (v > 0) return '▲ ';
-	if (v < 0) return '▼ ';
-	return '';
-};
-
-const rows = computed(() =>
-	(props.breakdown ?? []).map((dept) => {
-		const variance =
-			dept.actualHours == null ? null : Number((dept.actualHours - dept.piHours).toFixed(2));
-		return {
-			name: dept.name,
-			piText: `${dept.piHours.toFixed(1)} h`,
-			actualText: dept.actualHours == null ? '--' : `${dept.actualHours.toFixed(1)} h`,
-			status: dept.status,
-			statusLabel: STATUS_LABEL[dept.status],
-			varianceText:
-				variance == null
-					? '--'
-					: `${varianceArrow(variance)}${variance > 0 ? '+' : ''}${variance.toFixed(2)} h`,
-			varianceTone: variance == null ? 'neutral' : variance > 0 ? 'over' : 'under',
-		};
-	}),
+const cells = computed(() =>
+	(props.breakdown ?? []).map((dept) => ({
+		name: dept.name,
+		piText: `${dept.piHours}h`,
+		actualText: dept.actualHours == null ? '—' : `${dept.actualHours}h`,
+		status: dept.status,
+		statusLabel: STATUS_LABEL[dept.status],
+	})),
 );
 </script>
 
@@ -53,37 +38,59 @@ const rows = computed(() =>
 			<span class="oe-breakdown__bar" />
 			<h2 class="oe-breakdown__title">{{ title }}</h2>
 		</header>
-		<div v-if="rows.length === 0" class="oe-breakdown__empty">暂无四方评审明细</div>
-		<div v-else class="oe-breakdown__grid">
-			<div
-				v-for="row in rows"
-				:key="row.name"
-				class="oe-breakdown__cell"
-				:class="`oe-breakdown__cell--${row.status}`"
-			>
-				<div class="oe-breakdown__name">
-					{{ row.name }}
-					<span class="oe-breakdown__chip" :class="`oe-breakdown__chip--${row.status}`">
-						{{ row.statusLabel }}
-					</span>
-				</div>
-				<dl class="oe-breakdown__metrics">
-					<div class="oe-breakdown__metric">
-						<dt>PI 工时</dt>
-						<dd>{{ row.piText }}</dd>
-					</div>
-					<div class="oe-breakdown__metric">
-						<dt>实际</dt>
-						<dd>{{ row.actualText }}</dd>
-					</div>
-					<div class="oe-breakdown__metric">
-						<dt>偏差</dt>
-						<dd :class="`oe-breakdown__variance--${row.varianceTone}`">
-							{{ row.varianceText }}
-						</dd>
-					</div>
-				</dl>
-			</div>
+		<div v-if="cells.length === 0" class="oe-breakdown__empty">暂无四方评审明细</div>
+		<div v-else class="oe-breakdown__scroll">
+			<table class="oe-breakdown__matrix">
+				<tbody>
+					<tr>
+						<td class="oe-breakdown__row-head">涉及部门 / 组</td>
+						<td
+							v-for="cell in cells"
+							:key="`name-${cell.name}`"
+							class="oe-breakdown__col-head"
+						>
+							{{ cell.name }}
+						</td>
+					</tr>
+					<tr>
+						<td class="oe-breakdown__row-label">PI</td>
+						<td
+							v-for="cell in cells"
+							:key="`pi-${cell.name}`"
+							class="oe-breakdown__data oe-breakdown__data--pi"
+						>
+							{{ cell.piText }}
+						</td>
+					</tr>
+					<tr>
+						<td class="oe-breakdown__row-label">实际达成</td>
+						<td
+							v-for="cell in cells"
+							:key="`actual-${cell.name}`"
+							class="oe-breakdown__data oe-breakdown__data--actual"
+							:class="{ 'oe-breakdown__data--actual-red': cell.status === 'red' }"
+						>
+							{{ cell.actualText }}
+						</td>
+					</tr>
+					<tr>
+						<td class="oe-breakdown__row-label">达标情况</td>
+						<td
+							v-for="cell in cells"
+							:key="`status-${cell.name}`"
+							class="oe-breakdown__data oe-breakdown__data--status"
+						>
+							<span
+								class="oe-breakdown__dot"
+								:class="`oe-breakdown__dot--${cell.status}`"
+								role="img"
+								:title="cell.statusLabel"
+								:aria-label="cell.statusLabel"
+							/>
+						</td>
+					</tr>
+				</tbody>
+			</table>
 		</div>
 	</section>
 </template>
@@ -92,7 +99,8 @@ const rows = computed(() =>
 .oe-breakdown {
 	display: flex;
 	flex-direction: column;
-	gap: 10px;
+	gap: 8px;
+	margin-top: -4px;
 }
 
 .oe-breakdown__head {
@@ -110,8 +118,9 @@ const rows = computed(() =>
 
 .oe-breakdown__title {
 	margin: 0;
-	font-size: 16px;
+	font-size: 15px;
 	font-weight: 700;
+	color: var(--order-text-primary, #e1e2eb);
 }
 
 .oe-breakdown__empty {
@@ -124,89 +133,96 @@ const rows = computed(() =>
 	text-align: center;
 }
 
-.oe-breakdown__grid {
-	display: grid;
-	grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
-	gap: 12px;
+.oe-breakdown__scroll {
+	overflow-x: auto;
 }
 
-.oe-breakdown__cell {
-	display: flex;
-	flex-direction: column;
-	gap: 8px;
-	padding: 12px 14px;
-	border-radius: 12px;
-	background: var(--order-panel, rgba(25, 28, 34, 0.72));
-	border: 1px solid var(--order-border, rgba(144, 144, 151, 0.18));
-	border-left-width: 3px;
+.oe-breakdown__matrix {
+	width: 100%;
+	border-collapse: collapse;
+	background: rgba(25, 28, 34, 0.45);
+	border: 1px solid rgba(69, 70, 77, 0.22);
+	border-radius: 10px;
+	overflow: hidden;
+	font-size: 12px;
+	table-layout: fixed;
 }
 
-.oe-breakdown__cell--green { border-left-color: #88fd54; }
-.oe-breakdown__cell--yellow { border-left-color: #ffc107; }
-.oe-breakdown__cell--red { border-left-color: #ffb4ab; }
-.oe-breakdown__cell--pending { border-left-color: rgba(144, 144, 151, 0.35); }
-
-.oe-breakdown__name {
-	display: flex;
-	align-items: center;
-	gap: 8px;
-	font-size: 13px;
-	font-weight: 700;
+.oe-breakdown__matrix td {
+	padding: 10px;
+	text-align: center;
+	border-bottom: 1px solid rgba(69, 70, 77, 0.16);
+	border-right: 1px solid rgba(69, 70, 77, 0.16);
 	color: var(--order-text-primary, #e1e2eb);
+	font-family: 'JetBrains Mono', 'Cascadia Code', Menlo, monospace;
 }
 
-.oe-breakdown__chip {
-	padding: 1px 8px;
-	border-radius: 999px;
-	font-size: 11px;
-	line-height: 1.6;
-	font-weight: 400;
+.oe-breakdown__matrix tr:last-child td {
+	border-bottom: none;
 }
 
-.oe-breakdown__chip--green { background: rgba(109, 224, 57, 0.12); color: #88fd54; }
-.oe-breakdown__chip--yellow { background: rgba(255, 193, 7, 0.12); color: #ffc107; }
-.oe-breakdown__chip--red { background: rgba(255, 180, 171, 0.12); color: #ffb4ab; }
-.oe-breakdown__chip--pending { background: rgba(144, 144, 151, 0.12); color: var(--order-text-muted, #909097); }
-
-.oe-breakdown__metrics {
-	display: grid;
-	grid-template-columns: repeat(3, minmax(0, 1fr));
-	gap: 6px;
-	margin: 0;
+.oe-breakdown__matrix td:last-child {
+	border-right: none;
 }
 
-.oe-breakdown__metric {
-	display: flex;
-	flex-direction: column;
-	gap: 2px;
-	margin: 0;
+.oe-breakdown__row-head,
+.oe-breakdown__row-label {
+	width: 140px;
+	text-align: left;
+	color: var(--order-text-muted, #909097);
+	background: rgba(15, 19, 26, 0.5);
+	font-family: 'PingFang SC', sans-serif;
+	font-weight: 600;
 }
 
-.oe-breakdown__metric dt {
-	font-size: 11px;
-	color: var(--order-text-muted, #909097);
+.oe-breakdown__col-head {
+	background: rgba(15, 19, 26, 0.6);
+	color: var(--order-text-primary, #e1e2eb);
+	font-weight: 700;
+	font-family: 'PingFang SC', sans-serif;
+	font-size: 13px;
 }
 
-.oe-breakdown__metric dd {
-	margin: 0;
+.oe-breakdown__data--pi {
+	color: var(--order-text-secondary, #c6c6cd);
 	font-size: 13px;
 	font-weight: 600;
+}
+
+.oe-breakdown__data--actual {
 	color: var(--order-text-primary, #e1e2eb);
+	font-size: 14px;
+	font-weight: 800;
+}
+
+.oe-breakdown__data--actual-red {
+	color: #ff4d4f;
 }
 
-.oe-breakdown__variance--over { color: #ffb4ab; }
-.oe-breakdown__variance--under { color: #88fd54; }
-.oe-breakdown__variance--neutral { color: var(--order-text-muted, #909097); }
+.oe-breakdown__dot {
+	display: inline-block;
+	width: 14px;
+	height: 14px;
+	border-radius: 50%;
+	vertical-align: middle;
+}
+
+.oe-breakdown__dot--green {
+	background: #88fd54;
+	box-shadow: 0 0 8px #88fd54;
+}
+
+.oe-breakdown__dot--yellow {
+	background: #ffc107;
+	box-shadow: 0 0 8px #ffc107;
+}
 
-@media (max-width: 1080px) {
-	.oe-breakdown__grid {
-		grid-template-columns: repeat(2, minmax(0, 1fr));
-	}
+.oe-breakdown__dot--red {
+	background: #ff4d4f;
+	box-shadow: 0 0 8px #ff4d4f;
 }
 
-@media (max-width: 640px) {
-	.oe-breakdown__grid {
-		grid-template-columns: 1fr;
-	}
+.oe-breakdown__dot--pending {
+	background: rgba(144, 144, 151, 0.45);
 }
 </style>

+ 128 - 85
Web/src/views/aidop/s8/monitoring/components/order-execution/ReviewSubStepsCard.vue

@@ -1,5 +1,6 @@
 <script setup lang="ts" name="ReviewSubStepsCard">
-// ORDER-FLOW-CHAIN-DETAILS-1:订单评审五子节点表(仅 order_review 阶段有 subSteps)。
+// ORDER-FLOW-CHAIN-PAGE2-DETAIL-LAYOUT-ALIGN-1:L2 横向矩阵(4 行 × N 列),
+// 与源项目 ecc-sandbox/OrderChainOverviewPage L1095-1261 结构对齐。
 import { computed } from 'vue';
 import type { SubStepDetail } from '/@/views/aidop/s8/monitoring/data/order-execution/types';
 
@@ -9,7 +10,7 @@ interface Props {
 }
 
 const props = withDefaults(defineProps<Props>(), {
-	title: '评审子节点',
+	title: '节点详情:评审 / 排产 / 测算',
 });
 
 const STATUS_LABEL: Record<SubStepDetail['status'], string> = {
@@ -19,22 +20,14 @@ const STATUS_LABEL: Record<SubStepDetail['status'], string> = {
 	pending: '未开始',
 };
 
-const rows = computed(() =>
-	(props.subSteps ?? []).map((step) => {
-		const variance =
-			step.actualHours == null ? null : Number((step.actualHours - step.piHours).toFixed(2));
-		return {
-			name: step.name,
-			piText: `${step.piHours.toFixed(1)} h`,
-			actualText: step.actualHours == null ? '--' : `${step.actualHours.toFixed(1)} h`,
-			status: step.status,
-			statusLabel: STATUS_LABEL[step.status],
-			varianceText:
-				variance == null ? '--' : `${variance > 0 ? '+' : ''}${variance.toFixed(2)} h`,
-			varianceTone:
-				variance == null ? 'neutral' : variance > 0 ? 'over' : 'under',
-		};
-	}),
+const cells = computed(() =>
+	(props.subSteps ?? []).map((step) => ({
+		name: step.name,
+		piText: `${step.piHours}h`,
+		actualText: step.actualHours == null ? '—' : `${step.actualHours}h`,
+		status: step.status,
+		statusLabel: STATUS_LABEL[step.status],
+	})),
 );
 </script>
 
@@ -44,38 +37,59 @@ const rows = computed(() =>
 			<span class="oe-substeps__bar" />
 			<h2 class="oe-substeps__title">{{ title }}</h2>
 		</header>
-		<div v-if="rows.length === 0" class="oe-substeps__empty">暂无子节点明细</div>
-		<table v-else class="oe-substeps__table">
-			<thead>
-				<tr>
-					<th class="oe-substeps__th">子节点</th>
-					<th class="oe-substeps__th">PI 工时</th>
-					<th class="oe-substeps__th">实际工时</th>
-					<th class="oe-substeps__th">偏差</th>
-					<th class="oe-substeps__th">状态</th>
-				</tr>
-			</thead>
-			<tbody>
-				<tr
-					v-for="row in rows"
-					:key="row.name"
-					class="oe-substeps__row"
-					:class="`oe-substeps__row--${row.status}`"
-				>
-					<td class="oe-substeps__td oe-substeps__td--name">{{ row.name }}</td>
-					<td class="oe-substeps__td">{{ row.piText }}</td>
-					<td class="oe-substeps__td">{{ row.actualText }}</td>
-					<td class="oe-substeps__td" :class="`oe-substeps__td--${row.varianceTone}`">
-						{{ row.varianceText }}
-					</td>
-					<td class="oe-substeps__td">
-						<span class="oe-substeps__chip" :class="`oe-substeps__chip--${row.status}`">
-							{{ row.statusLabel }}
-						</span>
-					</td>
-				</tr>
-			</tbody>
-		</table>
+		<div v-if="cells.length === 0" class="oe-substeps__empty">暂无子节点明细</div>
+		<div v-else class="oe-substeps__scroll">
+			<table class="oe-substeps__matrix">
+				<tbody>
+					<tr>
+						<td class="oe-substeps__row-head">关键环节</td>
+						<td
+							v-for="cell in cells"
+							:key="`name-${cell.name}`"
+							class="oe-substeps__col-head"
+						>
+							{{ cell.name }}
+						</td>
+					</tr>
+					<tr>
+						<td class="oe-substeps__row-label">PI</td>
+						<td
+							v-for="cell in cells"
+							:key="`pi-${cell.name}`"
+							class="oe-substeps__data oe-substeps__data--pi"
+						>
+							{{ cell.piText }}
+						</td>
+					</tr>
+					<tr>
+						<td class="oe-substeps__row-label">实际达成</td>
+						<td
+							v-for="cell in cells"
+							:key="`actual-${cell.name}`"
+							class="oe-substeps__data oe-substeps__data--actual"
+						>
+							{{ cell.actualText }}
+						</td>
+					</tr>
+					<tr>
+						<td class="oe-substeps__row-label">达标情况</td>
+						<td
+							v-for="cell in cells"
+							:key="`status-${cell.name}`"
+							class="oe-substeps__data oe-substeps__data--status"
+						>
+							<span
+								class="oe-substeps__dot"
+								:class="`oe-substeps__dot--${cell.status}`"
+								role="img"
+								:title="cell.statusLabel"
+								:aria-label="cell.statusLabel"
+							/>
+						</td>
+					</tr>
+				</tbody>
+			</table>
+		</div>
 	</section>
 </template>
 
@@ -115,62 +129,91 @@ const rows = computed(() =>
 	text-align: center;
 }
 
-.oe-substeps__table {
+.oe-substeps__scroll {
+	overflow-x: auto;
+}
+
+.oe-substeps__matrix {
 	width: 100%;
 	border-collapse: collapse;
-	border-radius: 12px;
+	background: rgba(25, 28, 34, 0.55);
+	border: 1px solid rgba(69, 70, 77, 0.28);
+	border-radius: 10px;
 	overflow: hidden;
-	background: var(--order-panel, rgba(25, 28, 34, 0.72));
-	border: 1px solid var(--order-border, rgba(144, 144, 151, 0.18));
 	font-size: 12px;
+	table-layout: fixed;
 }
 
-.oe-substeps__th {
-	text-align: left;
-	padding: 10px 12px;
-	font-weight: 700;
-	color: var(--order-text-secondary, #c6c6cd);
-	background: rgba(15, 19, 26, 0.55);
-	border-bottom: 1px solid var(--order-border, rgba(144, 144, 151, 0.18));
+.oe-substeps__matrix td {
+	padding: 12px 10px;
+	text-align: center;
+	border-bottom: 1px solid rgba(69, 70, 77, 0.18);
+	border-right: 1px solid rgba(69, 70, 77, 0.18);
+	color: var(--order-text-primary, #e1e2eb);
+	font-family: 'JetBrains Mono', 'Cascadia Code', Menlo, monospace;
 }
 
-.oe-substeps__row {
-	transition: background 80ms ease;
+.oe-substeps__matrix tr:last-child td {
+	border-bottom: none;
 }
 
-.oe-substeps__row:hover {
-	background: rgba(123, 208, 255, 0.04);
+.oe-substeps__matrix td:last-child {
+	border-right: none;
 }
 
-.oe-substeps__row--red { background: rgba(255, 180, 171, 0.04); }
-.oe-substeps__row--yellow { background: rgba(255, 193, 7, 0.04); }
-
-.oe-substeps__row + .oe-substeps__row .oe-substeps__td {
-	border-top: 1px solid rgba(144, 144, 151, 0.08);
+.oe-substeps__row-head,
+.oe-substeps__row-label {
+	width: 140px;
+	text-align: left;
+	color: var(--order-text-muted, #909097);
+	background: rgba(15, 19, 26, 0.55);
+	font-family: 'PingFang SC', sans-serif;
+	font-weight: 600;
 }
 
-.oe-substeps__td {
-	padding: 10px 12px;
+.oe-substeps__col-head {
+	background: rgba(15, 19, 26, 0.7);
 	color: var(--order-text-primary, #e1e2eb);
+	font-weight: 700;
+	font-family: 'PingFang SC', sans-serif;
 }
 
-.oe-substeps__td--name {
+.oe-substeps__data--pi {
+	color: var(--order-text-secondary, #c6c6cd);
+	font-size: 13px;
 	font-weight: 600;
 }
 
-.oe-substeps__td--over { color: #ffb4ab; }
-.oe-substeps__td--under { color: #88fd54; }
-.oe-substeps__td--neutral { color: var(--order-text-muted, #909097); }
+.oe-substeps__data--actual {
+	color: var(--order-text-primary, #e1e2eb);
+	font-size: 14px;
+	font-weight: 800;
+}
 
-.oe-substeps__chip {
-	padding: 1px 10px;
-	border-radius: 999px;
-	font-size: 11px;
-	line-height: 1.6;
+.oe-substeps__dot {
+	display: inline-block;
+	width: 18px;
+	height: 18px;
+	border-radius: 50%;
+	vertical-align: middle;
 }
 
-.oe-substeps__chip--green { background: rgba(109, 224, 57, 0.12); color: #88fd54; }
-.oe-substeps__chip--yellow { background: rgba(255, 193, 7, 0.12); color: #ffc107; }
-.oe-substeps__chip--red { background: rgba(255, 180, 171, 0.12); color: #ffb4ab; }
-.oe-substeps__chip--pending { background: rgba(144, 144, 151, 0.12); color: var(--order-text-muted, #909097); }
+.oe-substeps__dot--green {
+	background: #88fd54;
+	box-shadow: 0 0 10px #88fd54;
+}
+
+.oe-substeps__dot--yellow {
+	background: #ffc107;
+	box-shadow: 0 0 10px #ffc107;
+}
+
+.oe-substeps__dot--red {
+	background: #ff4d4f;
+	box-shadow: 0 0 10px #ff4d4f;
+}
+
+.oe-substeps__dot--pending {
+	background: rgba(144, 144, 151, 0.45);
+}
 </style>

+ 3 - 3
Web/src/views/aidop/s8/monitoring/components/order-execution/StageMatrixCard.vue

@@ -163,7 +163,7 @@ const rows = computed(() =>
 
 .oe-stage-cell--green { border-left: 3px solid #88fd54; }
 .oe-stage-cell--yellow { border-left: 3px solid #ffc107; }
-.oe-stage-cell--red { border-left: 3px solid #ffb4ab; }
+.oe-stage-cell--red { border-left: 3px solid #ff4d4f; }
 .oe-stage-cell--pending { border-left: 3px solid rgba(144, 144, 151, 0.35); }
 
 .oe-stage-cell--focus {
@@ -226,7 +226,7 @@ const rows = computed(() =>
 
 .oe-stage-cell__status-dot--green { background: #88fd54; }
 .oe-stage-cell__status-dot--yellow { background: #ffc107; }
-.oe-stage-cell__status-dot--red { background: #ffb4ab; }
+.oe-stage-cell__status-dot--red { background: #ff4d4f; }
 .oe-stage-cell__status-dot--pending { background: rgba(144, 144, 151, 0.6); }
 
 .oe-stage-cell__metrics {
@@ -295,7 +295,7 @@ const rows = computed(() =>
 	color: var(--order-text-secondary, #c6c6cd);
 }
 
-.oe-stage-cell__variance--over { color: #ffb4ab; }
+.oe-stage-cell__variance--over { color: #ff4d4f; }
 .oe-stage-cell__variance--under { color: #88fd54; }
 .oe-stage-cell__variance--neutral { color: var(--order-text-muted, #909097); }
 

+ 3 - 3
Web/src/views/aidop/s8/monitoring/data/order-execution/builder.ts

@@ -106,7 +106,7 @@ export const buildLifecycle = (
 const hashStr = (s: string): number =>
   s.split('').reduce((acc, c) => ((acc * 31 + c.charCodeAt(0)) >>> 0), 7);
 
-const REVIEW_SUB_STEP_CONFIGS = [
+export const REVIEW_SUB_STEP_CONFIGS = [
   { name: '意见评审', piHours: 8 },
   { name: '意见反馈', piHours: 12 },
   { name: '二次评审', piHours: 8 },
@@ -114,7 +114,7 @@ const REVIEW_SUB_STEP_CONFIGS = [
   { name: '合同盖章', piHours: 2 },
 ] as const;
 
-const OPINION_DEPT_CONFIGS = [
+export const OPINION_DEPT_CONFIGS = [
   { name: '法律事务部', piHours: 2 },
   { name: '技术售前组', piHours: 3 },
   { name: '综合主计划', piHours: 1 },
@@ -145,7 +145,7 @@ const distributeHours = <T extends { name: string; piHours: number }>(
   });
 };
 
-const buildReviewSubSteps = (soNo: string, actualDays: number | null): SubStepDetail[] => {
+export const buildReviewSubSteps = (soNo: string, actualDays: number | null): SubStepDetail[] => {
   if (actualDays === null) {
     return REVIEW_SUB_STEP_CONFIGS.map((c) => ({
       name: c.name, piHours: c.piHours, actualHours: null, status: 'pending' as const,

+ 24 - 0
Web/src/views/aidop/s8/monitoring/data/order-execution/demo-step-config.ts

@@ -0,0 +1,24 @@
+// ORDER-FLOW-OVERVIEW-DEMO-PLAYBACK-1:SO-2026-001 演示模式步骤配置。
+// 工作台 status/node/variance 不在此硬编码,运行时按 stageKey 从 lifecycle 读取。
+import type { OrderNodeKey } from './types';
+
+export const DEMO_TARGET_SO_NO = 'SO-2026-001';
+
+export const DEMO_TOP_TITLE = '依据执行预测对实际时间节点进行进度跟进检查';
+
+export interface DemoStep {
+	stepNo: number;
+	stageKey: OrderNodeKey | null;
+	topNarrative: string;
+}
+
+export const DEMO_STEPS: readonly DemoStep[] = [
+	{ stepNo: 0, stageKey: null,                      topNarrative: '' },
+	{ stepNo: 1, stageKey: 'order_review',            topNarrative: '基础信息确认 / 按期纳入计划' },
+	{ stepNo: 2, stageKey: 'product_design',          topNarrative: '设计延期 / 异常通知 & 处理' },
+	{ stepNo: 3, stageKey: 'material_procurement',    topNarrative: '材料优先级调节 / 调节有效 / 整体进度可控' },
+	{ stepNo: 4, stageKey: 'body_production',         topNarrative: '成品按时入库' },
+	{ stepNo: 5, stageKey: 'final_assembly_shipping', topNarrative: '客户需求按期达成' },
+] as const;
+
+export const DEMO_MAX_STEP = DEMO_STEPS.length - 1;

+ 22 - 0
Web/src/views/aidop/s8/monitoring/data/order-execution/ppt-baseline.ts

@@ -0,0 +1,22 @@
+// ORDER-FLOW-CHAIN-PAGE2-ORIGINAL-LOGIC-RESTORE-1
+// PPT 第 2 页 L2 / L3 baseline 真值(与原项目 ecc-sandbox 同源)。
+// 严格使用边界:
+//   ✅ 仅 isUnfiltered === true 时使用
+//   ❌ 多订单筛选态:必须实时聚合 filteredChainOrders
+//   ❌ 单订单态:必须取该订单自身 subSteps / breakdown
+import type { SubStepDetail } from './types';
+
+export const PPT_REVIEW_SUBSTEPS: SubStepDetail[] = [
+	{ name: '意见评审', piHours: 8, actualHours: 14.4, status: 'red' },
+	{ name: '意见反馈', piHours: 12, actualHours: 14.2, status: 'yellow' },
+	{ name: '二次评审', piHours: 8, actualHours: 7, status: 'green' },
+	{ name: '领导意见', piHours: 10, actualHours: 12, status: 'yellow' },
+	{ name: '合同盖章', piHours: 2, actualHours: 2, status: 'green' },
+];
+
+export const PPT_OPINION_DRILLDOWN: SubStepDetail[] = [
+	{ name: '法律事务部', piHours: 2, actualHours: 2, status: 'green' },
+	{ name: '技术售前组', piHours: 3, actualHours: 9.2, status: 'red' },
+	{ name: '综合主计划', piHours: 1, actualHours: 1.2, status: 'yellow' },
+	{ name: '试验站', piHours: 2, actualHours: 2, status: 'green' },
+];

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

@@ -0,0 +1,196 @@
+// ORDER-FLOW-CHAIN-STAGE-DETAIL-MIGRATE-1:产品设计 / 材料采购 / 设备制造(本体生产)阶段静态结构化详情。
+// 来源:ecc-sandbox/frontend/src/pages/OrderChainOverviewPage.tsx 中的硬编码常量;显示侧必须明示「演示数据」。
+// 不连后端,不参与聚合,未来真实数据接入需替换 fixture 来源。
+
+export type StageDetailStatus = 'green' | 'yellow' | 'red';
+
+export interface DesignDetail {
+	demoData: true;
+	categoryRows: Array<{
+		type: '常规产品' | '非标产品' | '合计';
+		count: number;
+		ratio: string;
+		kpi: string;
+		avgCycle: string;
+		status: StageDetailStatus | null;
+		drawingCount: number;
+	}>;
+	drawings: Array<{
+		id: string;
+		owner: string;
+		category: '常规产品' | '非标产品';
+		planStart: string;
+		planEnd: string;
+		actualStart: string;
+		actualEnd: string;
+		cycle: string;
+		status: StageDetailStatus;
+	}>;
+}
+
+// ORDER-FLOW-CHAIN-PROCUREMENT-PIVOT-1:供应商 × 线材周期透视表列定义。
+// 列顺序固定,模板按此顺序渲染。
+export const PROCUREMENT_MATRIX_COLUMNS = ['L4.5*11.2', 'MT2*6.3', 'MT1*5.4', '合计'] as const;
+export type ProcurementMatrixColumn = (typeof PROCUREMENT_MATRIX_COLUMNS)[number];
+
+export interface ProcurementMatrixCell {
+	cycle: string;
+	status: StageDetailStatus;
+}
+
+export interface ProcurementMatrixRow {
+	supplier: 'A' | 'B' | 'C' | '合计';
+	values: Record<ProcurementMatrixColumn, ProcurementMatrixCell>;
+}
+
+export interface ProcurementDetail {
+	demoData: true;
+	keyMaterials: Array<{
+		material: 'XX' | 'YY' | 'ZZ' | '合计';
+		avgCycle: string;
+		cycleStatus: StageDetailStatus;
+		impactCount: string;
+		kitRate: string;
+		resultStatus: StageDetailStatus;
+	}>;
+	supplierBreakdown: Array<{
+		material: 'XX' | 'YY' | 'ZZ';
+		supplier: 'A' | 'B' | 'C';
+		avgCycle: string;
+		status: StageDetailStatus;
+	}>;
+	specBreakdown: Array<{
+		material: 'XX' | 'YY' | 'ZZ';
+		spec: string;
+		avgCycle: string;
+		status: StageDetailStatus;
+	}>;
+	// ORDER-FLOW-CHAIN-PROCUREMENT-PIVOT-1:供应商 × 线材周期透视表。
+	// YY = PPT 第 4 页真值;XX / ZZ = 源项目 ecc-sandbox 演示 fixture(非 PPT 真值)。
+	// 合计行 / 合计列 = fixture 给定加权平均,权重未暴露,前端不重算。
+	matrixByMaterial: Record<'XX' | 'YY' | 'ZZ', ProcurementMatrixRow[]>;
+}
+
+export interface ManufacturingDetail {
+	demoData: true;
+	processRows: Array<{
+		process: string;
+		pi: string;
+		avgCycle: string;
+		cycleStatus: StageDetailStatus;
+		impactCount: number | null;
+		planRate: string;
+		planRateStatus: StageDetailStatus;
+	}>;
+	lossFactors: Array<{
+		factor: string;
+		count: number;
+		ratio: string;
+		lossHours: string;
+	}>;
+	operators: Array<{
+		name: string;
+		avgHours: string;
+		status: StageDetailStatus;
+	}>;
+}
+
+export const DESIGN_DETAIL_FIXTURE: DesignDetail = {
+	demoData: true,
+	categoryRows: [
+		{ type: '常规产品', count: 354, ratio: '93.5%', kpi: '--', avgCycle: '--', status: 'green', drawingCount: 52 },
+		{ type: '非标产品', count: 28, ratio: '6.5%', kpi: '3天', avgCycle: '9.28天', status: 'red', drawingCount: 11 },
+		{ type: '合计', count: 382, ratio: '100%', kpi: '--', avgCycle: '--', status: null, drawingCount: 63 },
+	],
+	drawings: [
+		{ id: 'A1354', owner: '王工', category: '非标产品', planStart: '8月1日', planEnd: '8月3日', actualStart: '7月30日', actualEnd: '8月3日', cycle: '5天', status: 'red' },
+		{ id: 'A1128', owner: '赵工', category: '非标产品', planStart: '8月2日', planEnd: '8月5日', actualStart: '8月1日', actualEnd: '8月4日', cycle: '3天', status: 'green' },
+		{ id: 'A1153', owner: '张工', category: '非标产品', planStart: '8月3日', planEnd: '8月6日', actualStart: '7月25日', actualEnd: '8月6日', cycle: '12天', status: 'red' },
+		{ id: 'A1287', owner: '李工', category: '常规产品', planStart: '8月5日', planEnd: '8月8日', actualStart: '8月4日', actualEnd: '8月7日', cycle: '3天', status: 'green' },
+		{ id: 'A1205', owner: '刘工', category: '常规产品', planStart: '8月6日', planEnd: '8月8日', actualStart: '8月6日', actualEnd: '8月8日', cycle: '2天', status: 'green' },
+		{ id: 'A1301', owner: '陈工', category: '常规产品', planStart: '8月7日', planEnd: '8月10日', actualStart: '8月7日', actualEnd: '8月9日', cycle: '2天', status: 'green' },
+	],
+};
+
+export const PROCUREMENT_DETAIL_FIXTURE: ProcurementDetail = {
+	demoData: true,
+	keyMaterials: [
+		{ material: 'XX', avgCycle: '14.2天', cycleStatus: 'yellow', impactCount: '55台', kitRate: '85%', resultStatus: 'red' },
+		{ material: 'YY', avgCycle: '15.3天', cycleStatus: 'red', impactCount: '23台', kitRate: '84%', resultStatus: 'yellow' },
+		{ material: 'ZZ', avgCycle: '14天', cycleStatus: 'green', impactCount: '10台', kitRate: '97%', resultStatus: 'green' },
+		{ material: '合计', avgCycle: '15.6天', cycleStatus: 'yellow', impactCount: '88台', kitRate: '86%', resultStatus: 'yellow' },
+	],
+	supplierBreakdown: [
+		{ material: 'XX', supplier: 'A', avgCycle: '12.8', status: 'green' },
+		{ material: 'XX', supplier: 'B', avgCycle: '14.6', status: 'yellow' },
+		{ material: 'XX', supplier: 'C', avgCycle: '15.2', status: 'red' },
+		{ material: 'YY', supplier: 'A', avgCycle: '13', status: 'green' },
+		{ material: 'YY', supplier: 'B', avgCycle: '15.6', status: 'yellow' },
+		{ material: 'YY', supplier: 'C', avgCycle: '20.1', status: 'red' },
+		{ material: 'ZZ', supplier: 'A', avgCycle: '12.5', status: 'green' },
+		{ material: 'ZZ', supplier: 'B', avgCycle: '13.8', status: 'green' },
+		{ material: 'ZZ', supplier: 'C', avgCycle: '14.9', status: 'yellow' },
+	],
+	specBreakdown: [
+		{ material: 'XX', spec: 'L4.5*11.2', avgCycle: '13.8', status: 'green' },
+		{ material: 'XX', spec: 'MT2*6.3', avgCycle: '14.9', status: 'yellow' },
+		{ material: 'XX', spec: 'MT1*5.4', avgCycle: '14.3', status: 'yellow' },
+		{ material: 'YY', spec: 'L4.5*11.2', avgCycle: '14.5', status: 'yellow' },
+		{ material: 'YY', spec: 'MT2*6.3', avgCycle: '17.4', status: 'red' },
+		{ material: 'YY', spec: 'MT1*5.4', avgCycle: '14.8', status: 'yellow' },
+		{ material: 'ZZ', spec: 'L4.5*11.2', avgCycle: '13.4', status: 'green' },
+		{ material: 'ZZ', spec: 'MT2*6.3', avgCycle: '14.1', status: 'green' },
+		{ material: 'ZZ', spec: 'MT1*5.4', avgCycle: '14.6', status: 'yellow' },
+	],
+	// 供应商 × 线材周期透视表:4 行(A/B/C/合计) × 4 列(3 规格 + 合计) × 3 材料 = 48 单元格。
+	// 数据来源:ecc-sandbox/frontend/src/pages/OrderChainOverviewPage.tsx L118-140 逐字段迁移。
+	// YY = PPT 第 4 页真值;XX / ZZ = 源项目演示 fixture(按 A<B<C / L4.5 稳定 / MT2-MT1 放大差距 / 合计加权平均规则编造)。
+	matrixByMaterial: {
+		// XX 整体 14.2天/黄;演示 fixture(非 PPT 真值)
+		XX: [
+			{ supplier: 'A', values: { 'L4.5*11.2': { cycle: '13.5', status: 'green' }, 'MT2*6.3': { cycle: '12.5', status: 'green' }, 'MT1*5.4': { cycle: '12', status: 'green' }, '合计': { cycle: '12.7', status: 'green' } } },
+			{ supplier: 'B', values: { 'L4.5*11.2': { cycle: '14.2', status: 'yellow' }, 'MT2*6.3': { cycle: '15.5', status: 'yellow' }, 'MT1*5.4': { cycle: '14.8', status: 'yellow' }, '合计': { cycle: '14.8', status: 'yellow' } } },
+			{ supplier: 'C', values: { 'L4.5*11.2': { cycle: '14.8', status: 'yellow' }, 'MT2*6.3': { cycle: '17', status: 'red' }, 'MT1*5.4': { cycle: '16.5', status: 'yellow' }, '合计': { cycle: '16.1', status: 'yellow' } } },
+			{ supplier: '合计', values: { 'L4.5*11.2': { cycle: '14.1', status: 'yellow' }, 'MT2*6.3': { cycle: '14.8', status: 'yellow' }, 'MT1*5.4': { cycle: '14.2', status: 'yellow' }, '合计': { cycle: '14.2', status: 'yellow' } } },
+		],
+		// YY 整体 15.3天/红;PPT 第 4 页真值;C 在 MT 规格严重超标
+		YY: [
+			{ supplier: 'A', values: { 'L4.5*11.2': { cycle: '14.8', status: 'yellow' }, 'MT2*6.3': { cycle: '11', status: 'green' }, 'MT1*5.4': { cycle: '11', status: 'green' }, '合计': { cycle: '13', status: 'green' } } },
+			{ supplier: 'B', values: { 'L4.5*11.2': { cycle: '14.05', status: 'yellow' }, 'MT2*6.3': { cycle: '17.15', status: 'red' }, 'MT1*5.4': { cycle: '17.15', status: 'red' }, '合计': { cycle: '15.6', status: 'yellow' } } },
+			{ supplier: 'C', values: { 'L4.5*11.2': { cycle: '14.05', status: 'yellow' }, 'MT2*6.3': { cycle: '23.925', status: 'red' }, 'MT1*5.4': { cycle: '23.85', status: 'red' }, '合计': { cycle: '20.1', status: 'red' } } },
+			{ supplier: '合计', values: { 'L4.5*11.2': { cycle: '14.5', status: 'yellow' }, 'MT2*6.3': { cycle: '17.4', status: 'red' }, 'MT1*5.4': { cycle: '14.8', status: 'yellow' }, '合计': { cycle: '15.3', status: 'yellow' } } },
+		],
+		// ZZ 整体 14天/绿;演示 fixture(非 PPT 真值);仅 C 在 MT 规格略高
+		ZZ: [
+			{ supplier: 'A', values: { 'L4.5*11.2': { cycle: '13', status: 'green' }, 'MT2*6.3': { cycle: '12', status: 'green' }, 'MT1*5.4': { cycle: '11.5', status: 'green' }, '合计': { cycle: '12.2', status: 'green' } } },
+			{ supplier: 'B', values: { 'L4.5*11.2': { cycle: '14', status: 'green' }, 'MT2*6.3': { cycle: '14.5', status: 'yellow' }, 'MT1*5.4': { cycle: '14', status: 'green' }, '合计': { cycle: '14.2', status: 'yellow' } } },
+			{ supplier: 'C', values: { 'L4.5*11.2': { cycle: '14.2', status: 'yellow' }, 'MT2*6.3': { cycle: '16', status: 'yellow' }, 'MT1*5.4': { cycle: '15.5', status: 'yellow' }, '合计': { cycle: '15.2', status: 'yellow' } } },
+			{ supplier: '合计', values: { 'L4.5*11.2': { cycle: '13.6', status: 'green' }, 'MT2*6.3': { cycle: '14.1', status: 'yellow' }, 'MT1*5.4': { cycle: '13.5', status: 'green' }, '合计': { cycle: '14', status: 'green' } } },
+		],
+	},
+};
+
+export const MANUFACTURING_DETAIL_FIXTURE: ManufacturingDetail = {
+	demoData: true,
+	// 数值/单位/达标颜色严格对齐 PPT 第 5 页"工序明细"表(PPT 真值)。
+	// PI 与 平均周期 单位均为"天";planRateStatus 由 PPT 表格颜色直接给定,前端不按阈值重判。
+	processRows: [
+		{ process: '10工序', pi: '1天', avgCycle: '2.4天', cycleStatus: 'red', impactCount: 45, planRate: '55%', planRateStatus: 'red' },
+		{ process: '20工序', pi: '1天', avgCycle: '0.8天', cycleStatus: 'green', impactCount: 3, planRate: '97%', planRateStatus: 'yellow' },
+		{ process: '30工序', pi: '2.5天', avgCycle: '3天', cycleStatus: 'yellow', impactCount: 15, planRate: '85%', planRateStatus: 'green' },
+		{ process: '40工序', pi: '1.5天', avgCycle: '0.6天', cycleStatus: 'green', impactCount: 2, planRate: '98%', planRateStatus: 'red' },
+		{ process: '合计', pi: '6天', avgCycle: '6.8天', cycleStatus: 'yellow', impactCount: null, planRate: '90%', planRateStatus: 'yellow' },
+	],
+	lossFactors: [
+		{ factor: '材料影响', count: 9, ratio: '30%', lossHours: '9H' },
+		{ factor: '设备影响', count: 6, ratio: '20%', lossHours: '4.5H' },
+		{ factor: '质量影响', count: 3, ratio: '10%', lossHours: '3.4H' },
+		{ factor: '作业效率损失', count: 12, ratio: '40%', lossHours: '9.25H' },
+		{ factor: '损失小计', count: 30, ratio: '100%', lossHours: '7.5H' },
+	],
+	operators: [
+		{ name: '张师傅', avgHours: '20H', status: 'red' },
+		{ name: '李师傅', avgHours: '10H', status: 'yellow' },
+		{ name: '王师傅', avgHours: '8H', status: 'green' },
+	],
+};

+ 44 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S8/AdoS8DemoOrderFlowController.cs

@@ -0,0 +1,44 @@
+using Admin.NET.Plugin.AiDOP.Dto.S8.Demo;
+using Admin.NET.Plugin.AiDOP.Service.S8.Demo;
+
+namespace Admin.NET.Plugin.AiDOP.Controllers.S8;
+
+/// <summary>
+/// ORDER-FLOW-BE-DEMO-DATASET-RESET-1:S8 demo 订单执行档案只读端点。
+/// 双口径:/orders 返回 20 条明细;/aggregate-snapshot 返回独立 105 单基线快照(非明细汇总)。
+/// </summary>
+[ApiController]
+[Route("api/aidop/s8/demo/order-flow")]
+[NonUnify]
+public class AdoS8DemoOrderFlowController : ControllerBase
+{
+    private readonly S8DemoOrderFlowService _svc;
+
+    public AdoS8DemoOrderFlowController(S8DemoOrderFlowService svc) => _svc = svc;
+
+    /// <summary>20 条 demo 订单明细,含完整 5 阶段 lifecycle。供 overview / chain 单订单使用。</summary>
+    [HttpGet("orders")]
+    public async Task<IActionResult> ListOrdersAsync()
+    {
+        var list = await _svc.ListOrdersAsync();
+        return Ok(list);
+    }
+
+    /// <summary>单订单详情(备用接口;前端默认从 /orders 缓存中查 soNo)。</summary>
+    [HttpGet("orders/{soNo}")]
+    public async Task<IActionResult> GetOrderAsync(string soNo)
+    {
+        var order = await _svc.GetOrderAsync(soNo);
+        if (order == null) return NotFound(new { message = $"未找到订单 {soNo}" });
+        return Ok(order);
+    }
+
+    /// <summary>chain 默认全选基线快照(独立 105 单展示快照,与 /orders 明细不同源)。</summary>
+    [HttpGet("aggregate-snapshot")]
+    public async Task<IActionResult> GetAggregateSnapshotAsync()
+    {
+        var snap = await _svc.GetAggregateSnapshotAsync();
+        if (snap == null) return NotFound(new { message = "未找到 CHAIN_AGGREGATE_BASELINE 快照" });
+        return Ok(snap);
+    }
+}

+ 87 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Dto/S8/Demo/AdoDemoOrderFlowDtos.cs

@@ -0,0 +1,87 @@
+namespace Admin.NET.Plugin.AiDOP.Dto.S8.Demo;
+
+/// <summary>
+/// ORDER-FLOW-BE-DEMO-DATASET-RESET-1:阶段子表 DTO。
+/// 时间字符串:expected_date = "YYYY-MM-DD";actual_*_at = "YYYY-MM-DD HH:mm"。
+/// </summary>
+public class AdoDemoOrderFlowStageDto
+{
+    public string StageKey { get; set; } = string.Empty;
+    public string StageName { get; set; } = string.Empty;
+    public int SortNo { get; set; }
+    public decimal PlannedDays { get; set; }
+    public decimal? ActualDays { get; set; }
+    public string ExpectedDate { get; set; } = string.Empty;
+    public string? ActualStartAt { get; set; }
+    public string? ActualEndAt { get; set; }
+    public string Status { get; set; } = "pending";
+    public decimal? NodeVarianceDays { get; set; }
+    public decimal? CumulativeVarianceDays { get; set; }
+}
+
+/// <summary>
+/// ORDER-FLOW-BE-DEMO-DATASET-RESET-1:订单主表 DTO,含完整 5 阶段 lifecycle。
+/// 由 /api/aidop/s8/demo/order-flow/orders 返回。
+/// </summary>
+public class AdoDemoOrderFlowDto
+{
+    public string SoNo { get; set; } = string.Empty;
+    public string ProductName { get; set; } = string.Empty;
+    public string ProductLine { get; set; } = string.Empty;
+    public string CustomerName { get; set; } = string.Empty;
+    public string CustomerCode { get; set; } = string.Empty;
+    public string CustomerType { get; set; } = "KA";
+    public string Region { get; set; } = string.Empty;
+    public string Priority { get; set; } = "P2";
+    public string MaterialCode { get; set; } = "XX";
+    public string SupplierGroup { get; set; } = "A";
+    public string WorkflowStatus { get; set; } = "completed";
+    public string CurrentNodeKey { get; set; } = "final_assembly_shipping";
+    public string FocusNodeKey { get; set; } = "final_assembly_shipping";
+    public string CurrentNodeLabel { get; set; } = "总装发货";
+    public string NodeStatus { get; set; } = "green";
+    public string ReleaseAt { get; set; } = string.Empty;
+    public decimal TargetCycleDays { get; set; }
+    public decimal? CurrentCycleDays { get; set; }
+    public decimal? ActualCycleDays { get; set; }
+    public decimal? NodeVarianceDays { get; set; }
+    public decimal? CumulativeVarianceDays { get; set; }
+    public int ExceptionCount { get; set; }
+    public int? ResponseMinutes { get; set; }
+    public int? ProcessingMinutes { get; set; }
+    public int? TotalLossMinutes { get; set; }
+    public string ExceptionStatus { get; set; } = "已闭环";
+    public List<AdoDemoOrderFlowStageDto> Lifecycle { get; set; } = new();
+}
+
+/// <summary>
+/// ORDER-FLOW-BE-DEMO-DATASET-RESET-1:基线聚合快照 DTO。
+/// 注意:本 DTO 不是 /orders 明细的实时聚合,而是 105 单基线展示快照(独立快照表 1 行)。
+/// 由 /api/aidop/s8/demo/order-flow/aggregate-snapshot 返回。
+/// </summary>
+public class AdoDemoOrderFlowSnapshotDto
+{
+    public string SnapshotCode { get; set; } = "CHAIN_AGGREGATE_BASELINE";
+    public string SnapshotLabel { get; set; } = "基线聚合 · 样本 105";
+    public int TotalOrders { get; set; }
+    public int TotalCustomers { get; set; }
+    public decimal AvgResponseMinutes { get; set; }
+    public decimal AvgProcessingMinutes { get; set; }
+    public decimal AvgLossMinutes { get; set; }
+    public List<AdoDemoOrderFlowStageSnapshotItem> StageSnapshots { get; set; } = new();
+    public string? Remark { get; set; }
+}
+
+public class AdoDemoOrderFlowStageSnapshotItem
+{
+    public string StageKey { get; set; } = string.Empty;
+    public string StageName { get; set; } = string.Empty;
+    public string OwnerDept { get; set; } = string.Empty;
+    public decimal KpiAvgDays { get; set; }
+    public decimal ActualAvgDays { get; set; }
+    public int OnTimeRate { get; set; }
+    public int Green { get; set; }
+    public int Yellow { get; set; }
+    public int Red { get; set; }
+    public int Pending { get; set; }
+}

+ 118 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S8/Demo/AdoDemoOrderFlow.cs

@@ -0,0 +1,118 @@
+namespace Admin.NET.Plugin.AiDOP.Entity.S8.Demo;
+
+/// <summary>
+/// ORDER-FLOW-BE-DEMO-DATASET-RESET-1 演示主表:S8 订单执行档案 demo 主数据。
+/// 与真实业务订单(SeOrder/Order_*)完全解耦;仅供"订单执行档案"演示页面读取。
+/// </summary>
+[SugarTable("ado_demo_order_flow", "演示订单执行档案主表(demo only,不与真实业务表混用)")]
+[SugarIndex("uk_demo_order_flow_so_no", nameof(SoNo), OrderByType.Asc, true)]
+[SugarIndex("idx_demo_order_flow_tenant_factory", nameof(TenantId), OrderByType.Asc, nameof(FactoryId), OrderByType.Asc, nameof(SortNo), OrderByType.Asc)]
+public class AdoDemoOrderFlow
+{
+    [SugarColumn(ColumnName = "id", IsPrimaryKey = true, IsIdentity = false, ColumnDataType = "bigint")]
+    public long Id { get; set; }
+
+    [SugarColumn(ColumnName = "so_no", Length = 32)]
+    public string SoNo { get; set; } = string.Empty;
+
+    [SugarColumn(ColumnName = "product_name", Length = 128)]
+    public string ProductName { get; set; } = string.Empty;
+
+    [SugarColumn(ColumnName = "product_line", Length = 32)]
+    public string ProductLine { get; set; } = string.Empty;
+
+    [SugarColumn(ColumnName = "customer_name", Length = 128)]
+    public string CustomerName { get; set; } = string.Empty;
+
+    [SugarColumn(ColumnName = "customer_code", Length = 64)]
+    public string CustomerCode { get; set; } = string.Empty;
+
+    /// <summary>KA / SMB / MICRO。</summary>
+    [SugarColumn(ColumnName = "customer_type", Length = 8)]
+    public string CustomerType { get; set; } = "KA";
+
+    [SugarColumn(ColumnName = "region", Length = 32)]
+    public string Region { get; set; } = string.Empty;
+
+    /// <summary>P1 / P2 / P3。</summary>
+    [SugarColumn(ColumnName = "priority", Length = 4)]
+    public string Priority { get; set; } = "P2";
+
+    /// <summary>XX / YY / ZZ(与产品线对照)。</summary>
+    [SugarColumn(ColumnName = "material_code", Length = 8)]
+    public string MaterialCode { get; set; } = "XX";
+
+    /// <summary>A / B / C。</summary>
+    [SugarColumn(ColumnName = "supplier_group", Length = 8)]
+    public string SupplierGroup { get; set; } = "A";
+
+    /// <summary>completed / in_progress。</summary>
+    [SugarColumn(ColumnName = "workflow_status", Length = 16)]
+    public string WorkflowStatus { get; set; } = "completed";
+
+    /// <summary>5 阶段 key 之一:order_review / product_design / material_procurement / body_production / final_assembly_shipping。</summary>
+    [SugarColumn(ColumnName = "current_node_key", Length = 32)]
+    public string CurrentNodeKey { get; set; } = "final_assembly_shipping";
+
+    [SugarColumn(ColumnName = "focus_node_key", Length = 32)]
+    public string FocusNodeKey { get; set; } = "final_assembly_shipping";
+
+    [SugarColumn(ColumnName = "current_node_label", Length = 32)]
+    public string CurrentNodeLabel { get; set; } = "总装发货";
+
+    /// <summary>green / yellow / red。</summary>
+    [SugarColumn(ColumnName = "node_status", Length = 8)]
+    public string NodeStatus { get; set; } = "green";
+
+    [SugarColumn(ColumnName = "release_at")]
+    public DateTime ReleaseAt { get; set; }
+
+    [SugarColumn(ColumnName = "target_cycle_days", DecimalDigits = 1, Length = 6)]
+    public decimal TargetCycleDays { get; set; }
+
+    [SugarColumn(ColumnName = "current_cycle_days", DecimalDigits = 1, Length = 6, IsNullable = true)]
+    public decimal? CurrentCycleDays { get; set; }
+
+    [SugarColumn(ColumnName = "actual_cycle_days", DecimalDigits = 1, Length = 6, IsNullable = true)]
+    public decimal? ActualCycleDays { get; set; }
+
+    [SugarColumn(ColumnName = "node_variance_days", DecimalDigits = 1, Length = 6, IsNullable = true)]
+    public decimal? NodeVarianceDays { get; set; }
+
+    [SugarColumn(ColumnName = "cumulative_variance_days", DecimalDigits = 1, Length = 6, IsNullable = true)]
+    public decimal? CumulativeVarianceDays { get; set; }
+
+    [SugarColumn(ColumnName = "exception_count")]
+    public int ExceptionCount { get; set; }
+
+    [SugarColumn(ColumnName = "response_minutes", IsNullable = true)]
+    public int? ResponseMinutes { get; set; }
+
+    [SugarColumn(ColumnName = "processing_minutes", IsNullable = true)]
+    public int? ProcessingMinutes { get; set; }
+
+    [SugarColumn(ColumnName = "total_loss_minutes", IsNullable = true)]
+    public int? TotalLossMinutes { get; set; }
+
+    /// <summary>待响应 / 处理中 / 已闭环。</summary>
+    [SugarColumn(ColumnName = "exception_status", Length = 16)]
+    public string ExceptionStatus { get; set; } = "已闭环";
+
+    [SugarColumn(ColumnName = "sort_no")]
+    public int SortNo { get; set; }
+
+    [SugarColumn(ColumnName = "tenant_id", ColumnDataType = "bigint")]
+    public long TenantId { get; set; }
+
+    [SugarColumn(ColumnName = "factory_id", ColumnDataType = "bigint")]
+    public long FactoryId { get; set; }
+
+    [SugarColumn(ColumnName = "created_at")]
+    public DateTime CreatedAt { get; set; } = DateTime.Now;
+
+    [SugarColumn(ColumnName = "updated_at", IsNullable = true)]
+    public DateTime? UpdatedAt { get; set; }
+
+    [SugarColumn(ColumnName = "is_deleted", ColumnDataType = "boolean")]
+    public bool IsDeleted { get; set; }
+}

+ 59 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S8/Demo/AdoDemoOrderFlowSnapshot.cs

@@ -0,0 +1,59 @@
+namespace Admin.NET.Plugin.AiDOP.Entity.S8.Demo;
+
+/// <summary>
+/// ORDER-FLOW-BE-DEMO-DATASET-RESET-1 演示基线聚合快照:chain 默认全选展示用。
+/// 注意:本快照不是当前明细 20 单的实时聚合结果,而是冻结的 105 单基线展示快照,
+/// 由 SeedData 一次性写入。前端 chain 默认全选独立读取此快照,与 /orders 不同源。
+/// </summary>
+[SugarTable("ado_demo_order_flow_snapshot", "演示基线聚合快照(baseline,独立于 20 单明细)")]
+[SugarIndex("uk_demo_order_flow_snapshot_code", nameof(SnapshotCode), OrderByType.Asc, true)]
+public class AdoDemoOrderFlowSnapshot
+{
+    [SugarColumn(ColumnName = "id", IsPrimaryKey = true, IsIdentity = false, ColumnDataType = "bigint")]
+    public long Id { get; set; }
+
+    /// <summary>固定为 'CHAIN_AGGREGATE_BASELINE'。</summary>
+    [SugarColumn(ColumnName = "snapshot_code", Length = 64)]
+    public string SnapshotCode { get; set; } = "CHAIN_AGGREGATE_BASELINE";
+
+    /// <summary>UI 直接展示的标签文本,例如 "基线聚合 · 样本 105"。</summary>
+    [SugarColumn(ColumnName = "snapshot_label", Length = 64)]
+    public string SnapshotLabel { get; set; } = "基线聚合 · 样本 105";
+
+    [SugarColumn(ColumnName = "total_orders")]
+    public int TotalOrders { get; set; }
+
+    [SugarColumn(ColumnName = "total_customers")]
+    public int TotalCustomers { get; set; }
+
+    [SugarColumn(ColumnName = "avg_response_minutes", DecimalDigits = 2, Length = 8)]
+    public decimal AvgResponseMinutes { get; set; }
+
+    [SugarColumn(ColumnName = "avg_processing_minutes", DecimalDigits = 2, Length = 8)]
+    public decimal AvgProcessingMinutes { get; set; }
+
+    [SugarColumn(ColumnName = "avg_loss_minutes", DecimalDigits = 2, Length = 8)]
+    public decimal AvgLossMinutes { get; set; }
+
+    /// <summary>5 阶段聚合 JSON 数组:stageKey/stageName/ownerDept/kpiAvgDays/actualAvgDays/onTimeRate/green/yellow/red/pending。</summary>
+    [SugarColumn(ColumnName = "stage_snapshots_json", ColumnDataType = "longtext", IsJson = false)]
+    public string StageSnapshotsJson { get; set; } = "[]";
+
+    [SugarColumn(ColumnName = "remark", Length = 256, IsNullable = true)]
+    public string? Remark { get; set; }
+
+    [SugarColumn(ColumnName = "tenant_id", ColumnDataType = "bigint")]
+    public long TenantId { get; set; }
+
+    [SugarColumn(ColumnName = "factory_id", ColumnDataType = "bigint")]
+    public long FactoryId { get; set; }
+
+    [SugarColumn(ColumnName = "created_at")]
+    public DateTime CreatedAt { get; set; } = DateTime.Now;
+
+    [SugarColumn(ColumnName = "updated_at", IsNullable = true)]
+    public DateTime? UpdatedAt { get; set; }
+
+    [SugarColumn(ColumnName = "is_deleted", ColumnDataType = "boolean")]
+    public bool IsDeleted { get; set; }
+}

+ 68 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S8/Demo/AdoDemoOrderFlowStage.cs

@@ -0,0 +1,68 @@
+namespace Admin.NET.Plugin.AiDOP.Entity.S8.Demo;
+
+/// <summary>
+/// ORDER-FLOW-BE-DEMO-DATASET-RESET-1 演示阶段子表:每订单 5 行(5 个 stage_key 固定)。
+/// expected_date / actual_end_at / status / node_variance_days / cumulative_variance_days
+/// 必须直接以源真值落库,前端不再反向推导。
+/// </summary>
+[SugarTable("ado_demo_order_flow_stage", "演示订单执行档案阶段子表(每订单 5 阶段)")]
+[SugarIndex("uk_demo_order_flow_stage", nameof(OrderId), OrderByType.Asc, nameof(StageKey), OrderByType.Asc, true)]
+[SugarIndex("idx_demo_order_flow_stage_tenant_factory", nameof(TenantId), OrderByType.Asc, nameof(FactoryId), OrderByType.Asc)]
+public class AdoDemoOrderFlowStage
+{
+    [SugarColumn(ColumnName = "id", IsPrimaryKey = true, IsIdentity = false, ColumnDataType = "bigint")]
+    public long Id { get; set; }
+
+    [SugarColumn(ColumnName = "order_id", ColumnDataType = "bigint")]
+    public long OrderId { get; set; }
+
+    /// <summary>order_review / product_design / material_procurement / body_production / final_assembly_shipping。</summary>
+    [SugarColumn(ColumnName = "stage_key", Length = 32)]
+    public string StageKey { get; set; } = string.Empty;
+
+    [SugarColumn(ColumnName = "stage_name", Length = 32)]
+    public string StageName { get; set; } = string.Empty;
+
+    [SugarColumn(ColumnName = "sort_no")]
+    public int SortNo { get; set; }
+
+    [SugarColumn(ColumnName = "planned_days", DecimalDigits = 1, Length = 5)]
+    public decimal PlannedDays { get; set; }
+
+    [SugarColumn(ColumnName = "actual_days", DecimalDigits = 1, Length = 5, IsNullable = true)]
+    public decimal? ActualDays { get; set; }
+
+    [SugarColumn(ColumnName = "expected_date")]
+    public DateTime ExpectedDate { get; set; }
+
+    [SugarColumn(ColumnName = "actual_start_at", IsNullable = true)]
+    public DateTime? ActualStartAt { get; set; }
+
+    [SugarColumn(ColumnName = "actual_end_at", IsNullable = true)]
+    public DateTime? ActualEndAt { get; set; }
+
+    /// <summary>green / yellow / red / pending。直接落库,与 actual_days 解耦。</summary>
+    [SugarColumn(ColumnName = "status", Length = 8)]
+    public string Status { get; set; } = "pending";
+
+    [SugarColumn(ColumnName = "node_variance_days", DecimalDigits = 1, Length = 5, IsNullable = true)]
+    public decimal? NodeVarianceDays { get; set; }
+
+    [SugarColumn(ColumnName = "cumulative_variance_days", DecimalDigits = 1, Length = 5, IsNullable = true)]
+    public decimal? CumulativeVarianceDays { get; set; }
+
+    [SugarColumn(ColumnName = "tenant_id", ColumnDataType = "bigint")]
+    public long TenantId { get; set; }
+
+    [SugarColumn(ColumnName = "factory_id", ColumnDataType = "bigint")]
+    public long FactoryId { get; set; }
+
+    [SugarColumn(ColumnName = "created_at")]
+    public DateTime CreatedAt { get; set; } = DateTime.Now;
+
+    [SugarColumn(ColumnName = "updated_at", IsNullable = true)]
+    public DateTime? UpdatedAt { get; set; }
+
+    [SugarColumn(ColumnName = "is_deleted", ColumnDataType = "boolean")]
+    public bool IsDeleted { get; set; }
+}

+ 318 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/SeedData/S8DemoOrderFlowSeedData.cs

@@ -0,0 +1,318 @@
+using Admin.NET.Plugin.AiDOP.Entity.S8.Demo;
+
+namespace Admin.NET.Plugin.AiDOP;
+
+/// <summary>
+/// ORDER-FLOW-BE-DEMO-DATASET-RESET-1:demo 订单执行档案种子(1 主表 + 阶段子表 + 1 快照)。
+/// 全局基线:tenant_id=0 / factory_id=0。SO-2026-001 = PPT 第 1 页标准案例;其余 19 单覆盖筛选维度。
+/// </summary>
+[IncreSeed]
+public class S8DemoOrderFlowSeedData : ISqlSugarEntitySeedData<AdoDemoOrderFlow>
+{
+    public IEnumerable<AdoDemoOrderFlow> HasData() => S8DemoOrderFlowDataset.BuildOrders();
+}
+
+[IncreSeed]
+public class S8DemoOrderFlowStageSeedData : ISqlSugarEntitySeedData<AdoDemoOrderFlowStage>
+{
+    public IEnumerable<AdoDemoOrderFlowStage> HasData() => S8DemoOrderFlowDataset.BuildStages();
+}
+
+[IncreSeed]
+public class S8DemoOrderFlowSnapshotSeedData : ISqlSugarEntitySeedData<AdoDemoOrderFlowSnapshot>
+{
+    public IEnumerable<AdoDemoOrderFlowSnapshot> HasData() => S8DemoOrderFlowDataset.BuildSnapshot();
+}
+
+internal static class S8DemoOrderFlowDataset
+{
+    private static readonly DateTime CreatedAt = DateTime.Parse("2026-05-11 00:00:00");
+    private const long OrderIdBase = 1329909000000L;
+    private const long StageIdBase = 1329909010000L;
+    private const long SnapshotIdBase = 1329909020000L;
+
+    private record OrderSpec(
+        int Idx, string SoNo, string ProductName,
+        string CustomerName, string CustomerCode, string CustomerType,
+        string ProductLine, string MaterialCode, string Region,
+        string Priority, string SupplierGroup,
+        string WorkflowStatus, string CurrentNodeKey, string FocusNodeKey,
+        int ExceptionCount, int? ResponseMinutes, int? ProcessingMinutes, int? TotalLossMinutes);
+
+    // SO-2026-001 单独硬编码:必须严格匹配 PPT 第 1 页。其余 19 单按模板生成。
+    private static readonly OrderSpec[] Specs =
+    {
+        new(1,  "SO-2026-001", "高压接线盒",   "电气设备厂A", "C001", "KA",    "A产品线", "XX", "华北", "P2", "A", "completed",   "final_assembly_shipping", "final_assembly_shipping", 2, 2880, 1440, 4320),
+        new(2,  "SO-2026-002", "转向机总成",   "电气设备厂B", "C002", "KA",    "B产品线", "YY", "华东", "P3", "B", "completed",   "final_assembly_shipping", "material_procurement",    1, 110, 850,  960),
+        new(3,  "SO-2026-003", "电池箱横梁",   "电气设备厂C", "C003", "KA",    "C产品线", "ZZ", "华南", "P2", "C", "completed",   "final_assembly_shipping", "body_production",         1, 80,  720,  800),
+        new(4,  "SO-2026-004", "高压接线盒",   "电气设备厂D", "C004", "SMB",   "A产品线", "XX", "华北", "P3", "A", "completed",   "final_assembly_shipping", "product_design",          1, 95,  680,  775),
+        new(5,  "SO-2026-005", "电池箱横梁",   "电气设备厂E", "C005", "MICRO", "C产品线", "ZZ", "华南", "P2", "B", "completed",   "final_assembly_shipping", "product_design",          1, 70,  610,  680),
+        new(6,  "SO-2026-006", "驱动总成支架", "电气设备厂A", "C001", "KA",    "A产品线", "XX", "华北", "P2", "C", "completed",   "final_assembly_shipping", "material_procurement",    2, 130, 1240, 2350),
+        new(7,  "SO-2026-007", "减速器壳体",   "电气设备厂B", "C002", "KA",    "B产品线", "YY", "华东", "P2", "A", "completed",   "final_assembly_shipping", "body_production",         2, 145, 1080, 2210),
+        new(8,  "SO-2026-008", "项目定制骨架", "电气设备厂C", "C003", "KA",    "C产品线", "ZZ", "华南", "P1", "B", "completed",   "final_assembly_shipping", "body_production",         2, 165, 1320, 2640),
+        new(9,  "SO-2026-009", "控制器外壳",   "电气设备厂D", "C004", "SMB",   "A产品线", "XX", "华北", "P2", "C", "completed",   "final_assembly_shipping", "order_review",            1, 60,  540,  600),
+        new(10, "SO-2026-010", "试制急单件",   "电气设备厂E", "C005", "MICRO", "C产品线", "ZZ", "华南", "P2", "A", "completed",   "final_assembly_shipping", "product_design",          1, 88,  730,  818),
+        new(11, "SO-2026-011", "冷却模块框架", "电气设备厂A", "C001", "KA",    "B产品线", "YY", "华东", "P2", "B", "completed",   "final_assembly_shipping", "final_assembly_shipping", 1, 50,  410,  460),
+        new(12, "SO-2026-012", "动力舱隔板",   "电气设备厂B", "C002", "KA",    "B产品线", "YY", "华东", "P2", "C", "completed",   "final_assembly_shipping", "material_procurement",    1, 100, 920,  1020),
+        new(13, "SO-2026-013", "模组支架",     "电气设备厂C", "C003", "KA",    "C产品线", "ZZ", "华南", "P2", "A", "completed",   "final_assembly_shipping", "body_production",         1, 92,  860,  952),
+        new(14, "SO-2026-014", "一体化支撑件", "电气设备厂D", "C004", "SMB",   "A产品线", "XX", "华北", "P3", "B", "completed",   "final_assembly_shipping", "material_procurement",    1, 75,  700,  775),
+        new(15, "SO-2026-015", "项目定制骨架", "电气设备厂E", "C005", "MICRO", "C产品线", "ZZ", "华南", "P2", "C", "completed",   "final_assembly_shipping", "order_review",            1, 65,  580,  645),
+        new(16, "SO-2026-016", "控制器外壳",   "电气设备厂A", "C001", "KA",    "A产品线", "XX", "华北", "P2", "A", "in_progress", "order_review",            "order_review",            1, 90,  null, null),
+        new(17, "SO-2026-017", "转向机总成",   "电气设备厂B", "C002", "KA",    "B产品线", "YY", "华东", "P2", "B", "in_progress", "product_design",          "product_design",          1, 105, null, null),
+        new(18, "SO-2026-018", "项目定制骨架", "电气设备厂C", "C003", "KA",    "C产品线", "ZZ", "华南", "P2", "C", "in_progress", "material_procurement",    "material_procurement",    1, 120, null, null),
+        new(19, "SO-2026-019", "驱动总成支架", "电气设备厂D", "C004", "SMB",   "A产品线", "XX", "华北", "P1", "A", "in_progress", "body_production",         "body_production",         2, 140, null, null),
+        new(20, "SO-2026-020", "试制急单件",   "电气设备厂E", "C005", "MICRO", "C产品线", "ZZ", "华南", "P1", "B", "in_progress", "final_assembly_shipping", "final_assembly_shipping", 1, 55,  null, null),
+    };
+
+    private static readonly (string key, string name, decimal kpi)[] StageDef =
+    {
+        ("order_review",            "订单评审", 5m),
+        ("product_design",          "产品设计", 3m),
+        ("material_procurement",    "材料采购", 14m),
+        ("body_production",         "本体生产", 6m),
+        ("final_assembly_shipping", "总装发货", 3m),
+    };
+
+    public static IEnumerable<AdoDemoOrderFlow> BuildOrders()
+    {
+        foreach (var spec in Specs)
+        {
+            var lifecycle = BuildLifecycleValues(spec);
+            var release = new DateTime(2026, 4, 15, 9, 0, 0);
+            var lastEnd = lifecycle.Last().actualEnd;
+            var targetCycle = StageDef.Sum(s => s.kpi);
+            decimal? currentCycleDays = null;
+            decimal? actualCycleDays = null;
+            if (spec.WorkflowStatus == "completed" && lastEnd.HasValue)
+            {
+                var days = decimal.Round((decimal)(lastEnd.Value - release).TotalDays, 1);
+                currentCycleDays = days;
+                actualCycleDays = days;
+            }
+            var currentStage = lifecycle.FirstOrDefault(x => x.key == spec.CurrentNodeKey);
+            var nodeStatus = currentStage.status switch
+            {
+                "red" => "red",
+                "yellow" => "yellow",
+                "pending" => "yellow",
+                _ => "green",
+            };
+
+            yield return new AdoDemoOrderFlow
+            {
+                Id = OrderIdBase + spec.Idx,
+                SoNo = spec.SoNo,
+                ProductName = spec.ProductName,
+                ProductLine = spec.ProductLine,
+                CustomerName = spec.CustomerName,
+                CustomerCode = spec.CustomerCode,
+                CustomerType = spec.CustomerType,
+                Region = spec.Region,
+                Priority = spec.Priority,
+                MaterialCode = spec.MaterialCode,
+                SupplierGroup = spec.SupplierGroup,
+                WorkflowStatus = spec.WorkflowStatus,
+                CurrentNodeKey = spec.CurrentNodeKey,
+                FocusNodeKey = spec.FocusNodeKey,
+                CurrentNodeLabel = NameOf(spec.CurrentNodeKey),
+                NodeStatus = nodeStatus,
+                ReleaseAt = release,
+                TargetCycleDays = targetCycle,
+                CurrentCycleDays = currentCycleDays,
+                ActualCycleDays = actualCycleDays,
+                NodeVarianceDays = currentStage.nodeVar,
+                CumulativeVarianceDays = currentStage.cumVar,
+                ExceptionCount = spec.ExceptionCount,
+                ResponseMinutes = spec.ResponseMinutes,
+                ProcessingMinutes = spec.ProcessingMinutes,
+                TotalLossMinutes = spec.TotalLossMinutes,
+                ExceptionStatus = spec.WorkflowStatus == "completed" ? "已闭环" : "处理中",
+                SortNo = spec.Idx,
+                TenantId = 0,
+                FactoryId = 0,
+                CreatedAt = CreatedAt,
+                UpdatedAt = null,
+                IsDeleted = false,
+            };
+        }
+    }
+
+    public static IEnumerable<AdoDemoOrderFlowStage> BuildStages()
+    {
+        long stageSeq = 0;
+        foreach (var spec in Specs)
+        {
+            var orderId = OrderIdBase + spec.Idx;
+            var lifecycle = BuildLifecycleValues(spec);
+            for (var i = 0; i < lifecycle.Length; i++)
+            {
+                var s = lifecycle[i];
+                yield return new AdoDemoOrderFlowStage
+                {
+                    Id = StageIdBase + (++stageSeq),
+                    OrderId = orderId,
+                    StageKey = s.key,
+                    StageName = s.name,
+                    SortNo = i + 1,
+                    PlannedDays = s.kpi,
+                    ActualDays = s.actualDays,
+                    ExpectedDate = s.expected.Date,
+                    ActualStartAt = s.actualStart,
+                    ActualEndAt = s.actualEnd,
+                    Status = s.status,
+                    NodeVarianceDays = s.nodeVar,
+                    CumulativeVarianceDays = s.cumVar,
+                    TenantId = 0,
+                    FactoryId = 0,
+                    CreatedAt = CreatedAt,
+                    UpdatedAt = null,
+                    IsDeleted = false,
+                };
+            }
+        }
+    }
+
+    public static IEnumerable<AdoDemoOrderFlowSnapshot> BuildSnapshot()
+    {
+        const string stageJson = """
+[
+  {"stageKey":"order_review","stageName":"订单评审","ownerDept":"市场部","kpiAvgDays":5.0,"actualAvgDays":6.2,"onTimeRate":59,"green":62,"yellow":19,"red":24,"pending":0},
+  {"stageKey":"product_design","stageName":"产品设计","ownerDept":"研发中心","kpiAvgDays":3.0,"actualAvgDays":2.6,"onTimeRate":96,"green":99,"yellow":2,"red":2,"pending":2},
+  {"stageKey":"material_procurement","stageName":"材料采购","ownerDept":"供应链部","kpiAvgDays":14.0,"actualAvgDays":15.5,"onTimeRate":85,"green":86,"yellow":1,"red":14,"pending":4},
+  {"stageKey":"body_production","stageName":"本体生产","ownerDept":"生产部","kpiAvgDays":6.0,"actualAvgDays":6.7,"onTimeRate":89,"green":87,"yellow":2,"red":9,"pending":7},
+  {"stageKey":"final_assembly_shipping","stageName":"总装发货","ownerDept":"生产部","kpiAvgDays":3.0,"actualAvgDays":2.9,"onTimeRate":96,"green":92,"yellow":1,"red":3,"pending":9}
+]
+""";
+        yield return new AdoDemoOrderFlowSnapshot
+        {
+            Id = SnapshotIdBase + 1,
+            SnapshotCode = "CHAIN_AGGREGATE_BASELINE",
+            SnapshotLabel = "基线聚合 · 样本 105",
+            TotalOrders = 105,
+            TotalCustomers = 5,
+            AvgResponseMinutes = 91.20m,
+            AvgProcessingMinutes = 1060.50m,
+            AvgLossMinutes = 1472.50m,
+            StageSnapshotsJson = stageJson.Trim(),
+            Remark = "演示基线快照,与明细 20 单不一致是预期",
+            TenantId = 0,
+            FactoryId = 0,
+            CreatedAt = CreatedAt,
+            UpdatedAt = null,
+            IsDeleted = false,
+        };
+    }
+
+    private static string NameOf(string key) => StageDef.First(s => s.key == key).name;
+
+    private record StageRow(
+        string key, string name, decimal kpi, decimal actualDays,
+        DateTime expected, DateTime? actualStart, DateTime? actualEnd,
+        string status, decimal? nodeVar, decimal? cumVar);
+
+    /// <summary>
+    /// SO-2026-001 严格按 PPT;其余 19 单按 spec.FocusNodeKey 套模板。
+    /// </summary>
+    private static StageRow[] BuildLifecycleValues(OrderSpec spec)
+    {
+        if (spec.SoNo == "SO-2026-001") return BuildSo001Lifecycle();
+
+        // 模板:默认每阶段 actualDays=KPI, nodeVar=0, status=green;
+        // 焦点阶段按一份固定偏差替换;total 5 阶段连续日期。
+        var release = new DateTime(2026, 4, 15, 9, 0, 0);
+        var (focusNodeVar, focusActual) = spec.FocusNodeKey switch
+        {
+            "order_review"            => (+2m, 7m),
+            "product_design"          => (+2m, 5m),
+            "material_procurement"    => (+2m, 16m),
+            "body_production"         => (+2m, 8m),
+            "final_assembly_shipping" => (+1m, 4m),
+            _ => (0m, 0m),
+        };
+        var focusStatus = spec.FocusNodeKey switch
+        {
+            "order_review"            => "red",
+            "product_design"          => "red",
+            "material_procurement"    => "yellow",
+            "body_production"         => "yellow",
+            "final_assembly_shipping" => "yellow",
+            _ => "green",
+        };
+
+        var rows = new StageRow[5];
+        var cumVar = 0m;
+        var cursor = release;
+        for (var i = 0; i < 5; i++)
+        {
+            var (key, name, kpi) = StageDef[i];
+            decimal actualDays = kpi;
+            decimal nodeVar = 0;
+            string status = "green";
+            if (key == spec.FocusNodeKey)
+            {
+                actualDays = focusActual;
+                nodeVar = focusNodeVar;
+                status = focusStatus;
+            }
+            cumVar += nodeVar;
+
+            DateTime? actualStart = null;
+            DateTime? actualEnd = null;
+            string currentNodeKey = spec.CurrentNodeKey;
+            int currentNodeIdx = Array.FindIndex(StageDef, s => s.key == currentNodeKey);
+
+            // in_progress 单:当前节点之前 actualEnd 都填;当前节点仅 actualStart;之后阶段都为 null
+            if (spec.WorkflowStatus == "completed" || i < currentNodeIdx)
+            {
+                actualStart = cursor;
+                actualEnd = cursor.AddDays((double)actualDays);
+                cursor = actualEnd.Value;
+            }
+            else if (i == currentNodeIdx)
+            {
+                actualStart = cursor;
+                actualEnd = null;
+                actualDays = 0; // 仍未完成
+                status = "pending";
+                nodeVar = 0;
+                cumVar = (i == 0) ? 0m : rows[i - 1].cumVar ?? 0m;
+            }
+            else
+            {
+                actualStart = null;
+                actualEnd = null;
+                actualDays = 0;
+                status = "pending";
+                nodeVar = 0;
+            }
+
+            DateTime expected = release.AddDays((double)StageDef.Take(i + 1).Sum(s => s.kpi));
+
+            rows[i] = new StageRow(
+                key, name, kpi,
+                actualDays,
+                expected,
+                actualStart,
+                actualEnd,
+                status,
+                status == "pending" ? null : nodeVar,
+                status == "pending" ? null : cumVar);
+        }
+        return rows;
+    }
+
+    /// <summary>SO-2026-001 PPT 第 1 页 hard-coded 真值(不可改)。</summary>
+    private static StageRow[] BuildSo001Lifecycle()
+    {
+        DateTime D(int m, int d, int h, int mi) => new(2026, m, d, h, mi, 0);
+
+        return new[]
+        {
+            new StageRow("order_review",            "订单评审", 5m,  5m, new DateTime(2026, 4, 20), D(4, 15, 9, 0),  D(4, 20, 15, 0), "green",  0m,   0m),
+            new StageRow("product_design",          "产品设计", 3m,  5m, new DateTime(2026, 4, 23), D(4, 20, 15, 0), D(4, 25, 10, 0), "red",   +2m,  +2m),
+            new StageRow("material_procurement",    "材料采购", 14m, 13m, new DateTime(2026, 5, 6),  D(4, 25, 10, 0), D(5, 7,  14, 0), "yellow",-1m,  +1m),
+            new StageRow("body_production",         "本体生产", 6m,  5m, new DateTime(2026, 5, 12), D(5, 7,  14, 0), D(5, 12, 18, 0), "green", -1m,   0m),
+            new StageRow("final_assembly_shipping", "总装发货", 3m,  3m, new DateTime(2026, 5, 15), D(5, 12, 18, 0), D(5, 15, 10, 0), "green",  0m,   0m),
+        };
+    }
+}

+ 135 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Service/S8/Demo/S8DemoOrderFlowService.cs

@@ -0,0 +1,135 @@
+using System.Text.Json;
+using Admin.NET.Plugin.AiDOP.Dto.S8.Demo;
+using Admin.NET.Plugin.AiDOP.Entity.S8.Demo;
+
+namespace Admin.NET.Plugin.AiDOP.Service.S8.Demo;
+
+/// <summary>
+/// ORDER-FLOW-BE-DEMO-DATASET-RESET-1:订单执行档案 demo 只读服务。
+/// 双口径:/orders 返回 20 单明细;/aggregate-snapshot 返回独立 105 单基线快照(非明细汇总)。
+/// </summary>
+public class S8DemoOrderFlowService : ITransient
+{
+    private readonly SqlSugarRepository<AdoDemoOrderFlow> _orderRep;
+    private readonly SqlSugarRepository<AdoDemoOrderFlowStage> _stageRep;
+    private readonly SqlSugarRepository<AdoDemoOrderFlowSnapshot> _snapshotRep;
+
+    public S8DemoOrderFlowService(
+        SqlSugarRepository<AdoDemoOrderFlow> orderRep,
+        SqlSugarRepository<AdoDemoOrderFlowStage> stageRep,
+        SqlSugarRepository<AdoDemoOrderFlowSnapshot> snapshotRep)
+    {
+        _orderRep = orderRep;
+        _stageRep = stageRep;
+        _snapshotRep = snapshotRep;
+    }
+
+    public async Task<List<AdoDemoOrderFlowDto>> ListOrdersAsync()
+    {
+        var orders = await _orderRep.AsQueryable()
+            .OrderBy(x => x.SortNo)
+            .ToListAsync();
+        if (orders.Count == 0) return new List<AdoDemoOrderFlowDto>();
+
+        var orderIds = orders.Select(o => o.Id).ToList();
+        var stages = await _stageRep.AsQueryable()
+            .Where(s => orderIds.Contains(s.OrderId))
+            .OrderBy(s => s.OrderId).OrderBy(s => s.SortNo)
+            .ToListAsync();
+        var stagesByOrder = stages.GroupBy(s => s.OrderId).ToDictionary(g => g.Key, g => g.ToList());
+
+        return orders.Select(o => MapOrder(o, stagesByOrder.TryGetValue(o.Id, out var ls) ? ls : new())).ToList();
+    }
+
+    public async Task<AdoDemoOrderFlowDto?> GetOrderAsync(string soNo)
+    {
+        var order = await _orderRep.AsQueryable().FirstAsync(x => x.SoNo == soNo);
+        if (order == null) return null;
+        var stages = await _stageRep.AsQueryable()
+            .Where(s => s.OrderId == order.Id)
+            .OrderBy(s => s.SortNo)
+            .ToListAsync();
+        return MapOrder(order, stages);
+    }
+
+    public async Task<AdoDemoOrderFlowSnapshotDto?> GetAggregateSnapshotAsync()
+    {
+        var row = await _snapshotRep.AsQueryable()
+            .Where(x => x.SnapshotCode == "CHAIN_AGGREGATE_BASELINE")
+            .FirstAsync();
+        if (row == null) return null;
+        return new AdoDemoOrderFlowSnapshotDto
+        {
+            SnapshotCode = row.SnapshotCode,
+            SnapshotLabel = row.SnapshotLabel,
+            TotalOrders = row.TotalOrders,
+            TotalCustomers = row.TotalCustomers,
+            AvgResponseMinutes = row.AvgResponseMinutes,
+            AvgProcessingMinutes = row.AvgProcessingMinutes,
+            AvgLossMinutes = row.AvgLossMinutes,
+            StageSnapshots = ParseStageSnapshots(row.StageSnapshotsJson),
+            Remark = row.Remark,
+        };
+    }
+
+    private static AdoDemoOrderFlowDto MapOrder(AdoDemoOrderFlow o, List<AdoDemoOrderFlowStage> stages) => new()
+    {
+        SoNo = o.SoNo,
+        ProductName = o.ProductName,
+        ProductLine = o.ProductLine,
+        CustomerName = o.CustomerName,
+        CustomerCode = o.CustomerCode,
+        CustomerType = o.CustomerType,
+        Region = o.Region,
+        Priority = o.Priority,
+        MaterialCode = o.MaterialCode,
+        SupplierGroup = o.SupplierGroup,
+        WorkflowStatus = o.WorkflowStatus,
+        CurrentNodeKey = o.CurrentNodeKey,
+        FocusNodeKey = o.FocusNodeKey,
+        CurrentNodeLabel = o.CurrentNodeLabel,
+        NodeStatus = o.NodeStatus,
+        ReleaseAt = FormatDateTime(o.ReleaseAt),
+        TargetCycleDays = o.TargetCycleDays,
+        CurrentCycleDays = o.CurrentCycleDays,
+        ActualCycleDays = o.ActualCycleDays,
+        NodeVarianceDays = o.NodeVarianceDays,
+        CumulativeVarianceDays = o.CumulativeVarianceDays,
+        ExceptionCount = o.ExceptionCount,
+        ResponseMinutes = o.ResponseMinutes,
+        ProcessingMinutes = o.ProcessingMinutes,
+        TotalLossMinutes = o.TotalLossMinutes,
+        ExceptionStatus = o.ExceptionStatus,
+        Lifecycle = stages.OrderBy(s => s.SortNo).Select(MapStage).ToList(),
+    };
+
+    private static AdoDemoOrderFlowStageDto MapStage(AdoDemoOrderFlowStage s) => new()
+    {
+        StageKey = s.StageKey,
+        StageName = s.StageName,
+        SortNo = s.SortNo,
+        PlannedDays = s.PlannedDays,
+        ActualDays = s.ActualDays,
+        ExpectedDate = s.ExpectedDate.ToString("yyyy-MM-dd"),
+        ActualStartAt = s.ActualStartAt.HasValue ? FormatDateTime(s.ActualStartAt.Value) : null,
+        ActualEndAt = s.ActualEndAt.HasValue ? FormatDateTime(s.ActualEndAt.Value) : null,
+        Status = s.Status,
+        NodeVarianceDays = s.NodeVarianceDays,
+        CumulativeVarianceDays = s.CumulativeVarianceDays,
+    };
+
+    private static string FormatDateTime(DateTime dt) => dt.ToString("yyyy-MM-dd HH:mm");
+
+    private static readonly JsonSerializerOptions JsonOpts = new()
+    {
+        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+        PropertyNameCaseInsensitive = true,
+    };
+
+    private static List<AdoDemoOrderFlowStageSnapshotItem> ParseStageSnapshots(string json)
+    {
+        if (string.IsNullOrWhiteSpace(json)) return new();
+        var items = JsonSerializer.Deserialize<List<AdoDemoOrderFlowStageSnapshotItem>>(json, JsonOpts);
+        return items ?? new();
+    }
+}