最近开发接到一个需求,前端展现付款的验证码,验证码时效 10 分钟,到期过时,同时在二维码的外侧有一个倒计时条,本来的实现方式是经过 JS 来控制,设置左上,左下,右上,右下四个矩形,每一个矩形只显示一个折角的边框,从而模拟整个外框。css
根据倒计时的时间轮询计算比例,分别控制四个矩形的宽高,从而实现倒计时的 CountDown
效果。这样的实现方式有几个问题:html
本文主要介绍两种非 js 控制的矩形倒计时条的实现方法。前端
css 实现方法的原理是:react
background
,使用linear-gradient
造成纯色的图片背景。size
& position
,使他们分布在元素的四周。具体能够看代码markdown
.progress {
display: flex;
justify-content: center;
align-items: center;
height: var(--height);
width: var(--width);
border-radius: calc(var(--line) / 2);
background:
linear-gradient(to right, var(--green) 99.99%, var(--blue))
calc(-1 * var(--width)) 0rem
/ 100% var(--line),
linear-gradient(to bottom, var(--green) 99.99%, var(--blue))
calc(var(--width) - var(--line)) calc(-1 * var(--height))
/ var(--line) 100%,
linear-gradient(to right, var(--green) 99.99%, var(--blue))
var(--width) calc(var(--height) - var(--line))
/ 100% var(--line),
linear-gradient(to top, var(--green), 99.99%, var(--blue))
0rem var(--height)
/ var(--line) 100%;
background-repeat: no-repeat;
animation: progress var(--time) linear forwards infinite;
}
@keyframes progress {
0% {
background-position:
calc(-1 * var(--width)) 0rem,
calc(var(--width) - var(--line)) calc(-1 * var(--height)),
var(--width) calc(var(--height) - var(--line)),
0rem var(--height);
}
25% {
background-position:
0rem 0rem,
calc(var(--width) - var(--line)) calc(-1 * var(--height)),
var(--width) calc(var(--height) - var(--line)),
0rem var(--height);
}
50% {
background-position:
0rem 0rem,
calc(var(--width) - var(--line)) 0rem,
var(--width) calc(var(--height) - var(--line)),
0rem var(--height);
}
75% {
background-position:
0rem 0rem,
calc(var(--width) - var(--line)) 0rem,
0rem calc(var(--height) - var(--line)),
0rem var(--height);
}
100% {
background-position:
0rem 0rem,
calc(var(--width) - var(--line)) 0rem,
0rem calc(var(--height) - var(--line)),
0rem 0rem;
}
}
复制代码
svg
的实现则是hack了stroke-dasharray
利用这个属性造出间断线来模拟倒计时,只要这个线足够长那么从视觉来看就是能够造成从全满变成全空的效果,这里的代码是这样的:svg
<div class="father">
<svg class="progressSvg" style={{'--speed': speed, '--progress': progress}} viewBox="0 0 120 120">
<rect width="100" height="100" x="10" y="10" rx="10" ry="10" />
</svg>
<span class="son">{props.svg}</span>
</div>
复制代码
主要看rect部分,设置了一个圆角,因此矩形的起始位置设置成了x="10" y="10"
,而且因为设置了矩形的尺寸,为了能放下,因此 svg
标签的 viewBox="0 0 120 120"
从而放下这个圆角矩形。oop
这样以来,矩形的周长就是 400,因此设置stroke-dasharray
只要大于 400 便可,为了保险设置成 1000长度的实线,1000长度的虚线。组件化
.progressSvg rect {
fill: none;
stroke: blue;
stroke-width: 4; // 控制边框的宽度
/* stroke-linecap: round; */
stroke-dasharray: 1000 1000;
stroke-dashoffset: 0;
animation: spin 60s infinite linear;
}
复制代码
接着就是让它动起来,这里使用的是控制stroke-offset
来控制,就从0(彻底是边框)转到 -400(旋转了全部的边框),由于实线的前面是虚线,只要开始设置负的 offset
那么就会是相似于被吃掉的效果。flex
@keyframes spin {
to {
stroke-dashoffset: -400;
}
}
复制代码
这样咱们就实现了最简单的二维码倒计时进度条了。在线演示 codepen.io动画
样式基本不须要修改,修改一下js 文件,主要经过 css 变量来对倒计时时间,进度进行控制。
这里根据需求:
const CountedDown = (props) => {
const [color, setColor] = React.useState("green");
const [speed, setSpeed] = React.useState('100s');
const [progress] = React.useState('0.75');
return (
<div> <div class="flex" style={{ "--bg": color, "--time": speed }}> <div class="countdown"> <div class="progress"> <div class="inner"> {props.css} </div> </div> </div> </div> <div class="father"> <svg class="progressSvg" style={{'--speed': speed, '--progress': progress}} viewBox="0 0 120 120"> <rect width="100" height="100" x="10" y="10" rx="10" ry="10" /> </svg> <span class="son">{props.svg}</span> </div> </div>
);
}
复制代码
从上面的代码中,能够看出咱们给 css
传入了 --bg
控制进度条的颜色,--time
控制倒计时,读者能够自行查看在线演示代码。因为css版本的拐角存在问题,主要介绍svg版本。
在 svg 版本中, 传入了 --speed
控制速度,--progress
控制进度,对应的 css :
.progressSvg rect {
fill: none;
stroke: blue;
stroke-width: 4;
/* stroke-linecap: round; */
stroke-dasharray: 1000 1000;
- stroke-dashoffset: 0;
- animation: spin 60s infinite linear;
+ stroke-dashoffset: calc((1 - var(--progress)) * (-400));
+ animation: spin var(--speed) infinite linear;
}
复制代码
--speed
很好理解,主要解释--progress
,上文中,咱们知道使用动画就是让 stroke-offset
按照逆时针旋转到 -400, 那么保存进度就是保存这个 offset 值,当咱们认为如今的百分比进度是0.75的话,就须要提早 手动spin (1-0.75)*(-400)
。
能够用于生产的 React 组件 能够参考下面的代码:
/* CountDown.module.css */
.father {
position: relative;
}
.son {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 12rem;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.progress {
width: 100%;
height: 100%;
}
@keyframes spin {
to {
stroke-dashoffset: -400;
}
}
.progress rect {
fill: none;
stroke: var(--color);
stroke-width: 4;
/* stroke-linecap: round; */
stroke-dasharray: 1000 1000;
stroke-dashoffset: calc(-400 * var(--rate));
animation: spin 600s infinite linear;
/* animation-direction: alternate; */
}
复制代码
import React from 'react';
import styles from './CountDown.module.css';
interface MyCSSProperties extends React.CSSProperties {
'--color': string;
'--rate': string;
}
const CountDown = ({ color, timer, children, }: { color: string; timer: number; children: React.ReactNode; }) => {
const style: MyCSSProperties = {
// Add a CSS Custom Property
'--color': color,
'--rate': `${1 - timer / (600 * 1000)}`,
};
return (
<div className={styles.father}> <svg className={styles.progress} viewBox="0 0 120 120"> <rect style={style} width="100" height="100" x="10" y="10" rx="6" ry="6" /> </svg> <span className={styles.son}>{children}</span> </div>
);
};
export { CountDown };
复制代码
/* usage */
import { useCountDown } from 'ahooks';
import React, { useEffect, useState } from 'react';
const Index = ()=>{
const [barColor, setBarColor] = useState('blue'); // red
const [expiryTimer, setTargetDate, formattedRes] = useCountDown({
targetDate: dataRes.expiredAt,
onEnd,
});
useEffect(() => {
if (timer !== 0 && timer < 600 * 0.35 * 1000) {
setBarColor('red');
}
}, [expiryTimer]);
return (
<CountDown color={barColor} timer={timer}> <div className={classNames({ hidden: show, })} id="qrcode" ref={qrcodeRef} /> </CountDown>
)
}
复制代码