Prechádzať zdrojové kódy

fix(s8): align dashboard category keys and seed titles

P0-1: OVERVIEW category card lookup now uses stable categoryKey
mapped from cellCode (driver), not display cellTitle. Fixes the 5
category cards always showing 0 due to backend CategoryOf() outputs
("材料采购" / "本体生产" / "总装发货") never matching seed cellTitle
("物料采购异常" / "主体生产异常" / "总装交付异常") via title equality.

The mapping is centralized in useS8PageConfigDriver as a temporary
front-end table; follow-up: lift it to cell config DTO field.

P0-2: Sync ado_s8_dashboard_cell_config seed cellTitle for S1-S7
module cards across OVERVIEW / DELIVERY / PRODUCTION / SUPPLY,
aligning with sidebar menu business names + S8ModuleCode.Label():
S1 产销协同 / S2 制造协同 / S3 供应协同 / S4 采购执行 /
S5 物料仓储 / S6 生产执行 / S7 成品仓储.

OVERVIEW 5 category card cellTitle (订单评审异常 etc.) untouched —
they are customer-facing display text; data lookup now goes via
categoryKey, not title.

DB UPDATE in dev/test was applied with strict WHERE on
(page_code, cell_code) for the 14 module-card rows.

Verification:
- backend dotnet build: 0 Error
- frontend type check: pre-existing repo errors only; touched files
  produce no error
- API page-config OVERVIEW returns S1-S7 new business names
- API order-grid byCategory returns CategoryOf outputs (材料采购=8,
  本体生产=12) and front-end maps them via cellCode
- Chrome MCP /aidop/s8/monitoring/overview: module card titles
  updated; 物料采购异常=8, 主体生产异常=12 (previously 0)
- console: no error/warn

Out of scope: demo rule 10/11/12 enabled=0 + paused_until drift
(predates this task; not touched).
YY968XX 1 mesiac pred
rodič
commit
37253448eb

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

