DeviceStatusDonutChart.vue 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  1. <template>
  2. <div class="donut-chart-wrapper">
  3. <div class="echarts-wrapper">
  4. <div class="echarts-container" ref="chartRef"></div>
  5. <div class="center-text-overlay" v-if="activeFaults.length > 1">
  6. <span class="main-number" :style="{ fontSize: safePx2echarts(20) + 'px' }">
  7. {{ totalFaults }}
  8. </span>
  9. </div>
  10. </div>
  11. <div class="custom-legend">
  12. <div class="legend-item" v-for="(item, index) in chartData" :key="index">
  13. <span class="legend-name" :style="{ fontSize: safePx2echarts(12) + 'px' }">{{ item.name }}</span>
  14. <span class="legend-color-box" :style="{ backgroundColor: item.color }"></span>
  15. </div>
  16. </div>
  17. </div>
  18. </template>
  19. <script>
  20. import * as echarts from 'echarts';
  21. import echartsResize, { px2echarts } from '@/mixins/echartsResize.js';
  22. export default {
  23. name: 'DeviceStatusDonutChart',
  24. mixins: [echartsResize],
  25. props: {
  26. // 父组件传入的数据格式应为: [{name: '正常', value: 0, color: '#...'}, {name: '红灯故障', value: 2, color: '#...'}]
  27. chartData: {
  28. type: Array,
  29. required: true,
  30. default: () => []
  31. }
  32. },
  33. data() {
  34. return {
  35. chartInstance: null
  36. };
  37. },
  38. computed: {
  39. // 过滤出真正的“故障”数据(名称不包含正常,且值大于0)
  40. activeFaults() {
  41. return this.chartData.filter(item =>
  42. item.name.indexOf('正常') === -1 && Number(item.value) > 0
  43. );
  44. },
  45. // 计算故障总数(用于图3中心显示)
  46. totalFaults() {
  47. return this.activeFaults.reduce((sum, item) => sum + Number(item.value), 0);
  48. }
  49. },
  50. watch: {
  51. chartData: {
  52. deep: true,
  53. handler() {
  54. this.$nextTick(() => {
  55. if (this.chartInstance) {
  56. this.chartInstance.resize();
  57. }
  58. this.updateChart();
  59. });
  60. }
  61. }
  62. },
  63. mounted() {
  64. this.$nextTick(() => {
  65. this.initChart();
  66. });
  67. },
  68. beforeDestroy() {
  69. if (this.resizeObserver) this.resizeObserver.disconnect();
  70. if (this.chartInstance) this.chartInstance.dispose();
  71. },
  72. methods: {
  73. safePx2echarts(val) {
  74. return typeof px2echarts === 'function' ? px2echarts(val) : val;
  75. },
  76. initChart() {
  77. if (!this.$refs.chartRef) return;
  78. this.chartInstance = echarts.init(this.$refs.chartRef);
  79. this.updateChart();
  80. },
  81. updateChart() {
  82. if (!this.chartInstance) return;
  83. this.chartInstance.clear();
  84. const chartWidth = this.chartInstance.getWidth();
  85. let seriesData = [];
  86. if (this.activeFaults.length === 0) {
  87. const normalItem = this.chartData.find(item => item.name.indexOf('正常') !== -1);
  88. const normalColor = normalItem ? normalItem.color : '#9FE051';
  89. seriesData = [{
  90. name: '故障',
  91. value: 1,
  92. actualValue: 0,
  93. itemStyle: { color: normalColor },
  94. label: { color: normalColor },
  95. labelLine: { lineStyle: { color: normalColor } }
  96. }];
  97. } else {
  98. seriesData = this.activeFaults.map(item => ({
  99. name: item.name,
  100. value: item.value,
  101. itemStyle: { color: item.color },
  102. label: { color: item.color },
  103. labelLine: { lineStyle: { color: item.color } }
  104. }));
  105. }
  106. const option = {
  107. series: [
  108. {
  109. type: 'pie',
  110. // 【优化1】圆环再缩小一点点,给边缘文字留出绝对安全的距离
  111. radius: ['40%', '55%'],
  112. center: ['50%', '50%'],
  113. startAngle: 210,
  114. avoidLabelOverlap: true,
  115. label: {
  116. show: true,
  117. position: 'outside',
  118. // 【优化2】文字靠近圆环一点,防止撞墙
  119. distance: this.safePx2echarts(5),
  120. formatter: (params) => {
  121. const displayValue = params.data.actualValue !== undefined ? params.data.actualValue : params.value;
  122. return `{name|${params.name}}\n{val|${displayValue}}`;
  123. },
  124. rich: {
  125. name: {
  126. fontSize: this.safePx2echarts(12),
  127. color: 'inherit',
  128. fontWeight: 'bold',
  129. padding: [0, 0, this.safePx2echarts(4), 0]
  130. // 【核心修复】删掉了 width 和 overflow,让 ECharts 自然渲染
  131. },
  132. val: {
  133. fontSize: this.safePx2echarts(12),
  134. color: 'inherit',
  135. fontWeight: 'bold',
  136. padding: [this.safePx2echarts(4), 0, 0, 0]
  137. // 【核心修复】删掉了 width 和 overflow
  138. }
  139. }
  140. },
  141. labelLine: {
  142. show: true,
  143. // 【优化3】斜线改短,将文字往中心拉
  144. length: this.safePx2echarts(8),
  145. length2: this.safePx2echarts(15)
  146. },
  147. labelLayout: (params) => {
  148. const isLeft = params.labelRect.x < chartWidth / 2;
  149. const points = params.labelLinePoints;
  150. if (points) {
  151. points[2][0] = isLeft
  152. ? params.labelRect.x
  153. : params.labelRect.x + params.labelRect.width;
  154. points[1][1] = points[2][1] = params.labelRect.y + params.labelRect.height / 2;
  155. }
  156. return {
  157. labelLinePoints: points
  158. };
  159. },
  160. data: seriesData,
  161. animationType: 'expansion',
  162. animationEasing: 'elasticOut',
  163. animationDuration: 2000,
  164. animationDelay: 200,
  165. }
  166. ]
  167. };
  168. this.chartInstance.setOption(option, true);
  169. }
  170. }
  171. }
  172. </script>
  173. <style scoped>
  174. .donut-chart-wrapper {
  175. display: flex;
  176. align-items: stretch;
  177. justify-content: space-between;
  178. width: 100%;
  179. height: 100%;
  180. }
  181. /* 左侧饼图容器 */
  182. .echarts-wrapper {
  183. position: relative;
  184. width: 65%;
  185. flex: 1;
  186. min-height: 0;
  187. }
  188. .echarts-container {
  189. width: 100%;
  190. height: 100%;
  191. min-height: 0;
  192. }
  193. /* 饼图中心数字 */
  194. .center-text-overlay {
  195. position: absolute;
  196. top: 50%;
  197. left: 50%;
  198. transform: translate(-50%, -50%);
  199. pointer-events: none; /* 防止遮挡鼠标悬停饼图的事件 */
  200. z-index: 10;
  201. }
  202. .main-number {
  203. color: #8392b4;
  204. font-weight: bold;
  205. font-family: Arial, sans-serif;
  206. line-height: 1;
  207. }
  208. /* 右侧图例 */
  209. .custom-legend {
  210. display: flex;
  211. flex-direction: column;
  212. justify-content: center;
  213. gap: 12px;
  214. width: 35%; /* 占据右边 35% 空间 */
  215. padding-right: 10px;
  216. }
  217. .legend-item {
  218. display: flex;
  219. align-items: center;
  220. justify-content: flex-end; /* 靠右对齐 */
  221. gap: 10px;
  222. }
  223. .legend-name {
  224. color: #00d2ff; /* 对应图片中的青色文字 */
  225. font-weight: 400;
  226. }
  227. .legend-color-box {
  228. width: 24px; /* 宽方块 */
  229. height: 12px;
  230. border-radius: 3px;
  231. flex-shrink: 0;
  232. }
  233. </style>