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

219 lines
4.6 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>
<cl-text
:color="pt.color"
:pt="{
className: parseClass(['cl-rolling-number', pt.className])
}"
>{{ displayNumber }}</cl-text
>
</template>
<script setup lang="ts">
import { ref, watch, onMounted, computed } from "vue";
import { parseClass, parsePt } from "@/cool";
const props = defineProps({
// 透传样式对象
pt: {
type: Object,
default: () => ({})
},
// 目标数字
value: {
type: Number,
default: 0
},
// 动画持续时间(毫秒)
duration: {
type: Number,
default: 1000
},
// 显示的小数位数
decimals: {
type: Number,
default: 0
}
});
// 定义透传类型仅支持className
type PassThrough = {
className?: string;
color?: string;
};
// 计算pt样式便于组件内使用
const pt = computed(() => parsePt<PassThrough>(props.pt));
// 当前动画显示的数字
const currentNumber = ref<number>(0);
// 当前格式化后显示的字符串
const displayNumber = ref<string>("0");
// requestAnimationFrame动画ID用于取消动画
let animationId: number = 0;
// setTimeout定时器ID用于兼容模式
let timerId: number = 0;
// 动画起始值
let startValue: number = 0;
// 动画目标值
let targetValue: number = 0;
// 动画起始时间戳
let startTime: number = 0;
// 缓动函数ease out cubic动画更自然
function easeOut(t: number): number {
return 1 - Math.pow(1 - t, 3);
}
// 格式化数字,保留指定小数位
function formatNumber(num: number): string {
if (props.decimals == 0) {
return Math.round(num).toString();
}
return num.toFixed(props.decimals);
}
// 动画主循环每帧更新currentNumber和displayNumber
function animate(timestamp: number): void {
// 首帧记录动画起始时间
if (startTime == 0) {
startTime = timestamp;
}
// 计算已用时间
const elapsed = timestamp - startTime;
// 计算动画进度最大为1
const progress = Math.min(elapsed / props.duration, 1);
// 应用缓动函数
const easedProgress = easeOut(progress);
// 计算当前动画值
const currentValue = startValue + (targetValue - startValue) * easedProgress;
currentNumber.value = currentValue;
displayNumber.value = formatNumber(currentValue);
// 动画未结束,继续下一帧
if (progress < 1) {
animationId = requestAnimationFrame((t) => animate(t));
} else {
// 动画结束,确保显示最终值
currentNumber.value = targetValue;
displayNumber.value = formatNumber(targetValue);
animationId = 0;
}
}
/**
* 基于setTimeout的兼容动画实现
* 适用于不支持requestAnimationFrame的环境
*/
function animateWithTimeout(): void {
const frameRate = 60; // 60fps
const frameDuration = 1000 / frameRate; // 每帧时间间隔
const totalFrames = Math.ceil(props.duration / frameDuration); // 总帧数
let currentFrame = 0;
function loop(): void {
currentFrame++;
// 计算动画进度最大为1
const progress = Math.min(currentFrame / totalFrames, 1);
// 应用缓动函数
const easedProgress = easeOut(progress);
// 计算当前动画值
const currentValue = startValue + (targetValue - startValue) * easedProgress;
currentNumber.value = currentValue;
displayNumber.value = formatNumber(currentValue);
// 动画未结束,继续下一帧
if (progress < 1) {
// @ts-ignore
timerId = setTimeout(() => loop(), frameDuration);
} else {
// 动画结束,确保显示最终值
currentNumber.value = targetValue;
displayNumber.value = formatNumber(targetValue);
timerId = 0;
}
}
loop();
}
// 外部调用,停止动画
function stop() {
if (animationId != 0) {
cancelAnimationFrame(animationId);
animationId = 0;
}
if (timerId != 0) {
clearTimeout(timerId);
timerId = 0;
}
}
/**
* 启动动画从from到to
* @param from 起始值
* @param to 目标值
*/
function startAnimation(from: number, to: number): void {
// 若有未完成动画,先取消
stop();
startValue = from;
targetValue = to;
startTime = 0;
// #ifdef MP
animateWithTimeout();
// #endif
// #ifndef MP
// 启动动画
animationId = requestAnimationFrame(animate);
// #endif
}
// 外部调用,重头开始动画
function start() {
startAnimation(0, props.value);
}
// 监听value变化自动启动动画
watch(
computed(() => props.value),
(newValue: number, oldValue: number) => {
// 只有值变化时才启动动画
if (newValue != oldValue) {
startAnimation(currentNumber.value, newValue);
}
},
{ immediate: false }
);
// 组件挂载时,初始化动画
onMounted(() => {
start();
});
defineExpose({
start,
stop
});
</script>
<style lang="scss" scoped>
.cl-rolling-number {
}
</style>