商城类应用开发中经常要遇到秒杀价或者到时间点开始优惠,这种业务了逻辑通常需要使用到倒计时功能。
主要使用到setTimeout方法,循环的不断调用清除调用清除,具体代码实现
import { cancelRaf, rAF } from '@/utils/raf'
import { ref, computed, type Ref } from 'vue'
//定义一个时间类型
type currentTime = {
days: number
hours: number
minutes: number
seconds: number
millsecond: number
total: number
}
type UseCountDownOptions = {
time: number
millisecond?: boolean
onchenge?: (current: currentTime) => void
finish?: () => void
}
const SECOND = 1000
const MINUTE = SECOND * 60
const HOURS = 60 * MINUTE
const DAY = 24 * HOURS
const parseTime = (time: number) => {
const days = Math.floor(time / DAY)
const hours = Math.floor((time % DAY) / HOURS)
const minutes = Math.floor((time % HOURS) / MINUTE)
const seconds = Math.floor((time % MINUTE) / SECOND)
const millsecond = Math.floor(time % SECOND)
return {
days,
hours,
minutes,
seconds,
millsecond,
total: time,
}
}
const isSameSecond = (time1: number, time2: number) => {
return Math.floor(time1 / SECOND) === Math.floor(time2 / SECOND)
}
export function UseCountDown(optoin: UseCountDownOptions) {
let refId: number
let countting: boolean
let endTme: number
//倒计时剩余时间
const remain: Ref = ref(optoin.time)
const current = computed(() => parseTime(remain.value))
const pause = () => {
countting = false
optoin.finish?.()
}
const getCurrentRemain = () => Math.max(endTme - Date.now(), 0)
const resetRemain = (value: number) => {
remain.value = Math.max(endTme - Date.now(), 0)
optoin.onchenge?.(current.value)
if (value === 0) {
pause()
cancelRaf(refId)
}
}
//毫秒级倒计时循环
const miroTick = () => {
refId = rAF(() => {
if (countting) {
resetRemain(getCurrentRemain())
if (remain.value > 0) {
miroTick()
}
}
})
}
//秒级倒计时循环
const maroTick = () => {
refId = rAF(() => {
if (countting) {
const cRemain = getCurrentRemain()
if (!isSameSecond(cRemain, remain.value) || cRemain === 0) {
resetRemain(cRemain)
}
if (remain.value > 0) {
maroTick()
}
}
})
}
const start = () => {
if (!countting) {
countting = true
endTme = optoin.time + Date.now()
if (optoin.millisecond) {
miroTick()
} else {
maroTick()
}
}
}
const reset = (totalTime = optoin.time) => {
pause()
remain.value = totalTime
}
return {
start,
pause,
reset,
current,
}
}
上面的rAf实现如下
export const rAF =
requestAnimationFrame ||
function (callback) {
setTimeout(callback, 1000 / 60)
}
// requestAnimationFrame 屏幕刷新函数,每秒钟刷新60次,版本太低的浏览器没有这个函数,使用 1000 / 60 timout 模拟
export const cancelRaf =
cancelAnimationFrame ||
function (id: number) {
clearTimeout(id)
}
export const doubleRaf = (fn: () => void) => {
rAF(() => {
rAF(fn)
})
}
使用举例
<script setup lang="ts">
import type { ICountdown } from '@/types'
import { UseCountDown } from '@/use/useCountDown'
interface IProps {
data: ICountdown
}
const props = defineProps<IProps>()
const countDown = UseCountDown({ time: props.data.time })
// 开始计时
countDown.start()
const current = countDown.current
const padStart = (num: number) => {
return num.toString().padStart(2, '0')
}
</script>
<template>
<div class="home-countdown">
<div class="home-countdown__info">
<span class="number">{{ padStart(current.hours) }}</span>
<span class="colon">:</span>
<span class="number">{{ padStart(current.minutes) }}</span>
<span class="colon">:</span>
<span class="number">{{ padStart(current.seconds) }}</span>
</div>
</div>
</template>
.home-countdown {
border-radius: 8px;
width: 180px;
height: 180px;
background: linear-gradient(to bottom, rgb(252, 202, 202), white, white, white);
padding: 15px 10px;
box-sizing: border-box;
justify-content: end;
&__info {
margin: 0 auto;
text-align: center;
display: flex;
align-items: center;
.number {
font-size: 12px;
background: rgb(252, 78, 78);
color: white;
padding: 2px;
border-radius: 2px;
width: 20px;
font-weight: bold;
}
.colon {
margin: 0 1px;
color: red;
}
}
}