Files
WAI_Project_UNIX/uni_modules/cool-ui/components/cl-countdown/cl-countdown.uvue
2025-07-21 16:47:04 +08:00

313 lines
6.0 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>