圆环进度条
宽度自适应外层容器、自定义颜色、线条宽度、圆角、起始label
yarn add react-motion
import React, { useRef, useEffect, useState, useMemo } from 'react';
import { Motion, spring } from 'react-motion';
interface SemiCircleProgressProps {
/* 进度 (0 - 100) */
progress?: number;
color?: string;
/* 线条宽度 */
strokeWidth?: number;
/* 是否两端圆角 */
rounded?: boolean;
/* 标题 */
title: string | React.ReactNode;
/* 起始label - 位于左右两侧起点 - 内容尽量不要超过 radius */
startLabel?: string | React.ReactNode;
endLabel?: string | React.ReactNode;
}
const SemiCircleProgress: React.FC<SemiCircleProgressProps> = props => {
const {
progress = 50,
color = 'black',
strokeWidth,
rounded = false,
title,
startLabel,
endLabel,
} = props;
const containerRef = useRef<HTMLDivElement>(null);
const [radius, setRadius] = useState(50);
/* 线条宽度 */
const _strokeWidth = strokeWidth ?? radius / 4;
useEffect(() => {
const updateRadius = () => {
if (containerRef.current) {
const width = containerRef.current.offsetWidth;
setRadius(width / 4); // 因为外层容器宽度是 diameter * 2,所以这里除以4
}
};
updateRadius();
window.addEventListener('resize', updateRadius);
return () => window.removeEventListener('resize', updateRadius);
}, []);
const diameter = radius * 2;
const center = radius;
/* 计算内圈半径 */
const arcRadius = center - _strokeWidth / 2;
/* 半圆周长 */
const circumference = Math.PI * arcRadius;
/* 计算进度长度 */
const progressLength = (progress / 100) * circumference;
return (
<div
ref={containerRef}
style={{
position: 'relative',
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
>
<svg width={diameter} height={radius} viewBox={`0 0 ${diameter} ${radius}`}>
{/* 背景轨道 */}
<path
d={`M ${_strokeWidth / 2},${center} A ${arcRadius},${arcRadius} 0 0,1 ${diameter -
_strokeWidth / 2},${center}`}
fill="none"
stroke="lightgray"
strokeWidth={_strokeWidth}
strokeLinecap={rounded ? 'round' : 'butt'}
/>
{/* 进度条动画 */}
<Motion defaultStyle={{ t: 0 }} style={{ t: spring(progressLength) }}>
{({ t }) => (
<path
d={`M ${_strokeWidth / 2},${center} A ${arcRadius},${arcRadius} 0 0,1 ${diameter -
_strokeWidth / 2},${center}`}
fill="none"
stroke={color}
strokeWidth={_strokeWidth}
strokeLinecap={rounded ? 'round' : 'butt'}
strokeDasharray={`${t}, ${circumference}`}
strokeDashoffset={0}
/>
)}
</Motion>
</svg>
{/* title & progress */}
<div
style={{
position: 'absolute',
margin: 'auto',
top: radius / 2,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
>
<div style={{ fontSize: 'max(12px, 1.5vw)', fontWeight: 'bold' }}>{progress}%</div>
{title && <div style={{ color: '#868484', fontSize: 'max(10px, 0.8vw)' }}>{title}</div>}
</div>
{/* startLabel && endLabel */}
<div
style={{
width: '100%',
display: 'flex',
justifyContent: 'space-around',
color: '#868484',
marginTop: 2,
}}
>
<div style={{ width: '40%', marginLeft: _strokeWidth, fontSize: 'max(10px, 0.9vw)' }}>
{startLabel}
</div>
<div style={{ width: '40%', marginRight: _strokeWidth, fontSize: 'max(10px, 0.9vw)' }}>
{endLabel}
</div>
</div>
</div>
);
};
export default SemiCircleProgress;