🔥🔥Vue数字翻滚动画组件:让数据展示更具视觉冲击力

在数据可视化场景中,静态的数字展示往往难以吸引用户注意力。无论是大屏监控面板、数据仪表盘还是活动倒计时页面,动态的数字变化总能更直观地传递数据增长或更新的信息。今天为大家介绍一款功能强大、配置灵活的Vue数字翻滚动画组件,它能轻松实现平滑的数字滚动效果,同时支持多维度自定义配置,适配各类业务场景。

一、组件定位与核心价值

这款Vue数字翻滚动画组件基于writing-mode垂直排版原理实现,核心价值在于将枯燥的数字更新转化为流畅、有质感的翻滚动画,提升页面的视觉表现力与用户体验。与传统的数字切换效果相比,它具备以下核心优势:

  • 视觉吸引力强:模拟机械计数器的翻滚效果,数字切换平滑自然,瞬间抓住用户眼球;
  • 配置极度灵活:支持位数、前置零、分隔符、动画时长等多维度自定义,适配不同业务需求;
  • 兼容性优异:适配主流浏览器,针对Firefox等浏览器做了专项兼容性优化;
  • 易用性高:组件化封装,传入简单参数即可快速使用,无需关注复杂的实现细节。

二、核心功能详解

组件围绕"数字展示"与"动画效果"两大核心,提供了丰富的功能配置,覆盖从基础展示到高级定制的全场景需求。

1. 基础核心功能

  • 平滑翻滚动画:基于CSS过渡动画实现,数字切换无卡顿,支持自定义动画时长;
  • 自动增长模拟:可配置自动增长功能,支持自定义增长间隔与增长数值范围,完美模拟实时数据更新场景;
  • 金额格式化:自动添加千分位分隔符,让大数字展示更清晰(如123456 → 123,456)。

2. 灵活的数字显示配置

针对不同场景下的数字展示需求,组件提供了三大核心配置项,实现数字显示的精准控制:

  • 位数可配置(digitLength) :支持1-18位数字显示,默认8位,可根据业务场景(如验证码4位、订单号8位、销售额10位)灵活调整;
  • 前置零控制(showLeadingZero) :可配置是否显示前置零(或后置零),例如配置8位数字时,123可显示为00000123(显示前置零)或123(不显示前置零);
  • 补零方向(padDirection) :支持左补零(默认)与右补零,适配编码、验证码等特殊场景的需求。

3. 样式与交互优化

  • 响应式适配:小屏幕自动调整数字框大小与字体大小,确保多位数展示时不换行、不拥挤;
  • 质感样式设计:采用渐变背景、文字阴影等设计,营造科技感,适配大屏监控、数据仪表盘等场景;
  • 用户体验优化:禁止数字选中,避免滚动过程中误操作,提升交互流畅度。

三、快速上手指南

组件基于Vue3 + TypeScript开发,支持script setup语法,集成过程简单高效,三步即可完成集成。

1. 组件引入

将组件文件(如NumberRoll.vue)放入项目组件目录,在需要使用的页面中直接引入:

xml 复制代码
<script setup>
import NumberRoll from './components/NumberRoll.vue';
</script>

2. 基础使用

传入核心参数number即可实现基础的数字翻滚效果,默认8位数字、显示前置零、自动增长:

xml 复制代码
<template>
  <div class="dashboard">
    <h3>总访问量 <NumberRoll :number="12345" /> 次</h3>
  </div>
</template>

3. 效果预览

上述代码将实现8位数字翻滚效果,初始显示00001234,后续每3秒随机增长1-100,数字滚动平滑自然,带有科技感样式。

四、进阶配置示例

针对不同业务场景,组件提供了丰富的配置组合,以下是几个典型场景的配置示例,覆盖大部分常见需求。

1. 场景一:4位验证码(不自动增长、右补零)

需求:4位验证码,右补零,不自动增长,不显示分隔符

ini 复制代码
<NumberRoll 
  :number="123" 
  :digit-length="4" 
  pad-direction="right" 
  :auto-increase="false" 
  :show-separator="false" 
/>

