Преглед на файлове

qhse体系文件面包屑

yanghao преди 2 седмици
родител
ревизия
4b4040b564
променени са 5 файла, в които са добавени 538 реда и са изтрити 260 реда
  1. 5 1
      src/permission.ts
  2. 5 5
      src/store/modules/app.ts
  3. 40 0
      src/utils/auth.ts
  4. 8 7
      src/views/maotu/preview.vue
  5. 480 247
      src/views/pms/stat/maintenance.vue

+ 5 - 1
src/permission.ts

@@ -12,6 +12,8 @@ import { usePermissionStoreWithOut } from '@/store/modules/permission'
 import * as LoginApi from '@/api/login'
 import * as LoginApi from '@/api/login'
 import * as authUtil from '@/utils/auth'
 import * as authUtil from '@/utils/auth'
 
 
+authUtil.bootstrapAppWebtopoPreviewAuth()
+
 const { start, done } = useNProgress()
 const { start, done } = useNProgress()
 
 
 const { loadStart, loadDone } = usePageLoading()
 const { loadStart, loadDone } = usePageLoading()
@@ -85,7 +87,9 @@ router.beforeEach(async (to, from, next) => {
   start()
   start()
   loadStart()
   loadStart()
   if (getAccessToken()) {
   if (getAccessToken()) {
-    if (to.path === '/login') {
+    if (authUtil.isAppWebtopoPreview() && to.path.startsWith('/maotu/preview/')) {
+      next()
+    } else if (to.path === '/login') {
       next({ path: '/' })
       next({ path: '/' })
     } else if (to.fullPath.includes('portalLogin')) {
     } else if (to.fullPath.includes('portalLogin')) {
       // authUtil.removeToken()
       // authUtil.removeToken()

+ 5 - 5
src/store/modules/app.ts

@@ -67,7 +67,7 @@ export const useAppStore = defineStore('app', {
       greyMode: false, // 是否开始灰色模式,用于特殊悼念日
       greyMode: false, // 是否开始灰色模式,用于特殊悼念日
       fixedMenu: wsCache.get('fixedMenu') || false, // 是否固定菜单
       fixedMenu: wsCache.get('fixedMenu') || false, // 是否固定菜单
 
 
-      layout: wsCache.get(CACHE_KEY.LAYOUT) || 'classic', // layout布局
+      layout: wsCache.get(CACHE_KEY.LAYOUT) || 'cutMenu', // layout布局
       isDark: wsCache.get(CACHE_KEY.IS_DARK) || false, // 是否是暗黑模式
       isDark: wsCache.get(CACHE_KEY.IS_DARK) || false, // 是否是暗黑模式
       currentSize: wsCache.get('default') || 'default', // 组件尺寸
       currentSize: wsCache.get('default') || 'default', // 组件尺寸
       theme: wsCache.get(CACHE_KEY.THEME) || {
       theme: wsCache.get(CACHE_KEY.THEME) || {
@@ -92,13 +92,13 @@ export const useAppStore = defineStore('app', {
         // logo边框颜色
         // logo边框颜色
         logoBorderColor: 'inherit',
         logoBorderColor: 'inherit',
         // 头部背景颜色
         // 头部背景颜色
-        topHeaderBgColor: '#fff',
+        topHeaderBgColor: '#151515',
         // 头部字体颜色
         // 头部字体颜色
-        topHeaderTextColor: 'inherit',
+        topHeaderTextColor: '#fff',
         // 头部悬停颜色
         // 头部悬停颜色
-        topHeaderHoverColor: '#f6f6f6',
+        topHeaderHoverColor: '#242424',
         // 头部边框颜色
         // 头部边框颜色
-        topToolBorderColor: '#eee'
+        topToolBorderColor: '#151515'
       }
       }
     }
     }
   },
   },

+ 40 - 0
src/utils/auth.ts

@@ -7,12 +7,52 @@ const { wsCache: sessionCache } = useCache('sessionStorage')
 
 
 const AccessTokenKey = 'ACCESS_TOKEN'
 const AccessTokenKey = 'ACCESS_TOKEN'
 const RefreshTokenKey = 'REFRESH_TOKEN'
 const RefreshTokenKey = 'REFRESH_TOKEN'
+const AppWebtopoPreviewKey = 'APP_WEBTOPO_PREVIEW'
 
 
 // 获取token
 // 获取token
 export const getAccessToken = () => {
 export const getAccessToken = () => {
   return sessionCache.get(AccessTokenKey)
   return sessionCache.get(AccessTokenKey)
 }
 }
 
 
+/**
+ * App 内嵌组态预览使用 URL fragment 传递短期登录态。
+ * fragment 不会发送到服务端,读取完成后立即从地址栏移除。
+ */
+export const bootstrapAppWebtopoPreviewAuth = () => {
+  if (typeof window === 'undefined' || !window.location.hash) {
+    return false
+  }
+
+  const params = new URLSearchParams(window.location.hash.slice(1))
+  if (params.get('appPreview') !== '1') {
+    return false
+  }
+
+  const accessToken = params.get('appAccessToken')
+  if (!accessToken) {
+    return false
+  }
+
+  sessionCache.set(AccessTokenKey, accessToken)
+  const tenantId = params.get('tenantId')
+  if (tenantId) {
+    localCache.set(CACHE_KEY.TenantId, tenantId)
+  }
+  sessionStorage.setItem(AppWebtopoPreviewKey, '1')
+  window.history.replaceState(
+    null,
+    document.title,
+    window.location.pathname + window.location.search
+  )
+  return true
+}
+
+export const isAppWebtopoPreview = () => {
+  return (
+    typeof sessionStorage !== 'undefined' && sessionStorage.getItem(AppWebtopoPreviewKey) === '1'
+  )
+}
+
 // 刷新token
 // 刷新token
 export const getRefreshToken = () => {
 export const getRefreshToken = () => {
   return sessionCache.get(RefreshTokenKey)
   return sessionCache.get(RefreshTokenKey)

+ 8 - 7
src/views/maotu/preview.vue

@@ -6,11 +6,7 @@
       type="button"
       type="button"
       aria-label="切换显示方向"
       aria-label="切换显示方向"
       @click="orientationSwitched = !orientationSwitched">
       @click="orientationSwitched = !orientationSwitched">
-      <svg
-        class="orientation-toggle__icon"
-        aria-hidden="true"
-        viewBox="0 0 24 24"
-        fill="none">
+      <svg class="orientation-toggle__icon" aria-hidden="true" viewBox="0 0 24 24" fill="none">
         <path
         <path
           d="M20 11a8 8 0 1 0-2.34 5.66M20 4v7h-7"
           d="M20 11a8 8 0 1 0-2.34 5.66M20 4v7h-7"
           stroke="currentColor"
           stroke="currentColor"
@@ -27,6 +23,7 @@ import { WebtopoProjectApi, parseWebtopoDataModel } from '@/api/pms/maotu'
 import { IotDeviceApi } from '@/api/pms/device'
 import { IotDeviceApi } from '@/api/pms/device'
 import type { IExportJson } from '@/components/mt-edit/components/types'
 import type { IExportJson } from '@/components/mt-edit/components/types'
 import { MtPreview } from '@/export'
 import { MtPreview } from '@/export'
+import { isAppWebtopoPreview } from '@/utils/auth'
 import { ElMessage } from 'element-plus'
 import { ElMessage } from 'element-plus'
 import { nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
 import { nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
 import { useRoute, useRouter } from 'vue-router'
 import { useRoute, useRouter } from 'vue-router'
@@ -171,8 +168,12 @@ const onEventCallBack = (type: string, item_id: string, targetProjectId?: number
   if (type == 'test-dialog') {
   if (type == 'test-dialog') {
     ElMessage.success(`获取到了id:${item_id}`)
     ElMessage.success(`获取到了id:${item_id}`)
   } else if (type == 'openWebtopo' && targetProjectId) {
   } else if (type == 'openWebtopo' && targetProjectId) {
-    const routeInfo = router.resolve(`/maotu/preview/${targetProjectId}`)
-    window.open(routeInfo.href, '_blank')
+    if (isAppWebtopoPreview()) {
+      router.replace(`/maotu/preview/${targetProjectId}`)
+    } else {
+      const routeInfo = router.resolve(`/maotu/preview/${targetProjectId}`)
+      window.open(routeInfo.href, '_blank')
+    }
   }
   }
 }
 }
 
 

+ 480 - 247
src/views/pms/stat/maintenance.vue

@@ -1,206 +1,146 @@
 <template>
 <template>
-  <!-- 第一行:统计卡片行 -->
-  <el-row :gutter="16" class="mb-4">
-    <el-col :span="6">
-      <el-card class="stat-card" shadow="never">
-        <div class="flex flex-col">
-          <div class="flex justify-between items-center text-gray-400">
-            <span>昨日工单数量</span>
-            <Icon icon="ep:menu" class="text-[32px] text-blue-400" />
-          </div>
-          <el-divider />
-          <div class="flex justify-between items-center mb-1">
-            <span class="text-gray-500 text-base font-medium" style="font-size: 14px"
-            >总数量</span
-            >
-            <span class="text-gray-500 text-base font-medium" style="font-size: 14px"
-            >未完成</span
-            >
-          </div>
-          <div class="flex justify-between items-center mb-1">
-            <span class="text-3xl font-bold text-gray-700">
-              {{ day.total }}
-            </span>
-            <span class="text-3xl font-bold text-gray-700">
-              {{ day.todo }}
-            </span>
-          </div>
-          <!--          <el-divider class="my-2" />-->
-          <!--          <div class="flex justify-between items-center text-gray-400 text-sm">-->
-          <!--            <span>今日新增</span>-->
-          <!--            <span class="text-green-500">+{{ statsData.productCategoryTodayCount }}</span>-->
-          <!--          </div>-->
-        </div>
-      </el-card>
-    </el-col>
-    <el-col :span="6">
-      <el-card class="stat-card" shadow="never">
-        <div class="flex flex-col">
-          <div class="flex justify-between items-center text-gray-400">
-            <span>近一周工单数量</span>
-            <Icon icon="ep:menu" class="text-[32px] text-blue-400" />
-          </div>
-          <el-divider />
-          <div class="flex justify-between items-center mb-1">
-            <span class="text-gray-500 text-base font-medium" style="font-size: 14px"
-            >总数量</span
-            >
-            <span class="text-gray-500 text-base font-medium" style="font-size: 14px"
-            >未完成</span
-            >
-          </div>
-          <div class="flex justify-between items-center mb-1">
-            <span class="text-3xl font-bold text-gray-700">
-              {{ week.total }}
-            </span>
-            <span class="text-3xl font-bold text-gray-700">
-              {{ week.todo }}
-            </span>
-          </div>
-        </div>
-      </el-card>
-    </el-col>
-    <el-col :span="6">
-      <el-card class="stat-card" shadow="never">
-        <div class="flex flex-col">
-          <div class="flex justify-between items-center text-gray-400">
-            <span>近一月工单数量</span>
-            <Icon icon="ep:menu" class="text-[32px] text-blue-400" />
-          </div>
-          <el-divider />
-          <div class="flex justify-between items-center mb-1">
-            <span class="text-gray-500 text-base font-medium" style="font-size: 14px"
-            >总数量</span
-            >
-            <span class="text-gray-500 text-base font-medium" style="font-size: 14px"
-            >未完成</span
-            >
-          </div>
-          <div class="flex justify-between items-center mb-1">
-            <span class="text-3xl font-bold text-gray-700">
-              {{ month.total }}
-            </span>
-            <span class="text-3xl font-bold text-gray-700">
-              {{ month.todo }}
-            </span>
-          </div>
-        </div>
-      </el-card>
-    </el-col>
-    <el-col :span="6">
-      <el-card class="stat-card" shadow="never">
-        <div class="flex flex-col">
-          <div class="flex justify-between items-center text-gray-400">
-            <span>工单数量</span>
-            <Icon icon="ep:menu" class="text-[32px] text-blue-400" />
-          </div>
-          <el-divider />
-          <div class="flex justify-between items-center mb-1">
-            <span class="text-gray-500 text-base font-medium" style="font-size: 14px"
-            >总数量</span
-            >
-            <span class="text-gray-500 text-base font-medium" style="font-size: 14px"
-            >未完成</span
-            >
-          </div>
-          <div class="flex justify-between items-center mb-1">
-            <span class="text-3xl font-bold text-gray-700">
-              {{ total.total }}
-            </span>
-            <span class="text-3xl font-bold text-gray-700">
-              {{ total.todo }}
-            </span>
-          </div>
-          <!--          <el-divider class="my-2" />-->
-          <!--          <div class="flex justify-between items-center text-gray-400 text-sm">-->
-          <!--            <span>今日新增</span>-->
-          <!--            <span class="text-green-500">+{{ statsData.productCategoryTodayCount }}</span>-->
-          <!--          </div>-->
-        </div>
-      </el-card>
-    </el-col>
-  </el-row>
-
-  <!-- 第二行:图表行 -->
-  <el-row :gutter="16" class="mb-4">
-    <el-col :span="8">
-      <el-card class="chart-card" shadow="never">
-        <template #header>
-          <div class="flex items-center">
-            <span class="text-base font-medium text-gray-600">保养工单状态统计</span>
-          </div>
-        </template>
-        <el-row class="h-[220px]">
-          <el-col :span="12" class="flex flex-col items-center">
-            <div ref="writeChartRef" class="h-[160px] w-full"></div>
-            <div class="text-center mt-2">
-              <span class="text-sm text-gray-600">待执行</span>
-            </div>
-          </el-col>
-          <el-col :span="12" class="flex flex-col items-center">
-            <div ref="finishedChartRef" class="h-[160px] w-full"></div>
-            <div class="text-center mt-2">
-              <span class="text-sm text-gray-600">已执行</span>
-            </div>
-          </el-col>
-        </el-row>
-      </el-card>
-    </el-col>
-    <el-col :span="8">
-      <el-card class="chart-card" shadow="never">
-        <template #header>
-          <div class="flex items-center">
-            <span class="text-base font-medium text-gray-600">工单类型统计</span>
-          </div>
-        </template>
-        <div ref="typeChartRef" class="h-[220px]"></div>
-      </el-card>
-    </el-col>
-    <el-col :span="8">
-      <el-card class="chart-card" shadow="never">
-        <template #header>
-          <div class="flex items-center">
-            <span class="text-base font-medium text-gray-600">今日工单状态统计</span>
-          </div>
-        </template>
-        <el-row class="h-[220px]">
-          <el-col :span="12" class="flex flex-col items-center">
-            <div ref="writeTodayChartRef" class="h-[160px] w-full"></div>
-            <div class="text-center mt-2">
-              <span class="text-sm text-gray-600">待执行</span>
-            </div>
-          </el-col>
-          <el-col :span="12" class="flex flex-col items-center">
-            <div ref="finishedTodayChartRef" class="h-[160px] w-full"></div>
-            <div class="text-center mt-2">
-              <span class="text-sm text-gray-600">已执行</span>
-            </div>
-          </el-col>
-        </el-row>
-      </el-card>
-    </el-col>
-  </el-row>
-
-  <!-- 第三行:消息统计行 -->
-  <el-row>
-    <el-col :span="24">
-      <el-card class="chart-card" shadow="never">
-        <template #header>
-          <div class="flex items-center justify-between">
-            <span class="text-base font-medium text-gray-600">近一年数量统计</span>
-          </div>
-        </template>
-        <div ref="chartContainer" class="h-[300px]"></div>
-      </el-card>
-    </el-col>
-  </el-row>
-
-  <!-- TODO 第四行:地图 -->
+  <div ref="wrapperRef" class="maintenance-kb-wrapper bg absolute top-0 left-0 size-full z-10">
+    <div class="mx-a overflow-hidden" :style="targetWrapperStyle">
+      <div class="maintenance-kb-screen bg" :style="targetAreaStyle">
+        <header class="header">保养统计</header>
+
+        <main class="maintenance-kb-content">
+          <section class="summary-grid">
+            <button class="summary-card stat-card" type="button">
+              <span class="stat-icon-wrapper">
+                <Icon icon="ep:clock" class="stat-icon" />
+              </span>
+              <span class="stat-title">昨日工单数量</span>
+              <span class="stat-pair">
+                <span
+                  ><em>总数量</em><strong>{{ day.total }}</strong></span
+                >
+                <span
+                  ><em>未完成</em><strong>{{ day.todo }}</strong></span
+                >
+              </span>
+            </button>
+
+            <button class="summary-card stat-card" type="button">
+              <span class="stat-icon-wrapper">
+                <Icon icon="ep:trend-charts" class="stat-icon" />
+              </span>
+              <span class="stat-title">近一周工单数量</span>
+              <span class="stat-pair">
+                <span
+                  ><em>总数量</em><strong>{{ week.total }}</strong></span
+                >
+                <span
+                  ><em>未完成</em><strong>{{ week.todo }}</strong></span
+                >
+              </span>
+            </button>
+
+            <button class="summary-card stat-card" type="button">
+              <span class="stat-icon-wrapper">
+                <Icon icon="ep:calendar" class="stat-icon" />
+              </span>
+              <span class="stat-title">近一月工单数量</span>
+              <span class="stat-pair">
+                <span
+                  ><em>总数量</em><strong>{{ month.total }}</strong></span
+                >
+                <span
+                  ><em>未完成</em><strong>{{ month.todo }}</strong></span
+                >
+              </span>
+            </button>
+
+            <button class="summary-card stat-card" type="button">
+              <span class="stat-icon-wrapper">
+                <Icon icon="ep:pie-chart" class="stat-icon" />
+              </span>
+              <span class="stat-title">工单总数量</span>
+              <span class="stat-pair">
+                <span
+                  ><em>总数量</em><strong>{{ total.total }}</strong></span
+                >
+                <span
+                  ><em>未完成</em><strong>{{ total.todo }}</strong></span
+                >
+              </span>
+            </button>
+          </section>
+
+          <section class="status-grid">
+            <el-card class="panel chart-panel" shadow="never">
+              <div class="panel-title">
+                <div class="icon-decorator"><span></span><span></span></div>
+                保养工单状态统计
+              </div>
+              <div class="gauge-grid">
+                <div class="chart-item">
+                  <div ref="writeChartRef" class="gauge-chart"></div>
+                  <div class="chart-label">
+                    <span class="label-dot label-dot-indigo"></span>
+                    <span class="label-text">待执行</span>
+                  </div>
+                </div>
+                <div class="chart-item">
+                  <div ref="finishedChartRef" class="gauge-chart"></div>
+                  <div class="chart-label">
+                    <span class="label-dot label-dot-emerald"></span>
+                    <span class="label-text">已执行</span>
+                  </div>
+                </div>
+              </div>
+            </el-card>
+
+            <el-card class="panel chart-panel" shadow="never">
+              <div class="panel-title">
+                <div class="icon-decorator"><span></span><span></span></div>
+                工单类型统计
+              </div>
+              <div ref="typeChartRef" class="type-chart"></div>
+            </el-card>
+
+            <el-card class="panel chart-panel" shadow="never">
+              <div class="panel-title">
+                <div class="icon-decorator"><span></span><span></span></div>
+                今日工单状态统计
+              </div>
+              <div class="gauge-grid">
+                <div class="chart-item">
+                  <div ref="writeTodayChartRef" class="gauge-chart"></div>
+                  <div class="chart-label">
+                    <span class="label-dot label-dot-indigo"></span>
+                    <span class="label-text">待执行</span>
+                  </div>
+                </div>
+                <div class="chart-item">
+                  <div ref="finishedTodayChartRef" class="gauge-chart"></div>
+                  <div class="chart-label">
+                    <span class="label-dot label-dot-emerald"></span>
+                    <span class="label-text">已执行</span>
+                  </div>
+                </div>
+              </div>
+            </el-card>
+          </section>
+
+          <section class="trend-section">
+            <el-card class="panel trend-panel" shadow="never">
+              <div class="panel-title">
+                <div class="icon-decorator"><span></span><span></span></div>
+                近一年数量统计
+              </div>
+              <div ref="chartContainer" class="bar-chart-container"></div>
+            </el-card>
+          </section>
+        </main>
+      </div>
+    </div>
+  </div>
 </template>
 </template>
 
 
 <script setup lang="ts" name="Index">
 <script setup lang="ts" name="Index">
 import * as echarts from 'echarts/core'
 import * as echarts from 'echarts/core'
-import { BarChart } from 'echarts/charts'; // 显式导入柱状图模块
+import { BarChart } from 'echarts/charts' // 显式导入柱状图模块
 
 
 import {
 import {
   GridComponent,
   GridComponent,
@@ -218,6 +158,7 @@ import {
 } from '@/api/iot/statistics'
 } from '@/api/iot/statistics'
 import { formatDate } from '@/utils/formatTime'
 import { formatDate } from '@/utils/formatTime'
 import { IotStatApi } from '@/api/pms/stat'
 import { IotStatApi } from '@/api/pms/stat'
+import { DESIGN_HEIGHT, DESIGN_WIDTH } from '@/utils/kb'
 
 
 // TODO @super:参考下 /Users/yunai/Java/yudao-ui-admin-vue3/src/views/mall/home/index.vue,拆一拆组件
 // TODO @super:参考下 /Users/yunai/Java/yudao-ui-admin-vue3/src/views/mall/home/index.vue,拆一拆组件
 
 
@@ -248,16 +189,44 @@ const queryParams = reactive({
   endTime: Date.now() // 设置默认结束时间为当前时间
   endTime: Date.now() // 设置默认结束时间为当前时间
 })
 })
 
 
+const wrapperRef = ref<HTMLDivElement>()
+const scale = ref(1)
+
+let resizeObserver: ResizeObserver | null = null
+let resizeRaf = 0
+
+const targetWrapperStyle = computed(() => ({
+  width: `${DESIGN_WIDTH * scale.value}px`,
+  height: `${DESIGN_HEIGHT * scale.value}px`
+}))
+
+const targetAreaStyle = computed(() => ({
+  '--kb-scale': scale.value,
+  width: `${DESIGN_WIDTH * scale.value}px`,
+  height: `${DESIGN_HEIGHT * scale.value}px`
+}))
+
+const updateScale = () => {
+  cancelAnimationFrame(resizeRaf)
+  resizeRaf = requestAnimationFrame(() => {
+    const wrapper = wrapperRef.value
+    if (!wrapper) return
+
+    const { clientWidth, clientHeight } = wrapper
+    if (!clientWidth || !clientHeight) return
+
+    scale.value = Math.min(clientWidth / DESIGN_WIDTH, clientHeight / DESIGN_HEIGHT)
+    nextTick(handleResize)
+  })
+}
+
 const typeChartRef = ref() // 设备数量统计的图表
 const typeChartRef = ref() // 设备数量统计的图表
-const reportingChartRef = ref() // 在线设备统计的图表
-const dealFinishedChartRef = ref() // 离线设备统计的图表
-const transOrderChartRef = ref() // 待激活设备统计的图表
-const orderFinishChartRef = ref()
 const deviceMessageCountChartRef = ref() // 上下行消息量统计的图表
 const deviceMessageCountChartRef = ref() // 上下行消息量统计的图表
 const writeChartRef = ref() // 上下行消息量统计的图表
 const writeChartRef = ref() // 上下行消息量统计的图表
 const finishedChartRef = ref() // 上下行消息量统计的图表
 const finishedChartRef = ref() // 上下行消息量统计的图表
 const writeTodayChartRef = ref() // 上下行消息量统计的图表
 const writeTodayChartRef = ref() // 上下行消息量统计的图表
 const finishedTodayChartRef = ref() // 上下行消息量统计的图表
 const finishedTodayChartRef = ref() // 上下行消息量统计的图表
+const chartInstances: echarts.ECharts[] = []
 // 基础统计数据
 // 基础统计数据
 // TODO @super:初始为 -1,然后界面展示先是加载中?试试用 cursor 改哈
 // TODO @super:初始为 -1,然后界面展示先是加载中?试试用 cursor 改哈
 const statsData = ref<IotStatisticsSummaryRespVO>({
 const statsData = ref<IotStatisticsSummaryRespVO>({
@@ -396,46 +365,52 @@ const getStats = async () => {
 
 
 /** 初始化图表 */
 /** 初始化图表 */
 const initCharts = () => {
 const initCharts = () => {
+  disposeChartInstances()
+
   // 设备数量统计
   // 设备数量统计
-  echarts.init(typeChartRef.value).setOption({
-    tooltip: {
-      trigger: 'item'
-    },
-    legend: {
-      top: '5%',
-      right: '10%',
-      align: 'left',
-      orient: 'vertical',
-      icon: 'circle'
-    },
-    series: [
-      {
-        name: '',
-        type: 'pie',
-        radius: ['50%', '80%'],
-        avoidLabelOverlap: false,
-        center: ['30%', '50%'],
-        label: {
-          show: false,
-          position: 'outside'
-        },
-        emphasis: {
+  if (typeChartRef.value) {
+    const typeChart = echarts.init(typeChartRef.value)
+    chartInstances.push(typeChart)
+    typeChart.setOption({
+      tooltip: {
+        trigger: 'item'
+      },
+      legend: {
+        top: '5%',
+        right: '10%',
+        align: 'left',
+        orient: 'vertical',
+        icon: 'circle'
+      },
+      series: [
+        {
+          name: '',
+          type: 'pie',
+          radius: ['50%', '80%'],
+          avoidLabelOverlap: false,
+          center: ['30%', '50%'],
           label: {
           label: {
-            show: true,
-            fontSize: 20,
-            fontWeight: 'bold'
-          }
-        },
-        labelLine: {
-          show: false
-        },
-        data: Object.entries(typeData.value).map(([name, value]) => ({
-          name,
-          value
-        }))
-      }
-    ]
-  })
+            show: false,
+            position: 'outside'
+          },
+          emphasis: {
+            label: {
+              show: true,
+              fontSize: 20,
+              fontWeight: 'bold'
+            }
+          },
+          labelLine: {
+            show: false
+          },
+          data: Object.entries(typeData.value).map(([name, value]) => ({
+            name,
+            value
+          }))
+        }
+      ]
+    })
+  }
   //待执行
   //待执行
   initGaugeChart(
   initGaugeChart(
     writeTodayChartRef.value,
     writeTodayChartRef.value,
@@ -466,7 +441,11 @@ const initCharts = () => {
 
 
 /** 初始化仪表盘图表 */
 /** 初始化仪表盘图表 */
 const initGaugeChart = (el: any, value: number, color: string) => {
 const initGaugeChart = (el: any, value: number, color: string) => {
-  echarts.init(el).setOption({
+  if (!el) return
+
+  const chart = echarts.init(el)
+  chartInstances.push(chart)
+  chart.setOption({
     series: [
     series: [
       {
       {
         type: 'gauge',
         type: 'gauge',
@@ -630,7 +609,11 @@ const initMessageChart = () => {
 }
 }
 
 
 const chartContainer = ref(null)
 const chartContainer = ref(null)
-let chartInstance = null
+let chartInstance: echarts.ECharts | null = null
+
+const disposeChartInstances = () => {
+  chartInstances.splice(0).forEach((instance) => instance.dispose())
+}
 
 
 // 生成过去12个月份的标签 (格式: YYYY-MM)
 // 生成过去12个月份的标签 (格式: YYYY-MM)
 const generateMonthLabels = () => {
 const generateMonthLabels = () => {
@@ -652,8 +635,8 @@ const fetchChartData = async () => {
     setTimeout(() => {
     setTimeout(() => {
       resolve({
       resolve({
         months: generateMonthLabels(),
         months: generateMonthLabels(),
-        faults: [20,30,100,40,20,50,70,80,60,90,100,100],
-        repairs: [10,30,90,30,10,20,60,50,22,34,70,85],
+        faults: [20, 30, 100, 40, 20, 50, 70, 80, 60, 90, 100, 100],
+        repairs: [10, 30, 90, 30, 10, 20, 60, 50, 22, 34, 70, 85]
       })
       })
     }, 300)
     }, 300)
   })
   })
@@ -721,28 +704,278 @@ const initChart = async () => {
   }
   }
 
 
   // 初始化图表
   // 初始化图表
+  chartInstance?.dispose()
   chartInstance = echarts.init(chartContainer.value)
   chartInstance = echarts.init(chartContainer.value)
   chartInstance.setOption(option)
   chartInstance.setOption(option)
 
 
-  // 窗口缩放监听
-  window.addEventListener('resize', handleResize)
   handleResize()
   handleResize()
 }
 }
 
 
 // 自适应调整
 // 自适应调整
 const handleResize = () => {
 const handleResize = () => {
   chartInstance?.resize()
   chartInstance?.resize()
+  chartInstances.forEach((instance) => instance.resize())
 }
 }
 /** 初始化 */
 /** 初始化 */
 onMounted(() => {
 onMounted(() => {
+  nextTick(updateScale)
+  resizeObserver = new ResizeObserver(updateScale)
+  if (wrapperRef.value) {
+    resizeObserver.observe(wrapperRef.value)
+  }
+  window.addEventListener('resize', updateScale)
+
   getStats()
   getStats()
   initChart()
   initChart()
 })
 })
 onUnmounted(() => {
 onUnmounted(() => {
+  resizeObserver?.disconnect()
+  window.removeEventListener('resize', updateScale)
+  cancelAnimationFrame(resizeRaf)
+  disposeChartInstances()
   chartInstance?.dispose()
   chartInstance?.dispose()
-  window.removeEventListener('resize', handleResize)
+  chartInstance = null
 })
 })
 </script>
 </script>
 
 
 <style lang="scss" scoped>
 <style lang="scss" scoped>
+@import url('@/styles/kb.scss');
+
+.maintenance-kb-wrapper {
+  overflow: hidden;
+}
+
+.maintenance-kb-screen {
+  display: flex;
+  overflow: hidden;
+  color: #24364f;
+  flex-direction: column;
+}
+
+.maintenance-kb-content {
+  display: flex;
+  height: calc(100% - 52px * var(--kb-scale));
+  min-height: 0;
+  padding: calc(44px * var(--kb-scale)) calc(20px * var(--kb-scale)) calc(20px * var(--kb-scale));
+  flex-direction: column;
+  gap: calc(12px * var(--kb-scale));
+}
+
+.summary-grid {
+  display: grid;
+  grid-template-columns: repeat(4, minmax(0, 1fr));
+  gap: calc(12px * var(--kb-scale));
+  flex: 0 0 calc(154px * var(--kb-scale));
+}
+
+.status-grid {
+  display: grid;
+  min-height: 0;
+  grid-template-columns: repeat(3, minmax(0, 1fr));
+  gap: calc(12px * var(--kb-scale));
+  flex: 0 0 calc(300px * var(--kb-scale));
+}
+
+.trend-section {
+  min-height: 0;
+  flex: 1;
+}
+
+.chart-panel,
+.trend-panel {
+  height: 100%;
+  overflow: hidden;
+
+  :deep(.el-card__body) {
+    display: flex;
+    height: 100%;
+    min-height: 0;
+    padding: 0;
+    flex-direction: column;
+  }
+}
+
+.stat-card {
+  display: grid;
+  height: 100%;
+  min-width: 0;
+  padding: calc(14px * var(--kb-scale));
+  text-align: left;
+  cursor: default;
+  background: linear-gradient(180deg, rgb(255 255 255 / 42%) 0%, rgb(213 227 249 / 28%) 100%);
+  border: 1px solid rgb(255 255 255 / 74%);
+  border-radius: calc(16px * var(--kb-scale));
+  box-shadow:
+    inset 0 1px 0 rgb(255 255 255 / 78%),
+    0 calc(6px * var(--kb-scale)) calc(14px * var(--kb-scale)) rgb(63 103 171 / 7%);
+  transition: all 0.28s ease;
+  grid-template-rows: auto 1fr;
+  grid-template-columns: auto 1fr;
+  gap: calc(8px * var(--kb-scale)) calc(12px * var(--kb-scale));
+
+  &:hover {
+    border-color: rgb(76 132 214 / 26%);
+    transform: translateY(calc(-2px * var(--kb-scale)));
+    box-shadow:
+      inset 0 1px 0 rgb(255 255 255 / 84%),
+      0 calc(10px * var(--kb-scale)) calc(20px * var(--kb-scale)) rgb(63 103 171 / 11%);
+  }
+}
+
+.stat-icon-wrapper {
+  display: flex;
+  width: calc(44px * var(--kb-scale));
+  height: calc(44px * var(--kb-scale));
+  background: linear-gradient(180deg, rgb(255 255 255 / 96%) 0%, rgb(232 240 252 / 92%) 100%);
+  border: 1px solid rgb(205 219 239 / 90%);
+  border-radius: calc(12px * var(--kb-scale));
+  box-shadow:
+    inset 0 1px 0 rgb(255 255 255 / 95%),
+    inset 0 calc(-6px * var(--kb-scale)) calc(12px * var(--kb-scale)) rgb(205 220 242 / 34%),
+    0 calc(6px * var(--kb-scale)) calc(14px * var(--kb-scale)) rgb(66 104 168 / 8%);
+  align-items: center;
+  justify-content: center;
+}
+
+.stat-icon {
+  font-size: calc(24px * var(--kb-scale));
+  color: #1f5bb8;
+}
+
+.stat-title {
+  min-width: 0;
+  font-family: YouSheBiaoTiHei, sans-serif;
+  font-size: calc(22px * var(--kb-scale));
+  font-weight: normal;
+  line-height: 1.1;
+  color: #03409b;
+}
+
+.stat-pair {
+  display: grid;
+  min-width: 0;
+  grid-column: 1 / -1;
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+  gap: calc(10px * var(--kb-scale));
+
+  span {
+    display: flex;
+    min-width: 0;
+    padding: calc(8px * var(--kb-scale)) calc(10px * var(--kb-scale));
+    overflow: hidden;
+    background: linear-gradient(180deg, rgb(255 255 255 / 30%) 0%, rgb(213 227 249 / 18%) 100%);
+    border: 1px solid rgb(255 255 255 / 48%);
+    border-radius: calc(10px * var(--kb-scale));
+    flex-direction: column;
+    gap: calc(6px * var(--kb-scale));
+  }
+
+  em {
+    overflow: hidden;
+    font-size: calc(12px * var(--kb-scale));
+    font-style: normal;
+    font-weight: 700;
+    color: #536986;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+  }
+
+  strong {
+    font-family: YouSheBiaoTiHei, sans-serif;
+    font-size: calc(28px * var(--kb-scale));
+    font-weight: normal;
+    line-height: 1;
+    color: #1f5bb8;
+  }
+}
+
+.gauge-grid {
+  display: grid;
+  min-height: 0;
+  padding: calc(12px * var(--kb-scale)) calc(14px * var(--kb-scale)) calc(16px * var(--kb-scale));
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+  gap: calc(12px * var(--kb-scale));
+  flex: 1;
+}
+
+.chart-item {
+  display: flex;
+  min-width: 0;
+  min-height: 0;
+  padding: calc(8px * var(--kb-scale));
+  background: linear-gradient(180deg, rgb(255 255 255 / 42%) 0%, rgb(213 227 249 / 28%) 100%);
+  border: 1px solid rgb(255 255 255 / 54%);
+  border-radius: calc(14px * var(--kb-scale));
+  box-shadow:
+    inset 0 1px 0 rgb(255 255 255 / 64%),
+    0 calc(5px * var(--kb-scale)) calc(12px * var(--kb-scale)) rgb(63 103 171 / 6%);
+  align-items: center;
+  flex-direction: column;
+}
+
+.gauge-chart {
+  width: 100%;
+  min-height: 0;
+  flex: 1;
+}
+
+.chart-label {
+  display: flex;
+  height: calc(28px * var(--kb-scale));
+  padding: 0 calc(10px * var(--kb-scale));
+  background: linear-gradient(180deg, rgb(255 255 255 / 88%) 0%, rgb(242 247 255 / 76%) 100%);
+  border: 1px solid rgb(216 228 244 / 78%);
+  border-radius: calc(999px * var(--kb-scale));
+  box-shadow:
+    inset 0 1px 0 rgb(255 255 255 / 90%),
+    0 calc(4px * var(--kb-scale)) calc(10px * var(--kb-scale)) rgb(58 90 145 / 6%);
+  align-items: center;
+  gap: calc(6px * var(--kb-scale));
+}
+
+.label-dot {
+  width: calc(8px * var(--kb-scale));
+  height: calc(8px * var(--kb-scale));
+  border-radius: 50%;
+}
+
+.label-dot-indigo {
+  background: #6366f1;
+  box-shadow: 0 0 0 2px rgb(99 102 241 / 20%);
+}
+
+.label-dot-emerald {
+  background: #10b981;
+  box-shadow: 0 0 0 2px rgb(16 185 129 / 20%);
+}
+
+.label-text {
+  overflow: hidden;
+  font-size: calc(13px * var(--kb-scale));
+  font-weight: 700;
+  color: #536986;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.type-chart,
+.bar-chart-container {
+  width: 100%;
+  min-height: 0;
+  flex: 1;
+}
+
+.type-chart {
+  padding: calc(8px * var(--kb-scale)) calc(12px * var(--kb-scale)) calc(14px * var(--kb-scale));
+}
+
+.bar-chart-container {
+  padding: calc(8px * var(--kb-scale)) calc(12px * var(--kb-scale)) calc(14px * var(--kb-scale));
+  background: transparent;
+  box-shadow: none;
+}
+
+:deep(.el-card) {
+  --el-card-bg-color: transparent;
+}
 </style>
 </style>