react+echarts实现个性化评分展示(类进度条)
需求
如上图,封装一个组件,通过传入不同的数据展示对应的评分等级:
1-5分处于红色,评差;
6分处于粉色,评中;
7分处于橙色,评良;
8-10分处于绿色,评优秀。
代码
父组件通过接口拿到对应的数据,保存到data中,通过props将数据传给渲染组件。
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 组件中进项详细的逻辑处理。
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宽度为600const canvasHeight = hei; // 假设canvas高度为400const 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>;
};
.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)是否正确获取,同时可以拉伸页面视口看计算逻辑是否可以做到自适应页面宽度。
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,}})