看板
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
import type { ExtractPropTypes, PropType } from 'vue';
|
||||
import { baseProps } from '../common/props';
|
||||
|
||||
/**
|
||||
* u-count-to 数字滚动 Props
|
||||
* @description 该组件一般用于需要滚动数字到某一个值的场景,目标要求是一个递增的值。
|
||||
*/
|
||||
export const CountToProps = {
|
||||
...baseProps,
|
||||
/** 开始的数值,默认从0增长到某一个数 */
|
||||
startVal: { type: [Number, String] as PropType<number | string>, default: 0 },
|
||||
/** 要滚动的目标数值,必须 */
|
||||
endVal: { type: [Number, String] as PropType<number | string>, default: 0, required: true },
|
||||
/** 滚动到目标数值的动画持续时间,单位为毫秒(ms) */
|
||||
duration: { type: [Number, String] as PropType<number | string>, default: 2000 },
|
||||
/** 设置数值后是否自动开始滚动 */
|
||||
autoplay: { type: Boolean, default: true },
|
||||
/** 要显示的小数位数 */
|
||||
decimals: { type: [Number, String] as PropType<number | string>, default: 0 },
|
||||
/** 是否在即将到达目标数值的时候,使用缓慢滚动的效果 */
|
||||
useEasing: { type: Boolean, default: true },
|
||||
/** 十进制分割符号 */
|
||||
decimal: { type: [Number, String] as PropType<number | string>, default: '.' },
|
||||
/** 字体颜色 */
|
||||
color: { type: String, default: 'var(--u-main-color)' },
|
||||
/** 字体大小 */
|
||||
fontSize: { type: [Number, String] as PropType<number | string>, default: 50 },
|
||||
/** 是否加粗字体 */
|
||||
bold: { type: Boolean, default: false },
|
||||
/** 千位分隔符,类似金额的分割(¥23,321.05中的",") */
|
||||
separator: { type: String, default: '' }
|
||||
};
|
||||
|
||||
export type CountToProps = ExtractPropTypes<typeof CountToProps>;
|
||||
@@ -0,0 +1,266 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user