需求

如上图,封装一个组件,通过传入不同的数据展示对应的评分等级:
1-5分处于红色,评差;
6分处于粉色,评中;
7分处于橙色,评良;
8-10分处于绿色,评优秀。
代码
父组件通过接口拿到对应的数据,保存到data中,通过props将数据传给渲染组件。
javascript
import React, { useState, useEffect } from 'react';
const ParentCom = () => {
const [data, setData] = useState([]);
useEffect(() => {
// 这里可以做一些接口请求等操作
setData([
{ value: 9, icon: '', title: 'XXX1' },
{ value: 5, icon: '', title: 'XXX2' },
{ value: 6, icon: '', title: 'XXX3' },
{ value: 7, icon: '', title: 'XXX4' },
{ value: 3, icon: '', title: 'XXX5' }
]);
},[]);
return <div>
<div>{/*页面的其他渲染内容*/}</div>
<ProgressChartsCom data={data} />
</div>
};
export default ParentCom;
在 ProgressChartsCom 组件中进项详细的逻辑处理。
javascript
import React, { memo, useEffect, useRef } from 'react';
import { PieChart } from 'echarts/charts';
import { GraphicComponent, LegendComponent, TooltipComponent } from 'echarts/components';
import * as echarts from 'echarts/core';
import { LabelLayout } from 'echarts/features';
import { CanvasRenderer } from 'echarts/renderers';
import styles from './index.module.less';
echarts.use([
TooltipComponent,
LegendComponent,
PieChart,
CanvasRenderer,
LabelLayout,
GraphicComponent,
]);
interface IAnnulus {
value: number;
icon: string;
}
const ProgressChartsCom = ({ data }: { data: IAnnulus[] }) => {
/**
* 根据分数获取对应评价
* @param value 进度条数值
* @returns 评价
*/
const getGard = (value) => {
if (value <= 5) return '差';
if (value === 6) return '中';
if (value === 7) return '良';
if (value >= 8 && value <= 10) return '优秀';
return '未知';
};
return <div style={{ display: 'flex', justifyContent: 'space-around', height: '200px' }}>
{
data.map((item, index) => <div style={{ width: `${100 / data.length}%` }} className={styles.progressWrap} key={index}>
<RenderHandler value={item.value} />
<div className={styles.progressCard}>
<p>{item.value} {getGard(item.value)}</p>
<img src={item.icon} alt="" />
</div>
</div>)
}
</div>;
};
export default memo(ProgressChartsCom);
const RenderHandler = ({ value = 9 }) => {
// 创建一个ref,用于存储图表的DOM元素
const chartRef = useRef(null);
useEffect(() => {
initChart(chartRef, value);
const resizeHandler = () => {
const instance = echarts.getInstanceByDom(chartRef.current);
instance.dispose();
initChart(chartRef, value);
};
window.addEventListener('resize', resizeHandler);
return () => {
window.removeEventListener('resize', resizeHandler);
// 销毁实例
const instance = echarts.getInstanceByDom(chartRef.current);
instance.dispose();
};
}, []);
/**
* 获取所有坐标
* @param wid 画布宽度
* @param hei 画布高度
* @param totalData 点位数量
* @returns 1-10点位坐标
*/
function calculatePoints(wid, hei, totalData) {
const canvasWidth = wid; // 假设canvas宽度为600
const canvasHeight = hei; // 假设canvas高度为400
const centerX = canvasWidth / 2;
const centerY = canvasHeight / 2;
const radius = Math.min(centerX, centerY) * 0.9; // 取半径为较小边的一半的85%作为半径,乘以0.9是因为有radius: ['85%', '95%']设置
const totalAngle = 180; // 总角度
const anglePerSlice = totalAngle / totalData; // 每份的角度
const points = [];
for (let i = 0; i < totalData; i++) {
const startAngle = (i * anglePerSlice - 180) * Math.PI / 180; // 转换为弧度并减去90度(因为在饼图中通常是从x轴正方向开始计算的)
const endAngle = ((i + 1) * anglePerSlice - 180) * Math.PI / 180;
const midAngle = (startAngle + endAngle) / 2; // 中间角度
const x = centerX + Math.cos(midAngle) * radius; // x坐标
const y = centerY + Math.sin(midAngle) * radius; // y坐标
points.push({ x, y });
};
return points;
};
const getPointerColor = (val) => {
if (val <= 5) return 'rgba(230, 81, 81, 1)';
if (val === 6) return 'rgba(230, 81, 81, 0.7)';
if (val === 7) return 'rgba(253, 171, 57, 1)';
if (val >= 8 && val <= 10) return 'rgba(88, 187, 93, 1)';
return 'gray';
};
/**
* 初始化图表
* @param ref 图表容器
* @param value 分值
*/
const initChart = (ref, value) => {
if (!ref.current) return; // 关键:DOM 存在再初始化
const wid = ref.current.offsetWidth;
const hei = ref.current.offsetHeight;
const points = calculatePoints(wid, hei, 10);
if (value < 1 || value > points.length) return; // 验证数据有效性
const myChart = echarts.init(ref.current);
const option = {
animation: false,
series: [
{
type: 'pie',
radius: ['85%', '95%'],
center: ['50%', '50%'],
startAngle: 180,
endAngle: 0,
itemStyle: {
borderRadius: 10,
borderColor: '#fff',
borderWidth: 2,
},
labelLine: { show: false },
label: { show: false },
data: [
{ value: 5, name: '红', itemStyle: { color: 'rgba(230, 81, 81, 1)' } },
{ value: 1, name: '粉', itemStyle: { color: 'rgba(230, 81, 81, 0.7)' } },
{ value: 1, name: '橙', itemStyle: { color: 'rgba(253, 171, 57, 1)' } },
{ value: 3, name: '绿', itemStyle: { color: 'rgba(88, 187, 93, 1)' } },
], // 控制进度条样式数据
emphasis: { disabled: true },
markPoint: {
symbol: 'circle',
symbolSize: 17,
data: [
{
name: 'pointer',
coord: [0, 0], // 临时值
itemStyle: { color: getPointerColor(value) },
},
],
},
title: {
show: true,
},
},
],
graphic: [
{
type: 'circle',
shape: {
cx: points[value - 1]?.x ?? 0, // 圆心 x 坐标
cy: points[value - 1]?.y ?? 0, // 圆心 y 坐标
r: 6, // 圆的半径
},
style: {
stroke: getPointerColor(value), // 边框颜色
lineWidth: 3, // 边框宽度
fill: '#fff', // 填充颜色为透明,实现空心效果
},
zlevel: 1000, // 设置 zlevel 以确保圆圈在最上层
},
], // 自定义图形元素(用于表示当前分数对应位置)
};
// 先渲染基础图表
myChart.setOption(option);
};
return <div ref={chartRef} className={styles.progress}></div>;
};
css
.chartsWrap {
width: 100%;
height: 100%;
}
.progress {
width: 100%;
height: 100%;
margin-top: 50px;
}
.progressWrap {
height: 100%;
position: relative;
}
.progressCard {
position: absolute;
top: 60%;
left: 50%;
transform: translate(-50%, 0);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
p {
font-weight: 600;
font-size: 18px;
line-height: 24px;
}
img {
width: 24px;
height: 24px;
margin-top: 10px;
user-select: none;
}
}
思路
其实就是用echarts先绘制一个饼图180度的环状饼图作为等级进度条。
再通过计算获取到圆环上平均10等份的点位坐标,在1-10分对应的点位利用canvas画一个空心圆。
基于上面代码可以更改graphic逻辑,查看十个点位(points)是否正确获取,同时可以拉伸页面视口看计算逻辑是否可以做到自适应页面宽度。
javascript
graphic: points.map(item => {
return {
type: 'circle',
shape: {
cx: item.x, // 圆心 x 坐标
cy: item.y, // 圆心 y 坐标
r: 6, // 圆的半径
},
style: {
stroke: getPointerColor(value), // 边框颜色
lineWidth: 3, // 边框宽度
fill: '#fff', // 填充颜色为透明,实现空心效果
},
zlevel: 1000,
}
})
