|
|
@@ -1,12 +1,5 @@
|
|
|
<template>
|
|
|
- <div
|
|
|
- class="donut-chart-wrapper"
|
|
|
- :style="{
|
|
|
- height: finalHeight,
|
|
|
- width: finalWidth,
|
|
|
- '--chart-scale': scale
|
|
|
- }"
|
|
|
- >
|
|
|
+ <div class="donut-chart-wrapper">
|
|
|
<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>
|
|
|
@@ -20,79 +13,44 @@
|
|
|
|
|
|
<script>
|
|
|
import * as echarts from 'echarts';
|
|
|
-import echartsResize from '@/mixins/echartsResize.js';
|
|
|
+// 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: ''
|
|
|
},
|
|
|
+ // 圆环中间的小字(如 980/1000)
|
|
|
centerSubTitle: {
|
|
|
type: String,
|
|
|
default: ''
|
|
|
- },
|
|
|
- // 默认高度 163px
|
|
|
- customHeight: {
|
|
|
- type: [String, Number],
|
|
|
- default: '163px'
|
|
|
- },
|
|
|
- // 默认宽度为空,用于判断用户是否传入
|
|
|
- customWidth: {
|
|
|
- type: [String, Number],
|
|
|
- default: ''
|
|
|
- }
|
|
|
- },
|
|
|
- computed: {
|
|
|
- // 解析传入的高度数值
|
|
|
- parsedHeight() {
|
|
|
- return this.getPxValue(this.customHeight) || 163;
|
|
|
- },
|
|
|
- // 解析或计算宽度数值
|
|
|
- parsedWidth() {
|
|
|
- // 如果用户传入了宽度,直接解析
|
|
|
- if (this.customWidth) {
|
|
|
- return this.getPxValue(this.customWidth) || 376;
|
|
|
- }
|
|
|
- // 【核心逻辑】:如果只传入高度,根据默认比例 (376 / 163) 自动计算宽度
|
|
|
- return this.parsedHeight * (376 / 163);
|
|
|
- },
|
|
|
- // 最终绑定到 style 的高度
|
|
|
- finalHeight() {
|
|
|
- return this.formatSize(this.customHeight, this.parsedHeight);
|
|
|
- },
|
|
|
- // 最终绑定到 style 的宽度
|
|
|
- finalWidth() {
|
|
|
- return this.customWidth
|
|
|
- ? this.formatSize(this.customWidth, this.parsedWidth)
|
|
|
- : `${this.parsedWidth}px`;
|
|
|
- },
|
|
|
- // 【核心计算】:计算缩放因子
|
|
|
- scale() {
|
|
|
- const scaleW = this.parsedWidth / 376;
|
|
|
- const scaleH = this.parsedHeight / 163;
|
|
|
-
|
|
|
- // 按照宽高比来等比例缩放:取宽、高中缩放比例较小的一个
|
|
|
- // 这样能保证内容完整包裹在设定的宽高内而不发生溢出或变形
|
|
|
- return Math.min(scaleW, scaleH);
|
|
|
}
|
|
|
},
|
|
|
watch: {
|
|
|
+ // 深度监听数组变化,触发重绘
|
|
|
chartData: {
|
|
|
deep: true,
|
|
|
handler() {
|
|
|
this.$nextTick(() => {
|
|
|
- if (this.$_chart) this.$_chart.resize();
|
|
|
+ if (this.$_chart) {
|
|
|
+ this.$_chart.resize(); // 在更新数据前,先强制重算尺寸
|
|
|
+ }
|
|
|
this.updateChart();
|
|
|
});
|
|
|
}
|
|
|
},
|
|
|
+ // 监听标题变化
|
|
|
centerTitle() {
|
|
|
this.$nextTick(() => {
|
|
|
if (this.$_chart) this.$_chart.resize();
|
|
|
@@ -104,64 +62,54 @@ export default {
|
|
|
if (this.$_chart) this.$_chart.resize();
|
|
|
this.updateChart();
|
|
|
});
|
|
|
- },
|
|
|
- // 监听缩放比例变化,触发重绘
|
|
|
- scale() {
|
|
|
- this.$nextTick(() => {
|
|
|
- if (this.$_chart) this.$_chart.resize();
|
|
|
- this.updateChart();
|
|
|
- });
|
|
|
}
|
|
|
},
|
|
|
mounted() {
|
|
|
this.initChart();
|
|
|
},
|
|
|
+ beforeDestroy() {
|
|
|
+ // 别忘了销毁监听器,防止内存泄漏
|
|
|
+ if (this.resizeObserver) {
|
|
|
+ this.resizeObserver.disconnect();
|
|
|
+ }
|
|
|
+ if (this.$_chart) {
|
|
|
+ this.$_chart.dispose();
|
|
|
+ }
|
|
|
+ },
|
|
|
methods: {
|
|
|
- // 提取数值的辅助函数
|
|
|
- getPxValue(val) {
|
|
|
- if (!val) return null;
|
|
|
- if (typeof val === 'number') return val;
|
|
|
- if (typeof val === 'string' && val.endsWith('px')) {
|
|
|
- return parseFloat(val);
|
|
|
- }
|
|
|
- return null; // 无法解析(例如传入 100%)时返回 null
|
|
|
- },
|
|
|
- // 格式化输出 CSS 的尺寸值
|
|
|
- formatSize(originalVal, parsedVal) {
|
|
|
- // 如果包含 %,直接返回原始值(保留原生弹性),否则返回 px 字符串
|
|
|
- if (typeof originalVal === 'string' && originalVal.includes('%')) {
|
|
|
- return originalVal;
|
|
|
- }
|
|
|
- return `${parsedVal}px`;
|
|
|
- },
|
|
|
initChart() {
|
|
|
+ // 3. 按照 mixin 约定,将实例挂载到 this.$_chart
|
|
|
this.$_chart = echarts.init(this.$refs.chartRef);
|
|
|
this.updateChart();
|
|
|
},
|
|
|
+
|
|
|
+ // 4. Mixin 会在窗口变化时自动静默调用此方法
|
|
|
updateChart() {
|
|
|
if (!this.$_chart) return;
|
|
|
|
|
|
const colorPalette = this.chartData.map(item => item.color);
|
|
|
- const scale = this.scale;
|
|
|
|
|
|
const option = {
|
|
|
color: colorPalette,
|
|
|
title: {
|
|
|
+ // 【核心改造】:使用富文本结构,用 \n 换行
|
|
|
text: `{main|${this.centerTitle}}\n{sub|${this.centerSubTitle}}`,
|
|
|
left: 'center',
|
|
|
top: 'center',
|
|
|
textStyle: {
|
|
|
rich: {
|
|
|
+ // 全面使用 px2echarts 包裹尺寸数值
|
|
|
main: {
|
|
|
- fontSize: 26 * scale,
|
|
|
+ fontSize: px2echarts(26),
|
|
|
color: '#ffffff',
|
|
|
fontWeight: 'bold',
|
|
|
fontFamily: 'Arial',
|
|
|
- padding: [0, 0, 6 * scale, 0]
|
|
|
+ // 用 padding 的 bottom 值代替原来的 itemGap,实现精准缩放
|
|
|
+ padding: [0, 0, px2echarts(6), 0]
|
|
|
},
|
|
|
sub: {
|
|
|
- fontSize: 14 * scale,
|
|
|
- lineHeight: 18 * scale,
|
|
|
+ fontSize: px2echarts(14),
|
|
|
+ lineHeight: px2echarts(18),
|
|
|
color: '#cccccc'
|
|
|
}
|
|
|
}
|
|
|
@@ -170,6 +118,8 @@ export default {
|
|
|
series: [
|
|
|
{
|
|
|
type: 'pie',
|
|
|
+ // 半径原则上也可以写成具体像素如 [px2echarts(60), px2echarts(80)]
|
|
|
+ // 但写成百分比原生就自带外层容器的自适应,通常保留百分比即可
|
|
|
radius: ['60%', '80%'],
|
|
|
center: ['50%', '50%'],
|
|
|
avoidLabelOverlap: false,
|
|
|
@@ -188,47 +138,49 @@ export default {
|
|
|
|
|
|
<style scoped>
|
|
|
.donut-chart-wrapper {
|
|
|
- --chart-scale: 1;
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
- /* 取消写死的 100%,由内联 style 动态接管 */
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
}
|
|
|
|
|
|
+/* 左侧图例样式 */
|
|
|
.custom-legend {
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
justify-content: center;
|
|
|
- gap: calc(12px * var(--chart-scale));
|
|
|
+ gap: 12px;
|
|
|
width: 35%;
|
|
|
- margin-right: calc(8px * var(--chart-scale));
|
|
|
+ margin-right: 8px;
|
|
|
}
|
|
|
|
|
|
.legend-box {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
- background: linear-gradient(90deg, #2D426C 0%, rgba(45,66,108,0) 100%);
|
|
|
- height: calc(40px * var(--chart-scale));
|
|
|
+ background: linear-gradient( 90deg, #2D426C 0%, rgba(45,66,108,0) 100%);
|
|
|
+ height: 40px;
|
|
|
}
|
|
|
|
|
|
.color-dot {
|
|
|
- width: calc(8px * var(--chart-scale));
|
|
|
- height: calc(8px * var(--chart-scale));
|
|
|
- margin-right: calc(8px * var(--chart-scale));
|
|
|
- margin-left: calc(16px * var(--chart-scale));
|
|
|
+ width: 8px;
|
|
|
+ height: 8px;
|
|
|
+ margin-right: 8px;
|
|
|
+ margin-left: 16px;
|
|
|
flex-shrink: 0;
|
|
|
- border-radius: calc(2px * var(--chart-scale));
|
|
|
+ border-radius: 2px;
|
|
|
}
|
|
|
|
|
|
.legend-name {
|
|
|
font-weight: 400;
|
|
|
- font-size: calc(12px * var(--chart-scale));
|
|
|
+ font-size: 12px;
|
|
|
color: rgba(255, 255, 255, 0.60);
|
|
|
- line-height: calc(18px * var(--chart-scale));
|
|
|
+ line-height: 18px;
|
|
|
}
|
|
|
|
|
|
+/* 右侧图表样式 */
|
|
|
.echarts-container {
|
|
|
width: 65%;
|
|
|
height: 100%;
|
|
|
- min-height: 0;
|
|
|
+ min-height: 160px;
|
|
|
}
|
|
|
</style>
|