Prechádzať zdrojové kódy

feat(s8): t3h load order flow lifecycle on overview expand

- domainMapper: add ORDER_FLOW code → OrderNodeKey table + stage/lifecycle mappers; fix t3g cast on currentNodeKey/focusNodeKey
- orderExecution store: add loadDetailFromDomain action with per-order lifecycle cache
- OrderExecutionOrderList: emit expand-order on row open
- SoOrderExecutionDashboardPage: wire onExpandOrder to detail fetch
- bump Web 2.4.126
YY968XX 3 týždňov pred
rodič
commit
d49bdd0b06

+ 1 - 1
Web/package.json

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

+ 23 - 2
Web/src/stores/orderExecution.ts

@@ -27,8 +27,12 @@ import {
 	type S8OrderFlowStageDto,
 } from '/@/views/aidop/s8/api/s8OrderFlowApi';
 // ORDER-FLOW-S8-INTEGRATED-DOMAIN-RESET-1 t3g:正式 API client + mapper,loadFromDomain 旁路旧 demo 走正式端点。
-import { getOrderFlowOrders } from '/@/views/aidop/s8/api/s8OrderFlowDomainApi';
-import { mapDomainOrderListItem } from '/@/views/aidop/s8/monitoring/data/order-execution/domainMapper';
+// t3h:增订单详情按需加载,展开订单时调 getOrderFlowOrder 注入 lifecycle。
+import { getOrderFlowOrder, getOrderFlowOrders } from '/@/views/aidop/s8/api/s8OrderFlowDomainApi';
+import {
+	mapDomainOrderDetailLifecycle,
+	mapDomainOrderListItem,
+} from '/@/views/aidop/s8/monitoring/data/order-execution/domainMapper';
 
 export interface OrderExecutionFilterOption<V = string> {
 	value: V;
@@ -275,6 +279,23 @@ export const useOrderExecutionStore = defineStore('orderExecution', {
 				this.loading = false;
 			}
 		},
+		// t3h:展开订单时按需取详情;lifecycle 已有则跳过,失败不清空 orders。
+		async loadDetailFromDomain(orderCode: string) {
+			if (!orderCode) return;
+			const index = this.orders.findIndex((o) => o.soNo === orderCode);
+			if (index < 0) return;
+			if (this.orders[index].lifecycle.length > 0) return;
+			try {
+				const detail = await getOrderFlowOrder(orderCode);
+				if (!detail) return;
+				const lifecycle = mapDomainOrderDetailLifecycle(detail);
+				const next = this.orders.slice();
+				next[index] = { ...next[index], lifecycle };
+				this.orders = next;
+			} catch (err: unknown) {
+				this.loadError = err instanceof Error ? err.message : String(err);
+			}
+		},
 		setFilters(partial: Partial<OrderExecutionFilters>) {
 			this.filters = { ...this.filters, ...partial };
 		},

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

@@ -52,6 +52,11 @@ function onOpenChain(order: SalesOrderExecution) {
 	router.push('/aidop/s8/monitoring/order-execution/chain');
 }
 
+// t3h:展开订单时按需取详情,把 lifecycle 5 阶段写回该订单。
+function onExpandOrder(code: string) {
+	void store.loadDetailFromDomain(code);
+}
+
 const activeTab = ref<'overview' | 'execution' | 'source-dept' | 'handle-dept'>('overview');
 </script>
 
@@ -114,6 +119,7 @@ const activeTab = ref<'overview' | 'execution' | 'source-dept' | 'handle-dept'>(
 						v-if="!isEmptyResult"
 						:orders="filteredOrders"
 						@open-chain="onOpenChain"
+						@expand-order="onExpandOrder"
 					/>
 					<div v-else class="oe-dashboard__empty">
 						<template v-if="loadError">订单数据未能加载,请点击上方「重试」。</template>

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

@@ -21,6 +21,7 @@ const props = defineProps<Props>();
 
 const emit = defineEmits<{
 	'open-chain': [order: SalesOrderExecution];
+	'expand-order': [orderCode: string];
 }>();
 
 const expandedSoNo = ref<string | null>(null);
@@ -101,6 +102,8 @@ function toggleExpand(soNo: string) {
 	expandedSoNo.value = next;
 	// 切换/折叠订单时退出演示,避免跨订单状态污染
 	if (next !== demoModeSoNo.value) exitDemo();
+	// t3h:展开(next 非 null)时通知父级按需加载该订单的 lifecycle。
+	if (next !== null) emit('expand-order', next);
 }
 
 function onChain(order: SalesOrderExecution) {

+ 45 - 5
Web/src/views/aidop/s8/monitoring/data/order-execution/domainMapper.ts

@@ -1,13 +1,17 @@
 // ORDER-FLOW-S8-INTEGRATED-DOMAIN-RESET-1 t3g:正式 API DTO → 旧 SalesOrderExecution 字段映射。
-// 仅供订单执行档案总览 (SoOrderExecutionDashboardPage) 的最小消费,不含 lifecycle 详情。
-// Chain 页面暂不切换;总览页只读 store.filteredOrders / store.allOrders / kpiAverages / filterOptions / filteredCustomerGroups,
-// 这些 getter 全部基于 SalesOrderExecution 的扁平字段,不进入 lifecycle,所以 lifecycle 留空数组即可。
+// t3h:补 lifecycle mapper + OrderFlowCode 大写到 OrderNodeKey 小写的协议翻译表。
+// Chain 页面暂不切换;总览页展开后五阶段对照表通过 loadDetailFromDomain 注入 lifecycle。
 import { EXECUTION_OBSERVED_AT } from '/@/views/aidop/s8/monitoring/data/order-execution/customers';
 import type {
 	OrderNodeKey,
 	SalesOrderExecution,
+	StageSnapshot,
 } from '/@/views/aidop/s8/monitoring/data/order-execution/types';
-import type { OrderFlowOrderListItem } from '/@/views/aidop/s8/api/s8OrderFlowDomainApi';
+import type {
+	OrderFlowOrderDetail,
+	OrderFlowOrderListItem,
+	OrderFlowStage,
+} from '/@/views/aidop/s8/api/s8OrderFlowDomainApi';
 
 // 'YYYY-MM-DD HH:mm' → 'MM-DD HH:mm';'YYYY-MM-DD' → 'MM-DD'。与旧 store.mapStage 同义,复制以避免跨文件依赖。
 function toMonthDayTime(s: string | null | undefined): string {
@@ -36,8 +40,44 @@ function asExceptionStatus(v: string): SalesOrderExecution['exceptionStatus'] {
 	return v === '处理中' || v === '已闭环' ? v : '待响应';
 }
 
+// ORDER_FLOW 协议翻译表:后端 5 大写 code → 前端 OrderNodeKey 小写 union。
+// 未识别值兜底为 order_review(首阶段),与现网 5 阶段 seed 一致;不抛错以避免阻塞首屏渲染。
+const ORDER_FLOW_CODE_TO_NODE_KEY: Record<string, OrderNodeKey> = {
+	ORDER_REVIEW_PLAN_CALC: 'order_review',
+	PRODUCT_DESIGN: 'product_design',
+	MATERIAL_PURCHASE: 'material_procurement',
+	BODY_PRODUCTION: 'body_production',
+	FINAL_ASSEMBLY_DELIVERY: 'final_assembly_shipping',
+};
+
+export function mapOrderFlowCodeToNodeKey(code: string | null | undefined): OrderNodeKey {
+	return (code && ORDER_FLOW_CODE_TO_NODE_KEY[code]) || 'order_review';
+}
+
+function asStageStatus(v: string): StageSnapshot['status'] {
+	return v === 'green' || v === 'yellow' || v === 'red' || v === 'pending' ? v : 'pending';
+}
+
+export function mapDomainStage(stage: OrderFlowStage): StageSnapshot {
+	return {
+		key: mapOrderFlowCodeToNodeKey(stage.orderFlowCode),
+		name: stage.orderFlowName,
+		targetDate: toMonthDayTime(stage.targetAt),
+		actualReachedAt: stage.actualEndAt ? toMonthDayTime(stage.actualEndAt) : null,
+		actualDays: stage.actualDays,
+		nodeVarianceDays: stage.nodeVarianceDays,
+		cumulativeVarianceDays: stage.cumulativeVarianceDays,
+		status: asStageStatus(stage.status),
+	};
+}
+
+export function mapDomainOrderDetailLifecycle(detail: OrderFlowOrderDetail): StageSnapshot[] {
+	if (!detail || !Array.isArray(detail.lifecycle)) return [];
+	return detail.lifecycle.map(mapDomainStage);
+}
+
 export function mapDomainOrderListItem(dto: OrderFlowOrderListItem): SalesOrderExecution {
-	const currentKey = dto.currentOrderFlowCode as OrderNodeKey;
+	const currentKey = mapOrderFlowCodeToNodeKey(dto.currentOrderFlowCode);
 	return {
 		soNo: dto.orderCode,
 		orderId: dto.orderCode.replace(/^SO/, 'ORD'),