【React.js】渐变环形进度条

背景

最近页面需求开发遇到了一个场景 - 需要一个渐变的环形进度条! 当前项目是基于 antd-mobile 进行开发, 其物料不能满足当前的需求!

收益

  1. 封装一个公共的环形进度条,在以后需要展示渐变进度场景的能够用到

  2. write once,run anywhere ! - 所有的进度条场景都能使用这个组件

设计

具体用法

  1. 基本使用
tsx 复制代码
import GredientCircleProgress from '@/components/GredientCircleProgress'

export default () => <GredientCircleProgress percent={50} />
  1. 自定义进度条整体的尺寸
tsx 复制代码
import GredientCircleProgress from '@/components/GredientCircleProgress'

export default () => <GredientCircleProgress percent={50} width={200} height={200} />
  1. 自定义进度条颜色
tsx 复制代码
import GredientCircleProgress from '@/components/GredientCircleProgress'

export default () => <GredientCircleProgress percent={50} fromColor="#08C284" toColor="#86DC27" />
  1. 自定义进度条填充厚度
tsx 复制代码
import GredientCircleProgress from '@/components/GredientCircleProgress'

export default () => <GredientCircleProgress percent={50} strokeWidth={10} />

可用 API

props 属性:

属性 说明 类型 是否必传?
percent 当前的百分比 number
trackColor 轨道颜色 React.CSSProperties['color']
strokeWidth 圆环的宽度 `number string`
fromColor 渐变开始的颜色 React.CSSProperties['color']
toColor 渐变结束的颜色 React.CSSProperties['color']
animationTime 动画时间 (单位:ms) number
width 宽度 (默认单位:px, 如果需要传 rem需要使用字符串) `number string`
height 高度 (默认单位:px, 如果需要传 rem需要使用字符串) `number string`
showText 是否展示中间的文字 ? boolean
style svg 行内样式 React.CSSProperties

ref 暴露:

属性 说明 类型
getContainer 获取 Svg 元素 `() => SVGSVGElement null`
setProgress 设置圆环进度(注意,此方法不会有动画效果!) (percent: number) => void
animateProgress 设置圆环精度(带动画效果)1. targetPercent: 目标百分比 (targetPercent: number, duration?: number) => void
  1. duration: 动画持续时间(毫秒) | (targetPercent: number, duration?: number) => void

代码实现:

1. 类型定义

tsx 复制代码
import type { CSSProperties } from 'react'

// props 定义
export interface ICircleProgressProps {
  /**
   * 当前的百分比
   */
  percent?: number

  /**
   * 轨道颜色
   */
  trackColor?: CSSProperties['color']

  /**
   * 圆环的宽度
   */
  strokeWidth?: number | string

  /**
   * 渐变开始的颜色
   */
  fromColor?: CSSProperties['color']

  /**
   * 渐变结束的颜色
   */
  toColor?: CSSProperties['color']

  /**
   * 动画时间 (单位:`ms`)
   */
  animationTime?: number

  /**
   * 宽度 (单位:`px`)
   */
  width?: number | string

  /**
   * 高度 (单位:`px`)
   */
  height?: number | string

  /**
   * 是否展示中间的文字 ?
   */
  showText?: boolean

  /**
   * svg 行内样式
   */
  style?: CSSProperties
}

// expose 定义
export interface ICircleProgressExpose {
  /**
   * 获取 Svg 元素
   */
  getContainer: () => SVGSVGElement | null

  /**
   * 设置圆环进度(无动画)
   * @param {number} percent 进度百分比(0-100)
   */
  setProgress: (percent: number) => void

  /**
   * 带动画的设置圆环进度
   * @param {number} targetPercent - 目标百分比 (0-100)
   * @param {number} duration - 动画持续时间(毫秒)
   */
  animateProgress: (targetPercent: number, duration?: number) => void
}

2. 数据定义

2.1. id 定义

为了保证组件渲染的独立性,我们需要一些 id 来限制 GredientCircleProgress内部的绘制

tsx 复制代码
/* 这里的 id 必须要写在组件函数的内部! */

// 整体实例 id
const instanceId = `circleProgress_${Date.now()}_${Math.floor(Math.random() * 10000)}`
// 容器 id
const containerId = `${instanceId}_container`
// 绘制圆形进度条的 id
const progressCircleId = `${instanceId}_progress`
// 内部文案渲染的 id
const percentTextId = `${instanceId}_text`

2.2. 外层元素定义

tsx 复制代码
const elRef: MutableRefObject<SVGSVGElement | null> = useRef<SVGSVGElement>(null)

2.3. 渲染所需变量定义

在渲染之前,我们需要计算出一些必要的变量 (比如:宽度,高度,圆半径 etc.)

提示

parseSize 方法可以参考下面的实现

tsx 复制代码
// 宽度数字
const numWidth = parseSize(width)
// 高度数字
const numHeight = parseSize(height)
// 填充内容数字
const numStrokeWidth = parseSize(strokeWidth)
// 计算圆的半径 - 考虑 strokeWidth 并确保在容器内
const radius = Math.min(numWidth, numHeight) / 2 - numStrokeWidth / 2
// 计算中心点 - x 坐标
const centerX = numWidth / 2
// 计算中心点 - y 坐标
const centerY = numHeight / 2

2.4. svg 样式定义

合并默认样式和用户自定义样式,确保行内块展示不换行

ts 复制代码
const mergedStyle: CSSProperties = useMemo(() => {
  return {
    ...style,
    display: 'inline-block', // 设置为行内块
  }
}, [style])

