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>
|