一、实现效果
假如后端返了两个数据:
当前时间
的13
位时间戳currentTimestamp
、秒抢时间
的13
位时间戳seckillTimestamp
,想实现“距开始秒抢”的倒计时,即:秒抢时间与当前时间之间的时间差以“时分秒”
的形式进行倒计时,如下动图:
二、实现方式
第一步、封装方法——js
将时间戳转换成“时分秒” ⭐️
【注意】:是“时分秒”,不是 “天时分秒”哦~ (二者写法是有区别滴哦)
utils.js
:
/**
1、js获取 倒计时 时分秒
通过时间戳的方式来
let h = Math.trunc(dec / 3600); // 时
let m = Math.trunc(dec % 3600 / 60); // 分
let s = Math.trunc(dec % 3600 % 60); // 秒
2、参数分析:
@param {Num} inputTime: 需要转换成 时分秒 的 13位时间戳
@param {Boolean} isPop: 便于实现两种返回结果:true -> `${h}小时${m}分${s}秒` ; false -> 包含时分秒的finalDateObj对象
*/
export function timestampFormatter(inputTime, isPop = false) {
// 最终时间结果对象
const finalDateObj = {
h: null, // 小时
m: null, // 分钟
s: null, // 秒
}
// 剩余时间总的毫秒数 除以 1000 变为总秒数(时间戳为13位 需要除以1000,为10位 则不需要)
let dec = inputTime / 1000;
if (dec <= 0) {
dec = 0;
}
// 得到小时 格式化成前缀加零的样式
let h = Math.trunc(dec / 3600);
h = h < 10 ? '0' + h : h;
// 得到分钟 格式化成前缀加零的样式
let m = Math.trunc(dec % 3600 / 60);
m = m < 10 ? '0' + m : m;
// 得到秒 格式化成前缀加零的样式
let s = Math.trunc(dec % 3600 % 60);
s = s < 10 ? '0' + s : s;
finalDateObj.h = h;
finalDateObj.m = m;
finalDateObj.s = s;
return isPop ? `${h}小时${m}分${s}秒` : finalDateObj;
}
第二步、实现“时分秒”的倒计时
实现思路同我的另外一篇博文,相比其代码,只是有两处区别,如下:
countDownComponent.jsx
这是含倒计时的子组件。
1、倒计时初始值
参考项目代码:
// 1、初始化state
state = {
canCountDown: true, // 判断是否开启定时器
finalDate: '00天00小时00分00秒', // 倒计时初始值
}
现项目代码: ⭐️
// 1、初始化state
state = {
canCountDown: true, // 判断是否开启定时器
h1: '0', // 倒计时初始值-时
h2: '0', // 倒计时初始值-时
m1: '0', // 倒计时初始值-分
m2: '0', // 倒计时初始值-分
s1: '0', // 倒计时初始值-秒
s2: '0', // 倒计时初始值-秒
}
2、获取到倒计时最终值【位于方法updateTime
里】
参考项目代码:
/** 调用上面封装的方法timestampFormatter,计算到倒计时的展示结果,并state赋值,方便html里进行展示 */
const result = timestampFormatter(countDownTimestamp, true);
this.setState({
finalDate: result,
})
现项目代码: ⭐️
/** 调用上面封装的方法timestampFormatter,计算到倒计时的展示结果,并state赋值,方便html里进行展示 */
const _h = timestampFormatter(countDownTime).h + '';
const _m = timestampFormatter(countDownTime).m + '';
const _s = timestampFormatter(countDownTime).s + '';
this.setState({
h1: _h.slice(0, 1),
h2: _h.slice(-1),
m1: _m.slice(0, 1),
m2: _m.slice(-1),
s1: _s.slice(0, 1),
s2: _s.slice(-1),
})
第三步、HTML
渲染 ⭐️
render(){
const { h1, h2, m1, m2, s1, s2 } = this.state;
return(
<div __examplenotes="倒计时" className="countDown">
<span __examplenotes="倒计时背景" className="countDownBg"></span>
<span __examplenotes="时" className="commonTime hour1">{h1}</span>
<span __examplenotes="时" className="commonTime hour2">{h2}</span>
<span __examplenotes="分" className="commonTime minute1">{m1}</span>
<span __examplenotes="分" className="commonTime minute2">{m2}</span>
<span __examplenotes="秒" className="commonTime second1">{s1}</span>
<span __examplenotes="秒" className="commonTime second2">{s2}</span>
</div>
)
}
三、坑💥
如上结果动图可知:我们实现的是一个可以
tab
切换的倒计时项目。
- 实现思路:把倒计时相关的部分封装成一个组件,随之渲染页面。
- 坑💥:当
tab1
里的第一条数据的倒计时小于0
时,tab2
里的第一条数据的倒计时 自动归置为0
(即使 其并非为0
);当tab1
里的第二条数据的倒计时小于0
时,tab2
里的第二条数据的倒计时 自动归置为0
(即使 其并非为0
);依次类推。。。- 分析原因:原来是因为我们在更新倒计时时间时定义了如下代码【当倒计时小于
0
时,定时器被关闭了】,如下图所示:
- 解决方法:我们需要在切换
tab
时,判断组件的定时器timer
是不是null
,如果是null
且countDownTimestamp
大于0
,调下funTimer()
,重新开启定时器,代码如下:【需结合我的另外一篇博文一起看哦~】
countDownComponent.jsx
这是含倒计时的子组件。
componentDidMount() {
// 发送事件"startTimer"
document.addEventListener("startTimer", this.startTimer, this);
}
componentWillUnmount() {
// 移除事件"startTimer"
document.removeEventListener("startTimer", this.startTimer, this);
}
// 判断组件的定时器timer是不是null,如果是null且countDown大于0,调下funTimer()
startTimer = () => {
/** seckillTimestamp:秒杀开始时间的时间戳,currentTimestamp:当前时间的时间戳 */
const seckillTimestamp = 1664280000000; // 秒杀开始时间【实际情况时 由后台返回,这儿写死只是做个测试】
/** 计算倒计时的展示结果,并return值,方便html里进行展示 */
const countDownTimestamp = seckillTimestamp * 1 - this.currentTimestamp * 1; // 计算得到 倒计时 的时间戳
if (this.timer == null && countDownTimestamp > 0) {
this.FunTimer();
}
}
homePage.jsx
这是调用倒计时子组件的首页
// 标题tab切换按钮 - 0->今日秒抢, 1->明日秒抢
switchTitle = _throttle(async (index) => {
// 调用组件CountDownComponent里的方法
document.dispatchEvent(new CustomEvent("startTimer", {}));
}, 1000)
四、完整代码 ⭐️
父页面 homePage.jsx
'use strict';
import React from 'react';
import { observer } from 'mobx-react';
import store from '@src/store';
import { _throttle } from '@src/utils/utils';
import SeckillDetailComponent from './seckillDetailComponent/seckillDetailComponent'; // 引入秒杀详情组件
import modalStore from '@src/store/modal';
import { Toast } from '@spark/ui';
import { CHANNEL_ERROR } from '@src/utils/constants';
import './homePage.less';
@observer
class HomePage extends React.Component {
constructor(props) {
super(props);
}
componentDidMount() {
store.getHomeData(); // 请求接口-首页
}
// 点击「奖品」按钮,进入奖品页面
toPrizePage = _throttle(() => {
const {
// 首页接口是否报错
isError,
// 0:不在区域 1:北京2:上海3:深圳4:杭州
orgNo,
// 除错误码600004以外的提示信息
errorMessage,
} = store;
(!isError && orgNo != 0) ? store.changePage('myPrizePage') : Toast(errorMessage ? errorMessage : CHANNEL_ERROR);
})
// 点击「规则」按钮,打开【活动规则弹窗】
openRulePop = _throttle(() => {
const {
// 首页接口是否报错
isError,
// 0:不在区域 1:北京2:上海3:深圳4:杭州
orgNo,
// 除错误码600004以外的提示信息
errorMessage,
} = store;
(!isError && orgNo != 0) ? modalStore.pushPop('PublicPopupWindow', { type: 'rule' }) : Toast(errorMessage ? errorMessage : CHANNEL_ERROR);
})
// 标题切换按钮 - 0->今日秒抢, 1->明日秒抢
switchTitle = _throttle(async (index) => {
// 设置当前展示日的索引值
store.setSeckillIndex(index);
// 刷新首页数据
await store.getHomeData();
// 调用组件SeckillDetailComponent里的方法
document.dispatchEvent(new CustomEvent("startTimer", {}));
}, 1000)
render() {
const {
// 当前展示的秒杀数据
seckillData,
// 当前展示的秒杀数据的索引值 0-今日秒抢 1-明日秒抢
seckillIndex,
} = store;
return (
<div className="homePage">
<span __examplenotes="首页背景" className="homeBackground"></span>
<div __examplenotes="秒杀活动" className="seckill">
<div __examplenotes="第一场秒杀活动" className="seckillFirst">
<div __examplenotes="秒杀标题" className={`seckillBg ${seckillIndex == 0 ? 'todaySeckillBg' : 'tomorrowSeckillBg'}`}>
{
Array(2).fill('').map((item, index) => {
return (
<div __examplenotes="标题切换按钮-今日秒抢/明日秒抢" className="switchBtn" onClick={() => { this.switchTitle(index) }} key={index}></div>
)
})
}
</div>
<span __examplenotes="第一场秒杀活动背景" className="underFrame"></span>
<div __examplenotes="第一场秒杀活动详情" className="seckillItem">
{seckillData?.length > 0 && seckillData[0] != undefined && <SeckillDetailComponent seckillDetail={seckillData[0]} />}
</div>
</div>
{seckillData?.length > 1 && seckillData[1] != undefined && <div __examplenotes="第二场秒杀活动" className="seckillSecond">
<span __examplenotes="第二场秒杀活动背景" className="underBorder"></span>
<div __examplenotes="第二场秒杀活动详情" className="seckillItem2">
<SeckillDetailComponent seckillDetail={seckillData[1]} />
</div>
</div>}
</div>
<span __examplenotes="按钮-奖品" className="prize" onClick={this.toPrizePage}></span>
<span __examplenotes="按钮-规则" className="rule" onClick={this.openRulePop}></span>
</div>
);
}
}
export default HomePage;
子组件 seckillDetailComponent.jsx
'use strict';
import React from 'react';
import { observer } from 'mobx-react';
import store from '@src/store';
import { _throttle } from '@src/utils/utils';
import { Toast } from '@spark/ui';
import { timestampFormatter } from '@src/utils/utils'; // 引入上面封装好的方法——`js`将时间戳转换成“时分秒”
import './seckillDetailComponent.less';
@observer
class SeckillDetailComponent extends React.Component {
constructor(props) {
super(props);
// 1、初始化state
this.state = {
canCountDown: true, // 判断是否开启定时器
h1: '0', // 倒计时初始值-时
h2: '0', // 倒计时初始值-时
m1: '0', // 倒计时初始值-分
m2: '0', // 倒计时初始值-分
s1: '0', // 倒计时初始值-秒
s2: '0', // 倒计时初始值-秒
}
}
// 当前时间的时间戳【注意: currentTime一定要记得*1转换成数值型哟】—— 这里是组件,所以可以直接这样写获取到store?.currentTime
currentTime = store?.currentTime * 1;
// 2、初始化定时器
timer = null;
componentDidMount() {
this.handleCountDownLogic();
// 发送事件"startTimer"
document.addEventListener("startTimer", this.startTimer, this);
}
componentWillUnmount() {
// 清除定时器
clearInterval(this.timer);
// 移除事件"startTimer"
document.removeEventListener("startTimer", this.startTimer, this);
}
// 判断组件的定时器timer是不是null,如果是null且countDown大于0,调下funTimer()
startTimer = () => {
/** currentTime:当前时间,startTime:秒杀开始时间 */
const { startTime } = this.props?.seckillDetail || {};
/** 计算倒计时的展示结果,并return值,方便html里进行展示 */
const countDownTime = startTime * 1 - this.currentTime * 1; // 计算得到 倒计时 的时间戳
if (this.timer == null && countDownTime > 0) {
this.FunTimer();
}
}
// 处理倒计时总逻辑
handleCountDownLogic() {
// 倒计时
if (this.state.canCountDown) {
this.setState({ canCountDown: false })
this.FunTimer(); // 调用函数
} else {
this.setState({ canCountDown: true })
clearInterval(this.timer); // 关闭定时器
}
}
// 封装函数-倒计时
FunTimer = () => {
this.timer = setInterval(() => {
// 想实现倒计时的代码就写在这里啦~【我单独封装了一个函数】
this.updateTime();
}, 1000)
}
// 更新时间
updateTime = () => {
/** currentTime:当前时间,startTime:秒杀开始时间 */
const { startTime } = this.props?.seckillDetail || {};
/** 手动把获取到的当前时间 隔一秒加1s*/
this.currentTime += 1000;
/** 计算倒计时的展示结果,并return值,方便html里进行展示 */
const countDownTime = startTime * 1 - this.currentTime * 1; // 计算得到 倒计时 的时间戳
const _h = timestampFormatter(countDownTime).h + '';
const _m = timestampFormatter(countDownTime).m + '';
const _s = timestampFormatter(countDownTime).s + '';
this.setState({
h1: _h.slice(0, 1),
h2: _h.slice(-1),
m1: _m.slice(0, 1),
m2: _m.slice(-1),
s1: _s.slice(0, 1),
s2: _s.slice(-1),
})
// 倒计时小于0,则刷新首页数据、关闭定时器
if (countDownTime < 0) {
console.log('倒计时小于0');
clearInterval(this.timer); // 关闭定时器
this.timer = null; // 将定时器归置为null
// 根据自己需求进行操作
store.getHomeData(); // 刷新首页数据
}
}
/**
* 点击对应按钮
* @param {Num} type 1->按钮-立即秒杀 2->按钮-已抢到 3->按钮-已抢完
*/
clickButton = _throttle((type) => {
// orgNo 0:不在区域 1:北京2:上海3:深圳4:杭州
const { orgNo } = store;
// goodsId: 商品id, name: 奖品名称
const { goodsId, name } = this.props?.seckillDetail || {};
switch (type) {
case 1:
store.doSeckill(goodsId, orgNo, name);
break;
case 2:
Toast(`您已抢到${name},请前往【奖品】查看`);
break;
case 3:
Toast(`本场${name}已抢完`);
break;
}
})
render() {
// 需渲染的秒杀数据
const { seckillDetail } = this.props;
const {
// 首页接口是否报错
isError,
// 0:不在区域 1:北京2:上海3:深圳4:杭州
orgNo,
// 活动是否结束 true:活动结束
ifEnd,
} = store;
const { h1, h2, m1, m2, s1, s2 } = this.state;
return (
<div>
{
!isError && !ifEnd && orgNo != 0 &&
<div __examplenotes="秒杀详情组件" className="seckillDetailComponent">
<div __examplenotes="奖品介绍" className="prizeIntroduction">
<span __examplenotes="奖品介绍背景" className="prizeBg"></span>
<div __examplenotes="奖品图片" className="prizeImage">
<img src={seckillDetail?.icon} alt="" />
</div>
<span __examplenotes="奖品名称" className="prizeName text-hidden-ellipsis">{seckillDetail?.name}</span>
<span __examplenotes="奖品剩余量" className="surplus text-hidden-ellipsis">{seckillDetail?.status != 0 && seckillDetail?.status != 3 && seckillDetail?.status != 0 && '正在疯抢 '}剩余:{seckillDetail?.stock}</span>
<div __examplenotes="滚动条" className="scrollbar">
<div __examplenotes="滚动条外壳" className="outerScrollbar">
<span __examplenotes="滚动条内里" className="insideScrollbar" style={{ 'transform': `translateX(${seckillDetail?.process}%)` }}></span>
</div>
</div>
<div __examplenotes="按钮" className="button">
{seckillDetail?.status == 0 && <span __examplenotes="按钮-等待开抢" className="commonButton btn-waitToSeckill"></span>}
{seckillDetail?.status == 1 && <span __examplenotes="按钮-立即秒杀" className="commonButton btn-immediatelySeckill" onClick={() => { this.clickButton(1) }}></span>}
{seckillDetail?.status == 2 && <span __examplenotes="按钮-已抢到" className="commonButton btn-gotten" onClick={() => { this.clickButton(2) }}></span>}
{seckillDetail?.status == 3 && <span __examplenotes="按钮-已抢完" className="commonButton btn-robbed" onClick={() => { this.clickButton(3) }}></span>}
</div>
</div>
<div __examplenotes="倒计时" className="countDown">
<span __examplenotes="倒计时背景" className="countDownBg"></span>
<span __examplenotes="时" className="commonTime hour1">{h1}</span>
<span __examplenotes="时" className="commonTime hour2">{h2}</span>
<span __examplenotes="分" className="commonTime minute1">{m1}</span>
<span __examplenotes="分" className="commonTime minute2">{m2}</span>
<span __examplenotes="秒" className="commonTime second1">{s1}</span>
<span __examplenotes="秒" className="commonTime second2">{s2}</span>
</div>
<div __examplenotes="场次标题" className="openTitle">
<span __examplenotes="场次标题背景" className="titleBg"></span>
<span __examplenotes="场次标题内容" className="titleName text-hidden-ellipsis">{seckillDetail?.title}</span>
</div>
</div>
}
</div>
);
}
}
export default SeckillDetailComponent;