267 lines
7.0 KiB
Vue
267 lines
7.0 KiB
Vue
<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>
|