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

收益
-
封装一个公共的环形进度条,在以后需要展示渐变进度场景的能够用到
-
write once,run anywhere ! - 所有的进度条场景都能使用这个组件
设计
具体用法
- 基本使用
tsx
import GredientCircleProgress from '@/components/GredientCircleProgress'
export default () => <GredientCircleProgress percent={50} />
- 自定义进度条整体的尺寸
tsx
import GredientCircleProgress from '@/components/GredientCircleProgress'
export default () => <GredientCircleProgress percent={50} width={200} height={200} />
- 自定义进度条颜色
tsx
import GredientCircleProgress from '@/components/GredientCircleProgress'
export default () => <GredientCircleProgress percent={50} fromColor="#08C284" toColor="#86DC27" />
- 自定义进度条填充厚度
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 |
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. 效果预览