效果:显示1230,无自动增长,数字固定展示。

2. 场景二:销售额展示(10位、不显示前置零、自定义动画时长)

需求:10位销售额,不显示前置零,动画时长2秒,自动增长间隔5秒

ruby 复制代码
<NumberRoll 
  :number="987654321" 
  :digit-length="10" 
  :show-leading-zero="false" 
  :duration="2" 
  :increase-interval="5000" 
/>

效果:初始显示987,654,321,每5秒随机增长1-100,数字切换动画时长2秒。

3. 场景三:在线人数(3位、不显示前置零、关闭自动增长)

需求:3位在线人数,不显示前置零,固定显示当前人数,无自动增长

ruby 复制代码
<NumberRoll 
  :number="896" 
  :digit-length="3" 
  :show-leading-zero="false" 
  :auto-increase="false" 
  :show-separator="false" 
/>

效果:固定显示896,无滚动动画,简洁清晰。

4. 场景四:订单编号(8位、显示前置零、关闭自动增长)

需求:8位订单编号,必须显示前置零,固定编号,无自动增长

ruby 复制代码
<NumberRoll 
  :number="12345" 
  :digit-length="8" 
  :auto-increase="false" 
/>

效果:固定显示00001234,符合订单编号的标准化展示需求。

五、完整参数说明

为方便用户快速查阅配置项,以下是组件所有参数的详细说明:

参数名 类型 默认值 说明 可选值
number Number/String 0 初始显示的数字 任意非负整数
duration Number 1.5 数字翻滚动画的持续时间(单位:秒) 任意正数
autoIncrease Boolean true 是否开启数字自动增长 true/false
increaseInterval Number 3000 自动增长的时间间隔(单位:毫秒) 任意正数
increaseRange Array [1, 100] 每次自动增长的随机数值范围 两个非负整数组成的数组
digitLength Number 8 数字显示的位数,支持1-18位 1-18之间的整数
showSeparator Boolean true 是否显示千分位分隔符 true/false
padDirection String left 数字补零的方向 left(左补零)/right(右补零)
showLeadingZero Boolean true 是否显示前置零(左补零时)或后置零(右补零时) true/false

六、应用场景与扩展建议

1. 典型应用场景

该组件适用于各类需要动态数字展示的场景,尤其适合:

  • 大屏监控面板:如系统访问量、订单量、在线用户数等实时数据展示;
  • 电商运营后台:销售额、成交量、用户增长数等核心指标展示;
  • 活动营销页面:倒计时数字、参与人数、累计销售额等氛围营造;
  • 企业数据仪表盘:核心业务数据的可视化展示,提升数据可读性。

2. 扩展方向建议

根据实际业务需求,组件还可进一步扩展以下功能:

  • 自定义分隔符 :新增separator参数,支持将千分位分隔符替换为"."" "等字符;
  • 小数支持 :新增decimalLength参数,支持小数位数配置,适配金额、温度等需要小数的场景;
  • 样式主题:提供多套样式主题(如科技蓝、中国红、复古金),支持一键切换;
  • 动画曲线自定义 :新增easing参数,支持线性、缓进缓出等不同动画曲线;
  • 数字滚动触发方式:支持手动触发滚动(如通过按钮点击),适配非自动更新的场景。

七、源码

js 复制代码
<template>
  <div class="number-roll">
    <div class="number-container">
      <ul class="number-box">
        <li
          v-for="(item, index) in numArr"
          :key="index"
          :class="{ 'number-item': !isNaN(item), 'mark-item': isNaN(item) }"
        >
          <!-- 数字项 -->
          <span v-if="!isNaN(item)">
            <p
              ref="numberItem"
              :style="{
                transform: `translate(-50%, -${Number(item) * 10}%)`,
                transition: `transform ${duration}s ease-in-out`
              }"
            >
              0123456789
            </p>
          </span>
          <!-- 分隔符项 -->
          <span v-else class="mark-symbol">{{ item }}</span>
        </li>
      </ul>
    </div>
</template>

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

