javascript
<!--
文件名:number-flip.vue
作者:lywh
日期:2024-05-21
描述:数字翻牌动画组件,参考flipClock最佳实践,动画层和静态层分离,动画结束用animationend事件切换状态。
-->
<template>
<div class="number-flip-container">
<!-- 循环渲染每一位数字 -->
<div v-for="(digit, index) in digitCount" :key="index" class="flip-digit">
<div class="flip-card">
<!-- 上半部分 -->
<div class="half upper">
<!-- 静态层:未翻动时显示当前数字上半部分 -->
<span v-if="!isFlipping[index]">{{ currentDigits[index] }}</span>
<!-- 动画层:翻动时显示当前数字上半部分翻下去,下一个数字上半部分翻上来 -->
<span
v-else
class="flip-anim flip-topA"
:key="'topA-' + index + '-' + currentDigits[index]"
@animationend="onTopFlipEnd(index)"
>{{ currentDigits[index] }}</span>
<span
v-if="isFlipping[index]"
class="flip-anim flip-topB"
:key="'topB-' + index + '-' + nextDigits[index]"
>{{ nextDigits[index] }}</span>
</div>
<!-- 下半部分 -->
<div class="half lower">
<!-- 静态层:未翻动时显示当前数字下半部分 -->
<span v-if="!isFlipping[index]">{{ currentDigits[index] }}</span>
<!-- 动画层:翻动时显示当前数字下半部分翻上去,下一个数字下半部分翻下来 -->
<span
v-else
class="flip-anim flip-bottomA"
:key="'bottomA-' + index + '-' + currentDigits[index]"
@animationend="onBottomFlipEnd(index)"
>{{ currentDigits[index] }}</span>
<span
v-if="isFlipping[index]"
class="flip-anim flip-bottomB"
:key="'bottomB-' + index + '-' + nextDigits[index]"
>{{ nextDigits[index] }}</span>
</div>
</div>
</div>
</div>
</template>
<script>
/**
* 数字翻牌动画组件(flipClock风格重构)
* @author lywh
* @date 2024-05-21
* @description 经典机械数字翻牌效果,动画层和静态层分离,动画结束用animationend事件切换状态
*/
export default {
name: 'NumberFlip',
props: {
// 目标数字,组件会自动补零到 digitCount 位
target: {
type: Number,
required: true,
validator: (v) => v >= 0
},
// 显示的数字位数
digitCount: {
type: Number,
default: 3,
validator: (v) => v >= 1
},
// 翻牌动画的间隔时间(毫秒)
interval: {
type: Number,
default: 400,
validator: (v) => v >= 350
}
},
data() {
return {
// 当前显示的每一位数字
currentDigits: [],
// 下一个要翻到的每一位数字
nextDigits: [],
// 每一位是否正在翻动
isFlipping: [],
// 定时器句柄
timer: null
}
},
computed: {
// 目标数字转为补零后的数组,如 18 => [0,1,8]
targetArr() {
return this.target.toString().padStart(this.digitCount, '0').split('').map(Number);
}
},
watch: {
// 监听目标数字变化,立即重置并启动动画
target: {
immediate: true,
handler() {
this.resetAndStart();
}
}
},
methods: {
/**
* 重置所有数字为0,并启动翻牌动画
*/
resetAndStart() {
clearInterval(this.timer);
for (let i = 0; i < this.digitCount; i++) {
this.$set(this.currentDigits, i, 0);
this.$set(this.nextDigits, i, 0);
this.$set(this.isFlipping, i, false);
}
this.currentDigits.length = this.digitCount;
this.nextDigits.length = this.digitCount;
this.isFlipping.length = this.digitCount;
this.startFlipping();
},
/**
* 触发某一位数字的翻牌动画
* @param {number} index - 数字位索引
* @param {number} newVal - 要翻到的新数字
*/
flipDigit(index, newVal) {
this.$set(this.nextDigits, index, newVal);
this.$set(this.isFlipping, index, true);
// 动画结束后由 animationend 事件切换 currentDigits 和 isFlipping
},
/**
* 上半部分动画结束,切换 currentDigits
*/
onTopFlipEnd(index) {
this.$set(this.currentDigits, index, this.nextDigits[index]);
},
/**
* 下半部分动画结束,结束翻动
*/
onBottomFlipEnd(index) {
this.$set(this.isFlipping, index, false);
},
/**
* 启动所有数字的翻牌动画,直到全部到达目标值
*/
startFlipping() {
const targets = this.targetArr;
this.timer = setInterval(() => {
let allDone = true;
for (let i = 0; i < this.digitCount; i++) {
if (this.currentDigits[i] !== targets[i]) {
allDone = false;
let next = (this.currentDigits[i] + 1) % 10;
this.flipDigit(i, next);
}
}
if (allDone) {
clearInterval(this.timer);
}
}, this.interval);
}
},
beforeDestroy() {
// 组件销毁时清理定时器
clearInterval(this.timer);
}
}
</script>
<style scoped>
/*
数字翻牌动画组件样式(flipClock风格重构)
作者:lywh
日期:2024-05-21
*/
.number-flip-container {
display: inline-flex;
gap: 8px;
align-items: center;
padding: 2px;
}
.flip-digit {
width: 40px;
height: 50px;
perspective:1000px; /* 3D 透视,保证翻转有立体感 */
display: flex;
align-items: center;
justify-content: center;
}
.flip-card {
width: 100%;
height: 100%;
position: relative;
transform-style: preserve-3d; /* 保证子元素3D变换生效 */
}
.half {
width: 100%;
height: 50%;
overflow: hidden;
position: absolute;
left: 0;
background-color: transparent;
color: #FFCA10;
font-size: 40px;
font-family: "Helvetica Neue", sans-serif;
font-weight: bold;
user-select: none;
line-height: 1;
box-sizing: border-box;
display: flex;
justify-content: center;
}
.upper {
top: 0;
border-radius: 5px;
border-bottom: 1.5px solid transparent;
align-items: flex-start; /* 只显示上半部分 */
background: rgba(255,255,255,0.1);
}
.lower {
bottom: 0;
border-radius: 5px;
border-top: 1.5px solid transparent;
align-items: flex-end; /* 只显示下半部分 */
background: rgba(255,255,255,0.1);
}
.half span {
display: flex;
height: 200%; /* 让数字高度为整个flip-card的2倍 */
width: 100%;
line-height: 1;
align-items: center;
justify-content: center;
position: relative;
z-index: 1;
}
/* 动画层绝对定位覆盖静态层,防止动画和静态内容重叠 */
.flip-anim {
position: absolute;
left: 0;
width: 100%;
height: 100%;
top: 0;
z-index: 2;
display: flex;
align-items: center;
justify-content: center;
backface-visibility: hidden;
transform-style: preserve-3d;
}
/* 上半部分动画:当前数字上半部分翻下去 */
.flip-topA {
animation: flipTopA 0.3s ease-in-out forwards;
transform-origin: center bottom;
}
@keyframes flipTopA {
0% { transform: rotateX(0deg); }
100% { transform: rotateX(-90deg); }
}
/* 下半部分动画:当前数字下半部分翻上去 */
.flip-bottomA {
animation: flipBottomA 0.3s ease-in-out forwards;
transform-origin: center top;
}
@keyframes flipBottomA {
0% { transform: rotateX(0deg); }
100% { transform: rotateX(90deg); }
}
.flip-topB {
animation: flipTopB 0.3s ease-in-out forwards;
transform-origin: center bottom;
}
@keyframes flipTopB {
0% { transform: rotateX(90deg); }
100% { transform: rotateX(0deg); }
}
/* 下半部分动画:当前数字下半部分翻下来 */
.flip-bottomB {
animation: flipBottomB 0.3s ease-in-out forwards;
transform-origin: center top;
}
@keyframes flipBottomB {
0% { transform: rotateX(-90deg); }
100% { transform: rotateX(0deg); }
}
</style>
使用方法如下:
javascript
<template>
<div>
<!-- 示例1:3位数字翻至123 -->
<NumberFlip :target="123" :digitCount="3" />
<!-- 示例2:5位数字翻至9876,间隔150ms -->
<NumberFlip :target="9876" :digitCount="5" :interval="600" /><!--600以上-->
</div>
</template>
<script>
import NumberFlip from './NumberFlip.vue'
export default {
components: { NumberFlip }
}
</script>
关键技术点
- 数字拆分逻辑:将目标数字转换为指定长度的数组,逐位对比并触发翻牌
- 动画状态管理 :通过
isFlipping
数组跟踪每个数字的动画状态,避免重复触发 - CSS 伪元素技巧 :使用
before
/after
拆分数字为上下两部分,配合line-height:0
实现下半部分文字显示 - 3D 透视效果 :通过
perspective
和transform-origin
创建立体翻牌视觉效果,backface-visibility
避免翻转时显示背面
可根据需要调整 CSS 中的尺寸(width
/height
)、颜色和动画时长,以适配不同的设计风格。