可自定义设置以下属性:
-
倒计时标题(title),类型 string | slot,默认 undefined
-
设置标题的样式(titleStyle),类型:CSSProperties,默认 {}
-
倒计时的前缀(prefix),类型 string | slot,默认 undefined
-
倒计时的后缀(suffix),类型 string | slot,默认 undefined
-
完成后的展示文本(finishedText),类型 string | slot,默认 undefined
-
value 是否为未来某时刻的时间戳(future);为 false 表示相对剩余时间戳,类型:boolean,默认 true
-
倒计时展示格式(format),类型 string,默认 'HH:mm:ss',(Y/YY:年,M/MM:月,D/DD:日,H/HH:时,m/mm:分钟,s/ss:秒,SSS:毫秒)
-
倒计时数值(value),支持设置未来某时刻的时间戳 或 相对剩余时间,类型 number,单位 ms,默认 0
-
设置倒计时的样式(valueStyle),类型:CSSProperties,默认 {}
-
是否处于计时状态(active),仅当 future: false 时生效,类型:boolean,默认 true
效果如下图:在线预览
①创建倒计时组件Countdown.vue:
其中引入使用了以下工具函数:
<script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue'
import type { CSSProperties } from 'vue'
import { useSlotsExist } from '../utils'
interface Props {
title?: string // 倒计时标题 string | slot
titleStyle?: CSSProperties // 设置标题的样式
prefix?: string // 倒计时的前缀 string | slot
suffix?: string // 倒计时的后缀 string | slot
finishedText?: string // 完成后的展示文本 string | slot
future?: boolean // value 是否为未来某时刻的时间戳;为 false 表示相对剩余时间戳
format?: string // 倒计时展示格式,(Y/YY:年,M/MM:月,D/DD:日,H/HH:时,m/mm:分钟,s/ss:秒,SSS:毫秒)
value?: number // 倒计时数值,支持设置未来某时刻的时间戳 (ms) 或 相对剩余时间 (ms)
valueStyle?: CSSProperties // 设置倒计时的样式
active?: boolean // 是否处于计时状态,仅当 future: false 时生效
}
const props = withDefaults(defineProps<Props>(), {
title: undefined,
titleStyle: () => ({}),
prefix: undefined,
suffix: undefined,
finishedText: undefined,
future: true,
format: 'HH:mm:ss',
value: 0,
valueStyle: () => ({}),
active: true
})
const futureTime = ref(0) // 未来截止时间戳
const remainingTime = ref(0) // 剩余时间戳
const rafID = ref<number | null>(null) // requestAnimationFrame 返回的请求 ID 是一个 long 类型整数值,是在回调列表里的唯一标识符
const emit = defineEmits(['finish'])
const slotsExist = useSlotsExist(['title', 'prefix', 'suffix'])
const showTitle = computed(() => {
return slotsExist.title || props.title
})
const showPrefix = computed(() => {
return slotsExist.prefix || props.prefix
})
const showSuffix = computed(() => {
return slotsExist.suffix || props.suffix
})
const showType = computed(() => {
return {
showMillisecond: props.format.includes('SSS'),
showYear: props.format.includes('Y'),
showMonth: props.format.includes('M'),
showDay: props.format.includes('D'),
showHour: props.format.includes('H'),
showMinute: props.format.includes('m'),
showSecond: props.format.includes('s')
}
})
watch(
() => props.active,
(to: boolean) => {
if (!props.future) {
if (to) {
futureTime.value = remainingTime.value + Date.now()
rafID.value = requestAnimationFrame(CountDown)
} else {
rafID.value && cancelAnimationFrame(rafID.value)
rafID.value = null
}
}
}
)
watch(
() => [props.value, props.future],
() => {
initCountdown()
},
{
deep: true
}
)
onMounted(() => {
initCountdown()
})
function initCountdown() {
// 只有数值类型的值,且是有穷的(finite),才返回 true
if (Number.isFinite(props.value)) {
// 检测传入的参数是否是一个有穷数
if (props.future) {
// 未来某时刻的时间戳,单位ms
if (props.value > Date.now()) {
futureTime.value = props.value
} else {
finish()
}
} else {
// 相对剩余时间,单位 ms
if (props.value > 0) {
futureTime.value = props.value + Date.now()
} else {
finish()
}
}
remainingTime.value = futureTime.value - Date.now()
if (props.future || (!props.future && props.active)) {
rafID.value && cancelAnimationFrame(rafID.value)
rafID.value = requestAnimationFrame(CountDown)
}
} else {
remainingTime.value = 0
}
}
function finish() {
remainingTime.value = 0
emit('finish')
}
function CountDown() {
if (futureTime.value > Date.now()) {
remainingTime.value = futureTime.value - Date.now()
rafID.value = requestAnimationFrame(CountDown)
} else {
finish()
}
}
// 前置补 0
function padZero(value: number, targetLength: number = 2): string {
// 左侧补零函数
return String(value).padStart(targetLength, '0')
}
function timeFormat(time: number): string {
let showTime = props.format
if (showType.value.showMillisecond) {
var millisecond = time % 1000
showTime = showTime.replace('SSS', padZero(millisecond, 3))
}
time = Math.floor(time / 1000) // 将时间转为 s 为单位
if (showType.value.showYear) {
var Y = Math.floor(time / (60 * 60 * 24 * 30 * 12))
showTime = showTime.includes('YY') ? showTime.replace('YY', padZero(Y)) : showTime.replace('Y', String(Y))
} else {
var Y = 0
}
if (showType.value.showMonth) {
time = time - Y * 60 * 60 * 24 * 30 * 12
var M = Math.floor(time / (60 * 60 * 24 * 30))
showTime = showTime.includes('MM') ? showTime.replace('MM', padZero(M)) : showTime.replace('M', String(M))
} else {
var M = 0
}
if (showType.value.showDay) {
time = time - M * 60 * 60 * 24 * 30
var D = Math.floor(time / (60 * 60 * 24))
showTime = showTime.includes('DD') ? showTime.replace('DD', padZero(D)) : showTime.replace('D', String(D))
} else {
var D = 0
}
if (showType.value.showHour) {
time = time - D * 60 * 60 * 24
var H = Math.floor(time / (60 * 60))
showTime = showTime.includes('HH') ? showTime.replace('HH', padZero(H)) : showTime.replace('H', String(H))
} else {
var H = 0
}
if (showType.value.showMinute) {
time = time - H * 60 * 60
var m = Math.floor(time / 60)
showTime = showTime.includes('mm') ? showTime.replace('mm', padZero(m)) : showTime.replace('m', String(m))
} else {
var m = 0
}
if (showType.value.showSecond) {
var s = time - m * 60
showTime = showTime.includes('ss') ? showTime.replace('ss', padZero(s)) : showTime.replace('s', String(s))
}
return showTime
}
defineExpose({
reset: resetCountdown
})
function resetCountdown() {
// 重置倒计时
initCountdown()
}
</script>
<template>
<div class="m-countdown">
<div v-if="showTitle" class="countdown-title" :style="titleStyle">
<slot name="title">{{ props.title }}</slot>
</div>
<div class="countdown-time">
<template v-if="showPrefix">
<span class="time-prefix" v-if="showPrefix || remainingTime > 0">
<slot name="prefix">{{ prefix }}</slot>
</span>
</template>
<span v-if="finishedText && remainingTime === 0" class="time-value" :style="valueStyle">
<slot name="finish">{{ finishedText }}</slot>
</span>
<span v-else class="time-value" :style="valueStyle">
{{ timeFormat(remainingTime) }}
</span>
<template v-if="showSuffix">
<span class="time-suffix" v-if="showSuffix || remainingTime > 0">
<slot name="suffix">{{ suffix }}</slot>
</span>
</template>
</div>
</div>
</template>
<style lang="less" scoped>
.m-countdown {
display: inline-block;
line-height: 1.5714285714285714;
.countdown-title {
margin-bottom: 4px;
color: rgba(0, 0, 0, 0.45);
font-size: 14px;
}
.countdown-time {
color: rgba(0, 0, 0, 0.88);
font-size: 24px;
font-family: 'Helvetica Neue'; // 保证数字等宽显示
.time-prefix {
display: inline-block;
margin-right: 4px;
}
.time-value {
display: inline-block;
direction: ltr;
}
.time-suffix {
display: inline-block;
margin-left: 4px;
}
}
}
</style>
②在要使用的页面引入:
其中引入使用了以下组件:
<script setup lang="ts">
import Countdown from './Countdown.vue'
import { ref } from 'vue'
const active = ref(true)
const resetActive = ref(true)
const countdownRef = ref()
function onFinish() {
console.log('countdown finished')
}
function onReset() {
countdownRef.value.reset()
}
</script>
<template>
<div>
<h1>{{ $route.name }} {{ $route.meta.title }}</h1>
<h2 class="mt30 mb10">基本使用</h2>
<h3 class="mb10">format: MM月 DD天 HH:mm:ss</h3>
<Countdown
title="Countdown 1年"
:value="12 * 30 * 24 * 60 * 60 * 1000"
:future="false"
format="MM月 DD天 HH:mm:ss"
@finish="onFinish"
/>
<h2 class="mt30 mb10">毫秒倒计时</h2>
<h3 class="mb10">format: Y 年 M 月 D 天 H 时 m 分 s 秒 SSS 毫秒</h3>
<Countdown
title="Million Seconds"
:value="12 * 30 * 24 * 60 * 60 * 1000"
:future="false"
format="Y 年 M 月 D 天 H 时 m 分 s 秒 SSS 毫秒"
/>
<h2 class="mt30 mb10">随时暂停</h2>
<Space vertical>
<Switch v-model="active" />
<Countdown
:active="active"
title="Pause at any time"
:value="24 * 60 * 60 * 1000"
:future="false"
format="HH:mm:ss:SSS"
/>
</Space>
<h2 class="mt30 mb10">前缀和后缀</h2>
<Countdown :value="2471875200000" format="Y 年 M 月 D 天 H 时 m 分 s 秒 SSS 毫秒">
<template #title>2048年 五一 Countdown</template>
<template #prefix>There's only</template>
<template #suffix>left for the end.</template>
</Countdown>
<h2 class="mt30 mb10">自定义样式</h2>
<Countdown
:value="2485094400000"
format="Y 年 MM 月 DD 天 HH 时 mm 分 ss 秒 SSS 毫秒"
:title-style="{ fontWeight: 500, fontSize: '18px' }"
:value-style="{ fontWeight: 600, color: '#1677ff' }"
>
<template #title>2048年 十一 Countdown</template>
</Countdown>
<h2 class="mt30 mb10">倒计时已完成</h2>
<Space gap="small" vertical>
<Countdown />
<Countdown finished-text="Finished" />
</Space>
<h2 class="mt30 mb10">重置倒计时</h2>
<Space vertical>
<Space align="center">
<Switch v-model="resetActive" />
<Button type="primary" @click="onReset">Reset</Button>
</Space>
<Countdown
ref="countdownRef"
:active="resetActive"
:value="24 * 60 * 60 * 1000"
:future="false"
format="HH:mm:ss:SSS"
/>
</Space>
</div>
</template>