|
|
@@ -0,0 +1,226 @@
|
|
|
+<template>
|
|
|
+ <div class="dashboard-donut-wrapper" :style="{ gap: uiScale.gap + 'px' }">
|
|
|
+
|
|
|
+ <div class="chart-container" :style="{ width: uiScale.chartBox + 'px', height: uiScale.chartBox + 'px' }">
|
|
|
+ <div class="chart-dom" ref="chartRef"></div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="legend-container">
|
|
|
+ <div v-if="showTotal" class="total-header" :style="{ fontSize: uiScale.totalFont + 'px', marginBottom: uiScale.gap + 'px' }">
|
|
|
+ 总时长 <span class="total-num">{{ totalValue }}</span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="legend-list" :style="{ gap: (uiScale.gap * 0.6) + 'px' }">
|
|
|
+ <div
|
|
|
+ class="legend-item"
|
|
|
+ v-for="(item, index) in chartData"
|
|
|
+ :key="index"
|
|
|
+ :style="{ fontSize: uiScale.legendFont + 'px' }"
|
|
|
+ >
|
|
|
+ <i class="color-square" :style="{
|
|
|
+ backgroundColor: item.color,
|
|
|
+ width: uiScale.square + 'px',
|
|
|
+ height: uiScale.square + 'px',
|
|
|
+ marginRight: (uiScale.gap * 0.6) + 'px'
|
|
|
+ }"></i>
|
|
|
+ <span class="item-label" :style="{ minWidth: uiScale.labelWidth + 'px', marginRight: (uiScale.gap * 0.6) + 'px' }">{{ item.label }}</span>
|
|
|
+ <span class="item-value">{{ item.value }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+import * as echarts from 'echarts';
|
|
|
+import echartsResizeMixin from '@/mixins/echartsResize.js';
|
|
|
+
|
|
|
+export default {
|
|
|
+ name: 'PlanDonutChart',
|
|
|
+ // 仍然保留 mixin 用于监听容器尺寸变化触发重绘
|
|
|
+ mixins: [echartsResizeMixin],
|
|
|
+ props: {
|
|
|
+ chartData: { type: Array, required: true, default: () => [] },
|
|
|
+ centerValue: { type: [Number, String], default: 0 },
|
|
|
+ centerLabel: { type: String, default: '' },
|
|
|
+ showTotal: { type: Boolean, default: true },
|
|
|
+ totalValue: { type: [Number, String], default: 0 },
|
|
|
+ scale: { type: Number, default: 0 }
|
|
|
+ },
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ uiScale: {
|
|
|
+ gap: 12,
|
|
|
+ chartBox: 140,
|
|
|
+ totalFont: 13,
|
|
|
+ legendFont: 12,
|
|
|
+ square: 10,
|
|
|
+ labelWidth: 65
|
|
|
+ }
|
|
|
+ };
|
|
|
+ },
|
|
|
+ watch: {
|
|
|
+ chartData: {
|
|
|
+ deep: true,
|
|
|
+ handler() {
|
|
|
+ this.updateChart();
|
|
|
+ }
|
|
|
+ },
|
|
|
+ scale() {
|
|
|
+ this.updateChart();
|
|
|
+ }
|
|
|
+ },
|
|
|
+ mounted() {
|
|
|
+ this.initChart();
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ initChart() {
|
|
|
+ if (!this.$refs.chartRef) return;
|
|
|
+ this.$_chart = echarts.init(this.$refs.chartRef);
|
|
|
+ this.updateChart();
|
|
|
+ },
|
|
|
+
|
|
|
+ // 【核心改造】获取真实的容器缩放比例
|
|
|
+ getLocalScale() {
|
|
|
+ // 优先使用父组件传入的 scale prop
|
|
|
+ if (this.scale > 0) return this.scale;
|
|
|
+ if (!this.$el) return 1;
|
|
|
+ // 降级:读取 CSS 变量 --s
|
|
|
+ const sVal = getComputedStyle(this.$el).getPropertyValue('--s');
|
|
|
+ if (sVal && sVal.trim() !== '' && !isNaN(parseFloat(sVal))) {
|
|
|
+ return parseFloat(sVal);
|
|
|
+ }
|
|
|
+ return window.innerWidth / 1920;
|
|
|
+ },
|
|
|
+
|
|
|
+ calcSize(px) {
|
|
|
+ return Math.round(px * this.getLocalScale());
|
|
|
+ },
|
|
|
+
|
|
|
+ updateChart() {
|
|
|
+ // 每次重绘时,获取最新的局部缩放比例 s
|
|
|
+ const s = this.getLocalScale();
|
|
|
+
|
|
|
+ // 同步更新 HTML 元素的尺寸
|
|
|
+ this.uiScale = {
|
|
|
+ gap: Math.round(12 * s),
|
|
|
+ chartBox: Math.round(140 * s),
|
|
|
+ totalFont: Math.round(13 * s),
|
|
|
+ legendFont: Math.round(12 * s),
|
|
|
+ square: Math.round(10 * s),
|
|
|
+ labelWidth: Math.round(65 * s)
|
|
|
+ };
|
|
|
+
|
|
|
+ if (!this.$_chart) return;
|
|
|
+
|
|
|
+ const option = {
|
|
|
+ color: this.chartData.map(item => item.color),
|
|
|
+ graphic: [
|
|
|
+ {
|
|
|
+ type: 'text',
|
|
|
+ left: 'center',
|
|
|
+ top: '38%',
|
|
|
+ style: {
|
|
|
+ text: this.centerValue,
|
|
|
+ fill: '#ffffff',
|
|
|
+ fontSize: Math.round(24 * s),
|
|
|
+ fontWeight: 'bold'
|
|
|
+ }
|
|
|
+ },
|
|
|
+ {
|
|
|
+ type: 'text',
|
|
|
+ left: 'center',
|
|
|
+ top: '60%',
|
|
|
+ style: {
|
|
|
+ text: this.centerLabel,
|
|
|
+ fill: '#a0aec0',
|
|
|
+ fontSize: Math.round(12 * s)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ ],
|
|
|
+ series: [
|
|
|
+ {
|
|
|
+ type: 'pie',
|
|
|
+ radius: [Math.round(50 * s), Math.round(65 * s)],
|
|
|
+ center: ['50%', '50%'],
|
|
|
+ avoidLabelOverlap: false,
|
|
|
+ label: { show: false },
|
|
|
+ labelLine: { show: false },
|
|
|
+ hoverAnimation: false,
|
|
|
+ data: this.chartData.map(item => ({
|
|
|
+ name: item.label,
|
|
|
+ value: item.value
|
|
|
+ }))
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ };
|
|
|
+
|
|
|
+ this.$_chart.setOption(option);
|
|
|
+ }
|
|
|
+ }
|
|
|
+};
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+/* 整体容器:水平弹性布局 */
|
|
|
+.dashboard-donut-wrapper {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ background-color: transparent;
|
|
|
+ padding: 0;
|
|
|
+ color: #ffffff;
|
|
|
+ font-family: sans-serif;
|
|
|
+}
|
|
|
+
|
|
|
+.chart-container {
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.chart-dom {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+.legend-container {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ justify-content: center;
|
|
|
+}
|
|
|
+
|
|
|
+.total-header {
|
|
|
+ color: #a0aec0;
|
|
|
+}
|
|
|
+
|
|
|
+.total-num {
|
|
|
+ margin-left: 4px;
|
|
|
+ color: #ffffff;
|
|
|
+}
|
|
|
+
|
|
|
+.legend-list {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+}
|
|
|
+
|
|
|
+.legend-item {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ color: #cbd5e1;
|
|
|
+ white-space: nowrap;
|
|
|
+}
|
|
|
+
|
|
|
+.color-square {
|
|
|
+ border-radius: 1px;
|
|
|
+ display: inline-block;
|
|
|
+}
|
|
|
+
|
|
|
+.item-label {
|
|
|
+ display: inline-block;
|
|
|
+}
|
|
|
+
|
|
|
+.item-value {
|
|
|
+ color: #ffffff;
|
|
|
+}
|
|
|
+</style>
|