css 画一个渐变环形进度条

553 阅读5分钟

有没有碰到自定义环形进度条的需求呢,如果有该怎么自定义呢,当然可能有人直接就用上万能的 echarts 了,如果项目简单没有用到 echarts 直接引入那么项目会变大不少,并且很多自定义效果其都不满足,综合之下还是自己编写一个最简单,并且还能熟练技能

我们的目的是实现这样一个效果,当然核心是环形渐变进度条,其他的都是装饰

QQ_1727320443234.png

这里使用了 background + mask + conic-gradient + radial-gradient 实现

我们先画一个纯色进度条

QQ_1727667079546.png

首先画的是一个扇形,使用 background + conic-gradient 实现,conic-gradient由于是渐变,因此中间一个百分比一定是要有两个一样但是不同颜色,或者是渐变区间非常小,这样就能看到我们需要的弧形进度了

QQ_1727666901803.png

然后通过 mask + radial-gradient 实现环形 radial-gradient 是一个环形渐变色效果,默认从中间带外圈,但是使用到 mask 后,可以设计形成一个环形效果,具体可以自己尝试,一般50%~60%左右比较好看,这样就可以形成一个圆环了,当然别忘了 borderRadius,这样就是我们要的环形进度条效果了

ps: mask 这个效果不是在所有平台都能正常显示的,如果不是需要中间透明,直接中间覆盖上一个圆背景就完事了

QQ_1727667079546.png

代码很简单,如下所示,加入了一些样式吧

//一个环形进度条,只是一个进度条哈
<div
    className="size-[47px] flex justify-center items-center"
    style={{
        borderRadius: "50%",
        //设置渐变区间,为了突然变色,可以一个百分比设置两个颜色,也可以极小差距,例如25% 25.01%
        background: `conic-gradient(#fff 25%, #fff 25%, #0482FF 25%, #0482FF 100%)`,
        //设置mask,其会形成环形mask
        mask: "radial-gradient(transparent 0%, transparent 57%, #000 57%, #000 100%)",
    }}
/>

在画一个渐变色进度条,那就是中间多了几个过渡色罢了,我们先还原成这样,改动不大(外面的圈是 border 懒得改了哈)

QQ_1727667459739.png

<div
    className="margin-auto size-[196px]"
    style={{
        borderRadius: "50%",
        background: `conic-gradient( #00C0FF 0%, #475FFF 25%, #8F00FF 50%, #3C4F69 50%, #3C4F69 100%)`,
        mask: "radial-gradient(transparent, transparent 58%, #000 58%, #000 100%)",
    }}
/>

这样上面效果就完成了,过渡色百分比是实际的一半即可

<div
    className="margin-auto size-[196px]"
    style={{
        borderRadius: "50%",
        background: `conic-gradient( #00C0FF 0%, #475FFF ${perc/2}%, #8F00FF ${perc}%, #3C4F69 50%, #3C4F69 100%)`,
        mask: "radial-gradient(transparent, transparent 58%, #000 58%, #000 100%)",
    }}
/>

//更好的 mask,需要调整大小,就先这样,上面的mask径变角落不是半径,所以才出现的百分比不对问题
mask: radial-gradient(circle farthest-side at center,transparent, transparent 80%, #000 80%, #000 100%);

但是上面还是有点问题,就是,不管进度多少,永远都是那么两个渐变色,实际上,百分比少了可能只有第一个颜色那个才更会有这种百分比的感觉,可以背景 + 遮罩方式解决,遮罩颜色为非百分比颜色,百分比的颜色设置透明即可

上面的随着百分比变小,渐变过渡仍然会出现问题,也就是会一直有这两个颜色,实际上百分比低的则只显示右边蓝色才对,因此搞两个就行了,一个正常渲染的条,一个按照进度覆盖即可(前面透明表示进度,后面纯色覆盖即可)

<div className="zwxt-xxjs-circle-bkg size-[240px] relative flex justify-center items-center">
    <div
        className="absolute margin-auto size-[196px]"
        style={{
            borderRadius: "50%",
            background: `conic-gradient( #00C0FF 0%, #475FFF 50%, #8F00FF 100%)`,
            mask: "radial-gradient(transparent 58%, #000 58%)",
        }}
    />
    <div
        className="absolute margin-auto size-[196px]"
        style={{
            borderRadius: "50%",
            background: `conic-gradient(transparent 0%, transparent ${perc}, #3C4F69 ${perc}, #3C4F69 100%)`,
            mask: "radial-gradient(transparent 58%, #000 58%)",
        }}
    />
    ...img和文本
</div>

//更好的 mask,需要调整大小,就先这样,上面的mask径变角落不是半径,所以才出现的百分比不对问题
mask: radial-gradient(circle farthest-side at center,transparent, transparent 80%, #000 80%, #000 100%);

这样就实现了我们的进度条百分比

下面是再进一步改进后,加入背景和中间百分比的,是不是更好看了呢,这算是我们的最终效果了

QQ_1727320443234.png

对于背景的框,可以使用我们的 border + 图片解决就行,absolute 还记得怎么使用么

我们稍微封装一下,再加上一个动画

const ProgressView = (props: {
    progress: number;
    className?: string;
    timeInterval?: number;
}) => {
    const [progress, setProgress] = useState<number>(0);
    const intervalRef = useRef<number>();

    useEffect(() => {
        updateProgress(props.progress);
        return () => {
            if (intervalRef.current) {
                cancelAnimationFrame(intervalRef.current);
            }
        };
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [props]);

    const updateProgress = (newProgress: number) => {
        if (typeof newProgress !== "number") return;
        if (intervalRef.current) {
            cancelAnimationFrame(intervalRef.current);
        }
        const prg = newProgress ?? 0;
        const timeInterval = props.timeInterval ?? 900;
        const interval = (prg / timeInterval) * 16;

        let next = progress;
        const callback = () => {
            const newNext = next + interval;
            if (newNext > newProgress) {
                next = newProgress;
            } else {
                next = newNext;
            }
            setProgress(next);
            if (next >= newProgress && intervalRef.current) {
                cancelAnimationFrame(intervalRef.current);
            } else {
                requestAnimationFrame(callback);
            }
        };
        intervalRef.current = requestAnimationFrame(callback);
    };

    return (
        <div
            className={`size-[240px] relative flex justify-center items-center ${
                props.className ?? ""
            }`}
        >
            <div
                className="absolute margin-auto size-[196px]"
                style={{
                    borderRadius: "50%",
                    background: `conic-gradient( #00C0FF 0%, #475FFF 50%, #8F00FF 100%)`,
                    mask: "radial-gradient(transparent, transparent 58%, #000 58%, #000 100%)",
                }}
            />
            <div
                className="absolute margin-auto size-[196px]"
                style={{
                    borderRadius: "50%",
                    background: `conic-gradient(transparent 0%, transparent ${progress}%, #3C4F69 ${progress}%, #3C4F69 100%)`,
                    mask: "radial-gradient(transparent 58%, #000 58%)",
                }}
            />
            <div className="flex justify-center items-center absolute top-0 left-0 right-0 bottom-0">
                <img
                    alt=""
                    className="2-[141px] h-[139px] absolute"
                    src={CircleCenterImg}
                />
                <p className="absolute margin-auto text-center text-[32px] text-hexi-gradient">
                    {props.progress}%
                </p>
            </div>
        </div>
    );
};

//更好的 mask,需要调整大小,就先这样,上面的mask径变角落不是半径,所以才出现的百分比不对问题
mask: radial-gradient(circle farthest-side at center,transparent, transparent 80%, #000 80%, #000 100%);