CNCEC_APP/uni_modules/uview-pro/components/u-count-to/u-count-to.vue

267 lines
7.0 KiB
Vue
Raw Normal View History

2026-03-25 14:54:15 +08:00
<template>
<view
class="u-count-num"
:class="customClass"
:style="
$u.toStyle(
{
fontSize: props.fontSize + 'rpx',
fontWeight: props.bold ? 'bold' : 'normal',
color: props.color
},
customStyle
)
"
>
{{ displayValue }}
</view>
</template>
<script lang="ts">
export default {
name: 'u-count-to',
options: {
addGlobalClass: true,
// #ifndef MP-TOUTIAO
virtualHost: true,
// #endif
styleIsolation: 'shared'
}
};
</script>
<script setup lang="ts">
import { ref, computed, watch, onMounted, onUnmounted } from 'vue';
import { CountToProps } from './types';
import { $u } from '../../';
/**
* countTo 数字滚动
* @description 该组件一般用于需要滚动数字到某一个值的场景目标要求是一个递增的值
* @tutorial https://uviewpro.cn/zh/components/countTo.html
* @property {String | Number} start-val 开始值
* @property {String | Number} end-val 结束值
* @property {String | Number} duration 滚动过程所需的时间单位ms默认2000
* @property {Boolean} autoplay 是否自动开始滚动默认true
* @property {String | Number} decimals 要显示的小数位数见官网说明默认0
* @property {Boolean} use-easing 滚动结束时是否缓动结尾见官网说明默认true
* @property {String} separator 千位分隔符见官网说明
* @property {String} color 字体颜色默认var(--u-main-color)
* @property {String | Number} font-size 字体大小单位rpx默认50
* @property {Boolean} bold 字体是否加粗默认false
* @event {Function} end 数值滚动到目标值时触发
* @example <u-count-to ref="uCountTo" :end-val="endVal" :autoplay="autoplay"></u-count-to>
*/
const emit = defineEmits(['end']);
const props = defineProps(CountToProps);
const localStartVal = ref(Number(props.startVal));
const displayValue = ref(formatNumber(props.startVal));
const printVal = ref<number | null>(null);
const paused = ref(false); // 是否暂停
const localDuration = ref(Number(props.duration));
const startTime = ref<number | null>(null); // 开始的时间
const timestamp = ref<number | null>(null); // 时间戳
const remaining = ref<number | null>(null); // 停留的时间
const rAF = ref<number | null>(null);
const lastTime = ref(0); // 上一次的时间
/**
* 是否倒计时
*/
const countDown = computed(() => Number(props.startVal) > Number(props.endVal));
watch(
() => props.startVal,
() => {
if (props.autoplay) start();
}
);
watch(
() => props.endVal,
() => {
if (props.autoplay) start();
}
);
onMounted(() => {
if (props.autoplay) start();
});
/**
* 缓动函数
*/
function easingFn(t: number, b: number, c: number, d: number): number {
return (c * (-Math.pow(2, (-10 * t) / d) + 1) * 1024) / 1023 + b;
}
/**
* requestAnimationFrame polyfill
*/
function requestAnimationFrame(callback: (ts: number) => void): number {
const currTime = new Date().getTime();
// 为了使setTimteout的尽可能的接近每秒60帧的效果
const timeToCall = Math.max(0, 16 - (currTime - lastTime.value));
const id = setTimeout(() => {
callback(currTime + timeToCall);
}, timeToCall);
lastTime.value = currTime + timeToCall;
return id as unknown as number;
}
/**
* 取消动画帧
*/
function cancelAnimationFrame(id: number | null) {
if (id) clearTimeout(id);
}
/**
* 开始滚动数字
*/
function start() {
localStartVal.value = Number(props.startVal);
startTime.value = null;
localDuration.value = Number(props.duration);
paused.value = false;
rAF.value = requestAnimationFrame(count);
}
/**
* 暂停/恢复滚动
* 暂定状态重新再开始滚动或者滚动状态下暂停
*/
function reStart() {
if (paused.value) {
resume();
paused.value = false;
} else {
stop();
paused.value = true;
}
}
/**
* 暂停
*/
function stop() {
cancelAnimationFrame(rAF.value);
}
/**
* 重新开始(暂停的情况下)
*/
function resume() {
startTime.value = null;
localDuration.value = remaining.value || Number(props.duration);
localStartVal.value = printVal.value || Number(props.startVal);
rAF.value = requestAnimationFrame(count);
}
/**
* 重置
*/
function reset() {
startTime.value = null;
cancelAnimationFrame(rAF.value);
displayValue.value = formatNumber(props.startVal);
}
/**
* 数字滚动动画主逻辑
*/
function count(ts: number) {
if (!startTime.value) startTime.value = ts;
timestamp.value = ts;
const progress = ts - (startTime.value || 0);
remaining.value = localDuration.value - progress;
let val: number;
if (props.useEasing) {
if (countDown.value) {
val =
localStartVal.value -
easingFn(progress, 0, localStartVal.value - Number(props.endVal), localDuration.value);
} else {
val = easingFn(
progress,
localStartVal.value,
Number(props.endVal) - localStartVal.value,
localDuration.value
);
}
} else {
if (countDown.value) {
val = localStartVal.value - (localStartVal.value - Number(props.endVal)) * (progress / localDuration.value);
} else {
val = localStartVal.value + (Number(props.endVal) - localStartVal.value) * (progress / localDuration.value);
}
}
if (countDown.value) {
val = val < Number(props.endVal) ? Number(props.endVal) : val;
} else {
val = val > Number(props.endVal) ? Number(props.endVal) : val;
}
printVal.value = val;
displayValue.value = formatNumber(val);
if (progress < localDuration.value) {
rAF.value = requestAnimationFrame(count);
} else {
emit('end');
}
}
/**
* 判断是否数字
*/
function isNumber(val: unknown): boolean {
return !isNaN(parseFloat(String(val)));
}
/**
* 格式化数字
*/
function formatNumber(num: unknown): string {
// 将num转为Number类型因为其值可能为字符串数值调用toFixed会报错
let n = Number(num);
n = Number(n.toFixed(Number(props.decimals)));
let str = n + '';
const x = str.split('.');
let x1 = x[0];
const x2 = x.length > 1 ? String(props.decimal) + x[1] : '';
const rgx = /(\d+)(\d{3})/;
if (props.separator && !isNumber(props.separator)) {
while (rgx.test(x1)) {
x1 = x1.replace(rgx, '$1' + props.separator + '$2');
}
}
return x1 + x2;
}
// 销毁时清理动画帧
onUnmounted(() => {
cancelAnimationFrame(rAF.value);
});
// 暴露给父组件的函数
defineExpose({
start,
stop,
reStart,
resume,
reset
});
</script>
<style lang="scss" scoped>
@import '../../libs/css/style.components.scss';
.u-count-num {
/* #ifndef APP-NVUE */
display: inline-flex;
/* #endif */
text-align: center;
}
</style>