// 组件props
const props = defineProps({
  // 要显示的数字
  number: {
    type: [Number, String],
    default: 0
  },
  // 动画持续时间(秒)
  duration: {
    type: Number,
    default: 1.5
  },
  // 是否自动增长(模拟轮询)
  autoIncrease: {
    type: Boolean,
    default: true
  },
  // 增长间隔(毫秒)
  increaseInterval: {
    type: Number,
    default: 3000
  },
  // 每次增长的随机数范围
  increaseRange: {
    type: Array,
    default: () => [1, 100]
  },
  // 配置显示的位数
  digitLength: {
    type: Number,
    default: 8,
    validator: (value: number) => {
      return value >= 1 && value <= 18;
    }
  },
  // 是否显示金额分隔符
  showSeparator: {
    type: Boolean,
    default: true
  },
  // 补零方向 (left/right)
  padDirection: {
    type: String,
    default: 'left',
    validator: (value: string) => {
      return ['left', 'right'].includes(value);
    }
  },
  // 核心新增:是否显示前置零
  showLeadingZero: {
    type: Boolean,
    default: true
  }
});

// 状态管理
const numArr = ref<string[]>([]); // 处理后的数字数组(含分隔符)
const currentNumber = ref<number>(Number(props.number)); // 当前显示的数字
let timer: NodeJS.Timeout | null = null; // 自动增长定时器

/**
 * 格式化数字为带千分位分隔符的字符串
 * @param numStr 原始数字字符串
 * @returns 格式化后的字符串
 */
const formatWithSeparator = (numStr: string): string => {
  // 从右往左每3位添加分隔符
  return numStr.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
};

/**
 * 处理前置零显示逻辑
 * @param numStr 补零后的数字字符串
 * @returns 处理后的数字字符串
 */
const handleLeadingZero = (numStr: string): string => {
  if (props.showLeadingZero) return numStr;
  
  // 不显示前置零的处理逻辑
  if (props.padDirection === 'left') {
    // 左补零:去除前置零,但保留至少一位(防止空字符串)
    const trimmed = numStr.replace(/^0+/, '');
    return trimmed || '0';
  } else {
    // 右补零:去除后置零,但保留至少一位
    const trimmed = numStr.replace(/0+$/, '');
    return trimmed || '0';
  }
};

/**
 * 初始化数字处理
 * @param num 要处理的数字
 */
const initNumber = (num: number) => {
  // 转换为整数并转字符串
  let numberStr = Math.floor(num).toString();
  
  // 1. 补零处理
  if (numberStr.length < props.digitLength) {
    if (props.padDirection === 'left') {
      numberStr = numberStr.padStart(props.digitLength, '0');
    } else {
      numberStr = numberStr.padEnd(props.digitLength, '0');
    }
  } else if (numberStr.length > props.digitLength) {
    console.warn(`数字超过${props.digitLength}位,已截取前${props.digitLength}位`);
    // 截取对应位数
    numberStr = props.padDirection === 'left' 
      ? numberStr.slice(-props.digitLength) // 左侧截取:取后N位
      : numberStr.slice(0, props.digitLength); // 右侧截取:取前N位
  }

  // 2. 处理前置/后置零显示逻辑
  let processedStr = handleLeadingZero(numberStr);
  
  // 3. 金额分隔符处理
  let formatted = processedStr;
  if (props.showSeparator && processedStr.length > 3) {
    formatted = formatWithSeparator(processedStr);
  }
  
  // 4. 转换为数组供渲染
  numArr.value = formatted.split('');
};

/**
 * 生成随机数
 * @param min 最小值
 * @param max 最大值
 */
const getRandomNumber = (min: number, max: number) => {
  return Math.floor(Math.random() * (max - min + 1) + min);
};

/**
 * 自动增长数字
 */
const startAutoIncrease = () => {
  if (!props.autoIncrease) return;
  
  // 清除现有定时器
  if (timer) clearInterval(timer);
  
  // 启动新定时器
  timer = setInterval(() => {
    const [min, max] = props.increaseRange;
    currentNumber.value += getRandomNumber(min, max);
    initNumber(currentNumber.value);
  }, props.increaseInterval);
};

/**
 * 停止自动增长
 */