3. 核心方法实现

3.1. parseSize - 适配尺寸的方法

tsx 复制代码
// 解析尺寸值,支持 px 和 rem 单位
function parseSize(size: number | string): number {
  if (typeof size === 'number') {
    return size
  }

  // 处理 rem 单位
  if (typeof size === 'string' && size.endsWith('rem')) {
    // 获取根元素的字体大小,用于将 rem 转换为 px
    const rootFontSize = parseFloat(getComputedStyle(document.documentElement).fontSize)
    const remValue = parseFloat(size)
    return remValue * rootFontSize
  }

  // 默认处理(px 或纯数字字符串)
  return parseInt(size)
}

3.2. animateProgress定义

tsx 复制代码
function animateProgress(targetPercent: number, duration = 1500) {
  // 限制百分比范围在 0-100
  targetPercent = Math.max(0, Math.min(100, targetPercent))

  const startPercent = 0
  const startTime = performance.now()

  const animate = (currentTime) => {
    const elapsed = currentTime - startTime
    const progress = Math.min(elapsed / duration, 1) // 0 到 1

    // 使用缓动函数(ease-out)使动画更自然
    const easeOutProgress = 1 - Math.pow(1 - progress, 3)

    const currentPercent = startPercent + (targetPercent - startPercent) * easeOutProgress

    setProgress(currentPercent)

    // 如果动画未完成,继续下一帧
    if (progress < 1) {
      requestAnimationFrame(animate)
    } else {
      // 确保最后设置为精确的目标值
      setProgress(targetPercent)
    }
  }

  requestAnimationFrame(animate)
}

3.3. setProgress 定义

tsx 复制代码
function setProgress(percent: number) {
  // 限制百分比范围在 0-100
  percent = Math.max(0, Math.min(100, percent))

  // 使用唯一ID获取当前组件实例的圆环元素
  const circle = document.getElementById(progressCircleId)

  // 只在 circle 存在时设置进度条
  if (!circle) return

  // 计算圆周长: 2πr
  const circumference = 2 * Math.PI * radius

  // 计算显示的长度
  const dashLength = (circumference * percent) / 100
  const gapLength = circumference - dashLength

  // 设置 stroke-dasharray
  circle.setAttribute('stroke-dasharray', `${dashLength} ${gapLength}`)

  // 只在 showText 为 true 且 text 元素存在时更新文字
  if (showText) {
    // 使用唯一ID获取当前组件实例的文本元素
    const text = document.getElementById(percentTextId)
    if (text) {
      text.textContent = `${Math.round(percent)}%`
    }
  }
}

4. 视图渲染

使用 svg 技术并绑定上面的数据和方法即可进行渲染

tsx 复制代码
<svg
  ref={elRef}
  className={styles['container']}
  style={mergedStyle}
  width={width}
  height={height}
>
  <defs>
    <linearGradient id={containerId} x1="0%" y1="0%" x2="100%" y2="100%">
      <stop offset="0%" stopColor={fromColor} />
      <stop offset="100%" stopColor={toColor} />
    </linearGradient>
  </defs>

  <circle
    cx={centerX}
    cy={centerY}
    r={radius}
    fill="none"
    stroke={trackColor}
    strokeWidth={strokeWidth}
  />

  {percent !== 0 && (
    <circle
      id={progressCircleId}
      cx={centerX}
      cy={centerY}
      r={radius}
      fill="none"
      stroke={`url(#${containerId})`}
      strokeWidth={strokeWidth}
      strokeLinecap="round"
      transform={`rotate(-90 ${centerX} ${centerY})`}
    />
  )}

  {showText && (
    <text
      id={percentTextId}
      x={centerX}
      y={centerY + 10}
      textAnchor="middle"
      fontSize={Math.min(numWidth, numHeight) / 8} // 动态计算文字大小
      fontWeight="bold"
      fill="#333"
    >
      {Math.round(percent)}%
    </text>
  )}
</svg>

5. 绑定监听并启动组件

添加侦听并动态执行 animateProgress

tsx 复制代码
// 1. 使用 forwardRef 包裹组件
const GredientCircleProgress = forwardRef<ICircleProgressExpose, ICircleProgressProps>(
  (props, ref) => {
    // ...
    
    // 2. 在 ref 上面扩展这些方法
    useImperativeHandle(ref, () => {
      return {
        getContainer: () => elRef.current,
        setProgress,
        animateProgress,
      }
    })
  }
)

6. 效果预览

相关推荐
前端鳄鱼崽3 小时前
【react-native-inspector】全网唯一开源 react-native 点击组件跳转到编辑器
前端·react native·react.js
90后的晨仔3 小时前
Webpack完全指南:从零到一彻底掌握前端构建工具
前端·vue.js
Holin_浩霖3 小时前
JavaScript 语言革命:ES6+ 现代编程范式深度解析与工程实践
前端
前端拿破轮3 小时前
从0到1搭一个monorepo项目(一)
前端·javascript·git
m0_741412243 小时前
大文件上传与文件下载
前端
wu_jing_sheng03 小时前
Python中使用HTTP 206状态码实现大文件下载的完整指南
开发语言·前端·python
90后的晨仔3 小时前
Vue3项目全面部署指南:从构建到上线
前端·vue.js
小于小于09123 小时前
npx 与 npm 区别
前端·npm·node.js
望获linux4 小时前
【实时Linux实战系列】实时 Linux 在边缘计算网关中的应用
java·linux·服务器·前端·数据库·操作系统