|
|
@@ -0,0 +1,253 @@
|
|
|
+<template>
|
|
|
+ <div class="donut-chart-wrapper">
|
|
|
+ <div class="echarts-wrapper">
|
|
|
+ <div class="echarts-container" ref="chartRef"></div>
|
|
|
+
|
|
|
+ <div class="center-text-overlay" v-if="activeFaults.length > 1">
|
|
|
+ <span class="main-number" :style="{ fontSize: safePx2echarts(20) + 'px' }">
|
|
|
+ {{ totalFaults }}
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="custom-legend">
|
|
|
+ <div class="legend-item" v-for="(item, index) in chartData" :key="index">
|
|
|
+ <span class="legend-name" :style="{ fontSize: safePx2echarts(12) + 'px' }">{{ item.name }}</span>
|
|
|
+ <span class="legend-color-box" :style="{ backgroundColor: item.color }"></span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+import * as echarts from 'echarts';
|
|
|
+import echartsResize, { px2echarts } from '@/mixins/echartsResize.js';
|
|
|
+
|
|
|
+export default {
|
|
|
+ name: 'DeviceStatusDonutChart',
|
|
|
+ mixins: [echartsResize],
|
|
|
+ props: {
|
|
|
+ // 父组件传入的数据格式应为: [{name: '正常', value: 0, color: '#...'}, {name: '红灯故障', value: 2, color: '#...'}]
|
|
|
+ chartData: {
|
|
|
+ type: Array,
|
|
|
+ required: true,
|
|
|
+ default: () => []
|
|
|
+ }
|
|
|
+ },
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ chartInstance: null
|
|
|
+ };
|
|
|
+ },
|
|
|
+ computed: {
|
|
|
+ // 过滤出真正的“故障”数据(名称不包含正常,且值大于0)
|
|
|
+ activeFaults() {
|
|
|
+ return this.chartData.filter(item =>
|
|
|
+ item.name.indexOf('正常') === -1 && Number(item.value) > 0
|
|
|
+ );
|
|
|
+ },
|
|
|
+ // 计算故障总数(用于图3中心显示)
|
|
|
+ totalFaults() {
|
|
|
+ return this.activeFaults.reduce((sum, item) => sum + Number(item.value), 0);
|
|
|
+ }
|
|
|
+ },
|
|
|
+ watch: {
|
|
|
+ chartData: {
|
|
|
+ deep: true,
|
|
|
+ handler() {
|
|
|
+ this.$nextTick(() => {
|
|
|
+ if (this.chartInstance) {
|
|
|
+ this.chartInstance.resize();
|
|
|
+ }
|
|
|
+ this.updateChart();
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ mounted() {
|
|
|
+ this.$nextTick(() => {
|
|
|
+ this.initChart();
|
|
|
+ });
|
|
|
+ },
|
|
|
+ beforeDestroy() {
|
|
|
+ if (this.resizeObserver) this.resizeObserver.disconnect();
|
|
|
+ if (this.chartInstance) this.chartInstance.dispose();
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ safePx2echarts(val) {
|
|
|
+ return typeof px2echarts === 'function' ? px2echarts(val) : val;
|
|
|
+ },
|
|
|
+
|
|
|
+ initChart() {
|
|
|
+ if (!this.$refs.chartRef) return;
|
|
|
+ this.chartInstance = echarts.init(this.$refs.chartRef);
|
|
|
+ this.updateChart();
|
|
|
+ },
|
|
|
+
|
|
|
+ updateChart() {
|
|
|
+ if (!this.chartInstance) return;
|
|
|
+ this.chartInstance.clear();
|
|
|
+
|
|
|
+ const chartWidth = this.chartInstance.getWidth();
|
|
|
+ let seriesData = [];
|
|
|
+
|
|
|
+ if (this.activeFaults.length === 0) {
|
|
|
+ const normalItem = this.chartData.find(item => item.name.indexOf('正常') !== -1);
|
|
|
+ const normalColor = normalItem ? normalItem.color : '#9FE051';
|
|
|
+ seriesData = [{
|
|
|
+ name: '故障',
|
|
|
+ value: 1,
|
|
|
+ actualValue: 0,
|
|
|
+ itemStyle: { color: normalColor },
|
|
|
+ label: { color: normalColor },
|
|
|
+ labelLine: { lineStyle: { color: normalColor } }
|
|
|
+ }];
|
|
|
+ } else {
|
|
|
+ seriesData = this.activeFaults.map(item => ({
|
|
|
+ name: item.name,
|
|
|
+ value: item.value,
|
|
|
+ itemStyle: { color: item.color },
|
|
|
+ label: { color: item.color },
|
|
|
+ labelLine: { lineStyle: { color: item.color } }
|
|
|
+ }));
|
|
|
+ }
|
|
|
+
|
|
|
+ const option = {
|
|
|
+ series: [
|
|
|
+ {
|
|
|
+ type: 'pie',
|
|
|
+ // 【优化1】圆环再缩小一点点,给边缘文字留出绝对安全的距离
|
|
|
+ radius: ['40%', '55%'],
|
|
|
+ center: ['50%', '50%'],
|
|
|
+ startAngle: 210,
|
|
|
+ avoidLabelOverlap: true,
|
|
|
+ label: {
|
|
|
+ show: true,
|
|
|
+ position: 'outside',
|
|
|
+ // 【优化2】文字靠近圆环一点,防止撞墙
|
|
|
+ distance: this.safePx2echarts(5),
|
|
|
+ formatter: (params) => {
|
|
|
+ const displayValue = params.data.actualValue !== undefined ? params.data.actualValue : params.value;
|
|
|
+ return `{name|${params.name}}\n{val|${displayValue}}`;
|
|
|
+ },
|
|
|
+ rich: {
|
|
|
+ name: {
|
|
|
+ fontSize: this.safePx2echarts(12),
|
|
|
+ color: 'inherit',
|
|
|
+ fontWeight: 'bold',
|
|
|
+ padding: [0, 0, this.safePx2echarts(4), 0]
|
|
|
+ // 【核心修复】删掉了 width 和 overflow,让 ECharts 自然渲染
|
|
|
+ },
|
|
|
+ val: {
|
|
|
+ fontSize: this.safePx2echarts(12),
|
|
|
+ color: 'inherit',
|
|
|
+ fontWeight: 'bold',
|
|
|
+ padding: [this.safePx2echarts(4), 0, 0, 0]
|
|
|
+ // 【核心修复】删掉了 width 和 overflow
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ labelLine: {
|
|
|
+ show: true,
|
|
|
+ // 【优化3】斜线改短,将文字往中心拉
|
|
|
+ length: this.safePx2echarts(8),
|
|
|
+ length2: this.safePx2echarts(15)
|
|
|
+ },
|
|
|
+ labelLayout: (params) => {
|
|
|
+ const isLeft = params.labelRect.x < chartWidth / 2;
|
|
|
+ const points = params.labelLinePoints;
|
|
|
+
|
|
|
+ if (points) {
|
|
|
+ points[2][0] = isLeft
|
|
|
+ ? params.labelRect.x
|
|
|
+ : params.labelRect.x + params.labelRect.width;
|
|
|
+
|
|
|
+ points[1][1] = points[2][1] = params.labelRect.y + params.labelRect.height / 2;
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ labelLinePoints: points
|
|
|
+ };
|
|
|
+ },
|
|
|
+ data: seriesData,
|
|
|
+ animationType: 'scale',
|
|
|
+ animationEasing: 'elasticOut'
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ };
|
|
|
+
|
|
|
+ this.chartInstance.setOption(option, true);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.donut-chart-wrapper {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+/* 左侧饼图容器 */
|
|
|
+.echarts-wrapper {
|
|
|
+ position: relative;
|
|
|
+ width: 65%; /* 占据左边 65% 空间 */
|
|
|
+ height: 100%;
|
|
|
+ min-height: 140px;
|
|
|
+}
|
|
|
+
|
|
|
+.echarts-container {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ min-height: 160px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 饼图中心数字 */
|
|
|
+.center-text-overlay {
|
|
|
+ position: absolute;
|
|
|
+ top: 50%;
|
|
|
+ left: 50%;
|
|
|
+ transform: translate(-50%, -50%);
|
|
|
+ pointer-events: none; /* 防止遮挡鼠标悬停饼图的事件 */
|
|
|
+ z-index: 10;
|
|
|
+}
|
|
|
+
|
|
|
+.main-number {
|
|
|
+ color: #8392b4; /* 图3中数字4的灰色调 */
|
|
|
+ font-weight: bold;
|
|
|
+ font-family: Arial, sans-serif;
|
|
|
+ line-height: 1;
|
|
|
+}
|
|
|
+
|
|
|
+/* 右侧图例 */
|
|
|
+.custom-legend {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ justify-content: center;
|
|
|
+ gap: 12px;
|
|
|
+ width: 35%; /* 占据右边 35% 空间 */
|
|
|
+ padding-right: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.legend-item {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: flex-end; /* 靠右对齐 */
|
|
|
+ gap: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.legend-name {
|
|
|
+ color: #00d2ff; /* 对应图片中的青色文字 */
|
|
|
+ font-weight: 400;
|
|
|
+}
|
|
|
+
|
|
|
+.legend-color-box {
|
|
|
+ width: 24px; /* 宽方块 */
|
|
|
+ height: 12px;
|
|
|
+ border-radius: 3px;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+</style>
|