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