258 lines
8.4 KiB
Vue
258 lines
8.4 KiB
Vue
<template>
|
||
<!-- 动态绑定 ref:使用 props 中的 chartRef,而非硬编码 -->
|
||
<div :ref="chartRef" id="coreLineChart" style="height: 100%; width: 100%;"></div>
|
||
</template>
|
||
|
||
<script>
|
||
import * as echarts from 'echarts';
|
||
|
||
export default {
|
||
name: 'Container',
|
||
props: {
|
||
// 1. 重命名 props:ref → chartRef,避免关键字冲突,同时定义类型和默认值
|
||
chartRef: {
|
||
type: String,
|
||
default: 'pieChartRef' // 默认ref名称
|
||
},
|
||
chartData: {
|
||
type: Array, // 明确类型为数组(接收[{name, rate, value}, ...])
|
||
default: () => [] // 默认空数组,避免报错
|
||
},
|
||
chartTitle: { // 新增:动态标题(可选,灵活配置)
|
||
type: String,
|
||
default: '客户销量分析'
|
||
},
|
||
unit: { // 新增:动态单位(可选)
|
||
type: String,
|
||
default: '万m²'
|
||
}
|
||
},
|
||
data() {
|
||
return {
|
||
// 提取公共颜色配置,复用样式
|
||
customColors: [
|
||
'rgba(39, 96, 255, 1)',
|
||
'rgba(40, 138, 255, 1)',
|
||
'rgba(118, 218, 190, 1)',
|
||
'rgba(255, 206, 106, 1)',
|
||
'rgba(255, 123, 123, 1)', // 额外补充颜色,适配更多数据项
|
||
'rgba(153, 102, 255, 1)'
|
||
],
|
||
myChart: null, // 保存图表实例,便于销毁和缩放
|
||
resizeHandler: null, // 窗口resize事件处理器
|
||
isMounted: false // 图表挂载标志,避免过早执行
|
||
};
|
||
},
|
||
computed: {},
|
||
watch: {
|
||
// 监听 chartData 变化,只要数据变了,就更新图表
|
||
chartData: {
|
||
handler() {
|
||
if (!this.isMounted) return; // 挂载前保护
|
||
this.initData(); // 直接调用更新
|
||
},
|
||
deep: true // 深度监听数组/对象变化
|
||
}
|
||
},
|
||
mounted() {
|
||
this.isMounted = true;
|
||
this.$nextTick(() => {
|
||
this.initData(); // 初始化图表
|
||
});
|
||
// 注册窗口resize事件,使用稳定的引用以便后续移除
|
||
this.resizeHandler = () => {
|
||
if (this.myChart) {
|
||
this.myChart.resize();
|
||
}
|
||
};
|
||
window.addEventListener('resize', this.resizeHandler);
|
||
},
|
||
beforeDestroy() {
|
||
// 移除窗口resize事件监听器
|
||
if (this.resizeHandler) {
|
||
window.removeEventListener('resize', this.resizeHandler);
|
||
this.resizeHandler = null;
|
||
}
|
||
// 销毁图表,避免内存泄漏
|
||
if (this.myChart) {
|
||
this.myChart.dispose();
|
||
this.myChart = null;
|
||
}
|
||
},
|
||
methods: {
|
||
initData() {
|
||
console.log('动态饼图数据:', this.chartData);
|
||
|
||
const chartDom = this.$refs[this.chartRef];
|
||
if (!chartDom) {
|
||
if (process.env.NODE_ENV === 'development') console.warn(`图表容器未找到!请确认父组件传递的 chartRef 为 "${this.chartRef}"`);
|
||
return;
|
||
}
|
||
|
||
// 销毁旧实例,避免内存泄漏
|
||
if (this.myChart) {
|
||
this.myChart.dispose();
|
||
}
|
||
// 初始化新实例
|
||
this.myChart = echarts.init(chartDom);
|
||
|
||
// ========== 核心:动态生成饼图数据 ==========
|
||
const dynamicPieData = this.chartData.map((item, index) => {
|
||
const color = this.customColors[index % this.customColors.length]; // 循环取色,适配多数据项
|
||
return {
|
||
name: item.name, // 动态名称(产品1/产品2/客户1等)
|
||
value: item.value || 0, // 动态数值
|
||
rate: item.rate || 0, // 动态占比(备用)
|
||
// 标签样式(复用原有格式,动态匹配颜色)
|
||
label: {
|
||
normal: {
|
||
align: 'left',
|
||
distanceToLabelLine: 2,
|
||
formatter: (params) => {
|
||
const wrapMixedText = (text, maxUnits = 9, maxLines = 2) => {
|
||
if (!text) return ''
|
||
let line = ''
|
||
let units = 0
|
||
const lines = []
|
||
const charUnit = (ch) => {
|
||
// 简单权重:中文/全角更“宽”
|
||
if (/[\u4e00-\u9fa5]/.test(ch)) return 1
|
||
if (/[A-Z]/.test(ch)) return 0.85
|
||
if (/[a-z0-9]/.test(ch)) return 0.65
|
||
if (/\s/.test(ch)) return 0.3
|
||
return 0.8
|
||
}
|
||
for (let i = 0; i < text.length; i++) {
|
||
const ch = text[i]
|
||
const u = charUnit(ch)
|
||
// 超过阈值就换行
|
||
if (units + u > maxUnits && line) {
|
||
lines.push(line)
|
||
line = ch
|
||
units = u
|
||
if (lines.length >= maxLines) {
|
||
// 还有剩余就省略
|
||
const rest = text.slice(i + 1)
|
||
if (rest) lines[maxLines - 1] = (lines[maxLines - 1] + '...').replace(/\.\.\.\.+$/, '...')
|
||
return lines.join('\n')
|
||
}
|
||
} else {
|
||
line += ch
|
||
units += u
|
||
}
|
||
}
|
||
if (line) lines.push(line)
|
||
return lines.slice(0, maxLines).join('\n')
|
||
}
|
||
const nameWrapped = wrapMixedText(params.name, 6, 1)
|
||
return [
|
||
`{b|${Number(params.value || 0).toLocaleString()}(${item.rate}%)}`,
|
||
// 第2行放色块 + 名称第1行;如果名称有第2行,会自然变成第3行
|
||
`{hr|■}{c|${nameWrapped}}`
|
||
].join('\n')
|
||
},
|
||
rich: {
|
||
hr: {
|
||
color: color,
|
||
fontSize: 20,
|
||
padding: [26, 8, 0, 0]
|
||
},
|
||
b: {
|
||
color: 'rgba(0, 0, 0, 0.75)',
|
||
fontSize: 13,
|
||
padding: [0, 0, 0, 0]
|
||
},
|
||
c: {
|
||
color: 'rgba(64, 64, 64, 1)',
|
||
fontSize: 14,
|
||
padding: [30, 0, 0, -5]
|
||
}
|
||
}
|
||
}
|
||
},
|
||
// 标签线样式(动态匹配颜色)
|
||
labelLine: {
|
||
lineStyle: { color: color },
|
||
length: 5,
|
||
length2: 5,
|
||
},
|
||
// 扇区样式(动态匹配颜色)
|
||
itemStyle: { color: color }
|
||
};
|
||
});
|
||
|
||
// 图表配置项
|
||
const option = {
|
||
tooltip:{
|
||
confine:true,
|
||
formatter:params => {
|
||
const {name,value,percent} = params;
|
||
return `${name}:${value} (${percent}%)`;
|
||
}
|
||
},
|
||
// 动态标题(支持父组件传递)
|
||
title: [
|
||
{
|
||
text: this.chartTitle,
|
||
left: 'center',
|
||
top: '35%',
|
||
textStyle: {
|
||
fontSize: 15,
|
||
letterSpacing: 5,
|
||
color: 'rgba(0, 0, 0, 0.55)',
|
||
fontFamily: 'PingFangSC, PingFang SC'
|
||
}
|
||
},
|
||
{
|
||
text: `单位:${this.unit}`,
|
||
left: 'center',
|
||
top: '50%',
|
||
textStyle: {
|
||
fontSize: 14,
|
||
color: 'rgba(0, 0, 0, 0.55)',
|
||
fontFamily: 'PingFangSC, PingFang SC'
|
||
}
|
||
}
|
||
],
|
||
series: [
|
||
{
|
||
name: '销量',
|
||
type: 'pie',
|
||
radius: ['50%', '70%'],
|
||
center: ['50%', '50%'],
|
||
avoidLabelOverlap: false,
|
||
label: {
|
||
show: true,
|
||
position: 'outside',
|
||
distance: 10,
|
||
formatter: '{c}万m²\n{b}',
|
||
textStyle: {
|
||
fontSize: 14,
|
||
color: 'rgba(0, 0, 0, 0.7)',
|
||
fontFamily: 'PingFangSC, PingFang SC',
|
||
lineHeight: 1.5
|
||
}
|
||
},
|
||
labelLine: {
|
||
show: true,
|
||
lineStyle: {
|
||
// 若未单独配置标签线颜色,默认取对应数据项的颜色
|
||
color: (params) => this.customColors[params.dataIndex % this.customColors.length]
|
||
}
|
||
},
|
||
itemStyle: {
|
||
// 若未单独配置扇区颜色,默认取对应数据项的颜色
|
||
color: (params) => this.customColors[params.dataIndex % this.customColors.length]
|
||
},
|
||
data: dynamicPieData // 使用动态生成的数据,替代硬编码数据
|
||
}
|
||
]
|
||
};
|
||
|
||
// 设置图表配置
|
||
option && this.myChart.setOption(option);
|
||
}
|
||
},
|
||
};
|
||
</script>
|