版本发布
This commit is contained in:
@@ -0,0 +1,218 @@
|
||||
<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>
|
||||
21
uni_modules/cool-ui/components/cl-rolling-number/props.ts
Normal file
21
uni_modules/cool-ui/components/cl-rolling-number/props.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
export type ClRollingNumberProps = {
|
||||
/**
|
||||
* 绑定值
|
||||
*/
|
||||
modelValue: number
|
||||
/**
|
||||
* 动画持续时间,单位毫秒
|
||||
* @default 1000
|
||||
*/
|
||||
duration?: number
|
||||
/**
|
||||
* 保留小数位数
|
||||
* @default 0
|
||||
*/
|
||||
decimals?: number
|
||||
/**
|
||||
* 是否自动开始动画
|
||||
* @default true
|
||||
*/
|
||||
autoStart?: boolean
|
||||
}
|
||||
Reference in New Issue
Block a user