VUE实现数字翻牌效果

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>
关键技术点
  1. 数字拆分逻辑:将目标数字转换为指定长度的数组,逐位对比并触发翻牌
  2. 动画状态管理 :通过isFlipping数组跟踪每个数字的动画状态,避免重复触发
  3. CSS 伪元素技巧 :使用before/after拆分数字为上下两部分,配合line-height:0实现下半部分文字显示
  4. 3D 透视效果 :通过perspectivetransform-origin创建立体翻牌视觉效果,backface-visibility避免翻转时显示背面

可根据需要调整 CSS 中的尺寸(width/height)、颜色和动画时长,以适配不同的设计风格。

相关推荐
知了一笑7 分钟前
独立开发第二周:构建、执行、规划
java·前端·后端
UI前端开发工作室1 小时前
数字孪生技术为UI前端提供新视角:产品性能的实时模拟与预测
大数据·前端
Sapphire~1 小时前
重学前端004 --- html 表单
前端·html
TE-茶叶蛋1 小时前
Flutter、Vue 3 和 React 在 UI 布局比较
vue.js·flutter·react.js
Maybyy1 小时前
力扣242.有效的字母异位词
java·javascript·leetcode
遇到困难睡大觉哈哈1 小时前
CSS中的Element语法
前端·css
Real_man1 小时前
新物种与新法则:AI重塑开发与产品未来
前端·后端·面试
小彭努力中1 小时前
147.在 Vue3 中使用 OpenLayers 地图上 ECharts 模拟飞机循环飞行
前端·javascript·vue.js·ecmascript·echarts
老马聊技术1 小时前
日历插件-FullCalendar的详细使用
前端·javascript
zhu_zhu_xia1 小时前
cesium添加原生MVT矢量瓦片方案
javascript·arcgis·webgl·cesium