倒计时功能简易教程
Posted: 2025年 Apr 29日 18:58
核心部件:
倒计时动态圆环
Code: Select all
.countdown-opened-wrapper {
display: flex;
justify-content: center;
align-items: center;
.countdown-opened-text-button-wrapper {
position: absolute;
bottom: 38px;
left: 50%;
transform: translateX(-50%);
display: flex;
justify-content: center;
.countdown-opened-text-button {
width: 686rpx;
font-weight: 400;
font-size: 32rpx;
background: rgba(255, 255, 255, 0.1);
border-radius: 48rpx;
color: #DA3737;
}
}
.countdown-opened-circle {
position: relative;
margin-top: 30%;
margin-bottom: 33%;
border-radius: 240rpx;
height: 480rpx;
width: 480rpx;
background: #171515;
.countdown-opened-circle-tip {
width: 480rpx;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
display: flex;
justify-content: center;
align-items: baseline;
.num-text {
font-weight: 700;
font-size: 80rpx;
color: #FFFFFF;
margin-right: 4rpx;
}
.tip-text {
position: relative;
top: -10rpx;
font-weight: 400;
font-size: 28rpx;
color: rgba(255, 255, 255, 0.5);
}
}
.countdown-opened-circle-button {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
font-size: 24rpx;
font-weight: 400;
line-height: 32rpx;
margin-top: 80rpx;
.countdown-opened-circle-text {
font-style: normal;
font-weight: 400;
font-size: 24rpx;
color: #999999;
}
}
}
}
Code: Select all
import React, { CSSProperties, useEffect } from 'react';
import { View, Button, Text } from '@ray-js/ray';
import Strings from '../../i18n';
import styles from './index.module.less';
type Props = {
isDarkTheme: boolean;
maxSecond: number;
currentSecond: number;
onClose: () => void;
onResetting: () => void;
};
const CountdownCircle = (props: Props) => {
const { currentSecond, maxSecond, isDarkTheme = true, onClose, onResetting } = props;
const countdown = currentSecond;
useEffect(() => {
if (currentSecond <= 0) {
onResetting && onResetting();
}
}, [currentSecond]);
const handleClose = () => {
onClose && onClose();
};
const passColor = isDarkTheme ? '#1082FE' : '#3060c6';
const notPassColor = isDarkTheme ? '#272727' : '#dddddd';
const renderCircles = () => {
const circleNum = 120;
const diff = maxSecond - countdown;
const passedNum = maxSecond > 0 ? Math.floor((diff / maxSecond) * circleNum) : 0;
const circleList = new Array(circleNum).fill(0).map((_, idx) => {
const styles: CSSProperties = {
position: 'absolute',
left: '50%',
top: '50%',
transform: 'translate(-50%)',
};
const radius = 128;
const deg = idx * (360 / circleNum);
const paddingX = Math.sin((deg / 180) * Math.PI) * radius;
const paddingY = -Math.cos((deg / 180) * Math.PI) * radius;
let colorStyle = notPassColor;
if (passedNum > idx) {
colorStyle = passColor;
}
return (
<View key={idx} style={styles}>
<View
style={{
position: 'relative',
top: '-2.5px',
transform: `translateX(${paddingX}px) translateY(${paddingY}px) rotate(${deg}deg)`,
backgroundColor: `${colorStyle}`,
height: '5px',
width: '1px',
}}
/>
</View>
);
});
return circleList;
};
const getTipText = (secondRes: number) => {
const hourTxt = Strings.getLang('hour');
const minuteTxt = Strings.getLang('minute');
const secondTxt = Strings.getLang('second');
const SECOND = 60;
const hour = Math.floor(secondRes / 3600);
const minute = Math.floor((secondRes % 3600) / SECOND);
const second = secondRes % SECOND;
const tipTextStyle = {
color: isDarkTheme ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.5)',
};
const numTextStyle = {
color: isDarkTheme ? 'rgba(255, 255, 255, 1)' : 'rgba(0, 0, 0, 1)',
};
return (
<>
{!!hour && (
<Text className={styles['num-text']} style={numTextStyle}>
{hour < 10 ? `0${hour}` : hour}
</Text>
)}
{!!hour && (
<Text className={styles['tip-text']} style={tipTextStyle}>
{hourTxt}
</Text>
)}
<Text
className={styles['num-text']}
style={{
marginLeft: '14rpx',
...numTextStyle,
}}
>
{minute < 10 ? `0${minute}` : minute}
</Text>
<Text className={styles['tip-text']} style={tipTextStyle}>
{minuteTxt}
</Text>
<Text
className={styles['num-text']}
style={{
marginLeft: '14rpx',
...numTextStyle,
}}
>
{second < 10 ? `0${second}` : second}
</Text>
<Text className={styles['tip-text']} style={tipTextStyle}>
{secondTxt}
</Text>
</>
);
};
if (typeof props.maxSecond !== 'number' || props.maxSecond <= 0) {
return null;
}
const handleResetting = () => {
onResetting && onResetting();
};
const countdownOpenedCircleStyle = {
background: isDarkTheme ? 'rgba(0, 0, 0, 0.02)' : 'rgba(255, 255, 255, 0.02)',
};
const countdownOpenedCircleTextStyle = {};
const countdownOpenedTextButtonStyle = {
background: isDarkTheme ? 'rgba(255, 255, 255, .1)' : 'rgba(255, 255, 255, 1)',
};
return (
<View className={styles['countdown-opened-wrapper']}>
<View className={styles['countdown-opened-circle']} style={countdownOpenedCircleStyle}>
{renderCircles()}
<View className={styles['countdown-opened-circle-tip']}>{getTipText(countdown)}</View>
<View className={styles['countdown-opened-circle-button']} onClick={handleResetting}>
<Text
className={styles['countdown-opened-circle-text']}
style={countdownOpenedCircleTextStyle}
>
{Strings.getLang('resetting')}
</Text>
</View>
</View>
{/* 关闭倒计时 */}
<View className={styles['countdown-opened-text-button-wrapper']}>
<Button
className={styles['countdown-opened-text-button']}
style={countdownOpenedTextButtonStyle}
onClick={handleClose}
>
{Strings.getLang('closeCountdown')}
</Button>
</View>
</View>
);
};
export default CountdownCircle;
倒计时 选择页
Code: Select all
.countdown {
&_container {
height: 100vh;
--picker-option-unit-font-size: 24px;
--picker-background-color: transparent;
--popup-background-color: transparent;
--nav-bar-background-color: transparent;
}
&_scrollView {
position: relative;
height: calc(100vh - env(safe-area-inset-top) - env(safe-area-inset-bottom) - 46px);
}
&_picker {
.smart-picker-column__item {
font-size: 40px;
margin-right: 28px;
}
}
&_card-container {
background-color: var(--app-B3);
}
&_placeholder {
width: 40px;
}
&_tip {
padding: 12px 16px;
font-size: 14px;
color: rgba(255, 255, 255, 0.7);
text-align: center;
}
:global(.ty-picker-column) {
color: rgba(255, 255, 255, 0.9);
}
:global(.ty-picker-mask) {
background: linear-gradient(180deg, rgba(26, 26, 26, 0.9), rgba(26, 26, 26, 0.4));
}
&_popup {
position: relative;
display: flex;
}
&_confirm-button {
position: absolute;
bottom: calc(constant(safe-area-inset-bottom) + 24px);
bottom: calc(env(safe-area-inset-bottom) + 24px);
width: 100vw;
height: 48px;
border-radius: 24px;
display: flex;
padding: 0 16px;
}
}
Code: Select all
<View
style={{
backgroundColor: bgColor,
backgroundImage: `url(${bgImg})`,
}}
className={`${prefixClass}-content-container`}
<NavBar
leftText={Strings.getLang('cancel')}
title={Strings.formatValue('countdownTitle', onOffText)}
onClickLeftText={onCancel}
/>
<ScrollView refresherTriggered={false} scrollY className={`${prefixClass}_scrollView`}>
<View className={`${prefixClass}_tip`}>
{Strings.formatValue('countdownTips', onOffText)}
</View>
<View className={`${prefixClass}_picker-wrapper`}>
<Picker
columns={columns}
onChange={handleChange}
showToolbar={false}
itemHeight={70}
className={`${prefixClass}_picker`}
/>
</View>
<Button
className={`${prefixClass}_confirm-button`}
onClick={handleConfirm}
size="large"
round
color="#3b82f7"
>
{Strings.getLang('confirm')}
</Button>
</ScrollView>
</View>