在数据可视化场景中,静态的数字展示往往难以吸引用户注意力。无论是大屏监控面板、数据仪表盘还是活动倒计时页面,动态的数字变化总能更直观地传递数据增长或更新的信息。今天为大家介绍一款功能强大、配置灵活的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>