240 lines
5.2 KiB
Vue
240 lines
5.2 KiB
Vue
<script setup lang="ts">
|
|
import { computed, defineComponent, onBeforeMount, reactive, watch } from 'vue'
|
|
import { INPUT_EVENT, PREFIX, UPDATE_MODEL_EVENT } from '../_constants'
|
|
import { getMainClass, getTimeStamp, isH5, padZero } from '../_utils'
|
|
import requestAniFrame from '../_utils/raf'
|
|
import { countdownEmits, countdownProps } from './countdown'
|
|
|
|
const props = defineProps(countdownProps)
|
|
const emits = defineEmits(countdownEmits)
|
|
defineExpose({ start, pause, reset })
|
|
|
|
const state = reactive({
|
|
restTime: 0, // 倒计时剩余时间时间
|
|
timer: null,
|
|
counting: !props.paused && props.autoStart, // 是否处于倒计时中
|
|
handleEndTime: Date.now(), // 最终截止时间
|
|
diffTime: 0, // 设置了 startTime 时,与 date.now() 的差异
|
|
})
|
|
|
|
const classes = computed(() => {
|
|
return getMainClass(props, componentName)
|
|
})
|
|
|
|
// 将倒计时剩余时间格式化 参数: t 时间戳 type custom 自定义类型
|
|
function formatRemainTime(t: number, type?: string) {
|
|
const ts = t
|
|
const rest = {
|
|
d: 0,
|
|
h: 0,
|
|
m: 0,
|
|
s: 0,
|
|
ms: 0,
|
|
}
|
|
|
|
const SECOND = 1000
|
|
const MINUTE = 60 * SECOND
|
|
const HOUR = 60 * MINUTE
|
|
const DAY = 24 * HOUR
|
|
|
|
if (ts > 0) {
|
|
rest.d = ts >= SECOND ? Math.floor(ts / DAY) : 0
|
|
rest.h = Math.floor((ts % DAY) / HOUR)
|
|
rest.m = Math.floor((ts % HOUR) / MINUTE)
|
|
rest.s = Math.floor((ts % MINUTE) / SECOND)
|
|
rest.ms = Math.floor(ts % SECOND)
|
|
}
|
|
|
|
return type === 'custom' ? rest : parseFormat({ ...rest })
|
|
}
|
|
|
|
function parseFormat(time: { d: number, h: number, m: number, s: number, ms: number }) {
|
|
let { d, h, m, s, ms } = time
|
|
let format = props.format
|
|
|
|
if (format.includes('DD'))
|
|
format = format.replace('DD', padZero(d))
|
|
else
|
|
h += Number(d) * 24
|
|
|
|
if (format.includes('HH'))
|
|
format = format.replace('HH', padZero(h))
|
|
else
|
|
m += Number(h) * 60
|
|
|
|
if (format.includes('mm'))
|
|
format = format.replace('mm', padZero(m))
|
|
else
|
|
s += Number(m) * 60
|
|
|
|
if (format.includes('ss'))
|
|
format = format.replace('ss', padZero(s))
|
|
else
|
|
ms += Number(s) * 1000
|
|
|
|
if (format.includes('S')) {
|
|
const msC = padZero(ms, 3).toString()
|
|
|
|
if (format.includes('SSS'))
|
|
format = format.replace('SSS', msC)
|
|
else if (format.includes('SS'))
|
|
format = format.replace('SS', msC.slice(0, 2))
|
|
else if (format.includes('S'))
|
|
format = format.replace('S', msC.slice(0, 1))
|
|
}
|
|
return format
|
|
}
|
|
|
|
// 倒计时 interval
|
|
function initTime() {
|
|
state.handleEndTime = props.endTime as number
|
|
state.diffTime = Date.now() - getTimeStamp(props.startTime) // 时间差
|
|
if (!state.counting)
|
|
state.counting = true
|
|
tick()
|
|
}
|
|
|
|
function tick() {
|
|
function countdown() {
|
|
const currentTime = Date.now() - state.diffTime
|
|
const remainTime = Math.max(state.handleEndTime - currentTime, 0)
|
|
|
|
state.restTime = remainTime
|
|
|
|
if (!remainTime) {
|
|
state.counting = false
|
|
pause()
|
|
emits('onEnd')
|
|
}
|
|
|
|
if (remainTime > 0)
|
|
tick()
|
|
}
|
|
|
|
if (isH5) {
|
|
(state.timer as any) = requestAnimationFrame(() => {
|
|
if (state.counting)
|
|
countdown()
|
|
})
|
|
}
|
|
else {
|
|
(state.timer as any) = requestAniFrame(() => {
|
|
if (state.counting)
|
|
countdown()
|
|
})
|
|
}
|
|
}
|
|
/**
|
|
* @description 开始倒计时
|
|
*/
|
|
function start() {
|
|
if (!state.counting && !props.autoStart) {
|
|
state.counting = true
|
|
state.handleEndTime = Date.now() + Number(state.restTime)
|
|
tick()
|
|
emits('onRestart', state.restTime)
|
|
}
|
|
}
|
|
/**
|
|
* @description 暂停倒计时
|
|
*/
|
|
function pause() {
|
|
if (isH5)
|
|
cancelAnimationFrame(state.timer as any)
|
|
else
|
|
clearTimeout(state.timer as any)
|
|
|
|
state.counting = false
|
|
emits('onPaused', state.restTime)
|
|
}
|
|
/**
|
|
* @description 重设倒计时,若 `auto-start` 为 `true`,重设后会自动开始倒计时
|
|
*/
|
|
function reset() {
|
|
if (!props.autoStart) {
|
|
pause()
|
|
state.restTime = props.time as number
|
|
}
|
|
}
|
|
|
|
const renderTime = computed(() => {
|
|
return formatRemainTime(state.restTime)
|
|
})
|
|
|
|
onBeforeMount(() => {
|
|
if (props.autoStart)
|
|
initTime()
|
|
else
|
|
state.restTime = props.time as number
|
|
})
|
|
|
|
watch(
|
|
() => state.restTime,
|
|
(value) => {
|
|
const tranTime = formatRemainTime(value, 'custom')
|
|
emits(UPDATE_MODEL_EVENT, tranTime)
|
|
emits(INPUT_EVENT, tranTime)
|
|
},
|
|
)
|
|
|
|
watch(
|
|
() => props.paused,
|
|
(v, ov) => {
|
|
if (!ov) {
|
|
if (state.counting)
|
|
pause()
|
|
}
|
|
else {
|
|
if (!state.counting) {
|
|
state.counting = true
|
|
state.handleEndTime = Date.now() + Number(state.restTime)
|
|
tick()
|
|
}
|
|
|
|
emits('onRestart', state.restTime)
|
|
}
|
|
},
|
|
)
|
|
|
|
watch(
|
|
() => props.endTime,
|
|
() => {
|
|
initTime()
|
|
},
|
|
)
|
|
|
|
watch(
|
|
() => props.startTime,
|
|
() => {
|
|
initTime()
|
|
},
|
|
)
|
|
</script>
|
|
|
|
<script lang="ts">
|
|
const componentName = `${PREFIX}-countdown`
|
|
export default defineComponent({
|
|
name: componentName,
|
|
options: {
|
|
virtualHost: true,
|
|
addGlobalClass: true,
|
|
styleIsolation: 'shared',
|
|
},
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<view :class="classes" :style="customStyle">
|
|
<template v-if="$slots.default">
|
|
<slot />
|
|
</template>
|
|
<template v-else>
|
|
<rich-text class="nut-countdown__content" :nodes="renderTime as string" />
|
|
</template>
|
|
</view>
|
|
</template>
|
|
|
|
<style lang="scss">
|
|
@import './index';
|
|
</style>
|