214 lines
4.5 KiB
Plaintext
214 lines
4.5 KiB
Plaintext
|
|
<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>
|