浏览代码

feat(s8): externalize 4-screen exception monitoring UI to ado_s8_dashboard_cell_config (G-09 Phase 1)

- Backend: extend AdoS8DashboardCellConfig with icon/layout_area/display_mode/show_in_sidebar/remark
- Backend: add GET /api/aidop/s8/dashboard/page-config (baseline + tenant/factory override merge)
- Backend: sync SeedData factory methods + Startup InitTables registration
- Frontend: add s8PageConfigApi (X-Silent-Error + validateStatus silent fallback)
- Frontend: add useS8PageConfigDriver composable (cells / stageMeta / categoryDefs / sidebarTypes)
- Frontend: integrate driver into 4 monitoring pages (DELIVERY/PRODUCTION/SUPPLY/OVERVIEW) with hardcoded fallback preserved
- OVERVIEW boundary: stage baseline driven by page-config; display title preserves business-interface-priority historical behavior
YY968XX 2 月之前
父节点
当前提交
fd4ad36633

+ 67 - 0
Web/src/views/aidop/s8/api/s8PageConfigApi.ts

@@ -0,0 +1,67 @@
+import service from '/@/utils/request';
+
+export type S8PageCode = 'OVERVIEW' | 'DELIVERY' | 'PRODUCTION' | 'SUPPLY';
+
+export type S8LayoutArea = 'MODULES' | 'ANALYSIS' | 'SIDEBAR';
+
+export type S8DisplayMode =
+	| 'STAGE_CARD'
+	| 'CATEGORY_CARD'
+	| 'CATEGORY_COMPACT'
+	| 'DEPT_CLUSTER'
+	| 'CUSTOM';
+
+/**
+ * page-config 单卡 DTO(与后端 AdoS8PageConfigCellDto 对齐)。
+ * 注意:后端 DTO 不返回 id / tenantId / factoryId / filterExpression / createdAt / updatedAt / showInSidebar。
+ */
+export interface PageConfigCellDto {
+	cellCode: string;
+	cellTitle: string | null;
+	icon: string | null;
+	layoutArea: S8LayoutArea | string;
+	displayMode: S8DisplayMode | string;
+	sortNo: number;
+	bindingType: 'EXCEPTION_TYPE' | 'AGGREGATE' | 'CUSTOM' | string;
+	exceptionTypeCode: string | null;
+	aggregateScope: string | null;
+	statMetric: 'OPEN_COUNT' | 'FREQUENCY' | 'AVG_DURATION' | 'CLOSE_RATE' | string;
+	timeWindow: 'TODAY' | 'LAST_24H' | 'LAST_7D' | 'LAST_30D' | string;
+	deptGroupBy: 'OWNER' | 'OCCUR' | string;
+	enabled: boolean;
+}
+
+export interface PageConfigDto {
+	pageCode: string;
+	cells: PageConfigCellDto[];
+}
+
+function unwrap<T>(res: { data: T }): T {
+	return res.data;
+}
+
+export const s8PageConfigApi = {
+	/**
+	 * 拉取指定页面的渲染配置。后端只返回 enabled=true 的 cell,按 layoutArea + sortNo + cellCode 排序。
+	 * G-09 一期:前端只传 pageCode,租户/工厂走全局基线。
+	 *
+	 * 静默化(Part D 加固):
+	 * - X-Silent-Error: 绕开 request.ts 拦截器的 ElMessage.error 全局 toast
+	 * - validateStatus: 把 2xx/3xx/4xx/5xx 都当成功状态,不让 axios 在 dev 模式下抛 [Vue warn] 噪声
+	 *   caller 自行判断 cells 是否有效;非 200 时 cells 解析失败 → 走 catch → fallback
+	 */
+	get: (pageCode: S8PageCode): Promise<PageConfigDto> =>
+		service
+			.get<PageConfigDto>('/api/aidop/s8/dashboard/page-config', {
+				params: { pageCode },
+				timeout: 5000,
+				headers: { 'X-Silent-Error': '1' },
+				validateStatus: () => true,
+			} as any)
+			.then((res: { status?: number; data: PageConfigDto }) => {
+				if (typeof res?.status === 'number' && res.status >= 400) {
+					throw new Error(`page-config HTTP ${res.status}`);
+				}
+				return unwrap(res);
+			}),
+};

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

@@ -120,7 +120,7 @@
 </template>
 </template>
 
 
 <script setup lang="ts" name="aidopS8MonitoringDelivery">
 <script setup lang="ts" name="aidopS8MonitoringDelivery">
-import { computed, onMounted, reactive, shallowRef, type CSSProperties } from 'vue';
+import { computed, onMounted, reactive, shallowRef, type CSSProperties, type Component } from 'vue';
 import { Checked, Van } from '@element-plus/icons-vue';
 import { Checked, Van } from '@element-plus/icons-vue';
 import { ElMessage, ElMessageBox } from 'element-plus';
 import { ElMessage, ElMessageBox } from 'element-plus';
 import { deepClone, createPageLayout, LAYOUT_VERSION, type S8LayoutSchema, type GridItem } from './useS8Layout';
 import { deepClone, createPageLayout, LAYOUT_VERSION, type S8LayoutSchema, type GridItem } from './useS8Layout';
@@ -134,6 +134,7 @@ import S8MonitoringStageConfigDrawer from './components/S8MonitoringStageConfigD
 import { useS8UnsavedLayoutEditGuard } from './useS8UnsavedLayoutEditGuard';
 import { useS8UnsavedLayoutEditGuard } from './useS8UnsavedLayoutEditGuard';
 import { useS8StageConfig } from './useS8StageConfig';
 import { useS8StageConfig } from './useS8StageConfig';
 import { useS8CategoryConfig } from './useS8CategoryConfig';
 import { useS8CategoryConfig } from './useS8CategoryConfig';