const stopAutoIncrease = () => {
  if (timer) {
    clearInterval(timer);
    timer = null;
  }
};

// 初始化
nextTick(() => {
  currentNumber.value = Number(props.number);
  initNumber(currentNumber.value);
  startAutoIncrease();
});

// 监听props变化
watch(
  () => [props.number, props.digitLength, props.showSeparator, props.padDirection, props.showLeadingZero],
  () => {
    currentNumber.value = Number(props.number);
    initNumber(currentNumber.value);
  },
  { immediate: true, deep: true }
);

// 监听autoIncrease变化
watch(
  () => props.autoIncrease,
  (newVal) => {
    if (newVal) {
      startAutoIncrease();
    } else {
      stopAutoIncrease();
    }
  }
);

// 组件卸载时清除定时器
onUnmounted(() => {
  stopAutoIncrease();
});
</script>

<style scoped lang="scss">
.number-roll {
  display: inline-block;
  padding: 8px 16px;
  background-color: #0f172a;
  border-radius: 8px;

  .number-container {
    position: relative;
  }

  .number-box {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 4px;
    margin: 0;
    padding: 0;
    list-style: none;
  }

  // 数字项样式
  .number-item {
    width: 42px;
    height: 60px;
    position: relative;
    background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
    border-radius: 6px;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
    writing-mode: vertical-lr; // 垂直排版
    text-orientation: upright; // 文字直立
    overflow: hidden;
    user-select: none;

    span {
      display: block;
      width: 100%;
      height: 100%;
      position: relative;

      p {
        position: absolute;
        top: 0;
        left: 50%;
        transform: translateX(-50%);
        margin: 0;
        padding: 0;
        letter-spacing: 12px; // 数字间距
        font-family: 'Arial', sans-serif;
        font-weight: 700;
        font-size: 48px;
        color: #38bdf8;
        text-shadow: 0 0 10px rgba(56, 189, 248, 0.5);
      }
    }
  }

  // 分隔符项样式
  .mark-item {
    width: 16px;
    height: 60px;
    display: flex;
    align-items: center;
    justify-content: center;

    .mark-symbol {
      font-family: 'Arial', sans-serif;
      font-size: 32px;
      color: #38bdf8;
      font-weight: bold;
    }
  }

  // 响应式调整不同位数的显示
  :deep(.number-box) {
    flex-wrap: nowrap;
  }

  // Firefox兼容性调整
  @-moz-document url-prefix() {
    .number-item p {
      margin-top: 4px;
    }
  }

  // 小位数样式优化
  @media (max-width: 768px) {
    .number-item {
      width: 32px;
      height: 48px;

      span p {
        font-size: 36px;
        letter-spacing: 8px;
      }
    }

    .mark-item {
      width: 12px;
      height: 48px;

      .mark-symbol {
        font-size: 24px;
      }
    }
  }
</style>
相关推荐
一颗烂土豆2 小时前
🚴‍♂️ Vue3 + Three.js 实战:如何写一个“不晕车”的沉浸式骑行播放器 🎥
vue.js·游戏·three.js
HashTang2 小时前
【AI 编程实战】第 5 篇:Pinia 状态管理 - 从混乱代码到优雅架构
前端·vue.js·ai编程
青莲8432 小时前
Kotlin Flow 深度探索与实践指南——上部:基础与核心篇
android·前端
Bug生活20482 小时前
五年断更,AI助我半天复活小程序
前端·微信小程序·ai编程
狗头大军之江苏分军2 小时前
Node.js 性能优化实践,但老板只关心是否能跑
前端·后端
恋猫de小郭2 小时前
2025 年终醒悟,AI 让我误以为自己很强,未来程序员的转型之路
android·前端·flutter
用泥种荷花2 小时前
【前端学习AI】PromptTemplate的使用
前端
狗头大军之江苏分军2 小时前
Node.js 真香,但每次部署都想砸电脑
前端·javascript·后端
Shi_haoliu2 小时前
inno setup6.6.1实例,制作安装包,创建共享文件夹,写入注册表(提供给excel加载项,此文章解释iss文件)
前端·vue.js·windows·excel