|
|
@@ -0,0 +1,90 @@
|
|
|
+<template>
|
|
|
+ <span>{{ formattedNumber }}</span>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+import { ref, onMounted, watch, computed } from 'vue';
|
|
|
+
|
|
|
+export default {
|
|
|
+ name: 'NumberAnimation',
|
|
|
+ props: {
|
|
|
+ value: {
|
|
|
+ type: [Number, String],
|
|
|
+ default: 0
|
|
|
+ },
|
|
|
+ duration: {
|
|
|
+ type: Number,
|
|
|
+ default: 3000
|
|
|
+ },
|
|
|
+ decimal: {
|
|
|
+ type: Number,
|
|
|
+ default: 0
|
|
|
+ }
|
|
|
+ },
|
|
|
+ setup(props) {
|
|
|
+ const currentValue = ref(0);
|
|
|
+ let startTime = 0;
|
|
|
+ let animationFrameId = null;
|
|
|
+ let targetValue = 0;
|
|
|
+ let startValue = 0;
|
|
|
+
|
|
|
+ const formattedNumber = computed(() => {
|
|
|
+ if (props.decimal === 0) {
|
|
|
+ return Math.round(currentValue.value).toLocaleString();
|
|
|
+ } else {
|
|
|
+ return currentValue.value.toLocaleString(undefined, {
|
|
|
+ minimumFractionDigits: props.decimal,
|
|
|
+ maximumFractionDigits: props.decimal
|
|
|
+ });
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ const animate = (timestamp) => {
|
|
|
+ if (!startTime) startTime = timestamp;
|
|
|
+ const elapsed = timestamp - startTime;
|
|
|
+ const progress = Math.min(elapsed / props.duration, 1);
|
|
|
+
|
|
|
+ // 使用easeOutQuad缓动函数
|
|
|
+ const easeOutQuad = 1 - Math.pow(1 - progress, 2);
|
|
|
+ currentValue.value = startValue + (targetValue - startValue) * easeOutQuad;
|
|
|
+
|
|
|
+ if (progress < 1) {
|
|
|
+ animationFrameId = requestAnimationFrame(animate);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const startAnimation = (newValue) => {
|
|
|
+ // 取消之前的动画
|
|
|
+ if (animationFrameId) {
|
|
|
+ cancelAnimationFrame(animationFrameId);
|
|
|
+ }
|
|
|
+
|
|
|
+ startValue = currentValue.value;
|
|
|
+ targetValue = typeof newValue === 'string' ? parseFloat(newValue) || 0 : newValue;
|
|
|
+ startTime = 0;
|
|
|
+ animationFrameId = requestAnimationFrame(animate);
|
|
|
+ };
|
|
|
+
|
|
|
+ watch(() => props.value, (newValue) => {
|
|
|
+ startAnimation(newValue);
|
|
|
+ }, { immediate: true });
|
|
|
+
|
|
|
+ onMounted(() => {
|
|
|
+ currentValue.value = typeof props.value === 'string' ? parseFloat(props.value) || 0 : props.value;
|
|
|
+ });
|
|
|
+
|
|
|
+ return {
|
|
|
+ currentValue,
|
|
|
+ formattedNumber
|
|
|
+ };
|
|
|
+ }
|
|
|
+};
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+span {
|
|
|
+ display: inline-block;
|
|
|
+ min-width: 40px;
|
|
|
+ text-align: right;
|
|
|
+}
|
|
|
+</style>
|