+import { useS8PageConfigDriver } from './useS8PageConfigDriver';
 
 
 // ─── 布局定义 ─────────────────────────────────────────────────────────────
 // ─── 布局定义 ─────────────────────────────────────────────────────────────
 const DELIVERY_DEMO_LAYOUT: S8LayoutSchema = {
 const DELIVERY_DEMO_LAYOUT: S8LayoutSchema = {
@@ -221,21 +222,57 @@ function onAnalysisLayoutUpdate(analysis: GridItem[]) { draftLayout.analysis.spl
 // ─── 数据 ──────────────────────────────────────────────────────────────────
 // ─── 数据 ──────────────────────────────────────────────────────────────────
 const summary = reactive<S8MonitoringSummary>({ total: 0, red: 0, yellow: 0, green: 0, timeout: 0, byModule: [] });
 const summary = reactive<S8MonitoringSummary>({ total: 0, red: 0, yellow: 0, green: 0, timeout: 0, byModule: [] });
 const moduleData = reactive<{ list: S8ModuleOrderSummary[] }>({ list: [] });
 const moduleData = reactive<{ list: S8ModuleOrderSummary[] }>({ list: [] });
-const anomalyTypes = reactive<DeliveryAnomalyType[]>([
-	{ key: 'order-change',   label: '订单变更', total: 0, avgProcessHours: 0, closeRate: 0 },
-	{ key: 'delivery-delay', label: '交期延迟', total: 0, avgProcessHours: 0, closeRate: 0 },
-	{ key: 'stock-pending',  label: '入库待发', total: 0, avgProcessHours: 0, closeRate: 0 },
-]);
-
-const STAGE_META = [
-	{ code: 'S1', title: '销售评审', icon: Checked, metricLabel: '累计监控单量' },
-	{ code: 'S7', title: '订单交付', icon: Van,     metricLabel: '累计异常数量' },
+
+// ─── G-09 一期 Part D:page-config driver 接入 ─────────────────────────
+// 优先级:DB page-config(远端基线)→ 本机 fallback → localStorage 视觉覆盖
+const pageConfig = useS8PageConfigDriver({ pageCode: 'DELIVERY', pageCodePrefix: 'DELIVERY' });
+
+// fallback 硬编码(不删除,作 page-config 失败时回退)
+const STAGE_META_FALLBACK = [
+	{ code: 'S1', title: '销售评审', icon: Checked as Component, metricLabel: '累计监控单量' },
+	{ code: 'S7', title: '订单交付', icon: Van as Component,     metricLabel: '累计异常数量' },
 ] as const;
 ] as const;
 
 
+const ANOMALY_TYPES_FALLBACK: ReadonlyArray<{ key: string; label: string }> = [
+	{ key: 'order-change',   label: '订单变更' },
+	{ key: 'delivery-delay', label: '交期延迟' },
+	{ key: 'stock-pending',  label: '入库待发' },
+];
+
+const CATEGORY_DEFS_FALLBACK: ReadonlyArray<{ key: string; title: string; icon: Component }> = [
+	{ key: 'order-change',   title: '订单变更', icon: Checked },
+	{ key: 'delivery-delay', title: '交期延迟', icon: Van },
+	{ key: 'stock-pending',  title: '入库待发', icon: Van },
+];
+
+const effectiveStageMeta = computed(() => pageConfig.effectiveStageMeta(STAGE_META_FALLBACK));
+const effectiveCategoryDefs = computed(() => pageConfig.effectiveCategoryDefs(CATEGORY_DEFS_FALLBACK));
+
+const anomalyTypes = reactive<DeliveryAnomalyType[]>(
+	ANOMALY_TYPES_FALLBACK.map((d) => ({ ...d, total: 0, avgProcessHours: 0, closeRate: 0 })),
+);
+
+function rebuildAnomalyTypesFromConfig() {
+	const sidebarTypes = pageConfig.effectiveSidebarTypes(ANOMALY_TYPES_FALLBACK);
+	const next = sidebarTypes.map((t) => ({
+		key: t.key,
+		label: t.label,
+		total: 0,
+		avgProcessHours: 0,
+		closeRate: 0,
+	}));
+	anomalyTypes.splice(0, anomalyTypes.length, ...next);
+}
+
+async function loadPageConfig() {
+	await pageConfig.load();
+	rebuildAnomalyTypesFromConfig();
+}
+
 const moduleMap = computed(() => new Map(moduleData.list.map((m) => [m.moduleCode, m])));
 const moduleMap = computed(() => new Map(moduleData.list.map((m) => [m.moduleCode, m])));
 
 
 const stageCards = computed(() =>
 const stageCards = computed(() =>
-	STAGE_META.map((meta) => {
+	effectiveStageMeta.value.map((meta) => {
 		const m = moduleMap.value.get(meta.code);
 		const m = moduleMap.value.get(meta.code);
 		const total  = Math.max(m?.total ?? 0, 0);
 		const total  = Math.max(m?.total ?? 0, 0);
 		const red    = Math.max(m?.red ?? 0, 0);
 		const red    = Math.max(m?.red ?? 0, 0);
@@ -267,11 +304,7 @@ const stageKeys = computed(() => stageCards.value.map((c) => c.code));
 
 
 const categoryCards = computed<CategoryGridCardData[]>(() => {
 const categoryCards = computed<CategoryGridCardData[]>(() => {
 	const typeMap = new Map(anomalyTypes.map((t) => [t.key, t]));
 	const typeMap = new Map(anomalyTypes.map((t) => [t.key, t]));
-	const raw = [
-		{ key: 'order-change',   title: '订单变更', icon: Checked },
-		{ key: 'delivery-delay', title: '交期延迟', icon: Van },
-		{ key: 'stock-pending',  title: '入库待发', icon: Van },
-	].map((def) => {
+	const raw = effectiveCategoryDefs.value.map((def) => {
 		const t = typeMap.get(def.key);
 		const t = typeMap.get(def.key);
 		return {
 		return {
 			key: def.key,
 			key: def.key,
@@ -327,8 +360,10 @@ async function loadData() {
 		]);
 		]);
 		Object.assign(summary, summaryData);
 		Object.assign(summary, summaryData);
 		moduleData.list = modulesData;
 		moduleData.list = modulesData;
-		typesData.forEach((t, i) => {
-			if (anomalyTypes[i]) Object.assign(anomalyTypes[i], t);
+		// 按 key 合并,不依赖位置(兼容 page-config 重建后的 anomalyTypes 顺序)
+		typesData.forEach((t) => {
+			const target = anomalyTypes.find((a) => a.key === t.key);
+			if (target) Object.assign(target, t);
 		});
 		});
 		loadState.value = 'ok';
 		loadState.value = 'ok';
 	} catch (err) {
 	} catch (err) {
@@ -342,9 +377,13 @@ async function loadData() {
 function resetStageConfig() { resetStageConfigState(stageCards.value); }
 function resetStageConfig() { resetStageConfigState(stageCards.value); }
 function resetCategoryConfig() { resetCategoryConfigState(categoryCards.value); }
 function resetCategoryConfig() { resetCategoryConfigState(categoryCards.value); }
 
 
-onMounted(() => {
+onMounted(async () => {
+	// 1. 先拉 page-config(决定卡片结构)
+	await loadPageConfig();
+	// 2. 用最终的 stageCards / categoryCards 初始化 localStorage 覆盖层
 	initializeFromCards(stageCards.value);
 	initializeFromCards(stageCards.value);
 	initializeFromCategories(categoryCards.value);
 	initializeFromCategories(categoryCards.value);
+	// 3. 拉业务数据(loadData 按 key 合入 anomalyTypes,与 page-config 顺序解耦)
 	void loadData();
 	void loadData();
 });
 });
 </script>
 </script>

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

@@ -184,6 +184,7 @@ import { useS8StageConfig } from './useS8StageConfig';
 import { useS8CategoryConfig } from './useS8CategoryConfig';
 import { useS8CategoryConfig } from './useS8CategoryConfig';
 import { useS9DeptConfig } from './useS9DeptConfig';
 import { useS9DeptConfig } from './useS9DeptConfig';
 import { useS8UnsavedLayoutEditGuard } from './useS8UnsavedLayoutEditGuard';
 import { useS8UnsavedLayoutEditGuard } from './useS8UnsavedLayoutEditGuard';
+import { useS8PageConfigDriver } from './useS8PageConfigDriver';
 
 
 interface StageMeta {
 interface StageMeta {
 	code: string;
 	code: string;
@@ -207,7 +208,7 @@ interface DeptDisplay {
 
 
 const numberFormatter = new Intl.NumberFormat('zh-CN');
 const numberFormatter = new Intl.NumberFormat('zh-CN');
 
 
-const stageMeta: StageMeta[] = [
+const STAGE_META_FALLBACK: ReadonlyArray<StageMeta> = [
 	{ code: 'S1', title: '销售评审', icon: Checked, metricLabel: '累计监控单量', valueMode: 'total' },
 	{ code: 'S1', title: '销售评审', icon: Checked, metricLabel: '累计监控单量', valueMode: 'total' },
 	{ code: 'S2', title: '计划排产', icon: TrendCharts, metricLabel: '异常频率 / 百单', valueMode: 'frequency' },
 	{ code: 'S2', title: '计划排产', icon: TrendCharts, metricLabel: '异常频率 / 百单', valueMode: 'frequency' },
 	{ code: 'S3', title: '物料备料', icon: ShoppingBag, metricLabel: '累计异常数量', valueMode: 'total' },
 	{ code: 'S3', title: '物料备料', icon: ShoppingBag, metricLabel: '累计异常数量', valueMode: 'total' },
@@ -217,6 +218,36 @@ const stageMeta: StageMeta[] = [
 	{ code: 'S7', title: '订单交付', icon: Van, metricLabel: '累计异常数量', valueMode: 'total' },
 	{ code: 'S7', title: '订单交付', icon: Van, metricLabel: '累计异常数量', valueMode: 'total' },
 ];
 ];
 
 
+const CATEGORY_DEFS_FALLBACK: ReadonlyArray<{ key: string; title: string; icon: Component }> = [
+	{ key: 'order-review',      title: '订单评审',     icon: DataAnalysis },
+	{ key: 'product-design',    title: '产品设计',     icon: ShoppingBag },
+	{ key: 'material-purchase', title: '材料采购',     icon: Tools },
+	{ key: 'body-production',   title: '本体生产',     icon: Van },
+	{ key: 'final-assembly',    title: '总装发货',     icon: Van },
+];
+
+// ─── G-09 一期 Part D:page-config driver 接入 ─────────────────────────
+// driver 派生 stageMeta(保留 valueMode 由 fallback 同 code 映射);categoryCards 用 effectiveCategoryDefs;
+// dept 由 API 动态返回,driver 仅暴露 hasDeptCluster 开关(预留,本期保持原行为:始终渲染部门簇)。
+const pageConfig = useS8PageConfigDriver({ pageCode: 'OVERVIEW', pageCodePrefix: 'OVERVIEW' });
+
+const stageMeta = computed<StageMeta[]>(() => {
+	const merged = pageConfig.effectiveStageMeta(STAGE_META_FALLBACK);
+	return merged.map((m) => ({
+		code: m.code,
+		title: m.title,
+		icon: m.icon,
+		metricLabel: m.metricLabel,
+		valueMode: (STAGE_META_FALLBACK.find((f) => f.code === m.code)?.valueMode) ?? 'total',
+	}));
+});
+
+const effectiveCategoryDefs = computed(() => pageConfig.effectiveCategoryDefs(CATEGORY_DEFS_FALLBACK));
+
+async function loadPageConfig() {
+	await pageConfig.load();
+}
+
 // Layout state
 // Layout state
 const { layout: persistedLayout, save, resetToDefault, restoreDemo } = useS8Layout();
 const { layout: persistedLayout, save, resetToDefault, restoreDemo } = useS8Layout();
 const { stageConfigState, initializeFromCards, applyConfig, reset: resetStageConfigState } = useS8StageConfig();
 const { stageConfigState, initializeFromCards, applyConfig, reset: resetStageConfigState } = useS8StageConfig();
@@ -230,7 +261,7 @@ const draftLayout = reactive<S8LayoutSchema>({
 	...deepClone(persistedLayout),
 	...deepClone(persistedLayout),
 	sidebar: deepClone(persistedLayout.sidebar ?? []),
 	sidebar: deepClone(persistedLayout.sidebar ?? []),
 });
 });
-const stageKeys = stageMeta.map((item) => item.code);
+const stageKeys = computed(() => stageMeta.value.map((item) => item.code));
 const selectedStageConfigItem = computed(() => stageConfigState.items[stageConfigState.selectedKey]);
 const selectedStageConfigItem = computed(() => stageConfigState.items[stageConfigState.selectedKey]);
 /**
 /**
  * deptKeys:始终来自实际部门(API 或 fallback),按配置顺序排列。
  * deptKeys:始终来自实际部门(API 或 fallback),按配置顺序排列。
@@ -359,7 +390,7 @@ const moduleSummaryMap = computed(() => {
 });
 });
 
 
 const stageCards = computed(() => {
 const stageCards = computed(() => {
-	return stageMeta.map((meta) => {
+	return stageMeta.value.map((meta) => {
 		return buildStageCard(meta, moduleOrderMap.value.get(meta.code), moduleSummaryMap.value.get(meta.code));
 		return buildStageCard(meta, moduleOrderMap.value.get(meta.code), moduleSummaryMap.value.get(meta.code));
 	});
 	});
 });
 });
@@ -420,22 +451,14 @@ const overallEfficiency = computed(() => {
 });
 });
 
 
 const categoryCards = computed<CategoryGridCardData[]>(() => {
 const categoryCards = computed<CategoryGridCardData[]>(() => {
-	const orderedCategories = [
-		{ key: 'order-review', title: '订单评审' },
-		{ key: 'product-design', title: '产品设计' },
-		{ key: 'material-purchase', title: '材料采购' },
-		{ key: 'body-production', title: '本体生产' },
-		{ key: 'final-assembly', title: '总装发货' },
-	] as const;
-
 	const sourceMap = new Map(gridData.byCategory.map((item) => [item.category, item]));
 	const sourceMap = new Map(gridData.byCategory.map((item) => [item.category, item]));
 
 
-	return orderedCategories.map((category, index) => {
+	return effectiveCategoryDefs.value.map((category, index) => {
 		const item = sourceMap.get(category.title) ?? { category: category.title, total: 0, avgProcessHours: 0, closeRate: 0 };
 		const item = sourceMap.get(category.title) ?? { category: category.title, total: 0, avgProcessHours: 0, closeRate: 0 };
 		return {
 		return {
 			key: category.key,
 			key: category.key,
 			title: category.title,
 			title: category.title,
-			icon: resolveCategoryIcon(category.title, index),
+			icon: category.icon ?? resolveCategoryIcon(category.title, index),
 			totalText: formatInteger(item.total),
 			totalText: formatInteger(item.total),
 			avgHoursText: formatHours(item.avgProcessHours),
 			avgHoursText: formatHours(item.avgProcessHours),
 			closeRateText: formatPercent(item.closeRate, item.closeRate > 0 && item.closeRate < 10 ? 1 : 0),
 			closeRateText: formatPercent(item.closeRate, item.closeRate > 0 && item.closeRate < 10 ? 1 : 0),
@@ -655,7 +678,12 @@ function extractCodeOrder(code: string) {
 	return matched ? Number(matched[0]) : Number.MAX_SAFE_INTEGER;
 	return matched ? Number(matched[0]) : Number.MAX_SAFE_INTEGER;
 }
 }
 
 
-onMounted(() => {
+onMounted(async () => {
+	// 先 await page-config,再用最终的 cards 做 localStorage 初始化(避免 fallback 数据先入存覆盖 page-config)
+	await loadPageConfig();
+	initializeFromCards(stageCards.value);
+	initializeFromCategories(categoryCards.value);
+	initializeFromDepts(deptCards.value);
 	void loadData();
 	void loadData();
 });
 });
 
 
@@ -664,10 +692,9 @@ watch(
 	(cards) => {
 	(cards) => {
 		initializeFromCards(cards);
 		initializeFromCards(cards);
 	},
 	},
-	{ immediate: true }
 );
 );
 
 
-watch(categoryCards, (cats) => { initializeFromCategories(cats); }, { immediate: true });
+watch(categoryCards, (cats) => { initializeFromCategories(cats); });
 watch(deptCards, (depts) => {
 watch(deptCards, (depts) => {
 	initializeFromDepts(depts);
 	initializeFromDepts(depts);
 	// 若当前 selectedKey 不在实际部门中,修正到第一个有效项
 	// 若当前 selectedKey 不在实际部门中,修正到第一个有效项
@@ -675,7 +702,7 @@ watch(deptCards, (depts) => {
 	if (actualNames.length && !actualNames.includes(deptConfigState.selectedKey)) {
 	if (actualNames.length && !actualNames.includes(deptConfigState.selectedKey)) {
 		deptConfigState.selectedKey = actualNames[0];
 		deptConfigState.selectedKey = actualNames[0];
 	}
 	}
-}, { immediate: true });
+});
 </script>
 </script>
 
 
 <style scoped>
 <style scoped>

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

@@ -120,7 +120,7 @@
 </template>
 </template>
 
 
 <script setup lang="ts" name="aidopS8MonitoringProduction">
 <script setup lang="ts" name="aidopS8MonitoringProduction">
-import { computed, onMounted, reactive, shallowRef, type CSSProperties } from 'vue';
+import { computed, onMounted, reactive, shallowRef, type CSSProperties, type Component } from 'vue';
 import { TrendCharts, Box, Tools } from '@element-plus/icons-vue';
 import { TrendCharts, Box, Tools } from '@element-plus/icons-vue';
 import { ElMessage, ElMessageBox } from 'element-plus';
 import { ElMessage, ElMessageBox } from 'element-plus';
 import { deepClone, createPageLayout, LAYOUT_VERSION, type S8LayoutSchema, type GridItem } from './useS8Layout';
 import { deepClone, createPageLayout, LAYOUT_VERSION, type S8LayoutSchema, type GridItem } from './useS8Layout';
@@ -134,6 +134,7 @@ import S8MonitoringStageConfigDrawer from './components/S8MonitoringStageConfigD
 import { useS8UnsavedLayoutEditGuard } from './useS8UnsavedLayoutEditGuard';
 import { useS8UnsavedLayoutEditGuard } from './useS8UnsavedLayoutEditGuard';
 import { useS8StageConfig } from './useS8StageConfig';
 import { useS8StageConfig } from './useS8StageConfig';
 import { useS8CategoryConfig } from './useS8CategoryConfig';
 import { useS8CategoryConfig } from './useS8CategoryConfig';
+import { useS8PageConfigDriver } from './useS8PageConfigDriver';
 
 
 // ─── 布局定义 ─────────────────────────────────────────────────────────────
 // ─── 布局定义 ─────────────────────────────────────────────────────────────
 const PRODUCTION_DEMO_LAYOUT: S8LayoutSchema = {
 const PRODUCTION_DEMO_LAYOUT: S8LayoutSchema = {
@@ -221,21 +222,55 @@ function onAnalysisLayoutUpdate(analysis: GridItem[]) { draftLayout.analysis.spl
 // ─── 数据 ──────────────────────────────────────────────────────────────────
 // ─── 数据 ──────────────────────────────────────────────────────────────────
 const summary = reactive<S8MonitoringSummary>({ total: 0, red: 0, yellow: 0, green: 0, timeout: 0, byModule: [] });
 const summary = reactive<S8MonitoringSummary>({ total: 0, red: 0, yellow: 0, green: 0, timeout: 0, byModule: [] });
 const moduleData = reactive<{ list: S8ModuleOrderSummary[] }>({ list: [] });
 const moduleData = reactive<{ list: S8ModuleOrderSummary[] }>({ list: [] });
-const anomalyTypes = reactive<ProductionAnomalyType[]>([
-	{ key: 'equipment-fault', label: '设备异常', total: 0, avgProcessHours: 0, closeRate: 0 },
-	{ key: 'material-fault',  label: '物料异常', total: 0, avgProcessHours: 0, closeRate: 0 },
-	{ key: 'quality-fault',   label: '质量异常', total: 0, avgProcessHours: 0, closeRate: 0 },
-]);
-
-const STAGE_META = [
-	{ code: 'S2', title: '计划排产', icon: TrendCharts, metricLabel: '异常频率/百单' },
-	{ code: 'S6', title: '总装入库', icon: Box,         metricLabel: '累计异常数量' },
+
+// ─── G-09 一期 Part D:page-config driver 接入 ─────────────────────────
+const pageConfig = useS8PageConfigDriver({ pageCode: 'PRODUCTION', pageCodePrefix: 'PRODUCTION' });
+
+const STAGE_META_FALLBACK = [
+	{ code: 'S2', title: '计划排产', icon: TrendCharts as Component, metricLabel: '异常频率/百单' },
+	{ code: 'S6', title: '总装入库', icon: Box as Component,         metricLabel: '累计异常数量' },
 ] as const;
 ] as const;
 
 
+const ANOMALY_TYPES_FALLBACK: ReadonlyArray<{ key: string; label: string }> = [
+	{ key: 'equipment-fault', label: '设备异常' },
+	{ key: 'material-fault',  label: '物料异常' },
+	{ key: 'quality-fault',   label: '质量异常' },
+];
+
+const CATEGORY_DEFS_FALLBACK: ReadonlyArray<{ key: string; title: string; icon: Component }> = [
+	{ key: 'equipment-fault', title: '设备异常', icon: Tools },
+	{ key: 'material-fault',  title: '物料异常', icon: Box },
+	{ key: 'quality-fault',   title: '质量异常', icon: TrendCharts },
+];
+
+const effectiveStageMeta = computed(() => pageConfig.effectiveStageMeta(STAGE_META_FALLBACK));
+const effectiveCategoryDefs = computed(() => pageConfig.effectiveCategoryDefs(CATEGORY_DEFS_FALLBACK));
+
+const anomalyTypes = reactive<ProductionAnomalyType[]>(
+	ANOMALY_TYPES_FALLBACK.map((d) => ({ ...d, total: 0, avgProcessHours: 0, closeRate: 0 })),
+);
+
+function rebuildAnomalyTypesFromConfig() {
+	const sidebarTypes = pageConfig.effectiveSidebarTypes(ANOMALY_TYPES_FALLBACK);
+	const next = sidebarTypes.map((t) => ({
+		key: t.key,
+		label: t.label,
+		total: 0,
+		avgProcessHours: 0,
+		closeRate: 0,
+	}));
+	anomalyTypes.splice(0, anomalyTypes.length, ...next);
+}
+
+async function loadPageConfig() {
+	await pageConfig.load();
+	rebuildAnomalyTypesFromConfig();
+}
+
 const moduleMap = computed(() => new Map(moduleData.list.map((m) => [m.moduleCode, m])));
 const moduleMap = computed(() => new Map(moduleData.list.map((m) => [m.moduleCode, m])));
 
 
 const stageCards = computed(() =>
 const stageCards = computed(() =>
-	STAGE_META.map((meta) => {
+	effectiveStageMeta.value.map((meta) => {
 		const m = moduleMap.value.get(meta.code);
 		const m = moduleMap.value.get(meta.code);
 		const total  = Math.max(m?.total ?? 0, 0);
 		const total  = Math.max(m?.total ?? 0, 0);
 		const red    = Math.max(m?.red ?? 0, 0);
 		const red    = Math.max(m?.red ?? 0, 0);
@@ -270,11 +305,7 @@ const stageKeys = computed(() => stageCards.value.map((c) => c.code));
 
 
 const categoryCards = computed<CategoryGridCardData[]>(() => {
 const categoryCards = computed<CategoryGridCardData[]>(() => {
 	const typeMap = new Map(anomalyTypes.map((t) => [t.key, t]));
 	const typeMap = new Map(anomalyTypes.map((t) => [t.key, t]));
-	const raw = [
-		{ key: 'equipment-fault', title: '设备异常', icon: Tools },
-		{ key: 'material-fault',  title: '物料异常', icon: Box },
-		{ key: 'quality-fault',   title: '质量异常', icon: TrendCharts },
-	].map((def) => {
+	const raw = effectiveCategoryDefs.value.map((def) => {
 		const t = typeMap.get(def.key);
 		const t = typeMap.get(def.key);
 		return {
 		return {
 			key: def.key,
 			key: def.key,
@@ -331,8 +362,9 @@ async function loadData() {
 		]);
 		]);
 		Object.assign(summary, summaryData);
 		Object.assign(summary, summaryData);
 		moduleData.list = modulesData;
 		moduleData.list = modulesData;
-		typesData.forEach((t, i) => {
-			if (anomalyTypes[i]) Object.assign(anomalyTypes[i], t);
+		typesData.forEach((t) => {
+			const target = anomalyTypes.find((a) => a.key === t.key);
+			if (target) Object.assign(target, t);
 		});
 		});
 		loadState.value = 'ok';
 		loadState.value = 'ok';
 	} catch (err) {
 	} catch (err) {
@@ -346,7 +378,8 @@ async function loadData() {
 function resetStageConfig() { resetStageConfigState(stageCards.value); }
 function resetStageConfig() { resetStageConfigState(stageCards.value); }
 function resetCategoryConfig() { resetCategoryConfigState(categoryCards.value); }
 function resetCategoryConfig() { resetCategoryConfigState(categoryCards.value); }
 
 
-onMounted(() => {
+onMounted(async () => {
+	await loadPageConfig();
 	initializeFromCards(stageCards.value);
 	initializeFromCards(stageCards.value);
 	initializeFromCategories(categoryCards.value);
 	initializeFromCategories(categoryCards.value);
 	void loadData();
 	void loadData();

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

@@ -120,7 +120,7 @@
 </template>
 </template>
 
 
 <script setup lang="ts" name="aidopS8MonitoringSupply">
 <script setup lang="ts" name="aidopS8MonitoringSupply">
-import { computed, onMounted, reactive, shallowRef, type CSSProperties } from 'vue';
+import { computed, onMounted, reactive, shallowRef, type CSSProperties, type Component } from 'vue';
 import { ShoppingBag, Tools, DataAnalysis, Box, Van, Document, Promotion } from '@element-plus/icons-vue';
 import { ShoppingBag, Tools, DataAnalysis, Box, Van, Document, Promotion } from '@element-plus/icons-vue';
 import { ElMessage, ElMessageBox } from 'element-plus';
 import { ElMessage, ElMessageBox } from 'element-plus';
 import { deepClone, createPageLayout, LAYOUT_VERSION, type S8LayoutSchema, type GridItem } from './useS8Layout';
 import { deepClone, createPageLayout, LAYOUT_VERSION, type S8LayoutSchema, type GridItem } from './useS8Layout';
@@ -134,6 +134,7 @@ import S8MonitoringStageConfigDrawer from './components/S8MonitoringStageConfigD
 import { useS8UnsavedLayoutEditGuard } from './useS8UnsavedLayoutEditGuard';
 import { useS8UnsavedLayoutEditGuard } from './useS8UnsavedLayoutEditGuard';
 import { useS8StageConfig } from './useS8StageConfig';
 import { useS8StageConfig } from './useS8StageConfig';
 import { useS8CategoryConfig } from './useS8CategoryConfig';
 import { useS8CategoryConfig } from './useS8CategoryConfig';
+import { useS8PageConfigDriver } from './useS8PageConfigDriver';
 
 
 // ─── 布局定义 ─────────────────────────────────────────────────────────────
 // ─── 布局定义 ─────────────────────────────────────────────────────────────
 // 供应异常 analysis 7 张卡,3+3+1 三行布局(col-num=12,每张 w=4;CategoryGrid minW=3 允许窄屏下缩到 25%)
 // 供应异常 analysis 7 张卡,3+3+1 三行布局(col-num=12,每张 w=4;CategoryGrid minW=3 允许窄屏下缩到 25%)
@@ -227,26 +228,62 @@ function onAnalysisLayoutUpdate(analysis: GridItem[]) { draftLayout.analysis.spl
 // ─── 数据 ──────────────────────────────────────────────────────────────────
 // ─── 数据 ──────────────────────────────────────────────────────────────────
 const summary = reactive<S8MonitoringSummary>({ total: 0, red: 0, yellow: 0, green: 0, timeout: 0, byModule: [] });
 const summary = reactive<S8MonitoringSummary>({ total: 0, red: 0, yellow: 0, green: 0, timeout: 0, byModule: [] });
 const moduleData = reactive<{ list: S8ModuleOrderSummary[] }>({ list: [] });
 const moduleData = reactive<{ list: S8ModuleOrderSummary[] }>({ list: [] });
-const anomalyTypes = reactive<SupplyAnomalyType[]>([
-	{ key: 'supplier-reply-delay', label: '供应商回复交期异常', total: 0, avgProcessHours: 0, closeRate: 0 },
-	{ key: 'supplier-ship-fault',  label: '供应商发货异常',     total: 0, avgProcessHours: 0, closeRate: 0 },
-	{ key: 'warehouse-receipt',    label: '仓库收货异常',       total: 0, avgProcessHours: 0, closeRate: 0 },
-	{ key: 'iqc-inspection',       label: 'IQC检验异常',        total: 0, avgProcessHours: 0, closeRate: 0 },
-	{ key: 'warehouse-shelving',   label: '仓库上架入库异常',   total: 0, avgProcessHours: 0, closeRate: 0 },
-	{ key: 'work-order-prepare',   label: '仓库工单备料异常',   total: 0, avgProcessHours: 0, closeRate: 0 },
-	{ key: 'work-order-issue',     label: '仓库工单发料异常',   total: 0, avgProcessHours: 0, closeRate: 0 },
-]);
-
-const STAGE_META = [
-	{ code: 'S3', title: '物料备料', icon: ShoppingBag, metricLabel: '累计异常数量' },
-	{ code: 'S4', title: '生产制造', icon: Tools,       metricLabel: '平均处理时长' },
-	{ code: 'S5', title: '质量检测', icon: DataAnalysis, metricLabel: '异常关闭率' },
+
+// ─── G-09 一期 Part D:page-config driver 接入 ─────────────────────────
+const pageConfig = useS8PageConfigDriver({ pageCode: 'SUPPLY', pageCodePrefix: 'SUPPLY' });
+
+const STAGE_META_FALLBACK = [
+	{ code: 'S3', title: '物料备料', icon: ShoppingBag as Component,  metricLabel: '累计异常数量' },
+	{ code: 'S4', title: '生产制造', icon: Tools as Component,        metricLabel: '平均处理时长' },
+	{ code: 'S5', title: '质量检测', icon: DataAnalysis as Component, metricLabel: '异常关闭率' },
 ] as const;
 ] as const;
 
 
+const ANOMALY_TYPES_FALLBACK: ReadonlyArray<{ key: string; label: string }> = [
+	{ key: 'supplier-reply-delay', label: '供应商回复交期异常' },
+	{ key: 'supplier-ship-fault',  label: '供应商发货异常' },
+	{ key: 'warehouse-receipt',    label: '仓库收货异常' },
+	{ key: 'iqc-inspection',       label: 'IQC检验异常' },
+	{ key: 'warehouse-shelving',   label: '仓库上架入库异常' },
+	{ key: 'work-order-prepare',   label: '仓库工单备料异常' },
+	{ key: 'work-order-issue',     label: '仓库工单发料异常' },
+];
+
+const SUPPLY_CATEGORY_ICONS: ReadonlyArray<Component> = [ShoppingBag, Van, Box, DataAnalysis, Document, Promotion, Tools];
+
+const CATEGORY_DEFS_FALLBACK: ReadonlyArray<{ key: string; title: string; icon: Component }> = ANOMALY_TYPES_FALLBACK.map((t, idx) => ({
+	key: t.key,
+	title: t.label,
+	icon: SUPPLY_CATEGORY_ICONS[idx % SUPPLY_CATEGORY_ICONS.length],
+}));
+
+const effectiveStageMeta = computed(() => pageConfig.effectiveStageMeta(STAGE_META_FALLBACK));
+const effectiveCategoryDefs = computed(() => pageConfig.effectiveCategoryDefs(CATEGORY_DEFS_FALLBACK));
+
+const anomalyTypes = reactive<SupplyAnomalyType[]>(
+	ANOMALY_TYPES_FALLBACK.map((d) => ({ ...d, total: 0, avgProcessHours: 0, closeRate: 0 })),
+);
+
+function rebuildAnomalyTypesFromConfig() {
+	const sidebarTypes = pageConfig.effectiveSidebarTypes(ANOMALY_TYPES_FALLBACK);
+	const next = sidebarTypes.map((t) => ({
+		key: t.key,
+		label: t.label,
+		total: 0,
+		avgProcessHours: 0,
+		closeRate: 0,
+	}));
+	anomalyTypes.splice(0, anomalyTypes.length, ...next);
+}
+
+async function loadPageConfig() {
+	await pageConfig.load();
+	rebuildAnomalyTypesFromConfig();
+}
+
 const moduleMap = computed(() => new Map(moduleData.list.map((m) => [m.moduleCode, m])));
 const moduleMap = computed(() => new Map(moduleData.list.map((m) => [m.moduleCode, m])));
 
 
 const stageCards = computed(() =>
 const stageCards = computed(() =>
-	STAGE_META.map((meta) => {
+	effectiveStageMeta.value.map((meta) => {
 		const m = moduleMap.value.get(meta.code);
 		const m = moduleMap.value.get(meta.code);
 		const total  = Math.max(m?.total ?? 0, 0);
 		const total  = Math.max(m?.total ?? 0, 0);
 		const red    = Math.max(m?.red ?? 0, 0);
 		const red    = Math.max(m?.red ?? 0, 0);
@@ -285,19 +322,18 @@ const stageCards = computed(() =>
 
 
 const stageKeys = computed(() => stageCards.value.map((c) => c.code));
 const stageKeys = computed(() => stageCards.value.map((c) => c.code));
 
 
-const CATEGORY_ICONS = [ShoppingBag, Van, Box, DataAnalysis, Document, Promotion, Tools] as const;
 const categoryCards = computed<CategoryGridCardData[]>(() => {
 const categoryCards = computed<CategoryGridCardData[]>(() => {
 	const typeMap = new Map(anomalyTypes.map((t) => [t.key, t]));
 	const typeMap = new Map(anomalyTypes.map((t) => [t.key, t]));
-	const raw = anomalyTypes.map((def, idx) => {
-		const t = typeMap.get(def.key)!;
+	const raw = effectiveCategoryDefs.value.map((def) => {
+		const t = typeMap.get(def.key);
 		return {
 		return {
 			key: def.key,
 			key: def.key,
-			title: def.label,
-			icon: CATEGORY_ICONS[idx % CATEGORY_ICONS.length],
-			totalText: formatInteger(t.total),
-			avgHoursText: formatHours(t.avgProcessHours),
-			closeRateText: formatPercent(t.closeRate),
-			tone: resolveTone(t.closeRate),
+			title: def.title,
+			icon: def.icon,
+			totalText: formatInteger(t?.total ?? 0),
+			avgHoursText: formatHours(t?.avgProcessHours ?? 0),
+			closeRateText: formatPercent(t?.closeRate ?? 0),
+			tone: resolveTone(t?.closeRate ?? 0),
 		};
 		};
 	});
 	});
 	return raw.map((c) => applyCategoryConfig(c));
 	return raw.map((c) => applyCategoryConfig(c));
@@ -344,8 +380,9 @@ async function loadData() {
 		]);
 		]);
 		Object.assign(summary, summaryData);
 		Object.assign(summary, summaryData);
 		moduleData.list = modulesData;
 		moduleData.list = modulesData;
-		typesData.forEach((t, i) => {
-			if (anomalyTypes[i]) Object.assign(anomalyTypes[i], t);
+		typesData.forEach((t) => {
+			const target = anomalyTypes.find((a) => a.key === t.key);
+			if (target) Object.assign(target, t);
 		});
 		});
 		loadState.value = 'ok';
 		loadState.value = 'ok';
 	} catch (err) {
 	} catch (err) {
@@ -359,7 +396,8 @@ async function loadData() {
 function resetStageConfig() { resetStageConfigState(stageCards.value); }
 function resetStageConfig() { resetStageConfigState(stageCards.value); }
 function resetCategoryConfig() { resetCategoryConfigState(categoryCards.value); }
 function resetCategoryConfig() { resetCategoryConfigState(categoryCards.value); }
 
 
-onMounted(() => {
+onMounted(async () => {
+	await loadPageConfig();
 	initializeFromCards(stageCards.value);
 	initializeFromCards(stageCards.value);
 	initializeFromCategories(categoryCards.value);
 	initializeFromCategories(categoryCards.value);
 	void loadData();
 	void loadData();

+ 183 - 0
Web/src/views/aidop/s8/monitoring/useS8PageConfigDriver.ts

@@ -0,0 +1,183 @@
+import { computed, shallowRef, type Component } from 'vue';
+import { Document } from '@element-plus/icons-vue';
+import { s8PageConfigApi, type PageConfigCellDto, type S8PageCode } from '../api/s8PageConfigApi';
+import { STAGE_ICON_COMPONENTS } from './useS8StageConfig';
+
+/**
+ * G-09 一期 Part D:四屏共用的 page-config 接入 composable。
+ *
+ * 设计要点:
+ * 1. 加载策略:onMounted 时调用 load();接口失败 / cells 为空 → console.warn + 走 fallback。
+ * 2. 静默化:page-config 是可选配置入口,404/500/timeout 都不弹 ElMessage(依赖 s8PageConfigApi 内部 X-Silent-Error 头)。
+ * 3. fallback 不删:调用方传入硬编码 fallback 数组;page-config 失败时直接返回 fallback。
+ * 4. 强约束:DB enabled=false 或未返回的 cellCode,不会出现在 effective 派生中。
+ * 5. stage title 去重:若后端 cellTitle 已含 code 前缀(如 "S1 订单模块"),自动剥离前缀避免页面渲染 "S1 S1 订单模块"。
+ */
+export interface PageConfigDriverOptions {
+	pageCode: S8PageCode;
+	/** cellCode 派生 localKey 时的页面前缀。OVERVIEW/DELIVERY/PRODUCTION/SUPPLY 与 pageCode 同名。 */
+	pageCodePrefix: string;
+}
+
+export function useS8PageConfigDriver(opts: PageConfigDriverOptions) {
+	const cells = shallowRef<PageConfigCellDto[]>([]);
+	const usingFallback = shallowRef(true);
+
+	async function load(): Promise<void> {
+		try {
+			const resp = await s8PageConfigApi.get(opts.pageCode);
+			if (!resp || !Array.isArray(resp.cells) || resp.cells.length === 0) {
+				console.warn(
+					'S8 dashboard page-config unavailable, fallback to local spec',
+					'(empty cells)',
+				);
+				usingFallback.value = true;
+				cells.value = [];
+				return;
+			}
+			cells.value = resp.cells;
+			usingFallback.value = false;
+		} catch (err) {
+			console.warn(
+				'S8 dashboard page-config unavailable, fallback to local spec',
+				err,
+			);
+			usingFallback.value = true;
+			cells.value = [];
+		}
+	}
+
+	/**
+	 * cellCode → 本地 key
+	 *  - {PREFIX}_S\d+         → 'S\d+'(保留 code 大小写)
+	 *  - {PREFIX}_(ANOMALY|CAT)_XXX_YYY → 'xxx-yyy'
+	 *  - 其他(如 OVERVIEW_BY_DEPT / OVERVIEW_OVERALL_EFFICIENCY) → 去前缀,UPPER_SNAKE → kebab-case
+	 */
+	function cellCodeToLocalKey(cellCode: string): string {
+		const prefix = opts.pageCodePrefix;
+		const stagePattern = new RegExp(`^${prefix}_S\\d+$`);
+		if (stagePattern.test(cellCode)) return cellCode.replace(`${prefix}_`, '');
+		const stripped = cellCode.replace(new RegExp(`^${prefix}_(ANOMALY|CAT)_`), '');
+		if (stripped !== cellCode) return stripped.toLowerCase().replace(/_/g, '-');
+		return cellCode.replace(`${prefix}_`, '').toLowerCase().replace(/_/g, '-');
+	}
+
+	function resolveIcon(iconKey: string | null | undefined, fallback: Component = Document): Component {
+		if (!iconKey) return fallback;
+		return STAGE_ICON_COMPONENTS[iconKey] ?? fallback;
+	}
+
+	function deriveMetricLabel(statMetric: string): string {
+		switch (statMetric) {
+			case 'OPEN_COUNT':   return '累计监控单量';
+			case 'FREQUENCY':    return '累计异常数量';
+			case 'AVG_DURATION': return '平均处理时长';
+			case 'CLOSE_RATE':   return '异常关闭率';
+			default:             return '累计异常数量';
+		}
+	}
+
+	/**
+	 * 修复 SeedData 的 cellTitle 已带 code 前缀的情况(如 "S1 订单模块"),避免页面渲染成 "S1 S1 订单模块"。
+	 * 仅当 cellTitle 严格以 "{code}" + 空白 开头时剥离;其他情况原样返回。
+	 */
+	function normalizeStageTitle(code: string, cellTitle: string | null | undefined, fallback?: string): string {
+		const trimmed = (cellTitle ?? '').trim();
+		if (!trimmed) return fallback ?? code;
+		const re = new RegExp(`^${escapeRegExp(code)}\\s+`);
+		if (re.test(trimmed)) {
+			const stripped = trimmed.replace(re, '').trim();
+			return stripped || (fallback ?? code);
+		}
+		return trimmed;
+	}
+
+	function escapeRegExp(s: string): string {
+		return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+	}
+
+	/**
+	 * 由 page-config 派生 stage 卡定义(MODULES + STAGE_CARD)。
+	 * 若 fallback 中存在同 code 项,沿用其 metricLabel;否则按 statMetric 派生。
+	 */
+	function effectiveStageMeta<T extends { code: string; title: string; icon: Component; metricLabel: string }>(
+		fallback: ReadonlyArray<T>,
+	): ReadonlyArray<T> {
+		if (usingFallback.value || cells.value.length === 0) return fallback;
+		const result = cells.value
+			.filter((c) => c.layoutArea === 'MODULES' && c.displayMode === 'STAGE_CARD' && c.enabled)
+			.map((c) => {
+				const code = cellCodeToLocalKey(c.cellCode);
+				const fbItem = fallback.find((f) => f.code === code);
+				return {
+					...(fbItem ?? {}),
+					code,
+					title: normalizeStageTitle(code, c.cellTitle, fbItem?.title),
+					icon: resolveIcon(c.icon, fbItem?.icon),
+					metricLabel: fbItem?.metricLabel ?? deriveMetricLabel(c.statMetric),
+				} as unknown as T;
+			});
+		return result.length > 0 ? result : fallback;
+	}
+
+	/** 由 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;
+		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);
+				return {
+					...(fbItem ?? {}),
+					key,
+					title: c.cellTitle ?? fbItem?.title ?? key,
+					icon: resolveIcon(c.icon, fbItem?.icon),
+				} as unknown as T;
+			});
+		return result.length > 0 ? result : fallback;
+	}
+
+	/** 由 page-config 派生 sidebar compact types(SIDEBAR + CATEGORY_COMPACT) */
+	function effectiveSidebarTypes<T extends { key: string; label: string }>(
+		fallback: ReadonlyArray<T>,
+	): T[] {
+		if (usingFallback.value || cells.value.length === 0) {
+			return fallback.map((d) => ({ ...d } as T));
+		}
+		const result = cells.value
+			.filter((c) => c.layoutArea === 'SIDEBAR' && c.displayMode === 'CATEGORY_COMPACT' && c.enabled)
+			.map((c) => {
+				const key = cellCodeToLocalKey(c.cellCode);
+				const fbItem = fallback.find((f) => f.key === key);
+				return {
+					...(fbItem ?? {}),
+					key,
+					label: c.cellTitle ?? fbItem?.label ?? key,
+				} as unknown as T;
+			});
+		return result.length > 0 ? result : fallback.map((d) => ({ ...d } as T));
+	}
+
+	/** OVERVIEW 专用:page-config 中是否启用 DEPT_CLUSTER。fallback 时默认 true。 */
+	const hasDeptCluster = computed<boolean>(() => {
+		if (usingFallback.value) return true;
+		return cells.value.some((c) => c.displayMode === 'DEPT_CLUSTER' && c.enabled);
+	});
+
+	return {
+		cells,
+		usingFallback,
+		load,
+		effectiveStageMeta,
+		effectiveCategoryDefs,
+		effectiveSidebarTypes,
+		hasDeptCluster,
+		cellCodeToLocalKey,
+		resolveIcon,
+		deriveMetricLabel,
+		normalizeStageTitle,
+	};
+}

+ 18 - 1
server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S8/AdoS8DashboardController.cs

@@ -10,11 +10,16 @@ public class AdoS8DashboardController : ControllerBase
 {
 {
     private readonly S8DashboardService _svc;
     private readonly S8DashboardService _svc;
     private readonly S8DashboardCellDataService _cellSvc;
     private readonly S8DashboardCellDataService _cellSvc;
+    private readonly S8DashboardCellConfigService _cfgSvc;
 
 
-    public AdoS8DashboardController(S8DashboardService svc, S8DashboardCellDataService cellSvc)
+    public AdoS8DashboardController(
+        S8DashboardService svc,
+        S8DashboardCellDataService cellSvc,
+        S8DashboardCellConfigService cfgSvc)
     {
     {
         _svc = svc;
         _svc = svc;
         _cellSvc = cellSvc;
         _cellSvc = cellSvc;
+        _cfgSvc = cfgSvc;
     }
     }
 
 
     [HttpGet("overview")]
     [HttpGet("overview")]
@@ -56,4 +61,16 @@ public class AdoS8DashboardController : ControllerBase
         try { return Ok(await _cellSvc.GetAsync(q)); }
         try { return Ok(await _cellSvc.GetAsync(q)); }
         catch (S8BizException ex) { return BadRequest(new { message = ex.Message }); }
         catch (S8BizException ex) { return BadRequest(new { message = ex.Message }); }
     }
     }
+
+    /// <summary>
+    /// 按页面编码获取大屏页面结构配置(G-09 一期:四屏启动渲染入口)。
+    /// 只返回 enabled=true 的 cell,按 layout_area + sort_no 排序。
+    /// 不替代 cell-data,不触发 ado_s8_exception 查询。
+    /// </summary>
+    [HttpGet("page-config")]
+    public async Task<IActionResult> PageConfigAsync([FromQuery] AdoS8PageConfigQueryDto q)
+    {
+        try { return Ok(await _cfgSvc.GetPageConfigAsync(q)); }
+        catch (S8BizException ex) { return BadRequest(new { message = ex.Message }); }
+    }
 }
 }

+ 68 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Dto/S8/AdoS8PageConfigDto.cs

@@ -0,0 +1,68 @@
+namespace Admin.NET.Plugin.AiDOP.Dto.S8;
+
+/// <summary>
+/// 大屏页面结构配置出参(GET /api/aidop/s8/dashboard/page-config)。
+/// 面向四大屏启动渲染使用:只返回 enabled=true 的配置,按 layout_area + sort_no 排序。
+/// 不替代 CRUD 接口 /api/aidop/s8/config/dashboard-cells,也不触发 ado_s8_exception 查询。
+/// G-09 一期:DTO 不含 ShowInSidebar、FilterExpression、TenantId/FactoryId/Id、CreatedAt/UpdatedAt。
+/// </summary>
+public class AdoS8PageConfigDto
+{
+    public string PageCode { get; set; } = string.Empty;
+    public List<AdoS8PageConfigCellDto> Cells { get; set; } = new();
+}
+
+public class AdoS8PageConfigCellDto
+{
+    /// <summary>卡片编码(页面内唯一,前端 v-for key 锚点)</summary>
+    public string CellCode { get; set; } = string.Empty;
+
+    /// <summary>卡片标题(可为空,前端按组件默认标题渲染)</summary>
+    public string? CellTitle { get; set; }
+
+    /// <summary>前端图标 key(对应 @element-plus/icons-vue 组件名)</summary>
+    public string? Icon { get; set; }
+
+    /// <summary>主布局区域:MODULES / ANALYSIS / SIDEBAR</summary>
+    public string LayoutArea { get; set; } = "ANALYSIS";
+
+    /// <summary>展示形态:STAGE_CARD / CATEGORY_CARD / CATEGORY_COMPACT / DEPT_CLUSTER / CUSTOM</summary>
+    public string DisplayMode { get; set; } = "CATEGORY_CARD";
+
+    /// <summary>页面内排序</summary>
+    public int SortNo { get; set; }
+
+    /// <summary>绑定类型:EXCEPTION_TYPE / AGGREGATE / CUSTOM</summary>
+    public string BindingType { get; set; } = "CUSTOM";
+
+    /// <summary>binding_type=EXCEPTION_TYPE 时的异常类型编码</summary>
+    public string? ExceptionTypeCode { get; set; }
+
+    /// <summary>binding_type=AGGREGATE 时的聚合范围</summary>
+    public string? AggregateScope { get; set; }
+
+    /// <summary>统计指标:OPEN_COUNT / FREQUENCY / AVG_DURATION / CLOSE_RATE</summary>
+    public string StatMetric { get; set; } = "OPEN_COUNT";
+
+    /// <summary>时间窗:TODAY / LAST_24H / LAST_7D / LAST_30D</summary>
+    public string TimeWindow { get; set; } = "LAST_24H";
+
+    /// <summary>部门聚合维度:OWNER / OCCUR</summary>
+    public string DeptGroupBy { get; set; } = "OWNER";
+
+    /// <summary>是否启用(接口仅返回 enabled=true,此字段为前端知情副本)</summary>
+    public bool Enabled { get; set; }
+}
+
+/// <summary>page-config 查询入参</summary>
+public class AdoS8PageConfigQueryDto
+{
+    /// <summary>页面编码:OVERVIEW / DELIVERY / PRODUCTION / SUPPLY</summary>
+    public string PageCode { get; set; } = string.Empty;
+
+    /// <summary>租户 ID(0 = 仅全局基线;非 0 时会叠加工厂覆盖)</summary>
+    public long TenantId { get; set; } = 0;
+
+    /// <summary>工厂 ID(0 = 仅全局基线;非 0 时会叠加工厂覆盖)</summary>
+    public long FactoryId { get; set; } = 0;
+}

+ 22 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S8/AdoS8DashboardCellConfig.cs

@@ -65,6 +65,28 @@ public class AdoS8DashboardCellConfig
     [SugarColumn(ColumnName = "sort_no")]
     [SugarColumn(ColumnName = "sort_no")]
     public int SortNo { get; set; }
     public int SortNo { get; set; }
 
 
+    // ─── G-09 一期:页面结构配置字段(仅驱动 page-config 前端渲染,不影响查询主链) ───
+
+    /// <summary>前端图标 key(对应 @element-plus/icons-vue 组件名,如 ShoppingBag)</summary>
+    [SugarColumn(ColumnName = "icon", Length = 64, IsNullable = true)]
+    public string? Icon { get; set; }
+
+    /// <summary>主布局区域:MODULES / ANALYSIS / SIDEBAR</summary>
+    [SugarColumn(ColumnName = "layout_area", Length = 16)]
+    public string LayoutArea { get; set; } = "ANALYSIS";
+
+    /// <summary>展示形态:STAGE_CARD / CATEGORY_CARD / CATEGORY_COMPACT / DEPT_CLUSTER / CUSTOM</summary>
+    [SugarColumn(ColumnName = "display_mode", Length = 32)]
+    public string DisplayMode { get; set; } = "CATEGORY_CARD";
+
+    /// <summary>ANALYSIS 卡是否在 sidebar 以 compact 形式镜像展示。G-09 一期仅入表预留,本期不消费、DTO 不返回。</summary>
+    [SugarColumn(ColumnName = "show_in_sidebar", ColumnDataType = "boolean")]
+    public bool ShowInSidebar { get; set; } = false;
+
+    /// <summary>备注</summary>
+    [SugarColumn(ColumnName = "remark", Length = 500, IsNullable = true)]
+    public string? Remark { get; set; }
+
     [SugarColumn(ColumnName = "created_at")]
     [SugarColumn(ColumnName = "created_at")]
     public DateTime CreatedAt { get; set; } = DateTime.Now;
     public DateTime CreatedAt { get; set; } = DateTime.Now;
 
 

+ 125 - 64
server/Plugins/Admin.NET.Plugin.AiDOP/SeedData/S8DashboardCellConfigSeedData.cs

@@ -4,6 +4,8 @@ namespace Admin.NET.Plugin.AiDOP;
 /// S8 大屏卡片配置基线种子(灌入 ado_s8_dashboard_cell_config)。
 /// S8 大屏卡片配置基线种子(灌入 ado_s8_dashboard_cell_config)。
 /// 全局基线:tenant_id=0 / factory_id=0;覆盖 4 个大屏的全部稳定锚点卡片。
 /// 全局基线:tenant_id=0 / factory_id=0;覆盖 4 个大屏的全部稳定锚点卡片。
 /// 依赖 S8ExceptionTypeSeedData:EXCEPTION_TYPE 绑定的 exception_type_code 必须先存在。
 /// 依赖 S8ExceptionTypeSeedData:EXCEPTION_TYPE 绑定的 exception_type_code 必须先存在。
+/// G-09 一期:补齐 UI 配置字段(icon/layout_area/display_mode/show_in_sidebar/remark),
+/// 让新装环境跑 SeedData 即可获得完整渲染配置;现有环境通过手工 INSERT SQL 灌入同样数据。
 /// </summary>
 /// </summary>
 [IncreSeed]
 [IncreSeed]
 public class S8DashboardCellConfigSeedData : ISqlSugarEntitySeedData<Entity.S8.AdoS8DashboardCellConfig>
 public class S8DashboardCellConfigSeedData : ISqlSugarEntitySeedData<Entity.S8.AdoS8DashboardCellConfig>
@@ -15,94 +17,134 @@ public class S8DashboardCellConfigSeedData : ISqlSugarEntitySeedData<Entity.S8.A
         var list = new List<Entity.S8.AdoS8DashboardCellConfig>();
         var list = new List<Entity.S8.AdoS8DashboardCellConfig>();
 
 
         // ── OVERVIEW 页面 ──
         // ── OVERVIEW 页面 ──
-        // S1-S7 模块卡(7 张,CUSTOM 聚合)
-        var overviewModules = new[] { "S1", "S2", "S3", "S4", "S5", "S6", "S7" };
+        // S1-S7 模块卡(7 张,CUSTOM 聚合,layout_area=MODULES, display_mode=STAGE_CARD)
+        var overviewModules = new (string code, string icon)[]
+        {
+            ("S1", "Checked"),
+            ("S2", "TrendCharts"),
+            ("S3", "ShoppingBag"),
+            ("S4", "Tools"),
+            ("S5", "DataAnalysis"),
+            ("S6", "Box"),
+            ("S7", "Van"),
+        };
         var sort = 100;
         var sort = 100;
-        foreach (var m in overviewModules)
-            list.Add(Custom(seq++, "OVERVIEW", $"OVERVIEW_{m}", $"{m} 模块异常", "OPEN_COUNT", "LAST_24H", sort++, ct));
+        foreach (var (m, icon) in overviewModules)
+            list.Add(Custom(seq++, "OVERVIEW", $"OVERVIEW_{m}", $"{m} 模块异常", "OPEN_COUNT", "LAST_24H", sort++, ct,
+                icon: icon, layoutArea: "MODULES", displayMode: "STAGE_CARD"));
 
 
         // 部门效率聚合
         // 部门效率聚合
-        list.Add(Aggregate(seq++, "OVERVIEW", "OVERVIEW_BY_DEPT", "部门异常聚合", "ALL", "OPEN_COUNT", "LAST_24H", "OWNER", sort++, ct));
+        list.Add(Aggregate(seq++, "OVERVIEW", "OVERVIEW_BY_DEPT", "部门异常聚合", "ALL", "OPEN_COUNT", "LAST_24H", "OWNER", sort++, ct,
+            icon: "DataAnalysis", layoutArea: "SIDEBAR", displayMode: "DEPT_CLUSTER"));
 
 
-        // 类别异常卡(5 张,按场景聚合)
-        var overviewCats = new[]
+        // 类别异常卡(5 张,按场景聚合,layout_area=ANALYSIS, display_mode=CATEGORY_CARD
+        var overviewCats = new (string code, string title, string icon)[]
         {
         {
-            ("OVERVIEW_CAT_ORDER_REVIEW",     "订单评审异常"),
-            ("OVERVIEW_CAT_PRODUCT_DESIGN",   "产品设计异常"),
-            ("OVERVIEW_CAT_MATERIAL_PURCHASE","物料采购异常"),
-            ("OVERVIEW_CAT_BODY_PRODUCTION",  "主体生产异常"),
-            ("OVERVIEW_CAT_FINAL_ASSEMBLY",   "总装交付异常"),
+            ("OVERVIEW_CAT_ORDER_REVIEW",     "订单评审异常", "DataAnalysis"),
+            ("OVERVIEW_CAT_PRODUCT_DESIGN",   "产品设计异常", "ShoppingBag"),
+            ("OVERVIEW_CAT_MATERIAL_PURCHASE","物料采购异常", "Tools"),
+            ("OVERVIEW_CAT_BODY_PRODUCTION",  "主体生产异常", "Van"),
+            ("OVERVIEW_CAT_FINAL_ASSEMBLY",   "总装交付异常", "Van"),
         };
         };
-        foreach (var (code, title) in overviewCats)
-            list.Add(Aggregate(seq++, "OVERVIEW", code, title, "ALL", "FREQUENCY", "LAST_7D", "OWNER", sort++, ct));
+        foreach (var (code, title, icon) in overviewCats)
+            list.Add(Aggregate(seq++, "OVERVIEW", code, title, "ALL", "FREQUENCY", "LAST_7D", "OWNER", sort++, ct,
+                icon: icon, layoutArea: "ANALYSIS", displayMode: "CATEGORY_CARD"));
 
 
-        // 整体响应效能 + 状态统计条(CUSTOM)
-        list.Add(Custom(seq++, "OVERVIEW", "OVERVIEW_OVERALL_EFFICIENCY", "整体响应效能", "CLOSE_RATE", "LAST_7D", sort++, ct));
-        list.Add(Custom(seq++, "OVERVIEW", "OVERVIEW_STATUS_BAR",         "状态统计条",   "OPEN_COUNT", "TODAY",   sort++, ct));
+        // 整体响应效能 + 状态统计条(CUSTOM,无 icon)
+        list.Add(Custom(seq++, "OVERVIEW", "OVERVIEW_OVERALL_EFFICIENCY", "整体响应效能", "CLOSE_RATE", "LAST_7D", sort++, ct,
+            icon: null, layoutArea: "SIDEBAR", displayMode: "CUSTOM"));
+        list.Add(Custom(seq++, "OVERVIEW", "OVERVIEW_STATUS_BAR",         "状态统计条",   "OPEN_COUNT", "TODAY",   sort++, ct,
+            icon: null, layoutArea: "ANALYSIS", displayMode: "CUSTOM"));
 
 
         // ── DELIVERY 页面 ──
         // ── DELIVERY 页面 ──
         sort = 100;
         sort = 100;
-        list.Add(Custom(seq++, "DELIVERY", "DELIVERY_S1", "S1 订单模块", "OPEN_COUNT", "LAST_24H", sort++, ct));
-        list.Add(Custom(seq++, "DELIVERY", "DELIVERY_S7", "S7 交付模块", "OPEN_COUNT", "LAST_24H", sort++, ct));
-
-        // 异常类型卡(3 张,统计口径 OPEN_COUNT)
-        list.Add(ExceptionType(seq++, "DELIVERY", "DELIVERY_ANOMALY_ORDER_CHANGE",   "订单变更",     "ORDER_CHANGE",     "OPEN_COUNT", "LAST_24H", sort++, ct));
-        list.Add(ExceptionType(seq++, "DELIVERY", "DELIVERY_ANOMALY_DELIVERY_DELAY", "交期延迟",     "DELIVERY_DELAY",   "OPEN_COUNT", "LAST_24H", sort++, ct));
-        list.Add(ExceptionType(seq++, "DELIVERY", "DELIVERY_ANOMALY_STOCK_PENDING",  "入库待发",     "PENDING_SHIPMENT", "OPEN_COUNT", "LAST_24H", sort++, ct));
-
-        // 异常多维分析卡(3 张:FREQUENCY / AVG_DURATION / CLOSE_RATE)
-        list.Add(ExceptionType(seq++, "DELIVERY", "DELIVERY_CAT_ORDER_CHANGE",   "订单变更多维分析",   "ORDER_CHANGE",     "FREQUENCY",    "LAST_7D", sort++, ct));
-        list.Add(ExceptionType(seq++, "DELIVERY", "DELIVERY_CAT_DELIVERY_DELAY", "交期延迟多维分析",   "DELIVERY_DELAY",   "AVG_DURATION", "LAST_7D", sort++, ct));
-        list.Add(ExceptionType(seq++, "DELIVERY", "DELIVERY_CAT_STOCK_PENDING",  "入库待发多维分析",   "PENDING_SHIPMENT", "CLOSE_RATE",   "LAST_7D", sort++, ct));
-
-        list.Add(Custom(seq++, "DELIVERY", "DELIVERY_STATUS_BAR", "状态统计条", "OPEN_COUNT", "TODAY", 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,
+            icon: "Van", layoutArea: "MODULES", displayMode: "STAGE_CARD"));
+
+        // 异常类型卡(3 张:sidebar compact,OPEN_COUNT 总数)
+        list.Add(ExceptionType(seq++, "DELIVERY", "DELIVERY_ANOMALY_ORDER_CHANGE",   "订单变更",     "ORDER_CHANGE",     "OPEN_COUNT", "LAST_24H", sort++, ct,
+            icon: "Checked", layoutArea: "SIDEBAR", displayMode: "CATEGORY_COMPACT"));
+        list.Add(ExceptionType(seq++, "DELIVERY", "DELIVERY_ANOMALY_DELIVERY_DELAY", "交期延迟",     "DELIVERY_DELAY",   "OPEN_COUNT", "LAST_24H", sort++, ct,
+            icon: "Van",     layoutArea: "SIDEBAR", displayMode: "CATEGORY_COMPACT"));
+        list.Add(ExceptionType(seq++, "DELIVERY", "DELIVERY_ANOMALY_STOCK_PENDING",  "入库待发",     "PENDING_SHIPMENT", "OPEN_COUNT", "LAST_24H", sort++, ct,
+            icon: "Van",     layoutArea: "SIDEBAR", displayMode: "CATEGORY_COMPACT"));
+
+        // 异常多维分析卡(3 张:analysis 大卡,FREQUENCY/AVG_DURATION/CLOSE_RATE)
+        list.Add(ExceptionType(seq++, "DELIVERY", "DELIVERY_CAT_ORDER_CHANGE",   "订单变更多维分析",   "ORDER_CHANGE",     "FREQUENCY",    "LAST_7D", sort++, ct,
+            icon: "Checked", layoutArea: "ANALYSIS", displayMode: "CATEGORY_CARD"));
+        list.Add(ExceptionType(seq++, "DELIVERY", "DELIVERY_CAT_DELIVERY_DELAY", "交期延迟多维分析",   "DELIVERY_DELAY",   "AVG_DURATION", "LAST_7D", sort++, ct,
+            icon: "Van",     layoutArea: "ANALYSIS", displayMode: "CATEGORY_CARD"));
+        list.Add(ExceptionType(seq++, "DELIVERY", "DELIVERY_CAT_STOCK_PENDING",  "入库待发多维分析",   "PENDING_SHIPMENT", "CLOSE_RATE",   "LAST_7D", sort++, ct,
+            icon: "Van",     layoutArea: "ANALYSIS", displayMode: "CATEGORY_CARD"));
+
+        list.Add(Custom(seq++, "DELIVERY", "DELIVERY_STATUS_BAR", "状态统计条", "OPEN_COUNT", "TODAY", sort++, ct,
+            icon: null, layoutArea: "ANALYSIS", displayMode: "CUSTOM"));
 
 
         // ── PRODUCTION 页面 ──
         // ── PRODUCTION 页面 ──
         sort = 100;
         sort = 100;
-        list.Add(Custom(seq++, "PRODUCTION", "PRODUCTION_S2", "S2 排产模块", "OPEN_COUNT", "LAST_24H", sort++, ct));
-        list.Add(Custom(seq++, "PRODUCTION", "PRODUCTION_S6", "S6 生产模块", "OPEN_COUNT", "LAST_24H", sort++, ct));
-
-        list.Add(ExceptionType(seq++, "PRODUCTION", "PRODUCTION_ANOMALY_EQUIPMENT_FAULT", "设备异常", "EQUIP_FAULT",       "OPEN_COUNT", "LAST_24H", sort++, ct));
-        list.Add(ExceptionType(seq++, "PRODUCTION", "PRODUCTION_ANOMALY_MATERIAL_FAULT",  "物料异常", "MATERIAL_SHORTAGE", "OPEN_COUNT", "LAST_24H", sort++, ct));
-        list.Add(ExceptionType(seq++, "PRODUCTION", "PRODUCTION_ANOMALY_QUALITY_FAULT",   "质量异常", "QUALITY_DEFECT",    "OPEN_COUNT", "LAST_24H", sort++, ct));
-
-        list.Add(ExceptionType(seq++, "PRODUCTION", "PRODUCTION_CAT_EQUIPMENT_FAULT", "设备异常多维分析", "EQUIP_FAULT",       "FREQUENCY",    "LAST_7D", sort++, ct));
-        list.Add(ExceptionType(seq++, "PRODUCTION", "PRODUCTION_CAT_MATERIAL_FAULT",  "物料异常多维分析", "MATERIAL_SHORTAGE", "AVG_DURATION", "LAST_7D", sort++, ct));
-        list.Add(ExceptionType(seq++, "PRODUCTION", "PRODUCTION_CAT_QUALITY_FAULT",   "质量异常多维分析", "QUALITY_DEFECT",    "CLOSE_RATE",   "LAST_7D", sort++, ct));
-
-        list.Add(Custom(seq++, "PRODUCTION", "PRODUCTION_STATUS_BAR", "状态统计条", "OPEN_COUNT", "TODAY", 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,
+            icon: "Box",         layoutArea: "MODULES", displayMode: "STAGE_CARD"));
+
+        list.Add(ExceptionType(seq++, "PRODUCTION", "PRODUCTION_ANOMALY_EQUIPMENT_FAULT", "设备异常", "EQUIP_FAULT",       "OPEN_COUNT", "LAST_24H", sort++, ct,
+            icon: "Tools",       layoutArea: "SIDEBAR", displayMode: "CATEGORY_COMPACT"));
+        list.Add(ExceptionType(seq++, "PRODUCTION", "PRODUCTION_ANOMALY_MATERIAL_FAULT",  "物料异常", "MATERIAL_SHORTAGE", "OPEN_COUNT", "LAST_24H", sort++, ct,
+            icon: "Box",         layoutArea: "SIDEBAR", displayMode: "CATEGORY_COMPACT"));
+        list.Add(ExceptionType(seq++, "PRODUCTION", "PRODUCTION_ANOMALY_QUALITY_FAULT",   "质量异常", "QUALITY_DEFECT",    "OPEN_COUNT", "LAST_24H", sort++, ct,
+            icon: "TrendCharts", layoutArea: "SIDEBAR", displayMode: "CATEGORY_COMPACT"));
+
+        list.Add(ExceptionType(seq++, "PRODUCTION", "PRODUCTION_CAT_EQUIPMENT_FAULT", "设备异常多维分析", "EQUIP_FAULT",       "FREQUENCY",    "LAST_7D", sort++, ct,
+            icon: "Tools",       layoutArea: "ANALYSIS", displayMode: "CATEGORY_CARD"));
+        list.Add(ExceptionType(seq++, "PRODUCTION", "PRODUCTION_CAT_MATERIAL_FAULT",  "物料异常多维分析", "MATERIAL_SHORTAGE", "AVG_DURATION", "LAST_7D", sort++, ct,
+            icon: "Box",         layoutArea: "ANALYSIS", displayMode: "CATEGORY_CARD"));
+        list.Add(ExceptionType(seq++, "PRODUCTION", "PRODUCTION_CAT_QUALITY_FAULT",   "质量异常多维分析", "QUALITY_DEFECT",    "CLOSE_RATE",   "LAST_7D", sort++, ct,
+            icon: "TrendCharts", layoutArea: "ANALYSIS", displayMode: "CATEGORY_CARD"));
+
+        list.Add(Custom(seq++, "PRODUCTION", "PRODUCTION_STATUS_BAR", "状态统计条", "OPEN_COUNT", "TODAY", sort++, ct,
+            icon: null, layoutArea: "ANALYSIS", displayMode: "CUSTOM"));
 
 
         // ── SUPPLY 页面 ──
         // ── SUPPLY 页面 ──
         sort = 100;
         sort = 100;
-        list.Add(Custom(seq++, "SUPPLY", "SUPPLY_S3", "S3 供应模块", "OPEN_COUNT", "LAST_24H", sort++, ct));
-        list.Add(Custom(seq++, "SUPPLY", "SUPPLY_S4", "S4 入库模块", "OPEN_COUNT", "LAST_24H", sort++, ct));
-        list.Add(Custom(seq++, "SUPPLY", "SUPPLY_S5", "S5 发料模块", "OPEN_COUNT", "LAST_24H", sort++, ct));
-
-        // 供应域 7 类异常:类型卡 + 分析卡
-        var supplyTypes = new[]
+        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,
+            icon: "Tools",        layoutArea: "MODULES", displayMode: "STAGE_CARD"));
+        list.Add(Custom(seq++, "SUPPLY", "SUPPLY_S5", "S5 发料模块", "OPEN_COUNT", "LAST_24H", sort++, ct,
+            icon: "DataAnalysis", layoutArea: "MODULES", displayMode: "STAGE_CARD"));
+
+        // 供应域 7 类异常:类型卡(sidebar) + 分析卡(analysis)
+        var supplyTypes = new (string typeCode, string anchor, string title, string icon)[]
         {
         {
-            ("SUPPLIER_ETA_ISSUE",  "SUPPLIER_REPLY_DELAY",  "供应商回复交期异常"),
-            ("SUPPLIER_SHIP_ISSUE", "SUPPLIER_SHIP_FAULT",   "供应商发货异常"),
-            ("WH_INBOUND_ISSUE",    "WAREHOUSE_RECEIPT",     "仓库收货异常"),
-            ("IQC_ISSUE",           "IQC_INSPECTION",        "IQC 检验异常"),
-            ("WH_PUTAWAY_ISSUE",    "WAREHOUSE_SHELVING",    "仓库上架入库异常"),
-            ("WH_KIT_ISSUE",        "WORK_ORDER_PREPARE",    "仓库工单备料异常"),
-            ("WH_ISSUE_OUT_ISSUE",  "WORK_ORDER_ISSUE",      "仓库工单发料异常"),
+            ("SUPPLIER_ETA_ISSUE",  "SUPPLIER_REPLY_DELAY",  "供应商回复交期异常", "ShoppingBag"),
+            ("SUPPLIER_SHIP_ISSUE", "SUPPLIER_SHIP_FAULT",   "供应商发货异常",     "Van"),
+            ("WH_INBOUND_ISSUE",    "WAREHOUSE_RECEIPT",     "仓库收货异常",       "Box"),
+            ("IQC_ISSUE",           "IQC_INSPECTION",        "IQC 检验异常",       "DataAnalysis"),
+            ("WH_PUTAWAY_ISSUE",    "WAREHOUSE_SHELVING",    "仓库上架入库异常",   "Document"),
+            ("WH_KIT_ISSUE",        "WORK_ORDER_PREPARE",    "仓库工单备料异常",   "Promotion"),
+            ("WH_ISSUE_OUT_ISSUE",  "WORK_ORDER_ISSUE",      "仓库工单发料异常",   "Tools"),
         };
         };
-        foreach (var (typeCode, anchor, title) in supplyTypes)
-            list.Add(ExceptionType(seq++, "SUPPLY", $"SUPPLY_ANOMALY_{anchor}", title, typeCode, "OPEN_COUNT", "LAST_24H", sort++, ct));
+        foreach (var (typeCode, anchor, title, icon) in supplyTypes)
+            list.Add(ExceptionType(seq++, "SUPPLY", $"SUPPLY_ANOMALY_{anchor}", title, typeCode, "OPEN_COUNT", "LAST_24H", sort++, ct,
+                icon: icon, layoutArea: "SIDEBAR", displayMode: "CATEGORY_COMPACT"));
 
 
-        foreach (var (typeCode, anchor, title) in supplyTypes)
-            list.Add(ExceptionType(seq++, "SUPPLY", $"SUPPLY_CAT_{anchor}", $"{title}多维分析", typeCode, "FREQUENCY", "LAST_7D", sort++, ct));
+        foreach (var (typeCode, anchor, title, icon) in supplyTypes)
+            list.Add(ExceptionType(seq++, "SUPPLY", $"SUPPLY_CAT_{anchor}", $"{title}多维分析", typeCode, "FREQUENCY", "LAST_7D", sort++, ct,
+                icon: icon, layoutArea: "ANALYSIS", displayMode: "CATEGORY_CARD"));
 
 
-        list.Add(Custom(seq++, "SUPPLY", "SUPPLY_STATUS_BAR", "状态统计条", "OPEN_COUNT", "TODAY", sort++, ct));
+        list.Add(Custom(seq++, "SUPPLY", "SUPPLY_STATUS_BAR", "状态统计条", "OPEN_COUNT", "TODAY", sort++, ct,
+            icon: null, layoutArea: "ANALYSIS", displayMode: "CUSTOM"));
 
 
         return list;
         return list;
     }
     }
 
 
     private static Entity.S8.AdoS8DashboardCellConfig Custom(
     private static Entity.S8.AdoS8DashboardCellConfig Custom(
         long id, string pageCode, string cellCode, string cellTitle,
         long id, string pageCode, string cellCode, string cellTitle,
-        string statMetric, string timeWindow, int sortNo, DateTime ct) =>
+        string statMetric, string timeWindow, int sortNo, DateTime ct,
+        string? icon = null, string layoutArea = "ANALYSIS", string displayMode = "CATEGORY_CARD",
+        bool showInSidebar = false, string? remark = null) =>
         new()
         new()
         {
         {
             Id = id,
             Id = id,
@@ -120,12 +162,19 @@ public class S8DashboardCellConfigSeedData : ISqlSugarEntitySeedData<Entity.S8.A
             DeptGroupBy = "OWNER",
             DeptGroupBy = "OWNER",
             Enabled = true,
             Enabled = true,
             SortNo = sortNo,
             SortNo = sortNo,
+            Icon = icon,
+            LayoutArea = layoutArea,
+            DisplayMode = displayMode,
+            ShowInSidebar = showInSidebar,
+            Remark = remark,
             CreatedAt = ct,
             CreatedAt = ct,
         };
         };
 
 
     private static Entity.S8.AdoS8DashboardCellConfig ExceptionType(
     private static Entity.S8.AdoS8DashboardCellConfig ExceptionType(
         long id, string pageCode, string cellCode, string cellTitle,
         long id, string pageCode, string cellCode, string cellTitle,
-        string exceptionTypeCode, string statMetric, string timeWindow, int sortNo, DateTime ct) =>
+        string exceptionTypeCode, string statMetric, string timeWindow, int sortNo, DateTime ct,
+        string? icon = null, string layoutArea = "ANALYSIS", string displayMode = "CATEGORY_CARD",
+        bool showInSidebar = false, string? remark = null) =>
         new()
         new()
         {
         {
             Id = id,
             Id = id,
@@ -143,13 +192,20 @@ public class S8DashboardCellConfigSeedData : ISqlSugarEntitySeedData<Entity.S8.A
             DeptGroupBy = "OWNER",
             DeptGroupBy = "OWNER",
             Enabled = true,
             Enabled = true,
             SortNo = sortNo,
             SortNo = sortNo,
+            Icon = icon,
+            LayoutArea = layoutArea,
+            DisplayMode = displayMode,
+            ShowInSidebar = showInSidebar,
+            Remark = remark,
             CreatedAt = ct,
             CreatedAt = ct,
         };
         };
 
 
     private static Entity.S8.AdoS8DashboardCellConfig Aggregate(
     private static Entity.S8.AdoS8DashboardCellConfig Aggregate(
         long id, string pageCode, string cellCode, string cellTitle,
         long id, string pageCode, string cellCode, string cellTitle,
         string aggregateScope, string statMetric, string timeWindow, string deptGroupBy,
         string aggregateScope, string statMetric, string timeWindow, string deptGroupBy,
-        int sortNo, DateTime ct) =>
+        int sortNo, DateTime ct,
+        string? icon = null, string layoutArea = "ANALYSIS", string displayMode = "CATEGORY_CARD",
+        bool showInSidebar = false, string? remark = null) =>
         new()
         new()
         {
         {
             Id = id,
             Id = id,
@@ -167,6 +223,11 @@ public class S8DashboardCellConfigSeedData : ISqlSugarEntitySeedData<Entity.S8.A
             DeptGroupBy = deptGroupBy,
             DeptGroupBy = deptGroupBy,
             Enabled = true,
             Enabled = true,
             SortNo = sortNo,
             SortNo = sortNo,
+            Icon = icon,
+            LayoutArea = layoutArea,
+            DisplayMode = displayMode,
+            ShowInSidebar = showInSidebar,
+            Remark = remark,
             CreatedAt = ct,
             CreatedAt = ct,
         };
         };
 }
 }

+ 76 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Service/S8/S8DashboardCellConfigService.cs

@@ -1,3 +1,4 @@
+using Admin.NET.Plugin.AiDOP.Dto.S8;
 using Admin.NET.Plugin.AiDOP.Entity.S8;
 using Admin.NET.Plugin.AiDOP.Entity.S8;
 
 
 namespace Admin.NET.Plugin.AiDOP.Service.S8;
 namespace Admin.NET.Plugin.AiDOP.Service.S8;
@@ -61,6 +62,81 @@ public class S8DashboardCellConfigService : ITransient
 
 
     public async Task DeleteAsync(long id) => await _rep.DeleteByIdAsync(id);
     public async Task DeleteAsync(long id) => await _rep.DeleteByIdAsync(id);
 
 
+    /// <summary>
+    /// 获取指定页面的渲染配置(G-09 一期)。
+    /// 合并规则:全局基线 (0/0) + 工厂覆盖 (tenantId/factoryId),同 cell_code 工厂优先;
+    /// 过滤规则:合并后再过滤 enabled=true(禁止读取 override 时提前过滤 enabled);
+    /// 排序规则:layout_area(MODULES→ANALYSIS→SIDEBAR)→ sort_no → cell_code。
+    /// 本接口只读 ado_s8_dashboard_cell_config,不触发 ado_s8_exception 查询。
+    /// </summary>
+    public async Task<AdoS8PageConfigDto> GetPageConfigAsync(AdoS8PageConfigQueryDto q)
+    {
+        // 1. pageCode 严格校验(大写精确匹配,非法返回 400)
+        if (string.IsNullOrWhiteSpace(q.PageCode))
+            throw new S8BizException("pageCode 不能为空");
+        var allowed = new[] { "OVERVIEW", "DELIVERY", "PRODUCTION", "SUPPLY" };
+        if (!allowed.Contains(q.PageCode))
+            throw new S8BizException($"Invalid pageCode: {q.PageCode}");
+
+        // 2. 取全局基线(不过滤 enabled)
+        var baseline = await _rep.AsQueryable()
+            .Where(x => x.TenantId == 0 && x.FactoryId == 0 && x.PageCode == q.PageCode)
+            .ToListAsync();
+
+        // 3. 取工厂覆盖(若有;同样不过滤 enabled,否则工厂显式关闭的卡会被基线重新激活)
+        var overrides = (q.TenantId != 0 || q.FactoryId != 0)
+            ? await _rep.AsQueryable()
+                .Where(x => x.TenantId == q.TenantId && x.FactoryId == q.FactoryId
+                         && x.PageCode == q.PageCode)
+                .ToListAsync()
+            : new List<AdoS8DashboardCellConfig>();
+
+        // 4. 按 cell_code 合并,工厂覆盖优先
+        var merged = new Dictionary<string, AdoS8DashboardCellConfig>();
+        foreach (var cfg in baseline) merged[cfg.CellCode] = cfg;
+        foreach (var cfg in overrides) merged[cfg.CellCode] = cfg;
+
+        // 5. 合并后再过滤 enabled=true,然后按 layout_area → sort_no → cell_code 排序
+        var ordered = merged.Values
+            .Where(c => c.Enabled)
+            .OrderBy(c => LayoutAreaOrder(c.LayoutArea))
+            .ThenBy(c => c.SortNo)
+            .ThenBy(c => c.CellCode)
+            .ToList();
+
+        return new AdoS8PageConfigDto
+        {
+            PageCode = q.PageCode,
+            Cells = ordered.Select(ToCellDto).ToList(),
+        };
+    }
+
+    private static int LayoutAreaOrder(string? area) => (area ?? string.Empty).ToUpperInvariant() switch
+    {
+        "MODULES" => 1,
+        "ANALYSIS" => 2,
+        "SIDEBAR" => 3,
+        _ => 99,
+    };
+
+    private static AdoS8PageConfigCellDto ToCellDto(AdoS8DashboardCellConfig c) => new()
+    {
+        CellCode = c.CellCode,
+        CellTitle = c.CellTitle,
+        Icon = c.Icon,
+        LayoutArea = string.IsNullOrWhiteSpace(c.LayoutArea) ? "ANALYSIS" : c.LayoutArea,
+        DisplayMode = string.IsNullOrWhiteSpace(c.DisplayMode) ? "CATEGORY_CARD" : c.DisplayMode,
+        SortNo = c.SortNo,
+        BindingType = c.BindingType,
+        ExceptionTypeCode = c.ExceptionTypeCode,
+        AggregateScope = c.AggregateScope,
+        StatMetric = c.StatMetric,
+        TimeWindow = c.TimeWindow,
+        DeptGroupBy = c.DeptGroupBy,
+        Enabled = c.Enabled,
+        // 注意:不映射 ShowInSidebar / FilterExpression / TenantId / FactoryId / Id / CreatedAt / UpdatedAt
+    };
+
     private static void ValidateAndNormalize(AdoS8DashboardCellConfig body)
     private static void ValidateAndNormalize(AdoS8DashboardCellConfig body)
     {
     {
         if (string.IsNullOrWhiteSpace(body.PageCode) || string.IsNullOrWhiteSpace(body.CellCode))
         if (string.IsNullOrWhiteSpace(body.PageCode) || string.IsNullOrWhiteSpace(body.CellCode))

+ 1 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Startup.cs

@@ -117,6 +117,7 @@ public class Startup : AppStartup
                 typeof(AdoS8WatchRule),
                 typeof(AdoS8WatchRule),
                 typeof(AdoS8RolePermissionConfig),
                 typeof(AdoS8RolePermissionConfig),
                 typeof(AdoS8NotificationLog),
                 typeof(AdoS8NotificationLog),
+                typeof(AdoS8DashboardCellConfig),
                 typeof(ContractReview),
                 typeof(ContractReview),
                 typeof(ContractReviewFlow),
                 typeof(ContractReviewFlow),
                 typeof(ProductDesign),
                 typeof(ProductDesign),