VUE实现数字翻牌效果

发布于:2025-07-12 ⋅ 阅读:(13) ⋅ 点赞:(0)
<template>
  <div class="number-flip-container">
    <div 
      v-for="(digit, index) in digitCount" 
      :key="index" 
      class="flip-digit"
    >
      <div :class="flipClass(index)" ref="flipRefs[index]">
        <div class="digital front" :class="`number${currentDigits[index]}`"></div>
        <div class="digital back" :class="`number${nextDigits[index]}`"></div>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'NumberFlip',
  props: {
    // 目标数字
    target: {
      type: Number,
      required: true,
      validator: (v) => v >= 0
    },
    // 数字位数(不足自动补零)
    digitCount: {
      type: Number,
      default: 3
    },
    // 翻牌间隔时间(ms)
    interval: {
      type: Number,
      default: 100
    }
  },
  data() {
    return {
      currentDigits: [], // 当前显示的数字数组
      nextDigits: [], // 下一个要显示的数字数组
      isFlipping: [], // 每个数字的翻转动画状态
      timer: null // 滚动定时器
    }
  },
  computed: {
    // 格式化目标数字为指定长度的数组
    targetDigits() {
      const str = this.target.toString().padStart(this.digitCount, '0')
      return str.split('').map(Number)
    }
  },
  mounted() {
    // 初始化数字状态
    this.initDigits()
    // 开始从0翻牌到目标数字
    this.startFlipping()
  },
  beforeDestroy() {
    clearInterval(this.timer)
  },
  methods: {
    // 初始化数字数组
    initDigits() {
      this.currentDigits = Array(this.digitCount).fill(0)
      this.nextDigits = Array(this.digitCount).fill(0)
      this.isFlipping = Array(this.digitCount).fill(false)
    },

    // 获取翻牌容器的类名
    flipClass(index) {
      let base = 'flip down'
      if (this.isFlipping[index]) {
        base += ' go'
      }
      return base
    },

    // 单个数字翻牌
    flipDigit(index, newVal) {
      if (this.isFlipping[index]) return

      this.isFlipping[index] = true
      this.nextDigits[index] = newVal

      // 动画结束后更新状态
      setTimeout(() => {
        this.currentDigits[index] = newVal
        this.isFlipping[index] = false
      }, 600) // 与CSS动画时长保持一致
    },

    // 检查是否所有数字都已达到目标
    checkComplete() {
      return this.currentDigits.every((val, i) => val === this.targetDigits[i])
    },

    // 开始翻牌动画
    startFlipping() {
      let currentNum = 0
      this.timer = setInterval(() => {
        if (currentNum >= this.target) {
          clearInterval(this.timer)
          return
        }

        currentNum++
        // 格式化当前数字为指定长度的数组
        const currentStr = currentNum.toString().padStart(this.digitCount, '0')
        const currentNumArr = currentStr.split('').map(Number)

        // 逐位对比并触发翻牌
        currentNumArr.forEach((val, index) => {
          if (val !== this.currentDigits[index]) {
            this.flipDigit(index, val)
          }
        })
      }, this.interval)
    }
  },
  watch: {
    // 监听目标数字变化,重新开始翻牌
    target() {
      clearInterval(this.timer)
      this.initDigits()
      this.startFlipping()
    }
  }
}
</script>

<style scoped>
.number-flip-container {
  display: inline-flex;
  gap: 8px; /* 数字之间的间距 */
  align-items: center;
}

.flip-digit {
  /* 防止翻牌时数字抖动 */
  perspective: 200px;
}

/* 翻牌基础样式 */
.flip {
  display: inline-block;
  position: relative;
  width: 60px;
  height: 100px;
  line-height: 100px;
  border: solid 1px #000;
  border-radius: 8px;
  background: #fff;
  font-size: 66px;
  color: #fff;
  box-shadow: 0 0 6px rgba(0, 0, 0, 0.3);
  text-align: center;
  font-family: "Helvetica Neue", sans-serif;
}

.digital {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  overflow: hidden;
}

/* 数字上下部分拆分(核心技巧) */
.digital:before,
.digital:after {
  content: attr(class);
  position: absolute;
  left: 0;
  right: 0;
  background: #000;
  overflow: hidden;
  box-sizing: border-box;
  /* 从类名中提取数字(number0 -> 0) */
  content: "";
}

/* 提取数字显示 */
.number0:before, .number0:after { content: "0"; }
.number1:before, .number1:after { content: "1"; }
.number2:before, .number2:after { content: "2"; }
.number3:before, .number3:after { content: "3"; }
.number4:before, .number4:after { content: "4"; }
.number5:before, .number5:after { content: "5"; }
.number6:before, .number6:after { content: "6"; }
.number7:before, .number7:after { content: "7"; }
.number8:before, .number8:after { content: "8"; }
.number9:before, .number9:after { content: "9"; }

/* 上半部分样式 */
.digital:before {
  top: 0;
  bottom: 50%;
  border-radius: 8px 8px 0 0;
  border-bottom: solid 1px #666;
}

/* 下半部分样式(line-height:0 实现只显示下半部分文字) */
.digital:after {
  top: 50%;
  bottom: 0;
  border-radius: 0 0 8px 8px;
  line-height: 0;
}

/* 翻牌层级与透视设置 */
.flip.down .front:before {
  z-index: 3;
}

.flip.down .back:after {
  z-index: 2;
  transform-origin: 50% 0%; /* 以上边缘为旋转轴 */
  transform: perspective(160px) rotateX(180deg);
}

.flip.down .front:after,
.flip.down .back:before {
  z-index: 1;
}

/* 翻转动画实现 */
.flip.down.go .front:before {
  transform-origin: 50% 100%; /* 以下边缘为旋转轴 */
  animation: frontFlipDown 0.6s ease-in-out both;
  box-shadow: 0 -2px 6px rgba(255, 255, 255, 0.3);
  backface-visibility: hidden; /* 翻转后隐藏背面 */
}

.flip.down.go .back:after {
  animation: backFlipDown 0.6s ease-in-out both;
}

/* 向下翻转动画关键帧 */
@keyframes frontFlipDown {
  0% { transform: perspective(160px) rotateX(0deg); }
  100% { transform: perspective(160px) rotateX(-180deg); }
}

@keyframes backFlipDown {
  0% { transform: perspective(160px) rotateX(180deg); }
  100% { transform: perspective(160px) rotateX(0deg); }
}
</style>

使用方法如下:

<template>
  <div>
    <!-- 示例1:3位数字翻至123 -->
    <NumberFlip :target="123" :digitCount="3" />
    
    <!-- 示例2:5位数字翻至9876,间隔150ms -->
    <NumberFlip :target="9876" :digitCount="5" :interval="150" />
  </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)、颜色和动画时长,以适配不同的设计风格。


网站公告

今日签到

点亮在社区的每一天
去签到