Просмотр исходного кода

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 месяцев назад
Родитель
Сommit
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>
 
 <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 { ElMessage, ElMessageBox } from 'element-plus';
 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 { useS8StageConfig } from './useS8StageConfig';
 import { useS8CategoryConfig } from './useS8CategoryConfig';
+import { useS8PageConfigDriver } from './useS8PageConfigDriver';
 
 // ─── 布局定义 ─────────────────────────────────────────────────────────────
 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 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;
 
+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 stageCards = computed(() =>
-	STAGE_META.map((meta) => {
+	effectiveStageMeta.value.map((meta) => {
 		const m = moduleMap.value.get(meta.code);
 		const total  = Math.max(m?.total ?? 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 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);
 		return {
 			key: def.key,
@@ -327,8 +360,10 @@ async function loadData() {
 		]);
 		Object.assign(summary, summaryData);
 		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';
 	} catch (err) {
@@ -342,9 +377,13 @@ async function loadData() {
 function resetStageConfig() { resetStageConfigState(stageCards.value); }
 function resetCategoryConfig() { resetCategoryConfigState(categoryCards.value); }
 
-onMounted(() => {
+onMounted(async () => {
+	// 1. 先拉 page-config(决定卡片结构)
+	await loadPageConfig();
+	// 2. 用最终的 stageCards / categoryCards 初始化 localStorage 覆盖层
 	initializeFromCards(stageCards.value);
 	initializeFromCategories(categoryCards.value);
+	// 3. 拉业务数据(loadData 按 key 合入 anomalyTypes,与 page-config 顺序解耦)
 	void loadData();
 });
 </script>

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

@@ -184,6 +184,7 @@ import { useS8StageConfig } from './useS8StageConfig';
 import { useS8CategoryConfig } from './useS8CategoryConfig';
 import { useS9DeptConfig } from './useS9DeptConfig';
 import { useS8UnsavedLayoutEditGuard } from './useS8UnsavedLayoutEditGuard';
+import { useS8PageConfigDriver } from './useS8PageConfigDriver';
 
 interface StageMeta {
 	code: string;
@@ -207,7 +208,7 @@ interface DeptDisplay {
 
 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: 'S2', title: '计划排产', icon: TrendCharts, metricLabel: '异常频率 / 百单', valueMode: 'frequency' },
 	{ code: 'S3', title: '物料备料', icon: ShoppingBag, metricLabel: '累计异常数量', valueMode: 'total' },
@@ -217,6 +218,36 @@ const stageMeta: StageMeta[] = [
 	{ 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
 const { layout: persistedLayout, save, resetToDefault, restoreDemo } = useS8Layout();
 const { stageConfigState, initializeFromCards, applyConfig, reset: resetStageConfigState } = useS8StageConfig();
@@ -230,7 +261,7 @@ const draftLayout = reactive<S8LayoutSchema>({
 	...deepClone(persistedLayout),
 	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]);
 /**
  * deptKeys:始终来自实际部门(API 或 fallback),按配置顺序排列。
@@ -359,7 +390,7 @@ const moduleSummaryMap = 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));
 	});
 });
@@ -420,22 +451,14 @@ const overallEfficiency = computed(() => {
 });
 
 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]));
 
-	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 };
 		return {
 			key: category.key,
 			title: category.title,
-			icon: resolveCategoryIcon(category.title, index),
+			icon: category.icon ?? resolveCategoryIcon(category.title, index),
 			totalText: formatInteger(item.total),
 			avgHoursText: formatHours(item.avgProcessHours),
 			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;
 }
 
-onMounted(() => {
+onMounted(async () => {
+	// 先 await page-config,再用最终的 cards 做 localStorage 初始化(避免 fallback 数据先入存覆盖 page-config)
+	await loadPageConfig();
+	initializeFromCards(stageCards.value);
+	initializeFromCategories(categoryCards.value);
+	initializeFromDepts(deptCards.value);
 	void loadData();
 });
 
@@ -664,10 +692,9 @@ watch(
 	(cards) => {
 		initializeFromCards(cards);
 	},
-	{ immediate: true }
 );
 
-watch(categoryCards, (cats) => { initializeFromCategories(cats); }, { immediate: true });
+watch(categoryCards, (cats) => { initializeFromCategories(cats); });
 watch(deptCards, (depts) => {
 	initializeFromDepts(depts);
 	// 若当前 selectedKey 不在实际部门中,修正到第一个有效项
@@ -675,7 +702,7 @@ watch(deptCards, (depts) => {
 	if (actualNames.length && !actualNames.includes(deptConfigState.selectedKey)) {
 		deptConfigState.selectedKey = actualNames[0];
 	}
-}, { immediate: true });
+});
 </script>
 
 <style scoped>

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

@@ -120,7 +120,7 @@
 </template>
 
 <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 { ElMessage, ElMessageBox } from 'element-plus';
 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 { useS8StageConfig } from './useS8StageConfig';
 import { useS8CategoryConfig } from './useS8CategoryConfig';
+import { useS8PageConfigDriver } from './useS8PageConfigDriver';
 
 // ─── 布局定义 ─────────────────────────────────────────────────────────────
 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 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;
 
+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 stageCards = computed(() =>
-	STAGE_META.map((meta) => {
+	effectiveStageMeta.value.map((meta) => {
 		const m = moduleMap.value.get(meta.code);
 		const total  = Math.max(m?.total ?? 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 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);
 		return {
 			key: def.key,
@@ -331,8 +362,9 @@ async function loadData() {
 		]);
 		Object.assign(summary, summaryData);
 		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';
 	} catch (err) {
@@ -346,7 +378,8 @@ async function loadData() {
 function resetStageConfig() { resetStageConfigState(stageCards.value); }
 function resetCategoryConfig() { resetCategoryConfigState(categoryCards.value); }
 
-onMounted(() => {
+onMounted(async () => {
+	await loadPageConfig();
 	initializeFromCards(stageCards.value);
 	initializeFromCategories(categoryCards.value);
 	void loadData();

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

@@ -120,7 +120,7 @@
 </template>
 
 <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 { ElMessage, ElMessageBox } from 'element-plus';
 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 { useS8StageConfig } from './useS8StageConfig';
 import { useS8CategoryConfig } from './useS8CategoryConfig';
+import { useS8PageConfigDriver } from './useS8PageConfigDriver';
 
 // ─── 布局定义 ─────────────────────────────────────────────────────────────
 // 供应异常 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 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;
 
+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 stageCards = computed(() =>
-	STAGE_META.map((meta) => {
+	effectiveStageMeta.value.map((meta) => {
 		const m = moduleMap.value.get(meta.code);
 		const total  = Math.max(m?.total ?? 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 CATEGORY_ICONS = [ShoppingBag, Van, Box, DataAnalysis, Document, Promotion, Tools] as const;
 const categoryCards = computed<CategoryGridCardData[]>(() => {
 	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 {
 			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));
@@ -344,8 +380,9 @@ async function loadData() {
 		]);
 		Object.assign(summary, summaryData);
 		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';
 	} catch (err) {
@@ -359,7 +396,8 @@ async function loadData() {
 function resetStageConfig() { resetStageConfigState(stageCards.value); }
 function resetCategoryConfig() { resetCategoryConfigState(categoryCards.value); }
 
-onMounted(() => {
+onMounted(async () => {
+	await loadPageConfig();
 	initializeFromCards(stageCards.value);
 	initializeFromCategories(categoryCards.value);
 	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 S8DashboardCellDataService _cellSvc;
+    private readonly S8DashboardCellConfigService _cfgSvc;
 
-    public AdoS8DashboardController(S8DashboardService svc, S8DashboardCellDataService cellSvc)
+    public AdoS8DashboardController(
+        S8DashboardService svc,
+        S8DashboardCellDataService cellSvc,
+        S8DashboardCellConfigService cfgSvc)
     {
         _svc = svc;
         _cellSvc = cellSvc;
+        _cfgSvc = cfgSvc;
     }
 
     [HttpGet("overview")]
@@ -56,4 +61,16 @@ public class AdoS8DashboardController : ControllerBase
         try { return Ok(await _cellSvc.GetAsync(q)); }
         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")]
     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")]
     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)。
 /// 全局基线:tenant_id=0 / factory_id=0;覆盖 4 个大屏的全部稳定锚点卡片。
 /// 依赖 S8ExceptionTypeSeedData:EXCEPTION_TYPE 绑定的 exception_type_code 必须先存在。
+/// G-09 一期:补齐 UI 配置字段(icon/layout_area/display_mode/show_in_sidebar/remark),
+/// 让新装环境跑 SeedData 即可获得完整渲染配置;现有环境通过手工 INSERT SQL 灌入同样数据。
 /// </summary>
 [IncreSeed]
 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>();
 
         // ── 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;
-        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 页面 ──
         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 页面 ──
         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 页面 ──
         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;
     }
 
     private static Entity.S8.AdoS8DashboardCellConfig Custom(
         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()
         {
             Id = id,
@@ -120,12 +162,19 @@ public class S8DashboardCellConfigSeedData : ISqlSugarEntitySeedData<Entity.S8.A
             DeptGroupBy = "OWNER",
             Enabled = true,
             SortNo = sortNo,
+            Icon = icon,
+            LayoutArea = layoutArea,
+            DisplayMode = displayMode,
+            ShowInSidebar = showInSidebar,
+            Remark = remark,
             CreatedAt = ct,
         };
 
     private static Entity.S8.AdoS8DashboardCellConfig ExceptionType(
         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()
         {
             Id = id,
@@ -143,13 +192,20 @@ public class S8DashboardCellConfigSeedData : ISqlSugarEntitySeedData<Entity.S8.A
             DeptGroupBy = "OWNER",
             Enabled = true,
             SortNo = sortNo,
+            Icon = icon,
+            LayoutArea = layoutArea,
+            DisplayMode = displayMode,
+            ShowInSidebar = showInSidebar,
+            Remark = remark,
             CreatedAt = ct,
         };
 
     private static Entity.S8.AdoS8DashboardCellConfig Aggregate(
         long id, string pageCode, string cellCode, string cellTitle,
         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()
         {
             Id = id,
@@ -167,6 +223,11 @@ public class S8DashboardCellConfigSeedData : ISqlSugarEntitySeedData<Entity.S8.A
             DeptGroupBy = deptGroupBy,
             Enabled = true,
             SortNo = sortNo,
+            Icon = icon,
+            LayoutArea = layoutArea,
+            DisplayMode = displayMode,
+            ShowInSidebar = showInSidebar,
+            Remark = remark,
             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;
 
 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);
 
+    /// <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)
     {
         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(AdoS8RolePermissionConfig),
                 typeof(AdoS8NotificationLog),
+                typeof(AdoS8DashboardCellConfig),
                 typeof(ContractReview),
                 typeof(ContractReviewFlow),
                 typeof(ProductDesign),