2025-05-15 14:57:30 +08:00

493 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div ref="chartRef" :style="{ width: width, height: height }"></div>
</template>
<script setup>
import { ref, watch, onMounted, nextTick } from 'vue';
import { cloneDeep } from 'lodash';
import { useEcharts } from '@/hooks/useEcharts';
defineOptions({ name: 'NewHyalineCake' });
// 定义组件 props
const props = defineProps({
chartData: {
type: Array,
default: () => [
// 默认示例数据
{ name: '项目一', value: 60 },
{ name: '项目二', value: 44 },
{ name: '项目三', value: 32 },
],
},
option: {
type: Object,
default: () => ({
// 控制内外径关系的系数1 表示无内径(实心饼),值越小内径越大
k: 1,
// 扇形边缘向外偏移距离比例(用于选中效果),单位视图坐标,可调
itemGap: 0.2,
// 扇形高度影响z轴拉伸程度
itemHeight: 120,
// 自动计算扇形高度时使用的系数(>0时 itemHeight 失效,使用 autoItemHeight * value
autoItemHeight: 0,
// 透明度设置
opacity: 0.6,
// 图例后缀
legendSuffix: '',
}),
},
width: {
type: String,
default: '100%',
},
height: {
type: String,
default: '100%',
},
});
// 声明组件触发的事件
const emit = defineEmits(['click']);
// 绑定到DOM的容器引用
const chartRef = ref(null);
// 使用 useEcharts 钩子初始化 ECharts 实例,并获取控制方法
const { setOptions, getInstance } = useEcharts(chartRef);
// 存储当前的 ECharts 配置项
const chartOption = ref({});
// 参数方程函数:生成每个扇形曲面的参数方程,用于 series-surface.parametricEquation
function getParametricEquation(startRatio, endRatio, isSelected, isHovered, k, h) {
// 中心弧度用于计算偏移方向
const midRatio = (startRatio + endRatio) / 2;
const startRadian = startRatio * Math.PI * 2;
const endRadian = endRatio * Math.PI * 2;
const midRadian = midRatio * Math.PI * 2;
// 如果整个饼只剩一个扇形,则不实现选中效果
if (startRatio === 0 && endRatio === 1) {
isSelected = false;
}
// k 取默认值 1/3环的厚度比例如果传入 k 则使用传入值
k = typeof k !== 'undefined' ? k : 1 / 3;
// 计算选中效果的偏移量(基于扇形中心角度)
const offsetX = isSelected ? Math.cos(midRadian) * props.option.itemGap : 0;
const offsetY = isSelected ? Math.sin(midRadian) * props.option.itemGap : 0;
// 计算高亮时的放大比例未高亮时为1
const hoverRate = isHovered ? 1.05 : 1;
// 返回 parametric 曲面方程
return {
u: {
// u 参数控制饼的周向角度:从 -π 到 3π可以完整绘制一圈
min: -Math.PI,
max: Math.PI * 3,
step: Math.PI / 32,
},
v: {
// v 从 0 - 2π 参数控制扇形的径向方向(厚度方向)
min: 0,
max: Math.PI * 2,
step: Math.PI / 20,
},
x(u, v) {
// 如果在起始弧度之前,保持扇形起点的半径
if (u < startRadian) {
return offsetX + Math.cos(startRadian) * (1 + Math.cos(v) * k) * hoverRate;
}
// 如果在结束弧度之后,保持扇形终点的半径
if (u > endRadian) {
return offsetX + Math.cos(endRadian) * (1 + Math.cos(v) * k) * hoverRate;
}
// 扇形中间部分正常绘制
return offsetX + Math.cos(u) * (1 + Math.cos(v) * k) * hoverRate;
},
y(u, v) {
if (u < startRadian) {
return offsetY + Math.sin(startRadian) * (1 + Math.cos(v) * k) * hoverRate;
}
if (u > endRadian) {
return offsetY + Math.sin(endRadian) * (1 + Math.cos(v) * k) * hoverRate;
}
return offsetY + Math.sin(u) * (1 + Math.cos(v) * k) * hoverRate;
},
z(u, v) {
// 在 u < -π/2 时,防止出现不必要的翻转,直接使用基本正弦
if (u < -Math.PI * 0.5) {
return Math.sin(u);
}
// 在 u > 2.5π 时,处理扇形尾部厚度
if (u > Math.PI * 2.5) {
return Math.sin(u) * h * 0.1;
}
// 正常情况下z 根据 v 参数控制上下表面的高度差
// 当前图形的高度是Z根据h每个value的值决定的
return Math.sin(v) > 0 ? 1 * h * 0.1 : -1;
},
};
}
// 生成整个 3D 饼图的配置项
function getPie3D(pieData) {
const series = [];
let sumValue = 0;
// 计算总值
pieData.forEach((item) => {
sumValue += item.value;
});
// k 为外径厚度系数
const k = props.option.k ?? 1;
// 构建每个扇形的 series 数据
pieData.forEach((dataItem, idx) => {
const seriesItem = {
name: dataItem.name ?? `series${idx}`,
type: 'surface',
parametric: true,
wireframe: { show: false },
pieData: dataItem,
itemStyle: {
opacity: props.option.opacity,
borderRadius: 300,
borderColor: '#fff',
borderWidth: 0,
},
pieStatus: {
selected: false,
hovered: false,
k,
},
};
// 合并自定义样式(如果有)
if (dataItem.itemStyle) {
const customStyle = {};
if (dataItem.itemStyle.color !== undefined) {
customStyle.color = dataItem.itemStyle.color;
}
if (dataItem.itemStyle.opacity !== undefined) {
customStyle.opacity = dataItem.itemStyle.opacity;
}
seriesItem.itemStyle = { ...seriesItem.itemStyle, ...customStyle };
}
series.push(seriesItem);
});
// 计算每个扇形的 startRatio/endRatio 和参数方程
let startValue = 0;
series.forEach((serie) => {
const endValue = startValue + serie.pieData.value;
const startRatio = startValue / sumValue;
const endRatio = endValue / sumValue;
serie.pieData.startRatio = startRatio;
serie.pieData.endRatio = endRatio;
// 初始均为未选中未高亮
serie.parametricEquation = getParametricEquation(
startRatio,
endRatio,
false,
true,
k,
// 扇形高度:根据配置自动计算或使用固定高度
props.option.autoItemHeight > 0 ? props.option.autoItemHeight * serie.pieData.value : props.option.itemHeight
);
startValue = endValue;
});
// 添加一个透明圆环,用于实现 hover 在扇形外面时取消高亮
series.push({
name: 'mouseoutSeries',
type: 'surface',
parametric: true,
wireframe: { show: false },
itemStyle: { opacity: 0 },
parametricEquation: {
u: { min: 0, max: Math.PI * 2, step: Math.PI / 20 },
v: { min: 0, max: Math.PI, step: Math.PI / 20 },
x(u, v) {
return Math.sin(v) * Math.sin(u) + Math.sin(u);
},
y(u, v) {
return Math.sin(v) * Math.cos(u) + Math.cos(u);
},
z(u, v) {
return Math.cos(v) > 0 ? 0.1 : -0.1;
},
},
});
// 基础配置:坐标轴、视角、图例、提示框等
const option = Object.assign(
{
tooltip: {
backgroundColor: 'rgba(18, 55, 85, 0.8)',
borderColor: '#35d0c0',
color: '#fff',
position: (point, params, dom, rect, size) => {
// 动态调整 tooltip 位置,避免溢出
let x = point[0],
y = point[1];
const [viewW, viewH] = size.viewSize;
const [boxW, boxH] = size.contentSize;
if (x + boxW > viewW) x -= boxW;
if (y + boxH > viewH) y -= boxH;
if (x < 0) x = 0;
if (y < 0) y = 0;
return [x, y];
},
formatter: (params) => {
// 只对非透明环(实际扇形)显示信息
if (params.seriesName !== 'mouseoutSeries') {
return `
<span style="color:#FFF">
${params.seriesName}<br/>
<span style="
display:inline-block;
margin-right:5px;
border-radius:10px;
width:10px;
height:10px;
background-color:${params.color};"></span>
${chartOption.value.series[params.seriesIndex].pieData.value}
</span>`;
}
return '';
},
},
xAxis3D: { min: -1, max: 1 },
yAxis3D: { min: -1, max: 1 },
zAxis3D: { min: -1, max: 1 },
grid3D: {
show: false,
boxHeight: 5,
top: '0',
left: '-20%',
viewControl: {
// 3D 视角设置
alpha: 60, // 俯仰角度
distance: 240, // 视角距离
rotateSensitivity: 10,
zoomSensitivity: 10,
panSensitivity: 10,
autoRotate: true,
autoRotateAfterStill: 2,
},
},
legend: {
show: true,
selectedMode: false,
right: '5%',
top: '25%',
orient: 'vertical',
icon: 'circle',
itemHeight: 12,
itemWidth: 12,
itemGap: 10,
textStyle: {
color: '#fff',
fontSize: 14,
fontWeight: '400',
},
// 图例标签显示名称和数值
formatter: (name) => {
const item = props.chartData.find((d) => d.name === name);
return item ? ` ${name} ${item.value}${props.option.legendSuffix || ''}` : name;
},
},
series,
},
// 将外部配置项覆盖基础配置
props.option
);
return option;
}
// 初始化图表
function initChart() {
// 生成基础的3D饼图配置
const baseOption = getPie3D(props.chartData);
// 合并用户配置,确保不修改原始对象
const finalOption = Object.assign({}, baseOption, cloneDeep(props.option || {}));
chartOption.value = finalOption;
// 设置图表配置
setOptions(chartOption.value);
// 等待 DOM + ECharts 初始化完成
nextTick(() => {
const chart = getInstance();
if (!chart) {
console.warn('ECharts 实例未初始化,事件绑定失败');
return;
}
// 避免重复绑定事件
chart.off('click');
chart.off('mouseover');
chart.off('globalout');
chart.on('click', handleClick);
chart.on('mouseover', handleMouseover);
chart.on('globalout', handleGlobalout);
});
}
function handleClick(params) {
// 如果点击的是透明辅助环,则忽略
if (params.seriesName === 'mouseoutSeries') return;
const optionVal = chartOption.value;
const series = optionVal.series;
const idx = params.seriesIndex;
// 新的选中状态(取反)
const isSelected = !series[idx].pieStatus.selected;
const isHovered = series[idx].pieStatus.hovered;
const k = series[idx].pieStatus.k;
const startRatio = series[idx].pieData.startRatio;
const endRatio = series[idx].pieData.endRatio;
const h = series[idx].pieData.value;
// 如果之前有其他扇形被选中,取消其选中状态
if (selectedIndex !== null && selectedIndex !== idx) {
const prev = series[selectedIndex];
prev.parametricEquation = getParametricEquation(
prev.pieData.startRatio,
prev.pieData.endRatio,
false,
false,
prev.pieStatus.k,
prev.pieData.value
);
prev.pieStatus.selected = false;
selectedIndex = null;
}
// 设置当前扇形的选中/取消选中状态
series[idx].parametricEquation = getParametricEquation(startRatio, endRatio, isSelected, isHovered, k, h);
series[idx].pieStatus.selected = isSelected;
// 更新记录选中的系列索引
selectedIndex = isSelected ? idx : null;
// 更新图表配置
setOptions(optionVal);
// 触发组件 click 事件供父组件使用
emit('click', params);
}
// 在函数顶部声明(确保作用域覆盖所有需要的地方)
window.debugZValues = {
current: null,
history: [],
};
function handleMouseover(params) {
if (params.seriesName === 'mouseoutSeries') return;
const chart = getInstance();
const optionVal = chart.getOption(); // 改用实时获取最新配置
const series = optionVal.series;
const idx = params.seriesIndex;
const hh = series[idx].parametricEquation.z();
window.debugZValues.current = hh;
window.debugZValues.history.push({
event: 'mouseover',
series: series[idx].name,
zValue: hh,
time: new Date().toISOString(),
});
console.log('当前Z值:', hh, '历史记录:', window.debugZValues.history);
// 打印移入前的完整状态(调试用)
console.log(
'[移入] 当前所有扇形状态',
series.map((s) => ({
name: s.name,
hovered: s.pieStatus?.hovered,
selected: s.pieStatus?.selected,
height: s.parametricEquation?.z(Math.PI, Math.PI),
}))
);
// 取消之前的高亮(确保只修改状态,不破坏参数方程)
if (hoveredIndex !== null && hoveredIndex !== idx) {
const prev = series[hoveredIndex];
prev.pieStatus.hovered = false; // 仅修改状态
prev.parametricEquation = getParametricEquation(
// 重新生成正确方程
prev.pieData.startRatio,
prev.pieData.endRatio,
prev.pieStatus.selected,
false, // isHovered=false
prev.pieStatus.k,
prev.pieData.value
);
}
// 设置新高亮
const current = series[idx];
current.pieStatus.hovered = true;
current.parametricEquation = getParametricEquation(
current.pieData.startRatio,
current.pieData.endRatio,
current.pieStatus.selected,
true, // isHovered=true
current.pieStatus.k,
current.pieData.value
);
hoveredIndex = idx;
chart.setOption({ series }); // 仅更新series部分
}
function handleGlobalout() {
if (hoveredIndex !== null) {
const chart = getInstance();
const optionVal = chart.getOption();
const series = optionVal.series;
const prev = series[hoveredIndex];
// 打印修复前的错误状态(调试用)
console.warn('[修复前] 异常状态', {
name: prev.name,
z: prev.parametricEquation.z(Math.PI, Math.PI),
equation: prev.parametricEquation,
});
// 重置状态并重新生成方程
prev.pieStatus.hovered = false;
prev.parametricEquation = getParametricEquation(
prev.pieData.startRatio,
prev.pieData.endRatio,
prev.pieStatus.selected,
false, // isHovered=false
prev.pieStatus.k,
prev.pieData.value
);
hoveredIndex = null;
chart.setOption({ series }, { replaceMerge: 'series' }); // 强制替换series
}
}
// 记录当前选中和高亮的系列索引
let selectedIndex = null;
let hoveredIndex = null;
// 组件挂载后绑定事件
onMounted(() => {
initChart();
});
// 监听数据或配置变更,重新初始化图表
watch(
[() => props.chartData, () => props.option],
() => {
if (props.chartData && props.chartData.length) {
initChart();
}
},
{ immediate: true, deep: true }
);
</script>
<style scoped>
/* 可根据需要自定义图表容器样式 */
</style>