add AnimatedNumber component
This commit is contained in:
5
src/components/animated-number/index.ts
Normal file
5
src/components/animated-number/index.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import view from "./index.vue";
|
||||||
|
import Toolkit from "~/utils/Toolkit";
|
||||||
|
|
||||||
|
export const AnimatedNumber = Toolkit.withInstall(view);
|
||||||
|
export default AnimatedNumber;
|
||||||
184
src/components/animated-number/index.vue
Normal file
184
src/components/animated-number/index.vue
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
<template>
|
||||||
|
<span class="tui-animated-number">{{ displayValue }}</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
defineOptions({
|
||||||
|
name: "AnimatedNumber"
|
||||||
|
});
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{
|
||||||
|
/** 目标数值 */
|
||||||
|
value: number;
|
||||||
|
/** 动画持续时间(毫秒),默认 1000ms */
|
||||||
|
duration?: number;
|
||||||
|
/** 小数位数,undefined 表示自动 */
|
||||||
|
decimals?: number;
|
||||||
|
/** 缓动函数类型 */
|
||||||
|
easing?: "linear" | "easeOut" | "easeInOut";
|
||||||
|
/** 是否启用千分位分隔符 */
|
||||||
|
thousands?: boolean;
|
||||||
|
/** 千分位分隔符,默认逗号 */
|
||||||
|
separator?: string;
|
||||||
|
}>(), {
|
||||||
|
duration: 1000,
|
||||||
|
easing: "easeOut",
|
||||||
|
thousands: false,
|
||||||
|
separator: ","
|
||||||
|
});
|
||||||
|
|
||||||
|
const { value, duration, decimals, easing, thousands, separator } = toRefs(props);
|
||||||
|
|
||||||
|
// 当前显示的数值
|
||||||
|
const currentValue = ref(value.value);
|
||||||
|
// 格式化后的显示值
|
||||||
|
const displayValue = ref(formatNumber(value.value));
|
||||||
|
// 动画相关
|
||||||
|
let animationFrameId: number | null = null;
|
||||||
|
let startValue = value.value;
|
||||||
|
let startTime: number | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 缓动函数
|
||||||
|
*/
|
||||||
|
function ease(t: number, type: string): number {
|
||||||
|
switch (type) {
|
||||||
|
case "linear":
|
||||||
|
return t;
|
||||||
|
case "easeOut":
|
||||||
|
return 1 - Math.pow(1 - t, 3);
|
||||||
|
case "easeInOut":
|
||||||
|
return t < 0.5
|
||||||
|
? 4 * t * t * t
|
||||||
|
: 1 - Math.pow(-2 * t + 2, 3) / 2;
|
||||||
|
default:
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加千分位分隔符
|
||||||
|
*/
|
||||||
|
function addThousandsSeparator(numStr: string, sep: string): string {
|
||||||
|
const parts = numStr.split(".");
|
||||||
|
const integerPart = parts[0];
|
||||||
|
const decimalPart = parts[1] ? "." + parts[1] : "";
|
||||||
|
|
||||||
|
// 处理负号
|
||||||
|
const isNegative = integerPart.startsWith("-");
|
||||||
|
const absoluteInteger = isNegative ? integerPart.slice(1) : integerPart;
|
||||||
|
|
||||||
|
// 对整数部分添加千分位分隔符
|
||||||
|
const formatted = absoluteInteger.replace(/\B(?=(\d{3})+(?!\d))/g, sep);
|
||||||
|
|
||||||
|
return (isNegative ? "-" : "") + formatted + decimalPart;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化数字
|
||||||
|
*/
|
||||||
|
function formatNumber(num: number): string {
|
||||||
|
let result: string;
|
||||||
|
|
||||||
|
if (decimals.value !== undefined) {
|
||||||
|
result = num.toFixed(decimals.value);
|
||||||
|
} else {
|
||||||
|
// 自动检测小数位数
|
||||||
|
const str = num.toString();
|
||||||
|
if (str.includes(".")) {
|
||||||
|
// 保留原有的小数位数,但最多保留 6 位
|
||||||
|
const decimalPart = str.split(".")[1];
|
||||||
|
const decimalLength = Math.min(decimalPart.length, 6);
|
||||||
|
result = num.toFixed(decimalLength);
|
||||||
|
} else {
|
||||||
|
result = num.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用千分位分隔符
|
||||||
|
if (thousands.value) {
|
||||||
|
result = addThousandsSeparator(result, separator.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 动画更新函数
|
||||||
|
*/
|
||||||
|
function animate(timestamp: number) {
|
||||||
|
if (startTime === null) {
|
||||||
|
startTime = timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
const elapsed = timestamp - startTime;
|
||||||
|
const progress = Math.min(elapsed / duration.value, 1);
|
||||||
|
const easedProgress = ease(progress, easing.value);
|
||||||
|
|
||||||
|
// 计算当前值
|
||||||
|
currentValue.value = startValue + (value.value - startValue) * easedProgress;
|
||||||
|
displayValue.value = formatNumber(currentValue.value);
|
||||||
|
|
||||||
|
if (progress < 1) {
|
||||||
|
animationFrameId = requestAnimationFrame(animate);
|
||||||
|
} else {
|
||||||
|
// 动画结束,确保显示精确的目标值
|
||||||
|
currentValue.value = value.value;
|
||||||
|
displayValue.value = formatNumber(value.value);
|
||||||
|
animationFrameId = null;
|
||||||
|
startTime = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始动画
|
||||||
|
*/
|
||||||
|
function startAnimation() {
|
||||||
|
// 取消之前的动画
|
||||||
|
if (animationFrameId !== null) {
|
||||||
|
cancelAnimationFrame(animationFrameId);
|
||||||
|
}
|
||||||
|
|
||||||
|
startValue = currentValue.value;
|
||||||
|
startTime = null;
|
||||||
|
animationFrameId = requestAnimationFrame(animate);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听 value 变化
|
||||||
|
watch(value, (newValue) => {
|
||||||
|
if (newValue !== currentValue.value) {
|
||||||
|
startAnimation();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听 decimals 变化,立即更新显示格式
|
||||||
|
watch(decimals, () => {
|
||||||
|
displayValue.value = formatNumber(currentValue.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听 thousands 和 separator 变化,立即更新显示格式
|
||||||
|
watch([thousands, separator], () => {
|
||||||
|
displayValue.value = formatNumber(currentValue.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 组件卸载时清理动画
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (animationFrameId !== null) {
|
||||||
|
cancelAnimationFrame(animationFrameId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
onMounted(() => {
|
||||||
|
currentValue.value = value.value;
|
||||||
|
displayValue.value = formatNumber(value.value);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.tui-animated-number {
|
||||||
|
display: inline-block;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
font-feature-settings: "tnum";
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -3,17 +3,20 @@ import Popup from "./popup";
|
|||||||
import Captcha from "./captcha";
|
import Captcha from "./captcha";
|
||||||
import MarkdownView from "./markdown-view";
|
import MarkdownView from "./markdown-view";
|
||||||
import MarkdownEditor from "./markdown-editor";
|
import MarkdownEditor from "./markdown-editor";
|
||||||
|
import AnimatedNumber from "./animated-number";
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
Popup,
|
Popup,
|
||||||
Captcha,
|
Captcha,
|
||||||
MarkdownView,
|
MarkdownView,
|
||||||
MarkdownEditor
|
MarkdownEditor,
|
||||||
|
AnimatedNumber
|
||||||
];
|
];
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Popup,
|
Popup,
|
||||||
Captcha,
|
Captcha,
|
||||||
MarkdownView,
|
MarkdownView,
|
||||||
MarkdownEditor
|
MarkdownEditor,
|
||||||
|
AnimatedNumber
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user