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

feat(ui): 看板全屏、MDIOP 品牌与登录/诊断菜单修复

看板共用全屏按钮;展示名统一为 MDIOP/数智运营平台;登录遮罩在配置失败时不再卡住;侧栏不再前端注入智慧诊断。chore: bump version Web 2.4.393 / server 1.0.526

Co-authored-by: Cursor <cursoragent@cursor.com>
Pengxy 1 месяц назад
Родитель
Сommit
7915a461e6

+ 1 - 1
Web/index.html

@@ -7,7 +7,7 @@
 	<meta name="viewport" content="width=device-width, initial-scale=1.0" />
 	<link rel="stylesheet" type="text/css" media="print" href="/print-lock.css" />
 	<link rel="icon" id="favicon" href="data:;base64,=" />
-	<title>河南产互联制造业数据智能运营平台</title>
+	<title>制造业数据智能运营平台</title>
 </head>
 
 <body>

+ 1 - 1
Web/package.json

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

+ 10 - 4
Web/src/App.vue

@@ -24,6 +24,7 @@ import { SysConfigApi } from '/@/api-services';
 import { getAPI } from '/@/utils/axios-utils';
 import { useLangStore } from '/@/stores/useLangStore';
 import localeMap from '../lang/elementLocales'
+import { resolveBrandFullName, resolveBrandShortName, resolveBrandWatermark } from '/@/constants/mdiopBrand';
 
 const currentLang = Local.get('themeConfig')?.globalI18n || 'zh-cn'
 const locale = computed(() => localeMap[currentLang] || localeMap['zh-cn'])
@@ -88,7 +89,11 @@ onMounted(() => {
 		});
 		// 获取缓存中的布局配置
 		if (Local.get('themeConfig')) {
-			storesThemeConfig.setThemeConfig({ themeConfig: Local.get('themeConfig') });
+			const cached = Local.get('themeConfig');
+			cached.globalTitle = resolveBrandFullName(cached.globalTitle);
+			cached.globalViceTitle = resolveBrandShortName(cached.globalViceTitle);
+			cached.watermarkText = resolveBrandWatermark(cached.watermarkText);
+			storesThemeConfig.setThemeConfig({ themeConfig: cached });
 			document.documentElement.style.cssText = Local.get('themeConfigStyle');
 		}
 		// 获取缓存中的全屏配置
