PlanDonutChart.vue 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. <template>
  2. <div class="dashboard-donut-wrapper" :style="{ gap: uiScale.gap + 'px' }">
  3. <div class="chart-container" :style="{ width: uiScale.chartBox + 'px', height: uiScale.chartBox + 'px' }">
  4. <div class="chart-dom" ref="chartRef"></div>
  5. </div>
  6. <div class="legend-container">
  7. <div v-if="showTotal" class="total-header" :style="{ fontSize: uiScale.totalFont + 'px', marginBottom: uiScale.gap + 'px' }">
  8. 总时长 <span class="total-num">{{ totalValue }}</span>
  9. </div>
  10. <div class="legend-list" :style="{ gap: (uiScale.gap * 0.6) + 'px' }">
  11. <div
  12. class="legend-item"
  13. v-for="(item, index) in chartData"
  14. :key="index"
  15. :style="{ fontSize: uiScale.legendFont + 'px' }"
  16. >
  17. <i class="color-square" :style="{
  18. backgroundColor: item.color,
  19. width: uiScale.square + 'px',
  20. height: uiScale.square + 'px',
  21. marginRight: (uiScale.gap * 0.6) + 'px'
  22. }"></i>
  23. <span class="item-label" :style="{ minWidth: uiScale.labelWidth + 'px', marginRight: (uiScale.gap * 0.6) + 'px' }">{{ item.label }}</span>
  24. <span class="item-value">{{ item.value }}</span>
  25. </div>
  26. </div>
  27. </div>
  28. </div>
  29. </template>
  30. <script>
  31. import * as echarts from 'echarts';
  32. import echartsResizeMixin from '@/mixins/echartsResize.js';
  33. export default {
  34. name: 'PlanDonutChart',
  35. // 仍然保留 mixin 用于监听容器尺寸变化触发重绘
  36. mixins: [echartsResizeMixin],
  37. props: {
  38. chartData: { type: Array, required: true, default: () => [] },
  39. centerValue: { type: [Number, String], default: 0 },
  40. centerLabel: { type: String, default: '' },
  41. showTotal: { type: Boolean, default: true },
  42. totalValue: { type: [Number, String], default: 0 },
  43. scale: { type: Number, default: 0 }
  44. },
  45. data() {
  46. return {
  47. uiScale: {
  48. gap: 12,
  49. chartBox: 140,
  50. totalFont: 13,
  51. legendFont: 12,
  52. square: 10,
  53. labelWidth: 65
  54. }
  55. };
  56. },
  57. watch: {
  58. chartData: {
  59. deep: true,
  60. handler() {
  61. this.updateChart();
  62. }
  63. },
  64. scale() {
  65. this.updateChart();
  66. }
  67. },
  68. mounted() {
  69. this.initChart();
  70. },
  71. methods: {
  72. initChart() {
  73. if (!this.$refs.chartRef) return;
  74. this.$_chart = echarts.init(this.$refs.chartRef);
  75. this.updateChart();
  76. },
  77. // 【核心改造】获取真实的容器缩放比例
  78. getLocalScale() {
  79. // 优先使用父组件传入的 scale prop
  80. if (this.scale > 0) return this.scale;
  81. if (!this.$el) return 1;
  82. // 降级:读取 CSS 变量 --s
  83. const sVal = getComputedStyle(this.$el).getPropertyValue('--s');
  84. if (sVal && sVal.trim() !== '' && !isNaN(parseFloat(sVal))) {
  85. return parseFloat(sVal);
  86. }
  87. return window.innerWidth / 1920;
  88. },
  89. calcSize(px) {
  90. return Math.round(px * this.getLocalScale());
  91. },
  92. updateChart() {
  93. // 每次重绘时,获取最新的局部缩放比例 s
  94. const s = this.getLocalScale();
  95. // 同步更新 HTML 元素的尺寸
  96. this.uiScale = {
  97. gap: Math.round(12 * s),
  98. chartBox: Math.round(140 * s),
  99. totalFont: Math.round(13 * s),
  100. legendFont: Math.round(12 * s),
  101. square: Math.round(10 * s),
  102. labelWidth: Math.round(65 * s)
  103. };
  104. if (!this.$_chart) return;
  105. const option = {
  106. color: this.chartData.map(item => item.color),
  107. graphic: [
  108. {
  109. type: 'text',
  110. left: 'center',
  111. top: '38%',
  112. style: {
  113. text: this.centerValue,
  114. fill: '#ffffff',
  115. fontSize: Math.round(24 * s),
  116. fontWeight: 'bold'
  117. }
  118. },
  119. {
  120. type: 'text',
  121. left: 'center',
  122. top: '60%',
  123. style: {
  124. text: this.centerLabel,
  125. fill: '#a0aec0',
  126. fontSize: Math.round(12 * s)
  127. }
  128. }
  129. ],
  130. series: [
  131. {
  132. type: 'pie',
  133. radius: [Math.round(50 * s), Math.round(65 * s)],
  134. center: ['50%', '50%'],
  135. avoidLabelOverlap: false,
  136. label: { show: false },
  137. labelLine: { show: false },
  138. hoverAnimation: false,
  139. data: this.chartData.map(item => ({
  140. name: item.label,
  141. value: item.value
  142. }))
  143. }
  144. ]
  145. };
  146. this.$_chart.setOption(option);
  147. }
  148. }
  149. };
  150. </script>
  151. <style scoped>
  152. /* 整体容器:水平弹性布局 */
  153. .dashboard-donut-wrapper {
  154. display: flex;
  155. align-items: center;
  156. background-color: transparent;
  157. padding: 0;
  158. color: #ffffff;
  159. font-family: sans-serif;
  160. }
  161. .chart-container {
  162. display: flex;
  163. justify-content: center;
  164. align-items: center;
  165. flex-shrink: 0;
  166. }
  167. .chart-dom {
  168. width: 100%;
  169. height: 100%;
  170. }
  171. .legend-container {
  172. display: flex;
  173. flex-direction: column;
  174. justify-content: center;
  175. }
  176. .total-header {
  177. color: #a0aec0;
  178. }
  179. .total-num {
  180. margin-left: 4px;
  181. color: #ffffff;
  182. }
  183. .legend-list {
  184. display: flex;
  185. flex-direction: column;
  186. }
  187. .legend-item {
  188. display: flex;
  189. align-items: center;
  190. color: #cbd5e1;
  191. white-space: nowrap;
  192. }
  193. .color-square {
  194. border-radius: 1px;
  195. display: inline-block;
  196. }
  197. .item-label {
  198. display: inline-block;
  199. }
  200. .item-value {
  201. color: #ffffff;
  202. }
  203. </style>