Forráskód Böngészése

调整首页的Tab切换速度;修改DynamicdonuChart圆饼图组件的加载动画;

画安 1 napja%!(EXTRA string=óta)
szülő
commit
19ff11fd0b
2 módosított fájl, 114 hozzáadás és 106 törlés
  1. 112 104
      src/components/ui/DynamicDonutChart.vue
  2. 2 2
      src/views/Home.vue

+ 112 - 104
src/components/ui/DynamicDonutChart.vue

@@ -7,178 +7,154 @@
             </div>
         </div>
 
-        <div class="echarts-container" ref="chartRef"></div>
+        <div class="echarts-wrapper">
+            <div class="echarts-container" ref="chartRef"></div>
+            
+            <div class="center-text-overlay">
+                <div class="main-text" :style="{ fontSize: safePx2echarts(26) + 'px' }">
+                    {{ animatedValue }}{{ unit }}
+                </div>
+                <div class="sub-text" :style="{ fontSize: safePx2echarts(14) + 'px', marginTop: safePx2echarts(4) + 'px' }">
+                    {{ centerSubTitle }}
+                </div>
+            </div>
+        </div>
     </div>
 </template>
 
 <script>
 import * as echarts from 'echarts';
-// 1. 引入你的全局自适应 Mixin 和像素转换工具
 import echartsResize, { px2echarts } from '@/mixins/echartsResize.js';
 
 export default {
     name: 'DynamicDonutChart',
-    // 2. 注册混入,自动接管自适应逻辑
     mixins: [echartsResize],
     props: {
-        // 传入的图表数据:[{ name: '正常', value: 425, color: '#32F6F8' }, ...]
         chartData: {
             type: Array,
             required: true
         },
-        // 圆环中间的大字(如 98%)
         centerTitle: {
-            type: String,
-            default: ''
+            type: [String, Number], // 兼容父组件传入数字的情况
+            default: '0'
         },
-        // 圆环中间的小字(如 980/1000)
         centerSubTitle: {
-            type: String,
+            type: [String, Number],
             default: ''
         }
     },
+    data() {
+        return {
+            animatedValue: 0,
+            unit: '',
+            rafId: null
+        };
+    },
     watch: {
-        // 深度监听数组变化,触发重绘
         chartData: {
             deep: true,
             handler() {
                 this.$nextTick(() => {
                     if (this.$_chart) {
-                        this.$_chart.resize(); // 在更新数据前,先强制重算尺寸
+                        this.$_chart.resize();
                     }
                     this.updateChart();
                 });
             }
         },
-        // 监听标题变化
         centerTitle() {
-            this.$nextTick(() => {
-                if (this.$_chart) this.$_chart.resize();
-                this.updateChart();
-            });
+            this.$nextTick(() => this.updateChart());
         },
         centerSubTitle() {
-            this.$nextTick(() => {
-                if (this.$_chart) this.$_chart.resize();
-                this.updateChart();
-            });
+            this.$nextTick(() => this.updateChart());
         }
     },
     mounted() {
-        this.initChart();
+        // 使用 nextTick 确保 DOM 和 CSS 完全渲染,防止获取不到宽高的 0x0 bug
+        this.$nextTick(() => {
+            this.initChart();
+        });
     },
     beforeDestroy() {
-        // 别忘了销毁监听器,防止内存泄漏
-        if (this.resizeObserver) {
-            this.resizeObserver.disconnect();
-        }
-        if (this.$_chart) {
-            this.$_chart.dispose();
-        }
+        if (this.resizeObserver) this.resizeObserver.disconnect();
+        if (this.$_chart) this.$_chart.dispose();
+        if (this.rafId) cancelAnimationFrame(this.rafId);
     },
     methods: {
+        // 防御性包装像素转换函数,防止在 template 中调用失败
+        safePx2echarts(val) {
+            return typeof px2echarts === 'function' ? px2echarts(val) : val;
+        },
+
         initChart() {
-            // 3. 按照 mixin 约定,将实例挂载到 this.$_chart
+            if (!this.$refs.chartRef) return;
             this.$_chart = echarts.init(this.$refs.chartRef);
             this.updateChart();
         },
 
-        // 4. Mixin 会在窗口变化时自动静默调用此方法
         updateChart() {
             if (!this.$_chart) return;
 
-            // 1. 数据准备
-            const targetNumber = parseFloat(this.centerTitle) || 0;
-            const unit = this.centerTitle.replace(/[0-9.]/g, '');
+            // 清理上一轮未完成的动画
+            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. 基础配置
+            // 2. 纯净的 ECharts 配置
             const option = {
                 color: colorPalette,
                 title: { show: false },
                 series: [
                     {
                         type: 'pie',
-                        // 【核心修复】:缩小半径以减小图表直径。原有 60%, 80%
                         radius: ['60%', '80%'],
                         center: ['50%', '50%'],
                         avoidLabelOverlap: false,
                         label: { show: false },
                         labelLine: { show: false },
-                        animationDuration: 1000,
+                        // 同步初始化与数据更新时的动画配置
+                        animationType: 'expansion', 
+                        animationDuration: SYNC_DURATION,
+                        animationDurationUpdate: SYNC_DURATION, 
+                        animationEasing: 'cubicOut',
+                        animationEasingUpdate: 'cubicOut',
                         data: this.chartData
                     }
-                ],
-                graphic: [
-                    {
-                        type: 'group',
-                        left: 'center',
-                        top: 'center',
-                        children: [
-                            {
-                                id: 'main-text', // 给主文字一个 ID 方便局部更新
-                                type: 'text',
-                                z: 100,
-                                left: 'center',
-                                top: px2echarts(-15),
-                                style: {
-                                    fill: '#ffffff',
-                                    fontSize: px2echarts(26),
-                                    fontWeight: 'bold',
-                                    fontFamily: 'Arial',
-                                    textAlign: 'center',
-                                    text: '0' + unit
-                                }
-                            },
-                            {
-                                type: 'text',
-                                z: 100,
-                                left: 'center',
-                                top: px2echarts(22),
-                                style: {
-                                    text: this.centerSubTitle,
-                                    fill: '#cccccc',
-                                    fontSize: px2echarts(14),
-                                    fontFamily: 'Arial',
-                                    textAlign: 'center'
-                                }
-                            }
-                        ]
-                    }
                 ]
             };
 
-            // 3. 首次渲染
+            // 触发 ECharts 圆环绘制
             this.$_chart.setOption(option, true);
 
-            // 4. 【进阶逻辑】:数值滚动动画 (Lazy Update 版)
-            const animateNumber = () => {
-                // 检查实例是否在主渲染进程中已被销毁
-                if (!this.$_chart || this.$_chart.isDisposed()) return;
-
-                let obj = { val: 0 };
-                this.$_chart.getZr().animation.animate(obj)
-                    .when(1500, { val: targetNumber })
-                    .during(() => {
-                        // 将更新指令放入 requestAnimationFrame,并防止组件销毁报错
-                        window.requestAnimationFrame(() => {
-                            if (this.$_chart && !this.$_chart.isDisposed()) {
-                                // 使用局部 ID 更新,并开启 lazyUpdate,解决 [ECharts] main process 报错
-                                this.$_chart.setOption({
-                                    graphic: [{
-                                        id: 'main-text',
-                                        style: { text: Math.round(obj.val) + unit }
-                                    }]
-                                }, { lazyUpdate: true });
-                            }
-                        });
-                    })
-                    .start();
+            // 3. 原生 JS 驱动数字动画
+            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);
+                this.animatedValue = Math.round(targetNumber * easeProgress);
+
+                if (progress < 1) {
+                    this.rafId = requestAnimationFrame(animateStep);
+                }
             };
 
-            // 5. 确保在 DOM 和布局稳定后再启动动画
-            this.$nextTick(() => {
-                setTimeout(animateNumber, 100);
+            this.rafId = requestAnimationFrame((timestamp) => {
+                startTime = timestamp; 
+                animateStep(timestamp);
             });
         }
     }
@@ -193,7 +169,6 @@ export default {
     height: 100%;
 }
 
-/* 左侧图例样式 */
 .custom-legend {
     display: flex;
     flex-direction: column;
@@ -226,10 +201,43 @@ export default {
     line-height: 18px;
 }
 
-/* 右侧图表样式 */
-.echarts-container {
+/* --- 核心修复区 --- */
+.echarts-wrapper {
+    position: relative;
     width: 65%;
     height: 100%;
-    min-height: 160px;
+    min-height: 160px; /* 强制保底高度,防止由于父级未撑开导致高度变为 0 */
+}
+
+.echarts-container {
+    width: 100%;
+    height: 100%;
+    min-height: 160px; /* 双重保险,确保 ECharts 画布有尺寸 */
+}
+
+.center-text-overlay {
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%);
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    pointer-events: none; /* 必须穿透,否则会阻挡鼠标移动到内部饼图的事件 */
+    z-index: 10;
+}
+
+.main-text {
+    color: #ffffff;
+    font-weight: bold;
+    font-family: Arial;
+    line-height: 1;
+}
+
+.sub-text {
+    color: #cccccc;
+    font-family: Arial;
+    line-height: 1;
 }
 </style>

+ 2 - 2
src/views/Home.vue

@@ -22,7 +22,7 @@
         <div class="panel-item">
           <PanelContainer title="在线状态">
   
-            <TechTabs v-model="onlineStatusActiveTab" type="segmented" autoPlay  @tab-click="handleTabClick">
+            <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" />
@@ -65,7 +65,7 @@
         <div class="panel-item">
           <PanelContainer title="设备状态">
   
-            <TechTabs v-model="onlineStatusActiveTab" type="segmented" autoPlay @tab-click="handleTabClick">
+            <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"/>