313 lines
6.0 KiB
Plaintext
313 lines
6.0 KiB
Plaintext
|
|
<template>
|
|||
|
|
<view class="cl-countdown" :class="[pt.className]">
|
|||
|
|
<view
|
|||
|
|
class="cl-countdown__item"
|
|||
|
|
:class="[`${item.isSplitor ? pt.splitor?.className : pt.text?.className}`]"
|
|||
|
|
v-for="(item, index) in list"
|
|||
|
|
:key="index"
|
|||
|
|
>
|
|||
|
|
<slot name="item" :item="item">
|
|||
|
|
<cl-text>{{ item.value }}</cl-text>
|
|||
|
|
</slot>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script lang="ts" setup>
|
|||
|
|
import { ref, watch, nextTick, onBeforeUnmount, onBeforeMount, computed, type PropType } from "vue";
|
|||
|
|
import type { PassThroughProps } from "../../types";
|
|||
|
|
import { dayUts, get, has, isEmpty, parsePt } from "@/cool";
|
|||
|
|
|
|||
|
|
type Item = {
|
|||
|
|
value: string;
|
|||
|
|
isSplitor: boolean;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
defineOptions({
|
|||
|
|
name: "cl-countdown"
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
defineSlots<{
|
|||
|
|
item(props: { item: Item }): any;
|
|||
|
|
}>();
|
|||
|
|
|
|||
|
|
const props = defineProps({
|
|||
|
|
// 样式穿透配置
|
|||
|
|
pt: {
|
|||
|
|
type: Object,
|
|||
|
|
default: () => ({})
|
|||
|
|
},
|
|||
|
|
// 格式化模板,支持 {d}天{h}:{m}:{s} 格式
|
|||
|
|
format: {
|
|||
|
|
type: String,
|
|||
|
|
default: "{h}:{m}:{s}"
|
|||
|
|
},
|
|||
|
|
// 是否隐藏为0的单位
|
|||
|
|
hideZero: {
|
|||
|
|
type: Boolean,
|
|||
|
|
default: false
|
|||
|
|
},
|
|||
|
|
// 倒计时天数
|
|||
|
|
day: {
|
|||
|
|
type: Number,
|
|||
|
|
default: 0
|
|||
|
|
},
|
|||
|
|
// 倒计时小时数
|
|||
|
|
hour: {
|
|||
|
|
type: Number,
|
|||
|
|
default: 0
|
|||
|
|
},
|
|||
|
|
// 倒计时分钟数
|
|||
|
|
minute: {
|
|||
|
|
type: Number,
|
|||
|
|
default: 0
|
|||
|
|
},
|
|||
|
|
// 倒计时秒数
|
|||
|
|
second: {
|
|||
|
|
type: Number,
|
|||
|
|
default: 0
|
|||
|
|
},
|
|||
|
|
// 结束时间,可以是Date对象或日期字符串
|
|||
|
|
datetime: {
|
|||
|
|
type: [Date, String] as PropType<Date | string>,
|
|||
|
|
default: null
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 组件事件定义
|
|||
|
|
*/
|
|||
|
|
const emit = defineEmits(["stop", "done", "change"]);
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 样式穿透类型定义
|
|||
|
|
*/
|
|||
|
|
type PassThrough = {
|
|||
|
|
className?: string;
|
|||
|
|
text?: PassThroughProps;
|
|||
|
|
splitor?: PassThroughProps;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 解析样式穿透配置
|
|||
|
|
const pt = computed(() => parsePt<PassThrough>(props.pt));
|
|||
|
|
|
|||
|
|
// 定时器ID,用于清除定时器
|
|||
|
|
let timer: number = 0;
|
|||
|
|
|
|||
|
|
// 当前剩余秒数
|
|||
|
|
const seconds = ref(0);
|
|||
|
|
|
|||
|
|
// 倒计时运行状态
|
|||
|
|
const isRunning = ref(false);
|
|||
|
|
|
|||
|
|
// 显示列表
|
|||
|
|
const list = ref<Item[]>([]);
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 倒计时选项类型定义
|
|||
|
|
*/
|
|||
|
|
type Options = {
|
|||
|
|
day?: number;
|
|||
|
|
hour?: number;
|
|||
|
|
minute?: number;
|
|||
|
|
second?: number;
|
|||
|
|
datetime?: Date | string;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 将时间单位转换为总秒数
|
|||
|
|
* @param options 时间选项,支持天、时、分、秒或具体日期时间
|
|||
|
|
* @returns 总秒数
|
|||
|
|
*/
|
|||
|
|
function toSeconds({ day, hour, minute, second, datetime }: Options) {
|
|||
|
|
if (datetime != null) {
|
|||
|
|
// 如果提供了具体日期时间,计算与当前时间的差值
|
|||
|
|
const diff = dayUts(datetime).diff(dayUts());
|
|||
|
|
return Math.max(0, Math.floor(diff / 1000));
|
|||
|
|
} else {
|
|||
|
|
// 否则将各个时间单位转换为秒数
|
|||
|
|
return Math.max(
|
|||
|
|
0,
|
|||
|
|
(day ?? 0) * 86400 + (hour ?? 0) * 3600 + (minute ?? 0) * 60 + (second ?? 0)
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 执行倒计时逻辑
|
|||
|
|
* 计算剩余时间并格式化显示
|
|||
|
|
*/
|
|||
|
|
function countDown() {
|
|||
|
|
// 计算天、时、分、秒,使用更简洁的计算方式
|
|||
|
|
const totalSeconds = Math.floor(seconds.value);
|
|||
|
|
const day = Math.floor(totalSeconds / 86400); // 86400 = 24 * 60 * 60
|
|||
|
|
const hour = Math.floor((totalSeconds % 86400) / 3600); // 3600 = 60 * 60
|
|||
|
|
const minute = Math.floor((totalSeconds % 3600) / 60);
|
|||
|
|
const second = totalSeconds % 60;
|
|||
|
|
|
|||
|
|
// 格式化时间对象,用于模板替换
|
|||
|
|
const t = {
|
|||
|
|
d: day.toString(),
|
|||
|
|
h: hour.toString().padStart(2, "0"),
|
|||
|
|
m: minute.toString().padStart(2, "0"),
|
|||
|
|
s: second.toString().padStart(2, "0")
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 控制是否隐藏零值,初始为true表示隐藏
|
|||
|
|
let isHide = true;
|
|||
|
|
// 记录开始隐藏的位置索引,-1表示不隐藏
|
|||
|
|
let start = -1;
|
|||
|
|
|
|||
|
|
// 根据格式模板生成显示列表
|
|||
|
|
list.value = (props.format.split(/[{,}]/) as string[])
|
|||
|
|
.map((e, i) => {
|
|||
|
|
const value = has(t, e) ? (get(t, e) as string) : e;
|
|||
|
|
const isSplitor = /^\D+$/.test(value);
|
|||
|
|
|
|||
|
|
if (props.hideZero) {
|
|||
|
|
if (isHide && !isSplitor) {
|
|||
|
|
if (value == "00" || value == "0" || isEmpty(value)) {
|
|||
|
|
start = i;
|
|||
|
|
isHide = true;
|
|||
|
|
} else {
|
|||
|
|
isHide = false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
value,
|
|||
|
|
isSplitor
|
|||
|
|
} as Item;
|
|||
|
|
})
|
|||
|
|
.filter((e, i) => {
|
|||
|
|
return !isEmpty(e.value) && (start == -1 ? true : start < i);
|
|||
|
|
})
|
|||
|
|
.filter((e, i) => {
|
|||
|
|
if (i == 0 && e.isSplitor) {
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
return true;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 触发change事件
|
|||
|
|
emit("change", list.value);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 清除定时器并重置状态
|
|||
|
|
*/
|
|||
|
|
function clear() {
|
|||
|
|
clearTimeout(timer);
|
|||
|
|
timer = 0;
|
|||
|
|
isRunning.value = false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 停止倒计时
|
|||
|
|
*/
|
|||
|
|
function stop() {
|
|||
|
|
clear();
|
|||
|
|
emit("stop");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 倒计时结束处理
|
|||
|
|
*/
|
|||
|
|
function done() {
|
|||
|
|
clear();
|
|||
|
|
emit("done");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 继续倒计时
|
|||
|
|
* 启动定时器循环执行倒计时逻辑
|
|||
|
|
*/
|
|||
|
|
function next() {
|
|||
|
|
// 如果时间已到或正在运行,直接返回
|
|||
|
|
if (seconds.value <= 0 || isRunning.value) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
isRunning.value = true;
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 倒计时循环函数
|
|||
|
|
* 每秒执行一次,直到时间结束
|
|||
|
|
*/
|
|||
|
|
function loop() {
|
|||
|
|
countDown();
|
|||
|
|
|
|||
|
|
if (seconds.value <= 0) {
|
|||
|
|
done();
|
|||
|
|
return;
|
|||
|
|
} else {
|
|||
|
|
seconds.value--;
|
|||
|
|
// @ts-ignore
|
|||
|
|
timer = setTimeout(() => loop(), 1000);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
loop();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 开始倒计时
|
|||
|
|
* @param options 可选的倒计时参数,不传则使用props中的值
|
|||
|
|
*/
|
|||
|
|
function start(options: Options | null = null) {
|
|||
|
|
nextTick(() => {
|
|||
|
|
// 计算初始秒数
|
|||
|
|
seconds.value = toSeconds({
|
|||
|
|
day: options?.day ?? props.day,
|
|||
|
|
hour: options?.hour ?? props.hour,
|
|||
|
|
minute: options?.minute ?? props.minute,
|
|||
|
|
second: options?.second ?? props.second,
|
|||
|
|
datetime: options?.datetime ?? props.datetime
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 开始倒计时
|
|||
|
|
next();
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 监听时间单位变化,重新开始倒计时
|
|||
|
|
watch(
|
|||
|
|
computed(() => [props.day, props.hour, props.minute, props.second] as number[]),
|
|||
|
|
() => {
|
|||
|
|
start();
|
|||
|
|
}
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
// 监听结束时间变化,重新开始倒计时
|
|||
|
|
watch(
|
|||
|
|
computed(() => props.datetime),
|
|||
|
|
() => {
|
|||
|
|
start();
|
|||
|
|
}
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
// 组件销毁前停止倒计时
|
|||
|
|
onBeforeUnmount(() => stop());
|
|||
|
|
|
|||
|
|
// 组件挂载前开始倒计时
|
|||
|
|
onBeforeMount(() => start());
|
|||
|
|
|
|||
|
|
defineExpose({
|
|||
|
|
start,
|
|||
|
|
stop,
|
|||
|
|
done,
|
|||
|
|
isRunning
|
|||
|
|
});
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style lang="scss" scoped>
|
|||
|
|
.cl-countdown {
|
|||
|
|
@apply flex flex-row items-center;
|
|||
|
|
|
|||
|
|
&__item {
|
|||
|
|
@apply flex flex-row justify-center items-center;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</style>
|