@@ -123,9 +128,9 @@ const loadSysInfo = () => {
 			// 系统logo
 			themeConfig.value.logoUrl = data.logo;
 			// 主标题
-			themeConfig.value.globalTitle = data.title;
+			themeConfig.value.globalTitle = resolveBrandFullName(data.title);
 			// 副标题
-			themeConfig.value.globalViceTitle = data.viceTitle;
+			themeConfig.value.globalViceTitle = resolveBrandShortName(data.viceTitle);
 			// 系统说明
 			themeConfig.value.globalViceTitleMsg = data.viceDesc;
 			// Icp备案信息
@@ -133,7 +138,7 @@ const loadSysInfo = () => {
 			themeConfig.value.icpUrl = data.icpUrl;
 			// 水印
 			themeConfig.value.isWatermark = data.watermark != null;
-			themeConfig.value.watermarkText = data.watermark;
+			themeConfig.value.watermarkText = resolveBrandWatermark(data.watermark);
 			// 版权说明
 			themeConfig.value.copyright = data.copyright;
 			// 登录验证
@@ -159,6 +164,7 @@ const loadSysInfo = () => {
 		.catch(() => {
 			// 置空 logo 地址
 			themeConfig.value.logoUrl = '';
+			themeConfig.value.isLoaded = true;
 			// 保存配置
 			Local.remove('themeConfig');
 			Local.set('themeConfig', storesThemeConfig.themeConfig);

+ 48 - 0
Web/src/constants/mdiopBrand.ts

@@ -0,0 +1,48 @@
+/** 系统全称 */
+export const MDIOP_FULL_NAME = '制造业数据智能运营平台';
+/** 中文简称 */
+export const MDIOP_SHORT_ZH = '数智运营平台';
+/** 英文简称 */
+export const MDIOP_SHORT_EN = 'MDIOP';
+
+const LEGACY_FULL_TITLES = new Set([
+	'河南产互联制造业数据智能运营平台',
+	'Ai-DOP',
+	'AiDOP',
+	'AIDOP',
+	'智能运营平台',
+]);
+
+/** 仅替换展示文案中的旧简称,不改路径/接口编码 */
+export function rewriteAidopLabel(text?: string | null): string {
+	return String(text ?? '')
+		.replace(/Ai-DOP/g, MDIOP_SHORT_EN)
+		.replace(/AiDOP/g, MDIOP_SHORT_EN)
+		.replace(/AIDOP/g, MDIOP_SHORT_EN);
+}
+
+function isLegacyOfficialTitle(raw?: string | null): boolean {
+	const text = String(raw ?? '').trim();
+	if (!text) return true;
+	if (LEGACY_FULL_TITLES.has(text)) return true;
+	if (text.includes('河南产互联') && text.includes('制造业数据智能运营平台')) return true;
+	return false;
+}
+
+/** 登录页 / 浏览器标题 / 页脚:官方全称 */
+export function resolveBrandFullName(raw?: string | null): string {
+	if (isLegacyOfficialTitle(raw)) return MDIOP_FULL_NAME;
+	return rewriteAidopLabel(raw);
+}
+
+/** 侧栏短名、副标题:中文简称 */
+export function resolveBrandShortName(raw?: string | null): string {
+	if (isLegacyOfficialTitle(raw)) return MDIOP_SHORT_ZH;
+	return rewriteAidopLabel(raw);
+}
+
+export function resolveBrandWatermark(raw?: string | null): string {
+	if (raw == null || !String(raw).trim()) return String(raw ?? '');
+	if (isLegacyOfficialTitle(raw)) return `${MDIOP_SHORT_ZH} ${MDIOP_SHORT_EN}`;
+	return rewriteAidopLabel(raw);
+}

+ 2 - 2
Web/src/layout/logo/index.vue

@@ -16,8 +16,8 @@ import { useThemeConfig } from '/@/stores/themeConfig';
 const storesThemeConfig = useThemeConfig();
 const { themeConfig } = storeToRefs(storesThemeConfig);
 
-const shortTitle = '智能运营平台';
-const systemTitle = computed(() => themeConfig.value.globalTitle || '河南产互联制造业数据智能运营平台');
+const shortTitle = '数智运营平台';
+const systemTitle = computed(() => themeConfig.value.globalTitle || '制造业数据智能运营平台');
 const logoSrc = computed(() => themeConfig.value.logoUrl || '/app-icons/aidop-logo.png');
 
 // 设置 logo 的显示。classic 经典布局默认显示 logo

+ 2 - 2
Web/src/layout/navBars/topBar/settings.vue

@@ -19,7 +19,7 @@
 				<div class="layout-breadcrumb-seting-bar-flex mt10">
 					<div class="layout-breadcrumb-seting-bar-flex-label">主题预设</div>
 					<div class="layout-breadcrumb-seting-bar-flex-value">
-						<el-button size="small" type="primary" plain @click="onApplyAiDopPreset">AiDOP 风格</el-button>
+						<el-button size="small" type="primary" plain @click="onApplyAiDopPreset">MDIOP 风格</el-button>
 					</div>
 				</div>
 
@@ -624,7 +624,7 @@ const onApplyAiDopPreset = () => {
 	onBgColorPickerChange('columnsMenuBarColor');
 	onAddDarkChange();
 	setDispatchThemeConfig();
-	ElMessage.success('已应用 AiDOP 风格主题');
+	ElMessage.success('已应用 MDIOP 风格主题');
 };
 
 // 4、界面显示 --> 开启水印

+ 2 - 2
Web/src/stores/themeConfig.ts

@@ -137,9 +137,9 @@ export const useThemeConfig = defineStore('themeConfig', {
 			 * 全局网站标题 / 副标题
 			 */
 			// 网站主标题(菜单导航、浏览器当前网页标题)
-			globalTitle: '河南产互联制造业数据智能运营平台',
+			globalTitle: '制造业数据智能运营平台',
 			// 网站副标题(登录页顶部文字)
-			globalViceTitle: '河南产互联制造业数据智能运营平台',
+			globalViceTitle: '数智运营平台',
 			// 网站副标题(登录页顶部文字)
 			globalViceTitleMsg: '可视,可诊,可控,可溯,可闭环',
 			// 版权和备案文字

+ 4 - 9
Web/src/utils/aidopMenuDisplay.ts

@@ -5,6 +5,7 @@ import { patchAidopFuncCodes } from '/@/utils/aidopFuncCode';
  * 在登录菜单树写入路由前覆盖 meta.title,避免库中仍为旧标题时侧栏不更新。
  */
 export const AIDOP_DIRECTORY_TITLES: Record<string, string> = {
+	aidopRoot: 'MDIOP',
 	aidopDirS0: 'S0 运营建模',
 	aidopDirS1: 'S1 产销协同',
 	aidopDirS2: 'S2 制造协同',
@@ -232,14 +233,7 @@ function patchAidopCustomMenusIfMissing(routes: AMenu[] | undefined): void {
 		});
 	}
 
-	if (!existing.has('/aidop/smart-diagnosis')) {
-		upsertMenu(aidopRoot, {
-			path: '/aidop/smart-diagnosis',
-			name: 'aidopSmartDiagnosis',
-			component: '/aidop/diagnosis/index',
-			meta: { title: '智慧诊断', icon: 'ele-TrendCharts' },
-		});
-	}
+	// 智慧诊断已入库 sys_menu,只跟角色授权走;未勾选时不要前端再补一条侧栏。
 
 	for (const child of PRODUCTION_AUX_ROUTES) {
 		if (existing.has(child.path)) continue;
@@ -352,7 +346,7 @@ function patchS8ConfigHiddenRoutesIfMissing(routes: AMenu[] | undefined): void {
 
 /**
  * 递归覆盖 Ai-DOP 一级目录的 meta.title,供侧栏 / tagsView / 菜单搜索使用。
- * 智慧诊断、智慧运营看板以 sys_menu 为准;若租户/角色未关联到库中菜单,则仅补缺失 path,避免双份。
+ * 智慧运营看板分组若库中缺失则补路由(隐藏辅助页仍补);智慧诊断不再前端注入,以免盖过角色授权。
  */
 /** 强制智慧运营子菜单 component,避免库中仍为占位/错误路径时前端仍加载旧页 */
 function patchSmartOpsRouteComponents(nodes: AMenu[] | undefined): void {
@@ -408,6 +402,7 @@ export function patchAidopMenuTitles(routes: any[] | undefined): void {
 		if (name && AIDOP_DIRECTORY_TITLES[name]) {
 			item.meta = item.meta ?? {};
 			item.meta.title = AIDOP_DIRECTORY_TITLES[name];
+			if (item.title) item.title = AIDOP_DIRECTORY_TITLES[name];
 		}
 		const path = item.path as string | undefined;
 		if (path && AIDOP_COMPONENT_OVERRIDES[path]) {

+ 24 - 6
Web/src/views/aidop/chatbi/ChatBIFloat.vue

@@ -1,10 +1,11 @@
 <template>
 	<Teleport to="body">
-		<button type="button" class="chatbi-float" :class="{ 'chatbi-float--s1': moduleCode === 'S1' }" @click="visible = true">
+		<button v-if="pageActive" type="button" class="chatbi-float" @click="visible = true">
 			<span class="chatbi-float__dot" />
 			<span>{{ buttonText }}</span>
 		</button>
 		<ChatBIPanel
+			v-if="pageActive"
 			v-model:visible="visible"
 			:title="title"
 			:entry="entry"
@@ -17,7 +18,8 @@
 </template>
 
 <script setup lang="ts">
-import { ref } from 'vue';
+import { computed, onActivated, onDeactivated, onMounted, ref, watch } from 'vue';
+import { useRoute } from 'vue-router';
 import ChatBIPanel from './ChatBIPanel.vue';
 
 withDefaults(
@@ -41,7 +43,27 @@ withDefaults(
 	}
 );
 
+const route = useRoute();
+const ownerPath = ref(route.path);
+const keepAliveActive = ref(true);
 const visible = ref(false);
+const pageActive = computed(() => keepAliveActive.value && route.path === ownerPath.value);
+
+onMounted(() => {
+	ownerPath.value = route.path;
+	keepAliveActive.value = true;
+});
+onActivated(() => {
+	ownerPath.value = route.path;
+	keepAliveActive.value = true;
+});
+onDeactivated(() => {
+	keepAliveActive.value = false;
+	visible.value = false;
+});
+watch(pageActive, (active) => {
+	if (!active) visible.value = false;
+});
 </script>
 
 <style scoped lang="scss">
@@ -71,10 +93,6 @@ const visible = ref(false);
 	}
 }
 
-.chatbi-float--s1 {
-	bottom: 86px;
-}
-
 .chatbi-float__dot {
 	width: 9px;
 	height: 9px;

+ 1 - 1
Web/src/views/aidop/chatbi/ChatBIPanel.vue

@@ -3,7 +3,7 @@
 		<template #header>
 			<div class="chatbi-panel__header">
 				<div>
-					<div class="chatbi-panel__eyebrow">Ai-DOP ChatBI</div>
+					<div class="chatbi-panel__eyebrow">MDIOP ChatBI</div>
 					<div class="chatbi-panel__title">{{ title }}</div>
 				</div>
 			</div>

+ 1 - 1
Web/src/views/aidop/data-platform/dataMap.vue

@@ -8,7 +8,7 @@
 		<div class="data-map">
 			<section class="map-hero">
 				<div>
-					<div class="eyebrow">Ai-DOP Data Platform</div>
+					<div class="eyebrow">MDIOP Data Platform</div>
 					<h3>企业数据流转总览</h3>
 					<p>统一呈现数据库同步、API 入站、API 拉取与 API 出站服务,帮助演示数据从来源到业务应用的完整路径。</p>
 				</div>

+ 1 - 1
Web/src/views/aidop/demo/cockpit.vue

@@ -4,7 +4,7 @@
 			<div class="aidop-cockpit-title__left">
 				<div class="aidop-demo-shell__mark" aria-hidden="true" />
 				<div>
-					<span class="aidop-cockpit-title__dop">河南产互联制造业数据智能运营平台</span>
+					<span class="aidop-cockpit-title__dop">制造业数据智能运营平台</span>
 					<span class="aidop-cockpit-title__rest">企业运营看板</span>
 					<span class="aidop-cockpit-title__badge">DEMO</span>
 				</div>

+ 83 - 14
Web/src/views/aidop/diagnosis/index.vue

@@ -1159,9 +1159,10 @@ const statusCounts = computed(() => {
 })
 
 const mainProblem = computed(() => {
-  let best = {
+  const empty = {
     score: -1,
     severity: -1,
+    deviation: 0,
     level: 0,
     stageKey: '',
     tier1Key: '',
@@ -1172,20 +1173,16 @@ const mainProblem = computed(() => {
     status: 'green',
     dept: '',
   }
-  const accept = (candidate) => {
-    const score = candidate.severity * 10 + candidate.level
-    if (score > best.score) best = { ...candidate, score }
-  }
   if (!stages.length) {
     const status = diagnosisContext.status || 'green'
     const severity = statusSeverity(status)
     return {
-      score: severity * 10 + 1,
+      ...empty,
+      score: severity,
       severity,
+      deviation: problemDeviation(diagnosisContext.actual, diagnosisContext.target, status),
       level: 1,
       stageKey: diagnosisContext.metricCode || '',
-      tier1Key: '',
-      tier2Key: '',
       name: diagnosisContext.metricName || '',
       actual: diagnosisContext.actual || '-',
       plan: diagnosisContext.target || '-',
@@ -1193,15 +1190,65 @@ const mainProblem = computed(() => {
       dept: diagnosisContext.department || '未配置',
     }
   }
+
+  const toCandidate = (partial) => ({
+    ...empty,
+    ...partial,
+    deviation: problemDeviation(partial.actual, partial.plan, partial.status),
+    score: partial.severity,
+  })
+
+  let best = empty
+  const accept = (candidate) => {
+    if (isWorseProblem(candidate, best)) best = candidate
+  }
+
+  const stageCandidates = stages.map((s) =>
+    toCandidate({
+      severity: statusSeverity(s.status),
+      level: 1,
+      stageKey: s.key,
+      name: s.name,
+      actual: s.actualDays,
+      plan: s.kpi,
+      status: s.status,
+      dept: s.dept || '',
+    })
+  )
+  for (const c of stageCandidates) accept(c)
+  // 这一级(当前诊断根的直接下级)已有红/黄时,用该级最差项,不把绿灯分支下的深层小红灯当成主要问题。
+  if (best.severity >= 2) return best
+
   for (const s of stages) {
-    const severity = statusSeverity(s.status)
-    accept({ severity, level: 1, stageKey: s.key, tier1Key: '', tier2Key: '', name: s.name, actual: s.actualDays, plan: s.kpi, status: s.status, dept: s.dept || '' })
     for (const node of drillTier1ByStage[s.key] || []) {
-      const nodeSeverity = statusSeverity(node.status)
-      accept({ severity: nodeSeverity, level: 2, stageKey: s.key, tier1Key: node.key, tier2Key: '', name: node.name, actual: node.actual, plan: node.plan, status: node.status, dept: node.dept || s.dept || '' })
+      accept(
+        toCandidate({
+          severity: statusSeverity(node.status),
+          level: 2,
+          stageKey: s.key,
+          tier1Key: node.key,
+          name: node.name,
+          actual: node.actual,
+          plan: node.plan,
+          status: node.status,
+          dept: node.dept || s.dept || '',
+        })
+      )
       for (const child of problemTier2Rows(s.key, node.key)) {
-        const childSeverity = statusSeverity(child.status)
-        accept({ severity: childSeverity, level: 3, stageKey: s.key, tier1Key: node.key, tier2Key: child.key, name: child.name, actual: child.actual, plan: child.plan, status: child.status, dept: child.dept || node.dept || s.dept || '' })
+        accept(
+          toCandidate({
+            severity: statusSeverity(child.status),
+            level: 3,
+            stageKey: s.key,
+            tier1Key: node.key,
+            tier2Key: child.key,
+            name: child.name,
+            actual: child.actual,
+            plan: child.plan,
+            status: child.status,
+            dept: child.dept || node.dept || s.dept || '',
+          })
+        )
       }
     }
   }
@@ -1350,6 +1397,28 @@ function statusSeverity(status) {
   return map[status] || 0
 }
 
+function parseMetricNumber(raw) {
+  if (raw == null) return null
+  const match = String(raw).replace(/,/g, '').match(/-?\d+(?:\.\d+)?/)
+  return match ? Number(match[0]) : null
+}
+
+/** 相对目标的偏离幅度;绿灯为 0。单位不同时用 |实际-目标|/|目标|,避免「更深一层」压过「缺口更大」。 */
+function problemDeviation(actual, plan, status) {
+  if (statusSeverity(status) <= 1) return 0
+  const a = parseMetricNumber(actual)
+  const p = parseMetricNumber(plan)
+  if (a == null || p == null || p === 0) return 0
+  return Math.abs(a - p) / Math.abs(p)
+}
+
+function isWorseProblem(candidate, current) {
+  if (current.severity < 0) return true
+  if (candidate.severity !== current.severity) return candidate.severity > current.severity
+  if (Math.abs(candidate.deviation - current.deviation) > 1e-6) return candidate.deviation > current.deviation
+  return candidate.level < current.level
+}
+
 function withDiagnosisUnit(value, unit) {
   const text = value == null ? '' : String(value)
   const normalizedUnit = unit == null ? '' : String(unit).trim()

+ 67 - 0
Web/src/views/aidop/kanban/components/DashboardFullscreenButton.vue

@@ -0,0 +1,67 @@
+<template>
+	<button
+		v-if="tone === 'dark'"
+		type="button"
+		class="dash-fs-btn dash-fs-btn--dark"
+		:aria-label="label"
+		:title="label"
+		@click="toggle"
+	>
+		<el-icon :size="16">
+			<Close v-if="isFullscreen" />
+			<FullScreen v-else />
+		</el-icon>
+		<span>{{ label }}</span>
+	</button>
+	<el-button v-else size="small" :type="isFullscreen ? 'warning' : 'default'" plain @click="toggle">
+		<el-icon class="el-icon--left">
+			<Close v-if="isFullscreen" />
+			<FullScreen v-else />
+		</el-icon>
+		{{ label }}
+	</el-button>
+</template>
+
+<script setup lang="ts">
+import { computed } from 'vue';
+import { Close, FullScreen } from '@element-plus/icons-vue';
+import { useDashboardFullscreen } from '../composables/useDashboardFullscreen';
+
+withDefaults(
+	defineProps<{
+		tone?: 'default' | 'dark';
+	}>(),
+	{ tone: 'default' }
+);
+
+const { isFullscreen, toggle } = useDashboardFullscreen();
+const label = computed(() => (isFullscreen.value ? '退出全屏' : '全屏显示'));
+</script>
+
+<style scoped>
+.dash-fs-btn--dark {
+	display: inline-flex;
+	align-items: center;
+	justify-content: center;
+	gap: 6px;
+	height: 36px;
+	padding: 0 12px;
+	border-radius: 8px;
+	border: 1px solid rgba(42, 67, 101, 0.7);
+	background: rgba(10, 22, 40, 0.9);
+	color: #7eb8e8;
+	cursor: pointer;
+	font-size: 13px;
+	white-space: nowrap;
+	transition:
+		border-color 0.2s,
+		background 0.2s,
+		color 0.2s;
+}
+
+.dash-fs-btn--dark:hover {
+	border-color: rgba(96, 165, 250, 0.45);
+	background: rgba(30, 41, 59, 0.95);
+	color: #93c5fd;
+}
+</style>

+ 2 - 0
Web/src/views/aidop/kanban/components/DynamicModuleDashboard.vue

@@ -7,6 +7,7 @@
 				<p>{{ page?.subtitle || '由运营指标主数据和看板配置动态生成' }}</p>
 			</div>
 			<div class="dynamic-dashboard__actions">
+				<DashboardFullscreenButton />
 				<el-button size="small" type="success" plain @click="goKpiMaster">运营指标主数据</el-button>
 				<el-button size="small" type="primary" plain @click="goModeling">配置指标</el-button>
 				<el-button size="small" type="warning" plain @click="goDashboardConfig">详情看板配置</el-button>
@@ -328,6 +329,7 @@ import { ElMessage, ElMessageBox } from 'element-plus';
 import * as echarts from 'echarts';
 import DetailQueryBar from './DetailQueryBar.vue';
 import SmartOpsBaseQueryFields from './SmartOpsBaseQueryFields.vue';
+import DashboardFullscreenButton from './DashboardFullscreenButton.vue';
 import ChatBIFloat from '../../chatbi/ChatBIFloat.vue';
 import S7OrderCycleDimensionDrawer from './S7OrderCycleDimensionDrawer.vue';
 import KpiDimensionDrawer from './KpiDimensionDrawer.vue';

+ 88 - 0
Web/src/views/aidop/kanban/composables/useDashboardFullscreen.ts

@@ -0,0 +1,88 @@
+import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
+import screenfull from 'screenfull';
+import { storeToRefs } from 'pinia';
+import { useTagsViewRoutes } from '/@/stores/tagsViewRoutes';
+
+function notifyResize() {
+	nextTick(() => {
+		window.dispatchEvent(new Event('resize'));
+	});
+}
+
+/**
+ * 看板全屏:隐藏侧栏/顶栏(内容区全屏)并尽量进入浏览器全屏。
+ * ESC 或布局自带的关闭钮都会退出两边。
+ */
+export function useDashboardFullscreen() {
+	const tagsStore = useTagsViewRoutes();
+	const { isTagsViewCurrenFull } = storeToRefs(tagsStore);
+	const browserFullscreen = ref(false);
+
+	const isFullscreen = computed(() => !!isTagsViewCurrenFull.value || browserFullscreen.value);
+
+	function syncBrowser() {
+		browserFullscreen.value = !!(screenfull.isEnabled && screenfull.isFullscreen);
+	}
+
+	function onScreenfullChange() {
+		syncBrowser();
+		if (!browserFullscreen.value && isTagsViewCurrenFull.value) {
+			tagsStore.setCurrenFullscreen(false);
+		}
+		notifyResize();
+	}
+
+	async function enter() {
+		tagsStore.setCurrenFullscreen(true);
+		if (screenfull.isEnabled && !screenfull.isFullscreen) {
+			try {
+				await screenfull.request();
+			} catch {
+				/* 浏览器拦截系统全屏时,仍保留内容区全屏 */
+			}
+		}
+		syncBrowser();
+		notifyResize();
+	}
+
+	async function exit() {
+		tagsStore.setCurrenFullscreen(false);
+		if (screenfull.isEnabled && screenfull.isFullscreen) {
+			try {
+				await screenfull.exit();
+			} catch {
+				/* ignore */
+			}
+		}
+		syncBrowser();
+		notifyResize();
+	}
+
+	async function toggle() {
+		if (isFullscreen.value) await exit();
+		else await enter();
+	}
+
+	watch(isTagsViewCurrenFull, async (full) => {
+		if (!full && screenfull.isEnabled && screenfull.isFullscreen) {
+			try {
+				await screenfull.exit();
+			} catch {
+				/* ignore */
+			}
+			syncBrowser();
+			notifyResize();
+		}
+	});
+
+	onMounted(() => {
+		syncBrowser();
+		if (screenfull.isEnabled) screenfull.on('change', onScreenfullChange);
+	});
+
+	onUnmounted(() => {
+		if (screenfull.isEnabled) screenfull.off('change', onScreenfullChange);
+	});
+
+	return { isFullscreen, toggle, enter, exit };
+}

+ 4 - 0
Web/src/views/aidop/kanban/s8.vue

@@ -1,5 +1,8 @@
 <template>
 	<AidopDemoShell class="aidop-smart-ops-shell-fit" title="S8 异常监控看板" subtitle="移植 Demo 智慧运营首页中的 S8 异常监控能力">
+		<template #bar-right>
+			<DashboardFullscreenButton tone="dark" />
+		</template>
 		<DetailQueryBar dark band-title="基础查询:日期、产品、订单号、产线" @query="onQuery" @reset="onReset">
 			<SmartOpsBaseQueryFields v-model="baseQuery" compact />
 		</DetailQueryBar>
@@ -23,6 +26,7 @@ import AidopDemoShell from '../components/AidopDemoShell.vue';
 import { fetchS8Alerts, type S8AlertRow } from '../api/kanbanData';
 import DetailQueryBar from './components/DetailQueryBar.vue';
 import SmartOpsBaseQueryFields from './components/SmartOpsBaseQueryFields.vue';
+import DashboardFullscreenButton from './components/DashboardFullscreenButton.vue';
 import {
 	emptySmartOpsBaseQuery,
 	blobMatchesSmartOpsBase,

+ 3 - 3
Web/src/views/aidop/planning/index.vue

@@ -1,7 +1,7 @@
 <template>
 	<AidopDemoShell :title="pageTitle" :subtitle="pageSubtitle" :show-bar="true">
 		<p v-if="pageRemark" class="remark">{{ pageRemark }}</p>
-		<el-empty v-else description="后续在此接入 Ai-DOP 业务页面" />
+		<el-empty v-else description="后续在此接入 MDIOP 业务页面" />
 	</AidopDemoShell>
 </template>
 
@@ -11,8 +11,8 @@ import { useRoute } from 'vue-router';
 import AidopDemoShell from '../components/AidopDemoShell.vue';
 
 const route = useRoute();
-const pageTitle = computed(() => (route.meta?.title as string) || 'Ai-DOP');
-const pageSubtitle = '规划菜单占位 · 主内容区样式与 Ai-DOP 业务页一致';
+const pageTitle = computed(() => (route.meta?.title as string) || 'MDIOP');
+const pageSubtitle = '规划菜单占位 · 主内容区样式与 MDIOP 业务页一致';
 const pageRemark = computed(() => (route.meta as any)?.remark as string);
 </script>
 

+ 2 - 0
Web/src/views/aidop/s8/dashboard/S8DashboardPage.vue

@@ -4,6 +4,7 @@ import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, shallowR
 import { useRouter } from 'vue-router';
 import { ElMessage } from 'element-plus';
 import AidopDemoShell from '../../components/AidopDemoShell.vue';
+import DashboardFullscreenButton from '../../kanban/components/DashboardFullscreenButton.vue';
 import S8DashboardConfigDrawer from './S8DashboardConfigDrawer.vue';
 import {
 	s8DashboardApi,
@@ -667,6 +668,7 @@ onBeforeUnmount(() => { mainChart?.dispose(); trendChart?.dispose(); });
 						<span class="s8-refresh-time">最近刷新:{{ lastRefreshedAt || '—' }}</span>
 						<el-button size="small" plain @click="onQuery">刷新</el-button>
 					</div>
+					<DashboardFullscreenButton />
 					<el-button size="small" @click="openConfigDrawer">面板配置</el-button>
 				</div>
 			</div>

+ 4 - 2
Web/src/views/dashboard/home.vue

@@ -3,9 +3,9 @@
     <!-- 最上层:全宽标题栏(与参考设计稿一致) -->
     <header class="title-bar">
       <div class="title-bar__left">
-        <img class="title-bar__brand-mark" src="/app-icons/aidop-logo-vertical.png" alt="河南产互联制造业数据智能运营平台" />
+        <img class="title-bar__brand-mark" src="/app-icons/aidop-logo-vertical.png" alt="制造业数据智能运营平台" />
         <div class="title-bar__title-block">
-          <span class="title-bar__dop">河南产互联制造业数据智能运营平台</span>
+          <span class="title-bar__dop">制造业数据智能运营平台</span>
           <span class="title-bar__rest">企业运营看板 {{ dashboardVersion }}</span>
         </div>
       </div>
@@ -14,6 +14,7 @@
           <div class="title-bar__clock-label">Current Time</div>
           <div class="title-bar__clock-value">{{ headerDateTime }}</div>
         </div>
+        <DashboardFullscreenButton tone="dark" />
         <button type="button" class="title-bar__user-btn title-bar__user-btn--text" aria-label="我的工作台" title="我的工作台" @click="router.push('/dashboard/home')">
           我的工作台
         </button>
@@ -226,6 +227,7 @@ import {
 } from '../aidop/kanban/utils/smartOpsBaseQuery'
 import HomeModuleKpiCard from './components/HomeModuleKpiCard.vue'
 import ChatBIFloat from '../aidop/chatbi/ChatBIFloat.vue'
+import DashboardFullscreenButton from '../aidop/kanban/components/DashboardFullscreenButton.vue'
 
 const router = useRouter()
 const gridMainRef = ref(null)

+ 8 - 3
Web/src/views/login/index.vue

@@ -106,15 +106,20 @@ const state = reactive({
 const getThemeConfig = computed(() => {
 	return themeConfig.value;
 });
-const systemTitle = computed(() => themeConfig.value.globalTitle || '河南产互联制造业数据智能运营平台');
+const systemTitle = computed(() => themeConfig.value.globalTitle || '制造业数据智能运营平台');
 const systemLogo = computed(() => themeConfig.value.logoUrl || '/app-icons/aidop-logo.png');
 
 // 页面加载时
 onMounted(async () => {
 	// 地址栏存在wayid参数时,默认切换到注册界面
 	if (route.query.wayid != undefined) state.tabsActiveName = 'register';
-	await getTenantInfo();
-	NextLoading.done();
+	try {
+		await getTenantInfo();
+	} catch {
+		/* 后端未就绪时仍关掉启动遮罩,避免挡住登录表单 */
+	} finally {
+		NextLoading.done();
+	}
 });
 
 // 获取租户信息

+ 1 - 1
server/Admin.NET.Core/SeedData/SysTenantSeedData.cs

@@ -26,7 +26,7 @@ public class SysTenantSeedData : ISqlSugarEntitySeedData<SysTenant>
         var admin = userList.First(u => u.Account == "Admin.NET");
         return new[]
         {
-            new SysTenant{ Id=SqlSugarConst.DefaultTenantId, OrgId=SqlSugarConst.DefaultTenantId, UserId=admin.Id, Host="gitee.com", TenantType=TenantTypeEnum.Id, DbType=defaultDbConfig.DbType, Connection=defaultDbConfig.ConnectionString, ConfigId=SqlSugarConst.MainConfigId, Logo="/app-icons/aidop-logo.png", Title="河南产互联制造业数据智能运营平台", ViceTitle="河南产互联制造业数据智能运营平台", ViceDesc="可视,可诊,可控,可溯,可闭环", Watermark="河南产互联制造业数据智能运营平台", Copyright="Copyright \u00a9 2026", Icp="省ICP备12345678号", IcpUrl="https://beian.miit.gov.cn", Remark="系统默认", CreateTime=DateTime.Parse("2022-02-10 00:00:00") },
+            new SysTenant{ Id=SqlSugarConst.DefaultTenantId, OrgId=SqlSugarConst.DefaultTenantId, UserId=admin.Id, Host="gitee.com", TenantType=TenantTypeEnum.Id, DbType=defaultDbConfig.DbType, Connection=defaultDbConfig.ConnectionString, ConfigId=SqlSugarConst.MainConfigId, Logo="/app-icons/aidop-logo.png", Title="制造业数据智能运营平台", ViceTitle="数智运营平台", ViceDesc="可视,可诊,可控,可溯,可闭环", Watermark="数智运营平台 MDIOP", Copyright="Copyright \u00a9 2026", Icp="省ICP备12345678号", IcpUrl="https://beian.miit.gov.cn", Remark="系统默认", CreateTime=DateTime.Parse("2022-02-10 00:00:00") },
         };
     }
 }

+ 3 - 3
server/Admin.NET.Web.Entry/Admin.NET.Web.Entry.csproj

@@ -11,9 +11,9 @@
     <GenerateSatelliteAssembliesForCore>true</GenerateSatelliteAssembliesForCore>
     <Copyright>Admin.NET</Copyright>
     <Description>Admin.NET 通用权限开发平台</Description>
-    <AssemblyVersion>1.0.525</AssemblyVersion>
-    <FileVersion>1.0.525</FileVersion>
-    <Version>1.0.525</Version>
+    <AssemblyVersion>1.0.526</AssemblyVersion>
+    <FileVersion>1.0.526</FileVersion>
+    <Version>1.0.526</Version>
   </PropertyGroup>
 
   <ItemGroup>

+ 6 - 6
server/Plugins/Admin.NET.Plugin.AiDOP/ChatBI/ChatBIService.cs

@@ -418,7 +418,7 @@ WHERE v.tenant_id=@tenantId AND v.factory_id=@factoryId AND v.module_code=@modul
         string.IsNullOrWhiteSpace(metricHint) ? question : $"{question} {metricHint}";
 
     private const string ClassifySystemPrompt = """
-你是 Ai-DOP ChatBI 的问题分类器。只判断用户问题属于哪一类,不要回答业务内容,不要给数值。
+你是 MDIOP ChatBI 的问题分类器。只判断用户问题属于哪一类,不要回答业务内容,不要给数值。
 只输出一行 JSON,不要 Markdown,不要解释。格式:
 {"type":"data_query|compare|locate|diagnose|unrecognized","metricHint":""}
 
@@ -517,33 +517,33 @@ metricHint:若问题里提到了指标名,填该名称;否则空字符串
     private static string SystemPromptFor(string intent) => intent switch
     {
         "data_query" => """
-你是 Ai-DOP 制造运营 ChatBI 助手。用户在做数据查询(取值、来源、怎么算、哪些订单)。
+你是 MDIOP 制造运营 ChatBI 助手。用户在做数据查询(取值、来源、怎么算、哪些订单)。
 先如实给出 KPI 聚合摘要中的当前值、目标、状态。
 若用户问来自哪些订单、单据或明细:明确说当前只能查看板聚合结果,不能列出订单,也不生成 SQL。
 禁止改写成红灯诊断或改善计划。不得编造摘要里没有的数值。
 总字数控制在 200 字以内。
 """,
         "list_metrics" => """
-你是 Ai-DOP 制造运营 ChatBI 助手。用户要的是指标取值清单,不是诊断报告。
+你是 MDIOP 制造运营 ChatBI 助手。用户要的是指标取值清单,不是诊断报告。
 只根据「KPI 聚合摘要」如实列出模块、指标名、当前值、目标、状态。
 不得改写成红灯瓶颈分析,不得编造摘要里没有的数值。若摘要为空,明确说所选条件下没有日值。
 总字数控制在 280 字以内。
 """,
         "best_metric" => """
-你是 Ai-DOP 制造运营 ChatBI 助手。用户在问谁做得最好、哪项表现最好。
+你是 MDIOP 制造运营 ChatBI 助手。用户在问谁做得最好、哪项表现最好。
 直接点名最好的 1 个指标及其模块、当前值、目标、状态;最多再列 1-2 个对照。
 禁止套用「结论 / 证据 / 可能原因 / 下一步」。
 禁止把红灯或最差指标说成最好。只能用摘要里的绿灯或相对最好的项。
 总字数控制在 180 字以内。
 """,
         "metric_status" => """
-你是 Ai-DOP 制造运营 ChatBI 助手。用户在问某个指标现在怎么样。
+你是 MDIOP 制造运营 ChatBI 助手。用户在问某个指标现在怎么样。
 只回答该指标的当前值、目标、状态。不要写可能原因和下一步,除非用户明确问原因。
 不得把其它模块红灯拉进来。不得编造摘要里没有的数值。
 总字数控制在 160 字以内。
 """,
         _ => """
-你是 Ai-DOP 制造运营 ChatBI 助手。后端已经完成意图识别、指标选择和事实校验。
+你是 MDIOP 制造运营 ChatBI 助手。后端已经完成意图识别、指标选择和事实校验。
 先直接回答用户问题。仅当用户在问原因、异常或改善时,再补充可能原因 / 下一步。
 不得新增未给出的原因、单据、负责人或数值。
 如果用户问题已经指定某个指标,只能围绕焦点指标和它的真实下级指标回答,不得把其它模块、其它 L1 红灯写成原因。

+ 1 - 1
server/Plugins/Admin.NET.Plugin.AiDOP/SeedData/SysMenuSeedData.cs

@@ -20,7 +20,7 @@ public class SysMenuSeedData : ISqlSugarEntitySeedData<SysMenu>
             {
                 Id = AidopRootId,
                 Pid = 0,
-                Title = "Ai-DOP",
+                Title = "MDIOP",
                 Path = "/aidop",
                 Name = "aidopRoot",
                 Component = "Layout",