react+echarts实现个性化评分展示(类进度条)

需求

如上图,封装一个组件,通过传入不同的数据展示对应的评分等级:

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,
        }
      })