环形进度条组件CircleProgress

160 阅读2分钟

使用方法

CircleProgress组件主要的特别功能就是根据不同的区间展示不同的颜色,区间可定义。

<CircleProgress
          description={'小明的成绩'}
          // descriptionStyle={{ color: 'blue', fontWeight: 900 }}
          percent={95}
          unit={'分'}
          percentStyle={{ fontSize: '25px' }}
          // color='#4CAF50'
          color={[
            { color_value: '#4CAF50', color_range: [0.8, 1] },
            { color_value: '#F4D84E', color_range: [0.6, 0.8] },
            { color_value: '#F50C0C', color_range: [0, 0.6] },
          ]}
          trackColor="#EEEEEE"
          size="10rem"
          thickness={8}
          dur={0.1}></CircleProgress>

源码展示

import { CSSProperties, FC, ReactNode, useEffect, useState, useContext, memo } from 'react'
import classNames from 'classnames'
import './style.scss'
import UtilsClass from '../../utils/UtilsClass'
import context from '../../utils/context'
import { computedColor } from './utils'
import { animation } from '@/utils/tools/animation'

// strokeDasharray 的第一个值(填充部分长度)表示进度环的“实线”部分,应该有多长,而第二个值(圆周长)表示整个进度环的周长。
const getRingPercent = (percent: number, r: number) => {
  const perimeter = Math.PI * 2 * r
  return (percent / 100) * perimeter + ' ' + perimeter
}
interface ProgressCircleProps {
  className?: string
  style?: CSSProperties
  children?: ReactNode
  percent?: number
  // 圆环颜色
  color?: string
  // 圆环底色
  trackColor?: string
  // 圆环尺寸
  size?: string | number
  // 圆环厚度
  thickness?: number
  // 动画持续时间
  dur?: number
  description?: string
  percentStyle?: {}
  descriptionStyle?: {}
  unit: string
}

export const CircleProgress: FC<ProgressCircleProps> = (props) => {
  let obj = useContext(context)
  const {
    dur,
    className,
    style,
    children,
    percent = 0,
    color,
    trackColor,
    size,
    thickness = 4,
    description,
    percentStyle,
    descriptionStyle,
    unit,
    ...restProps
  } = props
  const [finalDashArray, setFinalDashArray] = useState('')
  const [trailStyle, setTrailStyle] = useState({})
  const [trackStyle, setTrackStyle] = useState({})
  const [animatedPercent, setAnimatedPercent] = useState(0)

  const radius = 50 - thickness / 2
  const perimeter = Math.PI * 2 * radius

  const progressClass = classNames('progress-circle', className)
  const progressStyle = {
    width: size,
    height: size,
    ...style,
  }

  const initAnimation = () => {
    const finalDash = getRingPercent(percent, radius)
    const renderValue = computedColor(color, percent) || color
    setTrackStyle({
      stroke: trackColor,
      strokeWidth: thickness,
      r: radius,
    })

    setTrailStyle({
      stroke: renderValue,
      strokeDasharray: finalDash,
      strokeWidth: thickness,
      r: radius,
    })
  }
  useEffect(() => {
    setFinalDashArray(getRingPercent(percent, radius))
    animation(dur * 20000, 0, percent, (val) => setAnimatedPercent(val))
  }, [percent, radius, dur])

  useEffect(() => {
    initAnimation()
  }, [percent])
  const animateFrom = `0 ${perimeter}`
  const animateTo = `${(percent / 100) * perimeter} ${perimeter}`

  useEffect(() => {
    const finalDash = getRingPercent(percent, radius)
    setFinalDashArray(finalDash)
  }, [percent, radius])
  const renderPercentStyle = Object.assign({}, percentStyle, { position: 'absolute' })
  const renderDescriptionStyle = Object.assign({}, descriptionStyle, {
    width: size,
    display: description ? 'block' : 'none',
  })

  return (
    <>
      <div {...restProps} className={progressClass} style={progressStyle}>
        <svg viewBox="0 0 100 100" className="progress-circle-graph">
          <circle cx="50" cy="50" fill="none" className="progress-circle-track" style={trackStyle} />
          <circle cx="50" cy="50" fill="none" className="progress-circle-trail" style={trailStyle}>
            <animate
              attributeName="stroke-dasharray"
              begin="0s"
              dur={dur + 's'} // 动画持续时间例如1秒
              from={animateFrom}
              to={animateTo}
              fill="freeze"
            />
          </circle>
        </svg>
        <div style={renderPercentStyle}>
          {Math.round(animatedPercent)}
          {unit}
        </div>
      </div>
      <div className="progress_circle_description" style={renderDescriptionStyle}>
        {description}
      </div>
    </>
  )
}

export default memo(CircleProgress)

utils

type colorProps = [
  { color_value: string; color_range: number[] },
  { color_value: string; color_range: number[] },
  { color_value: string; color_range: number[] },
]
type info_type = { color_value: string; color_range: number[] }
/**
 *计算值是否在区间内返回boolean
 * @param value
 * @param range_arr
 * @returns boolean
 */
export const isInRange = (value: number, range_arr: number[]): boolean => {
  const [min, max] = range_arr
  return value >= min && value <= max
}
/**
 * 根据不同值的范围显示不同的颜色
 * @param color
 * @param percent
 * @returns string
 */
export const computedColor = (color: colorProps, percent: number) => {
  if (color && Array.isArray(color)) {
    let renderValueColor = color
      .map((info: info_type) => {
        if (isInRange(percent / 100, info.color_range)) {
          return info.color_value
        }
      })
      .filter((item) => item !== undefined)
    return renderValueColor[0]
  }
  if (!color) {
    throw new Error('CircleProgress组件的color属性未定义')
  }
}

样式文件

.progress-circle {
  position: relative;
  z-index: 0;
  display: flex;
  justify-content: center;
  align-items: center;
}

.progress-circle-graph {
  // position: absolute;
  // top: 0;
  // left: 0;
  // z-index: -1;
}
.progress-circle-trail {
  stroke-linecap: round;
  transform: rotate(-90deg);
  transform-origin: center center;
  transition: stroke-dasharray 3s ease, stroke 0.3s;
}
.progress_circle_description {
  text-align: center;
}