浏览代码

新增RingDonutChart组件;修改状态监控路口模式的顶部显示图表模块;

画安 3 天之前
父节点
当前提交
24b1608d45
共有 3 个文件被更改,包括 284 次插入151 次删除
  1. 0 148
      src/components/ui/DeviceDonutChart.vue
  2. 216 0
      src/components/ui/RingDonutChart.vue
  3. 68 3
      src/views/StatusMonitoring.vue

+ 0 - 148
src/components/ui/DeviceDonutChart.vue

@@ -1,148 +0,0 @@
-<template>
-  <div class="chart-wrapper">
-    <div class="echarts-container" ref="chartRef"></div>
-
-    <div class="custom-legend">
-      <div class="legend-item">
-        <span class="dot dot-online"></span>
-        <span class="label">在线</span>
-      </div>
-      <div class="legend-item">
-        <span class="dot dot-offline"></span>
-        <span class="label">离线</span>
-      </div>
-    </div>
-  </div>
-</template>
-
-<script>
-import * as echarts from 'echarts';
-// 1. 引入全局自适应 Mixin 和像素转换神器
-import echartsResize, { px2echarts } from '@/mixins/echartsResize.js';
-
-export default {
-  name: 'DeviceDonutChart',
-  // 2. 注册混入,自动接管窗口监听、重绘和销毁!
-  mixins: [echartsResize],
-  props: {
-    online: { type: Number, required: true },
-    total: { type: Number, required: true }
-  },
-  computed: {
-    offline() {
-      return this.total - this.online;
-    },
-    percent() {
-      return this.total === 0 ? 0 : Math.round((this.online / this.total) * 100);
-    }
-  },
-  watch: {
-    online() { this.updateChart(); },
-    total() { this.updateChart(); }
-  },
-  mounted() {
-    this.initChart();
-    // 【清理】:删除了手写的 window.addEventListener
-  },
-  // 【清理】:彻底删除了 beforeDestroy 里的卸载逻辑
-  methods: {
-    initChart() {
-      // 3. 按照 mixin 约定,将 ECharts 实例赋值给 this.$_chart
-      this.$_chart = echarts.init(this.$refs.chartRef);
-      this.updateChart();
-    },
-
-    // Mixin 会在窗口变化时,自动算出新比例,并静默调用这个 updateChart!
-    updateChart() {
-      if (!this.$_chart) return;
-
-      const option = {
-        title: {
-          text: `{percent|${this.percent}%}\n{count|${this.online}/${this.total}}`,
-          left: 'center',
-          top: 'center',
-          textStyle: {
-            rich: {
-              // 4. 【核心替换】:全部换成优雅的 px2echarts(),干掉所有手写的 scale 乘法
-              percent: { 
-                fontSize: px2echarts(28), 
-                color: '#ffffff', 
-                fontWeight: 'bold', 
-                padding: [0, 0, px2echarts(8), 0] 
-              },
-              count: { 
-                fontSize: px2echarts(14), 
-                color: '#e2e8f0' 
-              }
-            }
-          }
-        },
-        series: [
-          {
-            type: 'pie',
-            radius: ['65%', '85%'], 
-            center: ['50%', '50%'],
-            avoidLabelOverlap: false,
-            label: { show: false }, 
-            labelLine: { show: false },
-            data: [
-              { value: this.online, name: '在线', itemStyle: { color: '#33ccff' } },
-              { value: this.offline, name: '离线', itemStyle: { color: '#ff7744' } }
-            ]
-          }
-        ]
-      };
-      
-      this.$_chart.setOption(option);
-    }
-    // 【清理】:彻底删除了冗长的 handleResize 和 getRealScale 方法
-  }
-};
-</script>
-
-<style scoped>
-/* ================== CSS 保持原样 ================== */
-.chart-wrapper {
-  display: flex;
-  align-items: center;
-  width: 100%;
-  height: 100%;
-}
-
-.echarts-container {
-  flex: 1; 
-  height: 100%;
-  min-height: 160px;
-}
-
-.custom-legend {
-  width: 120px;
-  display: flex;
-  flex-direction: column;
-  justify-content: center;
-  gap: 15px; 
-  margin-right: 10px;
-}
-
-.legend-item {
-  display: flex;
-  align-items: center;
-  background: rgba(255, 255, 255, 0.08); 
-  padding: 8px 15px;
-  border-radius: 2px;
-}
-
-.dot {
-  width: 8px;
-  height: 8px;
-  border-radius: 2px; 
-  margin-right: 10px;
-}
-.dot-online { background-color: #33ccff; }
-.dot-offline { background-color: #ff7744; }
-
-.label {
-  color: #c0c4cc;
-  font-size: 13px;
-}
-</style>

+ 216 - 0
src/components/ui/RingDonutChart.vue

@@ -0,0 +1,216 @@
+<template>
+
+        <div class="donut-chart-wrapper">
+            <div class="echarts-container" ref="chartRef"></div>
+
+            <div class="custom-legend">
+                <div class="legend-box" v-for="(item, index) in chartData" :key="index">
+                    <span class="color-dot" :style="{ backgroundColor: item.color }"></span>
+                    <span class="legend-name">{{ item.name }}</span>
+                </div>
+            </div>
+        </div>
+
+</template>
+
+<script>
+import * as echarts from 'echarts';
+// 保留自适应 mixin,但内部文字将改用容器比例计算,不再依赖 px2echarts
+import echartsResize from '@/mixins/echartsResize.js';
+
+export default {
+    name: 'RingDonutChart',
+    mixins: [echartsResize],
+    props: {
+        chartData: { type: Array, required: true },
+        centerTitle: { type: String, default: '' },
+        centerSubTitle: { type: String, default: '' }
+    },
+    watch: {
+        chartData: {
+            deep: true,
+            handler() {
+                this.$nextTick(() => {
+                    if (this.$_chart) {
+                        this.$_chart.resize(); 
+                    }
+                    this.updateChart();
+                });
+            }
+        },
+        centerTitle() {
+            this.$nextTick(() => {
+                if (this.$_chart) this.$_chart.resize();
+                this.updateChart();
+            });
+        },
+        centerSubTitle() {
+            this.$nextTick(() => {
+                if (this.$_chart) this.$_chart.resize();
+                this.updateChart();
+            });
+        }
+    },
+    mounted() {
+        this.initChart();
+        
+        // 监听当前 DOM 容器大小变化,实现小弹窗内文字的完美自适应缩放
+        this.domObserver = new ResizeObserver(() => {
+            if (this.$_chart) {
+                this.$_chart.resize();
+                this.updateChart(); 
+            }
+        });
+        if (this.$refs.chartRef) {
+            this.domObserver.observe(this.$refs.chartRef);
+        }
+    },
+    beforeDestroy() {
+        if (this.resizeObserver) this.resizeObserver.disconnect();
+        if (this.domObserver) this.domObserver.disconnect();
+        if (this.$_chart) this.$_chart.dispose();
+    },
+    methods: {
+        initChart() {
+            this.$_chart = echarts.init(this.$refs.chartRef);
+            this.updateChart();
+        },
+
+        updateChart() {
+            if (!this.$_chart) return;
+
+            const targetNumber = parseFloat(this.centerTitle) || 0;
+            const unit = this.centerTitle.replace(/[0-9.]/g, '');
+            const colorPalette = this.chartData.map(item => item.color);
+
+            // ================= 核心自适应字号计算 =================
+            // 获取当前容器的高度,按高度比例计算字号,避免文字撑爆小弹窗
+            const containerHeight = this.$refs.chartRef.clientHeight || 120;
+            const mainFontSize = Math.max(containerHeight * 0.18, 14); // 主字号
+            const subFontSize = Math.max(containerHeight * 0.10, 10);  // 副字号
+
+            const option = {
+                color: colorPalette,
+                // ================== 居中修正核心 ==================
+                title: {
+                    // 使用 rich 富文本标签包裹两行文字
+                    text: '{main|0' + unit + '}\n{sub|' + this.centerSubTitle + '}',
+                    left: '45%',            // 严格对齐 series 的 center X轴
+                    top: '50%',             // 严格对齐 series 的 center Y轴
+                    textAlign: 'center',    // 水平锚点对齐中心
+                    textBaseline: 'middle', // 垂直锚点对齐中心
+                    textStyle: {
+                        rich: {
+                            main: {
+                                color: '#ffffff',
+                                fontSize: mainFontSize,
+                                fontWeight: 'bold',
+                                fontFamily: 'Arial',
+                                padding: [0, 0, 4, 0], // 控制与下方文字的间距
+                                align: 'center'
+                            },
+                            sub: {
+                                color: '#cccccc',
+                                fontSize: subFontSize,
+                                fontFamily: 'Arial',
+                                align: 'center'
+                            }
+                        }
+                    }
+                },
+                series: [
+                    {
+                        type: 'pie',
+                        radius: ['60%', '80%'],
+                        center: ['45%', '50%'], // 整体向左偏一点,给图例留空间
+                        avoidLabelOverlap: false,
+                        label: { show: false },
+                        labelLine: { show: false },
+                        animationDuration: 1000,
+                        data: this.chartData
+                    }
+                ]
+            };
+
+            this.$_chart.setOption(option, true);
+
+            // ================= 动画更新逻辑同步修改 =================
+            const animateNumber = () => {
+                if (!this.$_chart || this.$_chart.isDisposed()) return;
+
+                let obj = { val: 0 };
+                this.$_chart.getZr().animation.animate(obj)
+                    .when(1500, { val: targetNumber })
+                    .during(() => {
+                        window.requestAnimationFrame(() => {
+                            if (this.$_chart && !this.$_chart.isDisposed()) {
+                                // 更新 title 时也要带上富文本标签格式
+                                this.$_chart.setOption({
+                                    title: {
+                                        text: '{main|' + Math.round(obj.val) + unit + '}\n{sub|' + this.centerSubTitle + '}'
+                                    }
+                                }, { lazyUpdate: true });
+                            }
+                        });
+                    })
+                    .start();
+            };
+
+            this.$nextTick(() => {
+                setTimeout(animateNumber, 100);
+            });
+        }
+    }
+}
+</script>
+
+<style scoped>
+
+
+.donut-chart-wrapper {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    width: 100%;
+    height: 100%;
+}
+
+/* 图表在左侧 */
+.echarts-container {
+    width: 55%;
+    height: 100%;
+    min-height: 90px;
+}
+
+/* 图例在右侧 */
+.custom-legend {
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    gap: 10px;
+    width: 45%;
+    padding-left: 8px;
+}
+
+/* 移除了旧版的 background 渐变,使其贴合新设计 */
+.legend-box {
+    display: flex;
+    align-items: center;
+}
+
+.color-dot {
+    width: 8px;
+    height: 8px;
+    margin-right: 8px;
+    flex-shrink: 0;
+    border-radius: 2px;
+}
+
+.legend-name {
+    font-weight: 400;
+    font-size: 13px;
+    color: #ffffff; /* 字体改为纯白 */
+    line-height: 1;
+    white-space: nowrap;
+}
+</style>

+ 68 - 3
src/views/StatusMonitoring.vue

@@ -51,7 +51,7 @@
         <template #dialogs>
             <SmartDialog v-for="dialog in activeDialogs" :key="dialog.id" :id="dialog.id" :visible.sync="dialog.visible"
                 :title="dialog.title" :defaultWidth="dialog.width || 400" :defaultHeight="dialog.height || 300"
-                :center="dialog.center !== false" :position="dialog.position" :showClose="dialog.showClose" :enableDblclickExpand="dialog.enableDblclickExpand"
+                :center="dialog.center !== false" :position="dialog.position" :showClose="dialog.showClose" :enableDblclickExpand="dialog.enableDblclickExpand" :noPadding="dialog.noPadding"
 
                 @close="handleDialogClose(dialog.id)" @expand="handleDoubleClickExpend(dialog.id)">
 
@@ -75,6 +75,7 @@ import SecurityRoutePanel from '@/components/ui/SecurityRoutePanel.vue';
 import IntersectionMapVideos from '@/components/ui/IntersectionMapVideos.vue';
 import TrafficTimeSpace from '@/components/ui/TrafficTimeSpace.vue';
 import MenuItem from '@/components/ui/MenuItem.vue';
+import RingDonutChart from '@/components/ui/RingDonutChart.vue';
 import { getIntersectionData, makeTrafficTimeSpaceData } from '@/mock/data';
 
 
@@ -92,7 +93,8 @@ export default {
         DeviceStatusPanel,
         SecurityRoutePanel,
         IntersectionMapVideos,
-        TrafficTimeSpace
+        TrafficTimeSpace,
+        RingDonutChart
     },
     data() {
         return {
@@ -228,7 +230,25 @@ export default {
 
     },
     mounted() {
-
+        // this.openDialog({
+        //         id: 'test', // 这里的 ID 可以根据实际业务场景动态生成,例如 'dev-security-route' 代表特勤安保路线弹窗
+        //         title: '',
+        //         component: 'RingDonutChart',
+        //         width: 228,
+        //         height: 124,
+        //         center: false,
+        //         showClose: false,
+        //         position: { x: 750, y: 130 },
+        //         noPadding: true,
+        //         data: {
+        //             chartData: [
+        //                 { name: '在线', value: 38, color: '#4DF5F8' },
+        //                 { name: '离线', value: 3, color: '#FFD369' }
+        //             ],
+        //             centerTitle: "98%",
+        //             centerSubTitle: "38/41"
+        //         }
+        //     });
     },
     methods: {
         // 处理tab点击
@@ -281,6 +301,7 @@ export default {
                 height: config.height || 300,      // 自定义高度
                 center: config.center !== false,   // 是否居中显示
                 position: config.position || null, // 自定义坐标 {x, y}
+                noPadding: config.noPadding !== false, // 无边距
                 enableDblclickExpand: config.enableDblclickExpand !== false, // 是否启用双击
                 showClose: config.showClose !== false, // 是否显示关闭按钮
                 data: config.data || {}            // 传给内部组件的业务数据
@@ -301,6 +322,50 @@ export default {
         // 显示路口弹窗组
         showCrossingDalogs(nodeData) {
             console.log('显示干线弹窗组', nodeData.id, nodeData.label);
+
+            this.openDialog({
+                id: 'crossing_' + nodeData.id, // 这里的 ID 可以根据实际业务场景动态生成,例如 'dev-security-route' 代表特勤安保路线弹窗
+                title: '',
+                component: 'RingDonutChart',
+                width: 228,
+                height: 124,
+                center: false,
+                showClose: 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.openDialog({
+                id: 'crossing2_' + nodeData.id, // 这里的 ID 可以根据实际业务场景动态生成,例如 'dev-security-route' 代表特勤安保路线弹窗
+                title: '',
+                component: 'RingDonutChart',
+                width: 228,
+                height: 124,
+                center: false,
+                showClose: 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"
+                }
+            });
+
+
         },
         // 显示干线弹窗组
         showTrunkLineDalogs(nodeData) {