2026/1/12 5:25:48
网站建设
项目流程
承德建设工程信息网站,百度收录链接,工厂管理培训课程,自己做网站表白前言 之前那篇文章已经实现3D饼图效果#xff0c;这次只是在其基础上进行了简单的组件封装。详情请看vue3中用echarts达到3D饼图的实现
效果演示 添加无数据时占位盒子。#xff08;自行根据ui设计更换样式#xff09; 封装组件 Pie3D.vue组件
templatedivv-i…前言之前那篇文章已经实现3D饼图效果这次只是在其基础上进行了简单的组件封装。详情请看vue3中用echarts达到3D饼图的实现效果演示添加无数据时占位盒子。自行根据ui设计更换样式封装组件Pie3D.vue组件template div v-ifdata.length 0 refchart :idchartId :style{ width: width, height: height } /div div classno-data v-else :style{ width: width, height: height }暂无数据/div /template script setup langts import { onMounted, ref, watch, onUnmounted } from vue import * as echarts from echarts import echarts-gl // 导入PropType用于定义复杂类型 import { PropType } from vue interface PieDataItem { name: string // 名称 val: number // 数值 itemStyle: { color: string // 颜色 } } // 组件props const props defineProps({ // 图表数据 data: { type: Array as PropTypePieDataItem[], required: true, }, // 图表宽度 width: { type: String, default: 600px, }, // 图表高度 height: { type: String, default: 400px, }, // 配置项 config: { type: Object, default: () ({}), }, }) // 图表实例引用 const chart ref(null) let chartInstance null // 生成唯一的图表ID const chartId chart-${Math.random().toString(36).substring(2, 9)} // 计算高度比例 const heightProportion 0.1 // 柱状扇形的高度比例 // 生成扇形的曲面参数方程用于 series-surface.parametricEquation const getParametricEquation (startRatio, endRatio, isSelected, isHovered, k, height) { // 计算 let midRatio (startRatio endRatio) / 3 // 添加间隔角度每个扇形之间的间隙 let gapAngle 0.02 // 间隔角度可调整大小 let startRadian startRatio * Math.PI * 2 gapAngle / 2 let endRadian endRatio * Math.PI * 2 - gapAngle / 2 let midRadian midRatio * Math.PI * 2 // 如果只有一个扇形则不实现选中效果。 if (startRatio 0 endRatio 1) { isSelected false } // 通过扇形内径/外径的值换算出辅助参数 k默认值 1/3 k typeof k ! undefined ? k : 1 / 3 // 计算选中效果分别在 x 轴、y 轴方向上的位移未选中则位移均为 0 let offsetX isSelected ? Math.cos(midRadian) * 0.1 : 0 let offsetY isSelected ? Math.sin(midRadian) * 0.1 : 0 // 计算高亮效果的放大比例未高亮则比例为 1 let hoverRate isHovered ? 1.1 : 1 // 返回曲面参数方程 return { u: { min: -Math.PI, max: Math.PI * 3, step: Math.PI / 32, }, v: { min: 0, max: Math.PI * 2, step: Math.PI / 20, }, x: function (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: function (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: function (u, v) { if (u -Math.PI * 0.5) { return Math.sin(u) } if (u Math.PI * 2.5) { return Math.sin(u) } return Math.sin(v) 0 ? heightProportion * height : -1 }, } } /** * 绘制3d图 * param pieData 总数据 * param config 配置选项 */ const getPie3D (pieData, config {}) { // 从配置中提取参数设置默认值 const { internalDiameterRatio 0, showLabelLine true } config as { internalDiameterRatio?: number showLabelLine?: boolean } let series [] let sumValue 0 let startValue 0 let endValue 0 let legendData [] let linesSeries [] // line3D模拟label指示线 let k typeof internalDiameterRatio ! undefined ? (1 - internalDiameterRatio) / (1 internalDiameterRatio) : 1 / 3 // 为每一个饼图数据生成一个 series-surface 配置 for (let i 0; i pieData.length; i) { sumValue pieData[i].value let seriesItem { name: typeof pieData[i].name undefined ? series${i} : pieData[i].name, type: surface, parametric: true, wireframe: { show: false, }, pieData: pieData[i], pieStatus: { selected: false, hovered: false, k: k, }, } if (typeof pieData[i].itemStyle ! undefined) { let itemStyle {} typeof pieData[i].itemStyle.color ! undefined ? (itemStyle[color] pieData[i].itemStyle.color) : null typeof pieData[i].itemStyle.opacity ! undefined ? (itemStyle[opacity] pieData[i].itemStyle.opacity) : null seriesItem[itemStyle] itemStyle } series.push(seriesItem) } // 使用上一次遍历时计算出的数据和 sumValue调用 getParametricEquation 函数 // 向每个 series-surface 传入不同的参数方程 series-surface.parametricEquation也就是实现每一个扇形。 for (let i 0; i series.length; i) { endValue startValue series[i].pieData.value series[i].pieData.startRatio startValue / sumValue series[i].pieData.endRatio endValue / sumValue series[i].parametricEquation getParametricEquation( series[i].pieData.startRatio, series[i].pieData.endRatio, false, false, k, series[i].pieData.value, ) startValue endValue // 如果需要显示引导线和引导框 if (showLabelLine) { // 计算label指示线的起始和终点位置 let midRadian (series[i].pieData.endRatio series[i].pieData.startRatio) * Math.PI let posX Math.cos(midRadian) * (1 Math.cos(Math.PI / 2)) let posY Math.sin(midRadian) * (1 Math.cos(Math.PI / 2)) let posZ Math.log(Math.abs(series[i].pieData.value 1)) * 0.1 let flag (midRadian 0 midRadian Math.PI / 2) || (midRadian (3 * Math.PI) / 2 midRadian Math.PI * 2) ? 1 : -1 let color pieData[i].itemStyle.color let turningPosArr [ posX * 1.8 i * 0.1 * flag (flag 0 ? -0.5 : 0), posY * 1.8 i * 0.1 * flag (flag 0 ? -0.5 : 0), posZ * 2, ] let endPosArr [ posX * 2.3 i * 0.1 * flag (flag 0 ? -0.5 : 0), posY * 2.7 i * 0.1 * flag (flag 0 ? -0.5 : 0), posZ * 15, ] linesSeries.push( { type: line3D, //引导线 lineStyle: { color: color, }, data: [[posX, posY, posZ], turningPosArr, endPosArr], }, { type: scatter3D, //数据框 label: { show: true, distance: 0, position: center, textStyle: { color: #ffffff, backgroundColor: color, borderWidth: 2, fontSize: 14, padding: 10, borderRadius: 4, }, formatter: {b}, }, symbolSize: 0, data: [{ name: series[i].name \n series[i].pieData.val, value: endPosArr }], }, ) } legendData.push(series[i].name) } series series.concat(linesSeries) // 最底下圆盘 series.push({ name: mouseoutSeries, type: surface, parametric: true, wireframe: { show: false, }, itemStyle: { opacity: 1, color: rgba(25, 93, 176, 0.1), }, parametricEquation: { u: { min: 0, max: Math.PI * 2, step: Math.PI / 20, }, v: { min: 0, max: Math.PI, step: Math.PI / 20, }, x: function (u, v) { return ((Math.sin(v) * Math.sin(u) Math.sin(u)) / Math.PI) * 2 }, y: function (u, v) { return ((Math.sin(v) * Math.cos(u) Math.cos(u)) / Math.PI) * 2 }, z: function (u, v) { return Math.cos(v) 0 ? -0 : -1.5 }, }, }) return series } // 初始化图表 const initChart () { if (!chart.value) return // 销毁已有实例 if (chartInstance) { chartInstance.dispose() } // 初始化图表 chartInstance echarts.init(chart.value) // 计算总数据 let total 0 props.data.forEach((item) { total item.val }) // 准备数据 const pieData props.data.map((item) { return { ...item, value: Number(((item.val / total) * 100).toFixed(2)), } }) // 准备待返回的配置项把准备好的 legendData、series 传入。 const option { legend: { selected: props.data.reduce((acc, item) { acc[item.name] Number(item.val) ! 0 return acc }, {}), tooltip: { show: true, }, data: props.data.map((item) item.name), top: 5%, left: center, icon: circle, textStyle: { color: #fff, fontSize: 14, }, }, animation: true, // 提示框配置 tooltip: { show: true, trigger: item, formatter: function (params) { if (params.seriesName ! mouseoutSeries params.seriesName ! pie2d) { let bfb ( (option.series[params.seriesIndex].pieData.endRatio - option.series[params.seriesIndex].pieData.startRatio) * 100 ).toFixed(2) return ( ${params.seriesName}br/ span styledisplay:inline-block;margin-right:5px;border-radius:10px;width:10px;height:10px;background-color:${params.color};/span 占比:${bfb}% ) } }, backgroundColor: rgba(0, 0, 0, 0.8), borderColor: #ccc, borderWidth: 1, textStyle: { color: #fff, fontSize: 14, }, }, backgroundColor: #333, labelLine: { show: true, lineStyle: { color: #7BC0CB, }, }, label: { show: false, }, // 3D X轴配置 - 控制饼图在水平方向上的显示范围 xAxis3D: { min: -1.5, // X轴最小值 max: 1.5, // X轴最大值 }, // 3D Y轴配置 - 控制饼图在垂直方向上的显示范围 yAxis3D: { min: -1.5, // Y轴最小值 max: 1.5, // Y轴最大值 }, // 3D Z轴配置 - 控制饼图在深度方向上的显示范围 zAxis3D: { min: -1, // Z轴最小值 max: 1, // Z轴最大值 }, grid3D: { show: false, boxHeight: 4, bottom: 50%, viewControl: { distance: props.config.distance || 250, // 相机与物体的距离 alpha: props.config.alpha || 25, // 相机围绕x轴旋转的角度 beta: props.config.beta || 60, // 相机围绕y轴旋转的角度 rotateSensitivity: 0, // 旋转灵敏度设置为0无法旋转 zoomSensitivity: 0, // 缩放灵敏度设置为0无法缩放 panSensitivity: 0, // 平移灵敏度设置为0无法平移 autoRotate: false, // 是否自动旋转 }, }, series: getPie3D(pieData, props.config), } // 设置图表配置 chartInstance.setOption(option) } // 监听数据变化重新渲染图表 watch( () props.data, () { initChart() }, { deep: true }, ) // 监听配置变化重新渲染图表 watch( () props.config, () { initChart() }, { deep: true }, ) // 监听窗口大小变化自动调整图表大小 const handleResize () { if (chartInstance) { chartInstance.resize() } } // 组件挂载时初始化图表 onMounted(() { initChart() window.addEventListener(resize, handleResize) }) // 组件卸载时销毁图表 onUnmounted(() { if (chartInstance) { chartInstance.dispose() } window.removeEventListener(resize, handleResize) }) /script style scoped .no-data { display: flex; justify-content: center; align-items: center; height: 100%; font-size: 14px; color: #333; background-color: #f5f5f5; } /style使用引入组件然后传入data数据和图表configtemplate Pie3D :datadataList :configconfig / /template script setup langts import Pie3D from ./components/Pie3D.vue const dataList [ { name: 办公费, val: 10000, //存储数据的地方 itemStyle: { color: rgba(255, 196, 0, 1), }, }, { name: 差旅费, val: 999, //存储数据的地方 itemStyle: { color: rgba(95, 144, 110, 1), }, }, { name: 伙食费, val: 699, //存储数据的地方 itemStyle: { color: rgba(95, 54, 110, 1), }, }, { name: 招待费, val: 1500, //存储数据的地方 itemStyle: { color: rgba(255, 99, 132, 1), }, }, { name: 水电费, val: 800, //存储数据的地方 itemStyle: { color: rgba(54, 162, 235, 1), }, }, ] const config { showLabelLine: true, // 是否显示引导线 distance: 250, // 相机与物体的距离 alpha: 25, // 相机围绕x轴旋转的角度 beta: 60, // 相机围绕y轴旋转的角度 internalDiameterRatio: 0, // 透明的空心占比 } /script