|
|
@@ -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>
|