@@ -464,7 +464,12 @@ const categoryCards = computed<CategoryGridCardData[]>(() => {
 	const sourceMap = new Map(gridData.byCategory.map((item) => [item.category, item]));
 
 	return effectiveCategoryDefs.value.map((category, index) => {
-		const item = sourceMap.get(category.title) ?? { category: category.title, total: 0, avgProcessHours: 0, closeRate: 0 };
+		// S8-DASHBOARD-P0-CATEGORY-SEED-FIX-1:稳定 categoryKey 优先(来自 driver 的 cellCode 映射),
+		// 它对齐后端 S8MonitoringService.CategoryOf() 输出;fallback 路径用 fallback.title 兜底
+		// (CATEGORY_DEFS_FALLBACK 的 title 已与后端 byCategory.category 字面一致)。
+		// cellTitle(展示标题)不再参与数据 lookup。
+		const lookupKey = (category as { categoryKey?: string }).categoryKey ?? category.title;
+		const item = sourceMap.get(lookupKey) ?? { category: lookupKey, total: 0, avgProcessHours: 0, closeRate: 0 };
 		return {
 			key: category.key,
 			title: category.title,
@@ -689,12 +694,17 @@ function compareModuleCode(a: { moduleCode: string }, b: { moduleCode: string })
 }
 
 onMounted(async () => {
-	// 先 await page-config,再用最终的 cards 做 localStorage 初始化(避免 fallback 数据先入存覆盖 page-config)
+	// 先 await page-config + loadData,再用真实数据 cards 重置 state。
+	// 关键:useS8StageConfig 的 state 是 module-level,跨 4 个 monitoring 页共享。
+	// 若另一页面(delivery/supply/production)先以 fallback 空数据初始化过,再回到本页时
+	// initializeFromCards 因 initialized=true 跳过,导致真实 API 数据无法覆盖。
+	// 改用 reset(无条件用最新 cards 重建 state.items),每次进入本页都对齐 API。
 	await loadPageConfig();
-	initializeFromCards(stageCards.value);
-	initializeFromCategories(categoryCards.value);
-	initializeFromDepts(deptCards.value);
-	void loadData();
+	await loadData();
+	resetStageConfigState(stageCards.value);
+	resetCategoryConfigState(categoryCards.value);
+	resetDeptConfigState(deptCards.value);
+	configDrawerVisible.value = false;
 });
 
 watch(

+ 35 - 4
Web/src/views/aidop/s8/monitoring/useS8PageConfigDriver.ts

@@ -120,24 +120,54 @@ export function useS8PageConfigDriver(opts: PageConfigDriverOptions) {
 		return result.length > 0 ? result : fallback;
 	}
 
+	/**
+	 * S8-DASHBOARD-P0-CATEGORY-SEED-FIX-1:
+	 * OVERVIEW 5 张 AGGREGATE 类别卡的稳定 categoryKey 映射。
+	 * 后端 S8MonitoringService.GetOrderGridAsync 的 byCategory 输出由
+	 * `S8MonitoringService.CategoryOf()` 决定,使用业务类别中文名(如 "材料采购")。
+	 * 而 cell config 的 cellTitle 是客户可读展示名(如 "物料采购异常"),
+	 * 二者不应依赖字面对齐 —— 此映射把英文稳定的 cellCode 锚到后端 byCategory key。
+	 *
+	 * 临时方案:mapping 表写在前端集中维护并加注释;
+	 * 后续建议后端 page-config 在 cell DTO 中追加 categoryKey 字段一次性配置化。
+	 */
+	const OVERVIEW_CATEGORY_KEY_BY_CELL_CODE: Readonly<Record<string, string>> = Object.freeze({
+		OVERVIEW_CAT_ORDER_REVIEW:      '订单评审',
+		OVERVIEW_CAT_PRODUCT_DESIGN:    '产品设计',
+		OVERVIEW_CAT_MATERIAL_PURCHASE: '材料采购',
+		OVERVIEW_CAT_BODY_PRODUCTION:   '本体生产',
+		OVERVIEW_CAT_FINAL_ASSEMBLY:    '总装发货',
+	});
+
+	/**
+	 * 暴露稳定 categoryKey 解析。优先用 OVERVIEW_CATEGORY_KEY_BY_CELL_CODE 命中;
+	 * 命中失败回退 null(caller 自决:用 cellTitle 或 fallback.key 兜底)。
+	 */
+	function resolveCategoryKey(cellCode: string): string | null {
+		return OVERVIEW_CATEGORY_KEY_BY_CELL_CODE[cellCode] ?? null;
+	}
+
 	/** 由 page-config 派生 category 卡 def(ANALYSIS + CATEGORY_CARD) */
 	function effectiveCategoryDefs<T extends { key: string; title: string; icon: Component }>(
 		fallback: ReadonlyArray<T>,
-	): ReadonlyArray<T> {
-		if (usingFallback.value || cells.value.length === 0) return fallback;
+	): ReadonlyArray<T & { categoryKey?: string }> {
+		if (usingFallback.value || cells.value.length === 0) return fallback as ReadonlyArray<T & { categoryKey?: string }>;
 		const result = cells.value
 			.filter((c) => c.layoutArea === 'ANALYSIS' && c.displayMode === 'CATEGORY_CARD' && c.enabled)
 			.map((c) => {
 				const key = cellCodeToLocalKey(c.cellCode);
 				const fbItem = fallback.find((f) => f.key === key);
+				const categoryKey = resolveCategoryKey(c.cellCode);
 				return {
 					...(fbItem ?? {}),
 					key,
 					title: c.cellTitle ?? fbItem?.title ?? key,
 					icon: resolveIcon(c.icon, fbItem?.icon),
-				} as unknown as T;
+					// 稳定 categoryKey 用于后端 byCategory 数据 lookup(不依赖展示标题)。
+					...(categoryKey ? { categoryKey } : {}),
+				} as unknown as T & { categoryKey?: string };
 			});
-		return result.length > 0 ? result : fallback;
+		return result.length > 0 ? result : (fallback as ReadonlyArray<T & { categoryKey?: string }>);
 	}
 
 	/** 由 page-config 派生 sidebar compact types(SIDEBAR + CATEGORY_COMPACT) */
@@ -179,5 +209,6 @@ export function useS8PageConfigDriver(opts: PageConfigDriverOptions) {
 		resolveIcon,
 		deriveMetricLabel,
 		normalizeStageTitle,
+		resolveCategoryKey,
 	};
 }

+ 21 - 17
server/Plugins/Admin.NET.Plugin.AiDOP/SeedData/S8DashboardCellConfigSeedData.cs

@@ -19,19 +19,20 @@ public class S8DashboardCellConfigSeedData : ISqlSugarEntitySeedData<Entity.S8.A
 
         // ── OVERVIEW 页面 ──
         // S1-S7 模块卡(7 张,CUSTOM 聚合,layout_area=MODULES, display_mode=STAGE_CARD)
-        var overviewModules = new (string code, string icon)[]
+        // S8-DASHBOARD-P0-CATEGORY-SEED-FIX-1:标题与侧栏菜单业务名 + S8ModuleCode.Label 对齐。
+        var overviewModules = new (string code, string title, string icon)[]
         {
-            ("S1", "Checked"),
-            ("S2", "TrendCharts"),
-            ("S3", "ShoppingBag"),
-            ("S4", "Tools"),
-            ("S5", "DataAnalysis"),
-            ("S6", "Box"),
-            ("S7", "Van"),
+            ("S1", "S1 产销协同", "Checked"),
+            ("S2", "S2 制造协同", "TrendCharts"),
+            ("S3", "S3 供应协同", "ShoppingBag"),
+            ("S4", "S4 采购执行", "Tools"),
+            ("S5", "S5 物料仓储", "DataAnalysis"),
+            ("S6", "S6 生产执行", "Box"),
+            ("S7", "S7 成品仓储", "Van"),
         };
         var sort = 100;
-        foreach (var (m, icon) in overviewModules)
-            list.Add(Custom(seq++, "OVERVIEW", $"OVERVIEW_{m}", $"{m} 模块异常", "OPEN_COUNT", "LAST_24H", sort++, ct,
+        foreach (var (m, title, icon) in overviewModules)
+            list.Add(Custom(seq++, "OVERVIEW", $"OVERVIEW_{m}", title, "OPEN_COUNT", "LAST_24H", sort++, ct,
                 icon: icon, layoutArea: "MODULES", displayMode: "STAGE_CARD"));
 
         // 部门效率聚合
@@ -58,10 +59,11 @@ public class S8DashboardCellConfigSeedData : ISqlSugarEntitySeedData<Entity.S8.A
             icon: null, layoutArea: "ANALYSIS", displayMode: "CUSTOM"));
 
         // ── DELIVERY 页面 ──
+        // S8-DASHBOARD-P0-CATEGORY-SEED-FIX-1:模块卡标题与 S8ModuleCode.Label 对齐。
         sort = 100;
-        list.Add(Custom(seq++, "DELIVERY", "DELIVERY_S1", "S1 订单模块", "OPEN_COUNT", "LAST_24H", sort++, ct,
+        list.Add(Custom(seq++, "DELIVERY", "DELIVERY_S1", "S1 产销协同", "OPEN_COUNT", "LAST_24H", sort++, ct,
             icon: "Checked", layoutArea: "MODULES", displayMode: "STAGE_CARD"));
-        list.Add(Custom(seq++, "DELIVERY", "DELIVERY_S7", "S7 交付模块", "OPEN_COUNT", "LAST_24H", sort++, ct,
+        list.Add(Custom(seq++, "DELIVERY", "DELIVERY_S7", "S7 成品仓储", "OPEN_COUNT", "LAST_24H", sort++, ct,
             icon: "Van", layoutArea: "MODULES", displayMode: "STAGE_CARD"));
 
         // 异常类型卡(3 张:sidebar compact,OPEN_COUNT 总数)
@@ -84,10 +86,11 @@ public class S8DashboardCellConfigSeedData : ISqlSugarEntitySeedData<Entity.S8.A
             icon: null, layoutArea: "ANALYSIS", displayMode: "CUSTOM"));
 
         // ── PRODUCTION 页面 ──
+        // S8-DASHBOARD-P0-CATEGORY-SEED-FIX-1:模块卡标题与 S8ModuleCode.Label 对齐。
         sort = 100;
-        list.Add(Custom(seq++, "PRODUCTION", "PRODUCTION_S2", "S2 排产模块", "OPEN_COUNT", "LAST_24H", sort++, ct,
+        list.Add(Custom(seq++, "PRODUCTION", "PRODUCTION_S2", "S2 制造协同", "OPEN_COUNT", "LAST_24H", sort++, ct,
             icon: "TrendCharts", layoutArea: "MODULES", displayMode: "STAGE_CARD"));
-        list.Add(Custom(seq++, "PRODUCTION", "PRODUCTION_S6", "S6 生产模块", "OPEN_COUNT", "LAST_24H", sort++, ct,
+        list.Add(Custom(seq++, "PRODUCTION", "PRODUCTION_S6", "S6 生产执行", "OPEN_COUNT", "LAST_24H", sort++, ct,
             icon: "Box",         layoutArea: "MODULES", displayMode: "STAGE_CARD"));
 
         list.Add(ExceptionType(seq++, "PRODUCTION", "PRODUCTION_ANOMALY_EQUIPMENT_FAULT", "设备异常", "EQUIP_FAULT",       "OPEN_COUNT", "LAST_24H", sort++, ct,
@@ -108,12 +111,13 @@ public class S8DashboardCellConfigSeedData : ISqlSugarEntitySeedData<Entity.S8.A
             icon: null, layoutArea: "ANALYSIS", displayMode: "CUSTOM"));
 
         // ── SUPPLY 页面 ──
+        // S8-DASHBOARD-P0-CATEGORY-SEED-FIX-1:模块卡标题与 S8ModuleCode.Label 对齐。
         sort = 100;
-        list.Add(Custom(seq++, "SUPPLY", "SUPPLY_S3", "S3 供应模块", "OPEN_COUNT", "LAST_24H", sort++, ct,
+        list.Add(Custom(seq++, "SUPPLY", "SUPPLY_S3", "S3 供应协同", "OPEN_COUNT", "LAST_24H", sort++, ct,
             icon: "ShoppingBag",  layoutArea: "MODULES", displayMode: "STAGE_CARD"));
-        list.Add(Custom(seq++, "SUPPLY", "SUPPLY_S4", "S4 入库模块", "OPEN_COUNT", "LAST_24H", sort++, ct,
+        list.Add(Custom(seq++, "SUPPLY", "SUPPLY_S4", "S4 采购执行", "OPEN_COUNT", "LAST_24H", sort++, ct,
             icon: "Tools",        layoutArea: "MODULES", displayMode: "STAGE_CARD"));
-        list.Add(Custom(seq++, "SUPPLY", "SUPPLY_S5", "S5 发料模块", "OPEN_COUNT", "LAST_24H", sort++, ct,
+        list.Add(Custom(seq++, "SUPPLY", "SUPPLY_S5", "S5 物料仓储", "OPEN_COUNT", "LAST_24H", sort++, ct,
             icon: "DataAnalysis", layoutArea: "MODULES", displayMode: "STAGE_CARD"));
 
         // 供应域 7 类异常:类型卡(sidebar) + 分析卡(analysis)