Vue 3 数字翻牌器动画组件:让数字滚动起来!

614 阅读2分钟

大屏数据展示必备!零依赖、高性能的数字动画解决方案

上周接了个需求需要在项目中,使用数字的动态变化效果。本文将介绍如何使用 Vue 3 实现一个高性能的数字动态滚动组件,让你的数据展示更加生动有趣。

为什么需要数字翻牌器?

数字翻牌器(Count Flop)是一种常见的UI效果,特别适用于:

  • 数据大屏展示

  • 实时统计指标

  • 金融数据变化

  • 游戏分数更新

  • 计数器应用

相比静态数字,动态翻牌效果能更好地吸引用户注意力,增强数据变化的感知。

核心功能实现

1. 数字格式化与拆分

javascript

const formatNumber = (num: number | string): string[] => {
  return num
    .toString()
    .replace(/\B(?=(\d{3})+(?!\d))/g, ",")
    .split("");
};

这个方法实现了:

  1. 将数字转为字符串

  2. 添加千分位分隔符

  3. 拆分为单个字符数组

2. 变化检测与动画触发

javascript

watch(() => props.val, newVal => {
  const newValueArray = formatNumber(newVal);
  const oldValueArray = previousValue.value;
  
  changedIndexes.value = [];
  
  newValueArray.forEach((digit, index) => {
    if (digit !== "," && (index >= oldValueArray.length || digit !== oldValueArray[index])) {
      changedIndexes.value.push(index);
    }
  });
  
  // 更新显示值
  displayValue.value = newValueArray;
  previousValue.value = newValueArray;
}, { immediate: true });

这段代码实现了:

  • 监听数值变化

  • 比较新旧值差异

  • 标记需要动画的数字位置

  • 只更新变化的部分,提升性能

3. CSS 动画实现

css

.count-flop-box {
  position: relative;
  display: inline-block;
  overflow: hidden;
  height: 100%;
}

.count-flop-content {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  animation-fill-mode: forwards !important;
}

.rolling_0 {
  animation: rolling_0 2.1s ease;
}

@keyframes rolling_0 {
  from { transform: translateY(-90%); }
  to { transform: translateY(0); }
}

动画原理:

  1. 每个数字容器设置为相对定位

  2. 数字列表使用绝对定位

  3. 通过 transform: translateY 控制显示位置

  4. 为每个数字定义独立的动画效果

完整组件代码

html

<template>
  <div :style="countflop" style="display: inline-block">
    <div 
      class="count-flop-box" 
      :style="countflopbox" 
      v-for="(item, index) in displayValue" 
      :key="index"
    >
      <div 
        v-if="item !== ','" 
        class="count-flop-content" 
        :class="getDigitClass(item, index)"
      >
        <div 
          v-for="(item2, index2) in numberList" 
          :key="index2" 
          class="count-flop-num"
        >
          {{ item2 }}
        </div>
      </div>
      <div v-else class="count-flop-content">,</div>
    </div>
    <div v-if="suffix" class="count-flop-unit">{{ suffix }}</div>
  </div>
</template>

<script setup lang="ts">
import { ref, watch, defineProps } from "vue";

const props = defineProps({
  val: {
    type: [Number, String],
    default: 0
  },
  suffix: {
    type: String,
    default: ""
  },
  countflop: {
    type: String,
    default: ""
  },
  countflopbox: {
    type: String,
    default: ``
  }
});

const displayValue = ref<string[]>([]);
const previousValue = ref<string[]>([]);
const numberList = ref<number[]>([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
const changedIndexes = ref<number[]>([]);

const formatNumber = (num: number | string): string[] => {
  return num
    .toString()
    .replace(/\B(?=(\d{3})+(?!\d))/g, ",")
    .split("");
};

const getDigitClass = (digit: string, index: number) => {
  if (digit === ",") return "";
  return changedIndexes.value.includes(index) ? `rolling_${digit}` : `static_${digit}`;
};

watch(
  () => props.val,
  newVal => {
    const newValueArray = formatNumber(newVal);
    const oldValueArray = previousValue.value;

    changedIndexes.value = [];

    newValueArray.forEach((digit, index) => {
      if (digit !== "," && (index >= oldValueArray.length || digit !== oldValueArray[index])) {
        changedIndexes.value.push(index);
      }
    });

    displayValue.value = newValueArray;
    previousValue.value = newValueArray;
  },
  { immediate: true }
);
</script>

<style scoped>
/* 样式部分与上面相同 */
</style>

组件使用示例

html

<CountFlop 
  :val="currentValue" 
  suffix="%"
  countflop="font-size: 24px; color: #42b883;"
  countflopbox="width: 20px; height: 30px; background: rgba(66, 184, 131, 0.1); border-radius: 4px; margin: 0 2px;"
/>

参数说明

性能优化技巧

  1. 局部更新:只对变化的数字应用动画,避免不必要的重绘

  2. CSS硬件加速:使用 transform 属性触发GPU加速

  3. 动画复用:预定义所有数字的动画效果

  4. 轻量级DOM:最小化DOM节点数量