3D饼图
使用three.js实现,选择threejs的原因:label
需要实际的显示在具体的饼对应的模块上
“three”: “^0.127.0”,
<template>
<div>
<div ref="chartContainer" class="chart-container"></div>
<div class="flex-space-between chart-legend">
<div v-for="(item, index) in lableData" :key="index" class="chart-item">
<div :style="{ color: item.color }" class="tc fs-16 chart-item-value">{{ (100 * (item.value /
chartData)).toFixed(0) }}%
</div>
<div class="chart-item-label mgt-4 tc">{{ item.label }}</div>
</div>
</div>
</div>
</template>
<script>
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
import fontData from './FangSong_Regular.json'
export default {
name: 'ThreeDPieChart',
computed: {
chartData() {
// 总和
return this.config.data.reduce((sum, item) => sum + item.value, 0);
},
},
data() {
return {
lableData:[
{ label: '未完成', value: 20,color:'#FCC943'},
{ label: '未下发', value: 45,color:'#459CFF' },
{ label: '已完成', value: 100 ,color:'#7EC512'},
],
config: {
data: [
{ label: '已完成', value: 100 },
{ label: '未下发', value: 45 },
{ label: '未完成', value: 20 }
],
colors: ['#459CFF', '#A2DC4A', '#F7DF42'],
height: 10,
heightFactor: 4
},
renderer: null,
scene: null,
camera: null,
controls: null,
font: null,
animationId: null
};
},
mounted() {
this.initChart();
window.addEventListener('resize', this.handleResize);
},
beforeDestroy() {
this.cleanup();
},
methods: {
initChart() {
console.log('chushih')
if (!this.$refs.chartContainer) return;
const containerWidth = this.$refs.chartContainer.clientWidth;
const containerHeight = this.$refs.chartContainer.clientHeight;
const outR = Math.min(containerWidth, containerHeight) / 2;
const innerR = outR * 0.6;
// 1. 初始化渲染器(启用抗锯齿和更高的阴影质量)
this.renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true // 允许透明背景
});
this.renderer.setSize(containerWidth, containerHeight);
this.renderer.shadowMap.enabled = true;
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap; // 更柔和的阴影
this.renderer.outputEncoding = THREE.sRGBEncoding; // 更好的颜色渲染
this.$refs.chartContainer.appendChild(this.renderer.domElement);
// 2. 创建场景(设置适当的背景色)
this.scene = new THREE.Scene();
// 添加光源
const light1 = new THREE.PointLight(0xFFF3E0, 0.8);
light1.position.set(0, 1200, 2160);
this.scene.add(light1);
// 环境光(调整强度解决颜色变暗)
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
this.scene.add(ambientLight);
// 4. 创建相机
this.camera = new THREE.OrthographicCamera(
containerWidth / -2,
containerWidth / 2,
containerHeight / 2,
containerHeight / -2,
1,
2000
);
this.camera.position.set(0, 800, 1200);
this.camera.lookAt(0, 0, 0);
// 5. 控制器设置
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
// 6. 加载字体(添加加载状态提示)
this.loadFont(outR, innerR);
},
loadFont(outR, innerR) {
const fontLoader = new THREE.FontLoader();
this.font = fontLoader.parse(fontData);
this.createPieChart(outR, innerR);
},
createPieChart(outR, innerR) {
const group = new THREE.Group();
group.rotation.x = -Math.PI / 2; // 更精确的旋转
this.scene.add(group);
const totalValue = this.config.data.reduce((sum, item) => sum + item.value, 0);
let startAngle = 0;
this.config.data.forEach((item, index) => {
const angleLength = (item.value / totalValue) * Math.PI * 2; // 使用弧度制更精确
const height = this.config.height + (item.value / totalValue) * this.config.height * this.config.heightFactor;
// 使用更鲜艳的颜色
const color = new THREE.Color(this.config.colors[index]);
color.convertSRGBToLinear(); // 确保颜色正确渲染
this.createPieSegment(
group,
outR,
innerR,
height,
startAngle,
angleLength,
color,
`${((item.value / totalValue) * 100).toFixed(0)}%`,
item.label // 添加标签显示
);
startAngle += angleLength;
});
this.animate();
},
createPieSegment(group, outR, innerR, height, startAngle, angleLength, color, text, label) {
// 1. 创建形状
const shape = new THREE.Shape();
shape.absarc(0, 0, outR, startAngle, startAngle + angleLength, false);
shape.lineTo(
Math.cos(startAngle + angleLength) * innerR,
Math.sin(startAngle + angleLength) * innerR
);
shape.absarc(0, 0, innerR, startAngle + angleLength, startAngle, true);
// 2. 挤出设置
const extrudeSettings = {
curveSegments: 100,
steps: 2,
depth: height,
bevelEnabled: true,
bevelThickness: 1,
bevelSize: 0,
bevelOffset: 0,
bevelSegments: 1,
};
// 3. 创建网格(使用更亮的材质)
const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings);
const material = new THREE.MeshPhongMaterial({
color: color,
shininess: 20, roughness: 0.6
});
const mesh = new THREE.Mesh(geometry, material);
group.add(mesh);
// 4. 添加文本(如果字体已加载)
if (this.font) {
this.addTextToSegment(mesh, outR, innerR, height, startAngle, angleLength, text);
}
// 5. 添加标签(可选)
this.addLabelToSegment(group, outR, startAngle, angleLength, label);
},
addTextToSegment(mesh, outR, innerR, height, startAngle, angleLength, text) {
try {
// 计算文本位置和角度
const midAngle = startAngle + angleLength / 2;
const radius = (outR + innerR) / 2;
// 创建文本几何体
const textGeometry = new THREE.TextGeometry(text, {
font: this.font,
size: 11,
height: 2,
curveSegments: 12,
bevelEnabled: false
});
// 计算文本居中
textGeometry.computeBoundingBox();
const textWidth = textGeometry.boundingBox.max.x - textGeometry.boundingBox.min.x;
// 创建文本材质(更醒目的颜色)
const textMaterial = new THREE.MeshPhongMaterial({
color: 0xffffff
});
const textMesh = new THREE.Mesh(textGeometry, textMaterial);
// 定位和旋转文本
textMesh.position.set(
Math.cos(midAngle) * radius - textWidth / 2,
Math.sin(midAngle) * radius - 10,
height + 0
);
textMesh.rotation.set(
120,// X轴旋转90度使文字立起来
0, // Y轴不需要旋转
0 // Z轴旋转使文字朝向圆心
);
// textMesh.rotation.z = midAngle + Math.PI / 2;
// textMesh.rotation.x = Math.PI / 2;
mesh.add(textMesh);
} catch (error) {
console.error('创建文本失败:', error);
}
},
addLabelToSegment(group, radius, startAngle, angleLength, label) {
// 创建简单的标签(使用CSS2DRenderer或Three.js精灵)
// 这里简化为控制台输出
console.log(`Segment Label: ${label}`);
},
animate() {
this.animationId = requestAnimationFrame(this.animate);
this.controls.update();
this.renderer.render(this.scene, this.camera);
},
handleResize() {
if (!this.renderer || !this.camera || !this.$refs.chartContainer) return;
const width = this.$refs.chartContainer.clientWidth;
const height = this.$refs.chartContainer.clientHeight;
this.camera.left = width / -2;
this.camera.right = width / 2;
this.camera.top = height / 2;
this.camera.bottom = height / -2;
this.camera.updateProjectionMatrix();
this.renderer.setSize(width, height);
},
cleanup() {
window.removeEventListener('resize', this.handleResize);
if (this.animationId) {
cancelAnimationFrame(this.animationId);
}
if (this.renderer && this.$refs.chartContainer && this.$refs.chartContainer.contains(this.renderer.domElement)) {
this.$refs.chartContainer.removeChild(this.renderer.domElement);
}
// 释放资源
if (this.scene) {
while (this.scene.children.length > 0) {
this.scene.remove(this.scene.children[0]);
}
}
}
}
};
</script>
<style lang="scss" scoped>
.chart-container {
width: 100%;
margin-top: 15px;
height: 150px;
padding: 0;
overflow: hidden;
background: url(~@/assets/images/taskManagement/piebg.png) center bottom 40%/200px 87px no-repeat;
}
.chart-legend {
padding: 0 15px;
.mgt-4 {
margin-top: 4px;
}
}
</style>