最近開發接到一個需求,前端展現付款的驗證碼,驗證碼時效 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>
)
}
複製代碼