Quellcode durchsuchen

新增ONlineStatusTabs和DeviceStatusTabs组件;修改Home页的在线状态面板和设备状态面板;修改DynmicDonutChart组件和DeviceStatusDonutChart组件自适应大小缩放;修改StatusMonitoring页面显示顶部图表信息;

画安 vor 1 Monat
Ursprung
Commit
7189a157ac

+ 9 - 7
src/components/ui/DeviceStatusDonutChart.vue

@@ -170,8 +170,10 @@ export default {
                             };
                         },
                         data: seriesData,
-                        animationType: 'scale',
-                        animationEasing: 'elasticOut'
+                        animationType: 'expansion',
+                        animationEasing: 'elasticOut',
+                        animationDuration: 2000, 
+                        animationDelay: 200,
                     }
                 ]
             };
@@ -185,7 +187,7 @@ export default {
 <style scoped>
 .donut-chart-wrapper {
     display: flex;
-    align-items: center;
+    align-items: stretch;
     justify-content: space-between;
     width: 100%;
     height: 100%;
@@ -194,15 +196,15 @@ export default {
 /* 左侧饼图容器 */
 .echarts-wrapper {
     position: relative;
-    width: 65%; /* 占据左边 65% 空间 */
-    height: 100%;
-    min-height: 140px;
+    width: 65%;
+    flex: 1;
+    min-height: 0;
 }
 
 .echarts-container {
     width: 100%;
     height: 100%;
-    min-height: 160px;
+    min-height: 0;
 }
 
 /* 饼图中心数字 */

+ 98 - 0
src/components/ui/DeviceStatusTabs.vue

@@ -0,0 +1,98 @@
+<template>
+  <div class="device-status-tabs">
+    <TechTabs v-model="activeTab" :interval="10000" type="segmented" autoPlay @tab-click="handleTabClick">
+      <TechTabPane label="信号机" name="signalMachineStatus">
+        <DeviceStatusDonutChart v-if="displayData" :chartData="displayData.chartData" />
+      </TechTabPane>
+      <TechTabPane label="检测器" name="detectorStatus">
+        <DeviceStatusDonutChart v-if="displayData" :chartData="displayData.chartData" />
+      </TechTabPane>
+      <TechTabPane label="红绿灯" name="trafficLightStatus">
+        <DeviceStatusDonutChart v-if="displayData" :chartData="displayData.chartData" />
+      </TechTabPane>
+    </TechTabs>
+  </div>
+</template>
+
+<script>
+import TechTabs from '@/components/ui/TechTabs.vue';
+import TechTabPane from '@/components/ui/TechTabPane.vue';
+import DeviceStatusDonutChart from '@/components/ui/DeviceStatusDonutChart.vue';
+
+// 提取 Mock 数据作为默认值
+const defaultMockStatusData = {
+  'signalMachineStatus': {
+    centerTitle: '98%',
+    centerSubTitle: '980/1000',
+    chartData: [
+        { name: '正常', value: 1, color: '#A0E551' }, 
+        { name: '故障', value: 0, color: '#D03030' } 
+    ]
+  },
+  'detectorStatus': {
+    centerTitle: '85%',
+    centerSubTitle: '425/500',
+    chartData: [
+        { name: '通信故障', value: 4, color: '#C6302B' } 
+    ]
+  },
+  'trafficLightStatus': {
+    centerTitle: '99%',
+    centerSubTitle: '1188/1200',
+    chartData: [
+        { name: '红绿冲突', value: 2, color: '#C6302B' }, 
+        { name: '红灯故障', value: 2, color: '#8F1E1E' }  
+    ]
+  }
+};
+
+export default {
+  name: 'DeviceStatusTabs',
+  components: {
+    TechTabs,
+    TechTabPane,
+    DeviceStatusDonutChart
+  },
+  props: {
+    // 接收父组件传入的数据
+    statusData: {
+      type: Object,
+      // 如果父组件没传,就使用 mock 数据演示
+      default: () => defaultMockStatusData
+    }
+  },
+  data() {
+    return {
+      activeTab: 'signalMachineStatus',
+      displayData: null
+    };
+  },
+  watch: {
+    statusData: {
+      deep: true,
+      immediate: true,
+      handler(newData) {
+        if (newData && newData[this.activeTab]) {
+          this.displayData = newData[this.activeTab];
+        }
+      }
+    }
+  },
+  methods: {
+    handleTabClick(selectedTabName) {
+      if (this.statusData && this.statusData[selectedTabName]) {
+        this.displayData = this.statusData[selectedTabName];
+      }
+    }
+  }
+};
+</script>
+
+<style scoped>
+.device-status-tabs {
+  width: 100%;
+  height: 100%;
+  display: flex; 
+  flex-direction: column;
+}
+</style>

+ 92 - 72
src/components/ui/DynamicDonutChart.vue

@@ -11,10 +11,10 @@
             <div class="echarts-container" ref="chartRef"></div>
             
             <div class="center-text-overlay">
-                <div class="main-text" :style="{ fontSize: safePx2echarts(26) + 'px' }">
-                    {{ animatedValue }}{{ unit }}
+                <div class="main-text">
+                    {{ animatedValue }}<span class="unit-text">{{ unit }}</span>
                 </div>
-                <div class="sub-text" :style="{ fontSize: safePx2echarts(14) + 'px', marginTop: safePx2echarts(4) + 'px' }">
+                <div class="sub-text">
                     {{ centerSubTitle }}
                 </div>
             </div>
@@ -24,18 +24,20 @@
 
 <script>
 import * as echarts from 'echarts';
+// 引入你的 mixin
 import echartsResize, { px2echarts } from '@/mixins/echartsResize.js';
 
 export default {
     name: 'DynamicDonutChart',
-    mixins: [echartsResize],
+    mixins: [echartsResize], // 注册 mixin
     props: {
         chartData: {
             type: Array,
-            required: true
+            required: true,
+            default: () => []
         },
         centerTitle: {
-            type: [String, Number], // 兼容父组件传入数字的情况
+            type: [String, Number],
             default: '0'
         },
         centerSubTitle: {
@@ -47,115 +49,124 @@ export default {
         return {
             animatedValue: 0,
             unit: '',
-            rafId: null
+            rafId: null,
+            dataWatchTimer: null // 用于数据监听的防抖
+            // 注意:不要在这里声明 $_chart 和 $_resizeObserver,mixin 已经代劳了
         };
     },
     watch: {
+        // 数据变化时防抖更新,避免前面提到的“连击”Bug
         chartData: {
             deep: true,
             handler() {
-                this.$nextTick(() => {
-                    if (this.$_chart) {
-                        this.$_chart.resize();
-                    }
+                if (this.dataWatchTimer) clearTimeout(this.dataWatchTimer);
+                this.dataWatchTimer = setTimeout(() => {
                     this.updateChart();
-                });
+                }, 50);
             }
         },
+        // 标题变化时,重新触发数字动画
         centerTitle() {
-            this.$nextTick(() => this.updateChart());
-        },
-        centerSubTitle() {
-            this.$nextTick(() => this.updateChart());
+            this.playNumberAnimation();
         }
     },
     mounted() {
-        // 使用 nextTick 确保 DOM 和 CSS 完全渲染,防止获取不到宽高的 0x0 bug
+        // 组件挂载时初始化图表
         this.$nextTick(() => {
-            this.initChart();
+            if (this.$refs.chartRef && !this.$_chart) {
+                // 注意:必须赋值给 this.$_chart,这是 mixin 识别的变量名!
+                this.$_chart = echarts.init(this.$refs.chartRef);
+                this.updateChart();
+                this.playNumberAnimation();
+            }
         });
     },
     beforeDestroy() {
-        if (this.resizeObserver) this.resizeObserver.disconnect();
-        if (this.$_chart) this.$_chart.dispose();
+        // 销毁组件内部的动画和定时器
         if (this.rafId) cancelAnimationFrame(this.rafId);
+        if (this.dataWatchTimer) clearTimeout(this.dataWatchTimer);
+        // ECharts 实例和 ResizeObserver 的销毁,你的 mixin 已经在 beforeDestroy 里处理了,这里无需再写
     },
     methods: {
-        // 防御性包装像素转换函数,防止在 template 中调用失败
         safePx2echarts(val) {
             return typeof px2echarts === 'function' ? px2echarts(val) : val;
         },
 
-        initChart() {
-            if (!this.$refs.chartRef) return;
-            this.$_chart = echarts.init(this.$refs.chartRef);
-            this.updateChart();
-        },
-
+        // 【核心】:方法名必须是 updateChart,配合你的 mixin 调用
         updateChart() {
             if (!this.$_chart) return;
 
-            // 清理上一轮未完成的动画
-            if (this.rafId) cancelAnimationFrame(this.rafId);
-            this.$_chart.clear();
-
-            // 1. 数据防御性处理 (防止 replace 报错中断渲染)
-            const safeTitle = String(this.centerTitle || '0');
-            const targetNumber = parseFloat(safeTitle) || 0;
-            this.unit = safeTitle.replace(/[0-9.]/g, '');
-            this.animatedValue = 0; 
-            
             const colorPalette = this.chartData.map(item => item.color);
-            const SYNC_DURATION = 1000;
 
-            // 2. 纯净的 ECharts 配置
             const option = {
                 color: colorPalette,
-                title: { show: false },
+                tooltip: {
+                    trigger: 'item',
+                    backgroundColor: 'rgba(13, 27, 62, 0.8)',
+                    borderColor: '#1FCEFB',
+                    textStyle: { color: '#fff' }
+                },
                 series: [
                     {
+                        name: '设备状态',
                         type: 'pie',
-                        radius: ['60%', '80%'],
+                        radius: ['60%', '80%'], // 内外圈比例
                         center: ['50%', '50%'],
                         avoidLabelOverlap: false,
                         label: { show: false },
                         labelLine: { show: false },
-                        // 同步初始化与数据更新时的动画配置
-                        animationType: 'expansion', 
-                        animationDuration: SYNC_DURATION,
-                        animationDurationUpdate: SYNC_DURATION, 
+                        animationType: 'expansion', // 动画
                         animationEasing: 'cubicOut',
-                        animationEasingUpdate: 'cubicOut',
+                        animationDuration: 2000, 
+                        animationDelay: 200,
                         data: this.chartData
                     }
                 ]
             };
 
-            // 触发 ECharts 圆环绘制
             this.$_chart.setOption(option, true);
+        },
+
+        // 独立的数字滚动动画
+        playNumberAnimation() {
+            if (this.rafId) cancelAnimationFrame(this.rafId);
 
-            // 3. 原生 JS 驱动数字动画
+            const safeTitle = String(this.centerTitle || '0');
+            const targetNumber = parseFloat(safeTitle) || 0;
+            this.unit = safeTitle.replace(/[0-9.-]/g, ''); 
+            this.animatedValue = 0;
+
+            // 👇 1. 动画总时长改为 2000 毫秒
+            const duration = 2000; 
+            // 👇 2. 延迟 200 毫秒,和图表保持一致
+            const delay = 200;     
+            
             let startTime = null;
+
             const animateStep = (timestamp) => {
                 if (!startTime) startTime = timestamp;
                 const elapsed = timestamp - startTime;
-                
-                let progress = elapsed / SYNC_DURATION;
-                if (progress > 1) progress = 1;
 
-                // 保持与 ECharts 一致的 'cubicOut' 缓动效果
-                const easeProgress = 1 - Math.pow(1 - progress, 3);
+                // 👇 3. 如果还在延迟时间内,先不跑数字
+                if (elapsed < delay) {
+                    this.rafId = requestAnimationFrame(animateStep);
+                    return;
+                }
+
+                // 计算实际的动画进度
+                let progress = Math.min((elapsed - delay) / duration, 1);
+
+                const easeProgress = 1 - Math.pow(1 - progress, 4);
                 this.animatedValue = Math.round(targetNumber * easeProgress);
 
                 if (progress < 1) {
                     this.rafId = requestAnimationFrame(animateStep);
+                } else {
+                    this.animatedValue = targetNumber; 
                 }
             };
 
-            this.rafId = requestAnimationFrame((timestamp) => {
-                startTime = timestamp; 
-                animateStep(timestamp);
-            });
+            this.rafId = requestAnimationFrame(animateStep);
         }
     }
 }
@@ -167,6 +178,7 @@ export default {
     align-items: center;
     width: 100%;
     height: 100%;
+    min-height: 0; 
 }
 
 .custom-legend {
@@ -176,43 +188,45 @@ export default {
     gap: 12px;
     width: 35%;
     margin-right: 8px;
+    padding-left: 10px;
 }
 
 .legend-box {
     display: flex;
     align-items: center;
-    background: linear-gradient(90deg, #2D426C 0%, rgba(45, 66, 108, 0) 100%);
-    height: 40px;
+    background: linear-gradient(90deg, rgba(45, 66, 108, 0.6) 0%, rgba(45, 66, 108, 0) 100%);
+    height: 32px;
+    border-radius: 4px;
 }
 
 .color-dot {
     width: 8px;
     height: 8px;
     margin-right: 8px;
-    margin-left: 16px;
+    margin-left: 12px;
     flex-shrink: 0;
     border-radius: 2px;
 }
 
 .legend-name {
     font-weight: 400;
-    font-size: 12px;
-    color: rgba(255, 255, 255, 0.60);
-    line-height: 18px;
+    font-size: 13px;
+    color: rgba(255, 255, 255, 0.8);
 }
 
-/* --- 核心修复区 --- */
 .echarts-wrapper {
     position: relative;
     width: 65%;
     height: 100%;
-    min-height: 160px; /* 强制保底高度,防止由于父级未撑开导致高度变为 0 */
+    flex: 1;
+    display: flex;
+    justify-content: center;
+    align-items: center;
 }
 
 .echarts-container {
     width: 100%;
     height: 100%;
-    min-height: 160px; /* 双重保险,确保 ECharts 画布有尺寸 */
 }
 
 .center-text-overlay {
@@ -224,20 +238,26 @@ export default {
     flex-direction: column;
     align-items: center;
     justify-content: center;
-    pointer-events: none; /* 必须穿透,否则会阻挡鼠标移动到内部饼图的事件 */
+    pointer-events: none; 
     z-index: 10;
 }
 
 .main-text {
     color: #ffffff;
     font-weight: bold;
-    font-family: Arial;
-    line-height: 1;
+    font-family: Arial, sans-serif;
+    font-size: 24px;
+    line-height: 1.2;
+}
+
+.unit-text {
+    font-size: 14px;
+    margin-left: 2px;
 }
 
 .sub-text {
-    color: #cccccc;
-    font-family: Arial;
-    line-height: 1;
+    color: rgba(255, 255, 255, 0.6);
+    font-size: 13px;
+    margin-top: 2px;
 }
 </style>

+ 115 - 0
src/components/ui/OnlineStatusTabs.vue

@@ -0,0 +1,115 @@
+<template>
+  <div class="online-status-tabs">
+    <TechTabs v-model="activeTab" :interval="10000" type="segmented"  @tab-click="handleTabClick">
+      <TechTabPane label="信号机" name="signalMachine">
+        <DynamicDonutChart 
+          v-if="activeTab === 'signalMachine' && displayData"
+          :chartData="displayData.chartData"
+          :centerTitle="displayData.centerTitle" 
+          :centerSubTitle="displayData.centerSubTitle"
+        />
+      </TechTabPane>
+      <TechTabPane label="检测器" name="detector">
+        <DynamicDonutChart 
+          v-if="activeTab === 'detector' && displayData"
+          :chartData="displayData.chartData"
+          :centerTitle="displayData.centerTitle" 
+          :centerSubTitle="displayData.centerSubTitle"
+        />
+      </TechTabPane>
+      <TechTabPane label="相机" name="camera">
+        <DynamicDonutChart 
+          v-if="activeTab === 'camera'  && displayData"
+          :chartData="displayData.chartData"
+          :centerTitle="displayData.centerTitle" 
+          :centerSubTitle="displayData.centerSubTitle"
+        />
+      </TechTabPane>
+    </TechTabs>
+  </div>
+</template>
+
+<script>
+import TechTabs from '@/components/ui/TechTabs.vue';
+import TechTabPane from '@/components/ui/TechTabPane.vue';
+import DynamicDonutChart from '@/components/ui/DynamicDonutChart.vue';
+
+// 提取 Mock 数据作为默认值
+const defaultMockData = {
+  'signalMachine': {
+    centerTitle: '82%',
+    centerSubTitle: '820/1000',
+    chartData: [
+      { name: '正常', value: 820, color: '#32F6F8' },
+      { name: '离线', value: 180, color: '#E4D552' }
+    ]
+  },
+  'detector': {
+    centerTitle: '85%',
+    centerSubTitle: '425/500',
+    chartData: [
+      { name: '正常', value: 425, color: '#32F6F8' },
+      { name: '离线', value: 75, color: '#faad14' },
+    ]
+  },
+  'camera': {
+    centerTitle: '99%',
+    centerSubTitle: '1188/1200',
+    chartData: [
+      { name: '正常', value: 1188, color: '#32F6F8' },
+      { name: '离线', value: 12, color: '#ff4d4f' }
+    ]
+  }
+};
+
+export default {
+  name: 'OnlineStatusTabs',
+  components: {
+    TechTabs,
+    TechTabPane,
+    DynamicDonutChart
+  },
+  props: {
+    // 接收父组件传入的数据
+    deviceData: {
+      type: Object,
+      // 如果父组件没传,就使用 mock 数据演示
+      default: () => defaultMockData
+    }
+  },
+  data() {
+    return {
+      activeTab: 'signalMachine',
+      displayData: null
+    };
+  },
+  watch: {
+    // 监听外部数据变化(比如接口请求回来了)
+    deviceData: {
+      deep: true,
+      immediate: true, // 组件初始化时立刻执行一次
+      handler(newData) {
+        if (newData && newData[this.activeTab]) {
+          this.displayData = newData[this.activeTab];
+        }
+      }
+    }
+  },
+  methods: {
+    handleTabClick(selectedTabName) {
+      if (this.deviceData && this.deviceData[selectedTabName]) {
+        this.displayData = this.deviceData[selectedTabName];
+      }
+    }
+  }
+};
+</script>
+
+<style scoped>
+.online-status-tabs {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+}
+</style>

+ 4 - 1
src/components/ui/TechTabPane.vue

@@ -35,6 +35,9 @@ export default {
 <style scoped>
 .tech-tab-pane {
   width: 100%;
-  height: 100%;
+  flex: 1;
+  min-height: 0;
+  display: flex;
+  flex-direction: column;
 }
 </style>

+ 5 - 1
src/components/ui/TechTabs.vue

@@ -152,13 +152,17 @@ export default {
 <style scoped>
 .tech-tabs-container {
   width: 100%;
+  height: 100%; 
   display: flex;
   flex-direction: column;
 }
 
 .tabs-content {
   flex: 1;
-  padding-top: 15px; /* 内容与头部的间距 */
+  min-height: 0; 
+  padding-top: 10px; /* 内容与头部的间距 */
+  display: flex;
+  flex-direction: column;
 }
 
 /* ================== 风格 1:下划线类型 (is-underline) ================== */

+ 5 - 1
src/layouts/DashboardLayout.vue

@@ -90,6 +90,8 @@ import SecurityRoutePanelSwitch from '@/components/ui/SecurityRoutePanelSwitch.v
 import SecurityRoutePanelSwitchSmall from '@/components/ui/SecurityRoutePanelSwitchSmall.vue';
 import DeviceRestart from '@/components/ui/DeviceRestart.vue';
 import DeviceUpgrade from '@/components/ui/DeviceUpgrade.vue';
+import OnlineStatusTabs from '@/components/ui/OnlineStatusTabs.vue';
+import DeviceStatusTabs from '@/components/ui/DeviceStatusTabs.vue';
 
 export default {
     name: 'DashboardLayout',
@@ -108,7 +110,9 @@ export default {
         SecurityRoutePanelSwitch,
         SecurityRoutePanelSwitchSmall,
         DeviceRestart,
-        DeviceUpgrade
+        DeviceUpgrade,
+        OnlineStatusTabs,
+        DeviceStatusTabs
     },
     provide() {
         return {

+ 1 - 0
src/styles/base.css

@@ -95,6 +95,7 @@ html, body {
   border: none;
   outline: 2px solid #3760A9;
   outline-offset: -2px;
+  flex: none !important;
 }
 
 .left-sidebar-wrap {

+ 6 - 109
src/views/Home.vue

@@ -22,20 +22,7 @@
         <div class="panel-item">
           <PanelContainer title="在线状态">
   
-            <TechTabs v-model="onlineStatusActiveTab" :interval="10000" type="segmented" autoPlay  @tab-click="handleTabClick">
-              <TechTabPane label="信号机" name="signalMachine" style="height: 100%;">
-                <DynamicDonutChart v-if="onlineStatusDisplayData" :chartData="onlineStatusDisplayData.chartData"
-                  :centerTitle="onlineStatusDisplayData.centerTitle" :centerSubTitle="onlineStatusDisplayData.centerSubTitle" customHeight="163px" />
-              </TechTabPane>
-              <TechTabPane label="检测器" name="detector">
-                <DynamicDonutChart v-if="onlineStatusDisplayData" :chartData="onlineStatusDisplayData.chartData"
-                  :centerTitle="onlineStatusDisplayData.centerTitle" :centerSubTitle="onlineStatusDisplayData.centerSubTitle" customHeight="163px"/>
-              </TechTabPane>
-              <TechTabPane label="相机" name="camera">
-                <DynamicDonutChart v-if="onlineStatusDisplayData" :chartData="onlineStatusDisplayData.chartData"
-                  :centerTitle="onlineStatusDisplayData.centerTitle" :centerSubTitle="onlineStatusDisplayData.centerSubTitle" customHeight="163px"/>
-              </TechTabPane>
-            </TechTabs>
+            <OnlineStatusTabs />
   
           </PanelContainer>
         </div>
@@ -65,17 +52,7 @@
         <div class="panel-item">
           <PanelContainer title="设备状态">
   
-            <TechTabs v-model="deviceStatusActiveTab" :interval="10000" type="segmented" autoPlay @tab-click="handleDeviceStatusTabClick">
-              <TechTabPane label="信号机" name="signalMachineStatus" style="height: 100%;">
-                <DeviceStatusDonutChart v-if="deviceStatusDisplayData" :chartData="deviceStatusDisplayData.chartData"/>
-              </TechTabPane>
-              <TechTabPane label="检测器" name="detectorStatus">
-                <DeviceStatusDonutChart v-if="deviceStatusDisplayData" :chartData="deviceStatusDisplayData.chartData"/>
-              </TechTabPane>
-              <TechTabPane label="红绿灯" name="trafficLightStatus">
-                <DeviceStatusDonutChart v-if="deviceStatusDisplayData" :chartData="deviceStatusDisplayData.chartData"/>
-              </TechTabPane>
-            </TechTabs>
+            <DeviceStatusTabs />
   
           </PanelContainer>
         </div>
@@ -132,71 +109,14 @@ import DashboardLayout from '@/layouts/DashboardLayout.vue';
 import WeatherWidget from '@/components/ui/WeatherWidget.vue';
 import DateTimeWidget from '@/components/ui/DateTimeWidget.vue';
 import PanelContainer from '@/components/ui/PanelContainer.vue';
-import DynamicDonutChart from '@/components/ui/DynamicDonutChart.vue';
-import TechTabs from '@/components/ui/TechTabs.vue';
-import TechTabPane from '@/components/ui/TechTabPane.vue';
 import TickDonutChart from '@/components/ui/TickDonutChart.vue';
 import AlarmMessageList from '@/components/ui/AlarmMessageList.vue';
 import TechTable from '@/components/ui/TechTable.vue';
 import TongzhouTrafficMap from '@/components/TongzhouTrafficMap.vue';
-import DeviceStatusDonutChart from '@/components/ui/DeviceStatusDonutChart.vue';
+import OnlineStatusTabs from '@/components/ui/OnlineStatusTabs.vue';
+import DeviceStatusTabs from '@/components/ui/DeviceStatusTabs.vue';
 
 
-const mockDeviceData = {
-  'signalMachine': {
-    centerTitle: '98%',
-    centerSubTitle: '980/1000',
-    chartData: [
-      { name: '正常', value: 980, color: '#32F6F8' },
-      { name: '离线', value: 20, color: '#E4D552' }
-    ]
-  },
-  'detector': {
-    centerTitle: '85%',
-    centerSubTitle: '425/500',
-    chartData: [
-      { name: '正常', value: 425, color: '#32F6F8' },
-      { name: '离线', value: 75, color: '#faad14' },
-    ]
-  },
-  'camera': {
-    centerTitle: '99%',
-    centerSubTitle: '1188/1200',
-    chartData: [
-      { name: '正常', value: 1188, color: '#32F6F8' },
-      { name: '离线', value: 12, color: '#ff4d4f' }
-    ]
-  }
-};
-
-const mockDeviceStatusData = {
-  'signalMachineStatus': {
-    centerTitle: '98%',
-    centerSubTitle: '980/1000',
-    chartData: [
-        { name: '正常', value: 1, color: '#A0E551' }, // 这里的 value 多少无所谓,主要取颜色
-        { name: '故障', value: 0, color: '#D03030' } 
-    ]
-  },
-  'detectorStatus': {
-    centerTitle: '85%',
-    centerSubTitle: '425/500',
-    chartData: [
-        // { name: '正常', value: 0, color: '#A0E551' }, 
-        { name: '通信故障', value: 4, color: '#C6302B' } 
-    ]
-  },
-  'trafficLightStatus': {
-    centerTitle: '99%',
-    centerSubTitle: '1188/1200',
-    chartData: [
-        // { name: '正常', value: 0, color: '#A0E551' },
-        { name: '红绿冲突', value: 2, color: '#C6302B' }, // 亮红
-        { name: '红灯故障', value: 2, color: '#8F1E1E' }  // 暗红
-    ]
-  }
-};
-
 export default {
   name: "HomePage",
   components: {
@@ -204,22 +124,16 @@ export default {
     WeatherWidget,
     DateTimeWidget,
     PanelContainer,
-    DynamicDonutChart,
-    TechTabs,
-    TechTabPane,
     TickDonutChart,
     AlarmMessageList,
     TechTable,
     TongzhouTrafficMap,
-    DeviceStatusDonutChart
+    OnlineStatusTabs,
+    DeviceStatusTabs
   },
   data() {
     return {
       // 在线状态面板
-      onlineStatusActiveTab: 'signalMachine',
-      onlineStatusDisplayData: mockDeviceData['signalMachine'],
-      deviceStatusActiveTab: 'signalMachineStatus',
-      deviceStatusDisplayData: mockDeviceStatusData['signalMachineStatus'],
       controlInfoData: [
         { name: '定周期控制', value: 400, color: '#33a3ff' }, // 蓝色
         { name: '感应控制',   value: 50,  color: '#e6734d' }, // 橙色
@@ -297,23 +211,6 @@ export default {
 
   },
   methods: {
-    // 监听 Tab 切换事件
-    handleTabClick(selectedTabName) {
-      console.log('用户切换了设备类型:', selectedTabName);
-
-      // 从 mock 字典中取出对应的数据并赋值,图表会自动响应式更新!
-      if (mockDeviceData[selectedTabName]) {
-        this.onlineStatusDisplayData = mockDeviceData[selectedTabName];
-      }
-    },
-    handleDeviceStatusTabClick(selectedTabName) {
-      console.log('用户切换了设备类型:', selectedTabName);
-
-      // 从 mock 字典中取出对应的数据并赋值,图表会自动响应式更新!
-      if (mockDeviceStatusData[selectedTabName]) {
-        this.deviceStatusDisplayData = mockDeviceStatusData[selectedTabName];
-      }
-    },
     // 处理忽略逻辑
     onAlarmIgnore({ item, index }) {
       console.log('点击了忽略:', item.title);

+ 90 - 139
src/views/StatusMonitoring.vue

@@ -60,7 +60,7 @@ import TechTabPane from '@/components/ui/TechTabPane.vue';
 import TongzhouTrafficMap from '@/components/TongzhouTrafficMap.vue';
 import MenuItem from '@/components/ui/MenuItem.vue';
 import ButtonGroup from '@/components/ui/ButtonGroup.vue';
-import { getIntersectionData, makeTrafficTimeSpaceData } from '@/mock/data';
+import { makeTrafficTimeSpaceData } from '@/mock/data';
 
 
 export default {
@@ -212,6 +212,7 @@ export default {
 
     },
     mounted() {
+        this.showTopChartDalogs();
         // this.$refs.layout.openDialog({
         //         id: 'test', // 这里的 ID 可以根据实际业务场景动态生成,例如 'dev-security-route' 代表特勤安保路线弹窗
         //         title: 'dddd',
@@ -244,27 +245,24 @@ export default {
                     showClose: true,
                     noPadding: false,
                     enableDblclickExpand: false,
-                    position: {x: 100, y:150},
+                    position: { x: 100, y: 150 },
                     data: {
                         onViewDetail: (rowData) => this.handleCrossingViewDetail(rowData)
                     }
                 });
+            } else {
+                this.showCrossingTopDialogs();
             }
         },
         // 处理tab点击
-        handleTabClick(nodeData) {
-            console.log('父组件接收到了tab点击事件:', nodeData);
+        handleTabClick(tabName) {
+            console.log('父组件接收到了tab点击事件:', tabName);
             this.$refs.layout.clearDialogs(); // 清空全部弹窗
+            this.showTopChartDalogs(); // 根据当前Tab显示对应的顶部常驻图表
         },
         // 处理菜单点击
         handleMenuClick(nodeData) {
             console.log('父组件接收到了最底层路口点击事件:', nodeData);
-            // 这里可以根据 nodeData 的经纬度来控制地图组件的视角
-            // this.testOpenDeviceStatus();
-            // this.testOpenSecurityRoute();
-            // this.testOpenSecurityRoute2();
-            // this.testOpenTrafficTimeSpace();
-
             // 根据Tab来显示不同的弹窗内容
             if (this.activeLeftTab === 'overview') { // 总览
                 this.showOverviewDalogs(nodeData);
@@ -284,57 +282,55 @@ export default {
                 this.showCrossingDetailDialogs(nodeData);
             }
         },
+        // 显示顶部常驻图表(根据当前Tab状态)
+        showTopChartDalogs() {
+            if (this.activeLeftTab === 'overview') { // 总览
+                this.showOverviewTopDialogs();
+            } else if (this.activeLeftTab === 'crossing') { // 路口
+                this.showCrossingTopDialogs();
+            } else if (this.activeLeftTab === 'trunkLine') { // 干线
+                // TODO: 干线Tab的顶部图表
+            } else if (this.activeLeftTab === 'specialDuty') { // 特勤
+                // TODO: 特勤Tab的顶部图表
+            }
+        },
         // 显示总览弹窗组
         showOverviewDalogs(nodeData) {
             console.log('显示总览弹窗组', nodeData.id, nodeData.label);
         },
-        // 显示路口弹窗组
-        showCrossingDalogs(nodeData) {
-            console.log('显示路口弹窗组', nodeData.id, nodeData.label);
-
+        showOverviewTopDialogs() {
             this.$refs.layout.openDialog({
-                id: 'crossing_' + nodeData.id, // 这里的 ID 可以根据实际业务场景动态生成
+                id: 'top-chart-overview-1',
                 title: '',
-                component: 'RingDonutChart',
-                width: 228,
-                height: 124,
+                component: 'OnlineStatusTabs',
+                width: 300,
+                height: 160,
                 center: false,
                 showClose: false,
-                position: { x: 730, y: 130 },
-                noPadding: true,
+                draggable: false,
                 resizable: false,
-                data: {
-                    chartData: [
-                        { name: '在线', value: 38, color: '#4DF5F8' },
-                        { name: '离线', value: 3, color: '#FFD369' }
-                    ],
-                    centerTitle: "98%",
-                    centerSubTitle: "38/41"
-                }
+                position: { x: 630, y: 130 },
+                noPadding: true,
+                data: {}
             });
-
             this.$refs.layout.openDialog({
-                id: 'crossing2_' + nodeData.id, // 这里的 ID 可以根据实际业务场景动态生成
+                id: 'top-chart-overview-2',
                 title: '',
-                component: 'RingDonutChart',
-                width: 228,
-                height: 124,
+                component: 'DeviceStatusTabs',
+                width: 300,
+                height: 160,
                 center: false,
                 showClose: false,
+                draggable: false,
+                resizable: false,
                 position: { x: 980, y: 130 },
                 noPadding: true,
-                resizable: false,
-                data: {
-                    chartData: [
-                        { name: '通信', value: 10, color: '#4DF5F8' },
-                        { name: '检测器', value: 8, color: '#FFA033' },
-                        { name: '灯控', value: 15, color: '#FFF587' },
-                        { name: '冲突', value: 5, color: '#FF4D4F' }
-                    ],
-                    centerTitle: "98%",
-                    centerSubTitle: "38/41"
-                }
+                data: {}
             });
+        },
+        // 显示路口弹窗组
+        showCrossingDalogs(nodeData) {
+            console.log('显示路口弹窗组', nodeData.id, nodeData.label);
 
             // 路口弹窗
             this.$refs.layout.openDialog({
@@ -352,8 +348,8 @@ export default {
                     onExpand: (data) => this.handleDoubleClickExpend(data)
                 },
                 onClose: () => {
-                    this.$refs.layout.handleDialogClose('crossing_' + nodeData.id);
-                    this.$refs.layout.handleDialogClose('crossing2_' + nodeData.id);
+                    this.$refs.layout.handleDialogClose('top-chart-crossing-1');
+                    this.$refs.layout.handleDialogClose('top-chart-crossing-2');
                 }
             });
 
@@ -376,6 +372,53 @@ export default {
                 data: nodeData
             });
         },
+        showCrossingTopDialogs() {
+            this.$refs.layout.openDialog({
+                id: 'top-chart-crossing-1',
+                title: '',
+                component: 'RingDonutChart',
+                width: 228,
+                height: 124,
+                center: false,
+                showClose: false,
+                draggable: false,
+                resizable: false,
+                position: { x: 730, y: 130 },
+                noPadding: true,
+                data: {
+                    chartData: [
+                        { name: '在线', value: 38, color: '#4DF5F8' },
+                        { name: '离线', value: 3, color: '#FFD369' }
+                    ],
+                    centerTitle: "98%",
+                    centerSubTitle: "38/41"
+                }
+            });
+
+            this.$refs.layout.openDialog({
+                id: 'top-chart-crossing-2',
+                title: '',
+                component: 'RingDonutChart',
+                width: 228,
+                height: 124,
+                center: false,
+                showClose: false,
+                draggable: false,
+                resizable: false,
+                position: { x: 980, y: 130 },
+                noPadding: true,
+                data: {
+                    chartData: [
+                        { name: '通信', value: 10, color: '#4DF5F8' },
+                        { name: '检测器', value: 8, color: '#FFA033' },
+                        { name: '灯控', value: 15, color: '#FFF587' },
+                        { name: '冲突', value: 5, color: '#FF4D4F' }
+                    ],
+                    centerTitle: "98%",
+                    centerSubTitle: "38/41"
+                }
+            });
+        },
         // 路口列表模式下弹窗
         handleCrossingViewDetail(rowData) {
             console.log('显示路口列表查看', rowData);
@@ -402,99 +445,6 @@ export default {
             console.log('显示干线弹窗组', nodeData.id, nodeData.label);
         },
 
-        // ================= 测试用例:模拟各种点击行为 =================
-
-        // 模拟 1:打开设备状态面板
-        testOpenDeviceStatus() {
-            this.$refs.layout.openDialog({
-                id: 'device-status-node-101', // 这里的 ID 可以根据实际业务场景动态生成,例如 'node-101' 代表某个路口
-                title: '',
-                component: 'DeviceStatusPanel', // 对应 components 里注册的名字
-                width: 300,
-                height: 240,
-                center: false,
-                position: { x: 400, y: 200 }, // 直接指定坐标,SmartDialog 内部会自动转换成 left/top
-                showClose: false, // 是否显示关闭按钮
-            });
-
-            this.$refs.layout.openDialog({
-                id: 'device-status-node-102', // 这里的 ID 可以根据实际业务场景动态生成,例如 'node-101' 代表某个路口
-                title: '',
-                component: 'DeviceStatusPanel', // 对应 components 里注册的名字
-                width: 300,
-                height: 240,
-                center: false,
-                position: { x: 1600, y: 100 }, // 直接指定坐标,SmartDialog 内部会自动转换成 left/top
-                showClose: false, // 是否显示关闭按钮
-            });
-        },
-
-        // 模拟 2:打开特勤安保路线面板
-        testOpenSecurityRoute() {
-            this.$refs.layout.openDialog({
-                id: 'dev-security-route', // 这里的 ID 可以根据实际业务场景动态生成,例如 'dev-security-route' 代表特勤安保路线弹窗
-                title: '特勤安保路线 未开始 一级',
-                component: 'SecurityRoutePanel',
-                width: 540,
-                height: 300,
-                center: false,
-                position: { x: 400, y: 450 },
-            });
-        },
-
-        // 模拟 3:打开本地协调控制面板
-        testOpenSecurityRoute2() {
-            const dialogId = 'dev-security-route2';
-            this.$refs.layout.openDialog({
-                id: dialogId,
-                title: '长安街-府右街口 本地协调控制',
-                component: 'IntersectionMapVideos',
-                width: 300,
-                height: 200,
-                center: false,
-                enableDblclickExpand: true,
-                position: { x: 1100, y: 200 },
-                data: {
-                    mapData: {},
-                    intersectionName: '长安街-府右街口',
-                    videos: [
-                        { id: 'cam-1', name: '信号机视频', url: 'https://example.com/video1' },
-                        { id: 'cam-2', name: '路口全景', url: 'https://example.com/video2' },
-                        { id: 'cam-3', name: '人行横道', url: 'https://example.com/video3' },
-                    ]
-                }
-            });
-
-            // 异步获取数据后更新弹窗
-            getIntersectionData().then(mapData => {
-                const dialogs = this.$refs.layout.getDialogs();
-                const dialog = dialogs.find(d => d.id === dialogId);
-                if (dialog) {
-                    this.$set(dialog.data, 'mapData', mapData);
-                }
-            });
-        },
-
-        // 模拟 4:打开新干线协调控制面板
-        testOpenTrafficTimeSpace() {
-            const tsData = makeTrafficTimeSpaceData();
-            this.$refs.layout.openDialog({
-                id: 'dev-traffic-time-space',
-                title: '新干线协调控制 早高峰',
-                component: 'TrafficTimeSpace',
-                width: 300,
-                height: 300,
-                center: false,
-                position: { x: 1400, y: 500 },
-                data: {
-                    intersections: tsData.intersections,
-                    distances: tsData.distances,
-                    waveData: tsData.waveData,
-                    greenData: tsData.greenData,
-                }
-            });
-        }
-
     }
 }
 </script>
@@ -504,6 +454,7 @@ export default {
     flex-direction: row;
     justify-content: flex-end;
 }
+
 .mode-switch>div {
     width: 200px;
 }