Vue3 数字翻转动画组件:打造酷炫的数字计数器

Vue3 数字翻转动画组件:打造酷炫的数字计数器

今天我们来分享一个有趣的Vue3组件开发教程------一个数字翻转动画效果。这种效果常见于老式计数器或倒计时器,给用户带来一种复古又炫酷的视觉体验。

效果预览

首先,让我们看看最终效果:一个由数字组成的计数器,每个数字会优雅地翻转到下一个数字,就像老式数字显示装置一样。这种效果特别适合用于倒计时、计数器、进度显示等场景。

项目准备

首先,我们需要创建一个Vue3项目。如果你还没有安装Vue CLI,可以通过以下命令安装:

bash 复制代码
npm install -g @vue/cli

然后创建新项目:

bash 复制代码
vue create digital-flip-counter

选择"Manually select features"并勾选"Vue 3"选项。安装完成后,我们就可以开始开发了。

组件开发

基础结构

src/components目录下创建DigitalFlipCounter.vue文件,先添加基础结构:

vue 复制代码
<template>
  <div class="flip-counter-container">
    <div class="flip-counter">
      <div class="flip-counter-segment" v-for="(digit, index) in digits" :key="index">
        <div class="flip-counter-digit" :class="{ 'flip': isFlipping[index] }">
          <div class="flip-counter-front">{{ digit }}</div>
          <div class="flip-counter-back">{{ nextDigits[index] }}</div>
        </div>
      </div>
    </div>
  </div>
</template>
<script setup>
import { ref, computed } from 'vue';
const props = defineProps({
  value: {
    type: Number,
    required: true
  }
});
const digits = computed(() => {
  return props.value.toString().padStart(2, '0').split('');
});
const nextDigits = computed(() => {
  const nextValue = props.value + 1;
  return nextValue.toString().padStart(2, '0').split('');
});
const isFlipping = ref(Array(digits.value.length).fill(false));
</script>
<style scoped>
.flip-counter-container {
  perspective: 1000px;
  margin: 20px;
}
.flip-counter {
  display: flex;
  justify-content: center;
  gap: 5px;
}
.flip-counter-segment {
  position: relative;
  width: 40px;
  height: 60px;
  background: #f0f0f0;
  border-radius: 5px;
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
  overflow: hidden;
}
.flip-counter-digit {
  position: relative;
  width: 100%;
  height: 100%;
  transform-style: preserve-3d;
  transition: transform 0.5s;
}
.flip-counter-digit.flip {
  transform: rotateX(-180deg);
}
.flip-counter-front, .flip-counter-back {
  position: absolute;
  width: 100%;
  height: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 36px;
  font-weight: bold;
  backface-visibility: hidden;
}
.flip-counter-front {
  background: #f0f0f0;
  color: #333;
}
.flip-counter-back {
  background: #e0e0e0;
  color: #333;
  transform: rotateX(180deg);
}
</style>

添加翻转逻辑

现在我们需要添加翻转逻辑,让数字在变化时产生翻转效果:

vue 复制代码
<script setup>
import { ref, computed, watch } from 'vue';
const props = defineProps({
  value: {
    type: Number,
    required: true
  }
});
const emit = defineEmits(['update:value']);
const digits = computed(() => {
  return props.value.toString().padStart(2, '0').split('');
});
const nextDigits = computed(() => {
  const nextValue = props.value + 1;
  return nextValue.toString().padStart(2, '0').split('');
});
const isFlipping = ref(Array(digits.value.length).fill(false));
watch(() => props.value, (newValue, oldValue) => {
  if (newValue !== oldValue) {
    isFlipping.value = Array(digits.value.length).fill(true);
    
![image.png](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/3c92a7f321134eb5a6166755dcaab9b7~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5YmN56uv6b6Z6Zuo5rqq:q75.awebp?rk3s=f64ab15b&x-expires=1753927948&x-signature=ziAdAvxU%2BnwGlqnEbK%2BKhh8WDZM%3D)
![image.png](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/ff74ecf64a1540c5be587bf217d313e9~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5YmN56uv6b6Z6Zuo5rqq:q75.awebp?rk3s=f64ab15b&x-expires=1753927948&x-signature=Qezm5EgU5kAkeKSB2LG29Kxbyi0%3D)
    // 翻转完成后更新值
    setTimeout(() => {
      isFlipping.value = Array(digits.value.length).fill(false);
      emit('update:value', newValue);
    }, 500); // 与CSS过渡时间匹配
  }
}, { immediate: true });
</style>

完整组件代码

让我们完善一下组件,添加更多功能:

vue 复制代码
<template>
  <div class="flip-counter-container">
    <div class="flip-counter">
      <div class="flip-counter-segment" v-for="(digit, index) in digits" :key="index">
        <div class="flip-counter-digit" :class="{ 'flip': isFlipping[index] }">
          <div class="flip-counter-front">{{ digit }}</div>
          <div class="flip-counter-back">{{ nextDigits[index] }}</div>
        </div>
      </div>
    </div>
  </div>
</template>
<script setup>
import { ref, computed, watch } from 'vue';
const props = defineProps({
  modelValue: {
    type: Number,
    required: true
  },
  increment: {
    type: Function,
    default: () => {}
  }
});
const emit = defineEmits(['update:modelValue', 'increment']);
const digits = computed(() => {
  return props.modelValue.toString().padStart(2, '0').split('');
});
const nextDigits = computed(() => {
  const nextValue = props.modelValue + 1;
  return nextValue.toString().padStart(2, '0').split('');
});
const isFlipping = ref(Array(digits.value.length).fill(false));
watch(() => props.modelValue, (newValue, oldValue) => {
  if (newValue !== oldValue) {
    isFlipping.value = Array(digits.value.length).fill(true);
    
    // 翻转完成后更新值
    setTimeout(() => {
      isFlipping.value = Array(digits.value.length).fill(false);
      emit('update:modelValue', newValue);
      emit('increment', newValue);
    }, 500); // 与CSS过渡时间匹配
  }
}, { immediate: true });
const increment = () => {
  emit('update:modelValue', props.modelValue + 1);
};
</script>
<style scoped>
.flip-counter-container {
  perspective: 1000px;
  margin: 20px;
}
.flip-counter {
  display: flex;
  justify-content: center;
  gap: 5px;
}
.flip-counter-segment {
  position: relative;
  width: 40px;
  height: 60px;
  background: #f0f0f0;
  border-radius: 5px;
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
  overflow: hidden;
}
.flip-counter-digit {
  position: relative;
  width: 100%;
  height: 100%;
  transform-style: preserve-3d;
  transition: transform 0.5s;
}
.flip-counter-digit.flip {
  transform: rotateX(-180deg);
}
.flip-counter-front, .flip-counter-back {
  position: absolute;
  width: 100%;
  height: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 36px;
  font-weight: bold;
  backface-visibility: hidden;
}
.flip-counter-front {
  background: #f0f0f0;
  color: #333;
}
.flip-counter-back {
  background: #e0e0e0;
  color: #333;
  transform: rotateX(180deg);
}
/* 添加一些额外的样式 */
.flip-counter-segment::before,
.flip-counter-segment::after {
  content: '';
  position: absolute;
  left: 0;
  width: 100%;
  height: 1px;
  background: #ddd;
}
.flip-counter-segment::before {
  top: 50%;
}
.flip-counter-segment::after {
  bottom: 50%;
}
</style>

使用组件

现在让我们在App.vue中使用这个组件:

vue 复制代码
<template>
  <div id="app">
    <h1>数字翻转计数器</h1>
    
    <div class="counter-container">
      <DigitalFlipCounter v-model="counter" @increment="handleIncrement" />
      
      <div class="controls">
        <button @click="decrement">-</button>
        <button @click="increment">+</button>
        <button @click="reset">重置</button>
      </div>
    </div>
    
    <div class="info">
      <p>当前值: {{ counter }}</p>
      <p>已增加次数: {{ incrementCount }}</p>
    </div>
  </div>
</template>
<script setup>
import { ref } from 'vue';
import DigitalFlipCounter from './components/DigitalFlipCounter.vue';
const counter = ref(0);
const incrementCount = ref(0);
const increment = () => {
  counter.value++;
  incrementCount.value++;
};
const decrement = () => {
  if (counter.value > 0) {
    counter.value--;
  }
};
const reset = () => {
  counter.value = 0;
};
const handleIncrement = (value) => {
  console.log('计数器增加到:', value);
};
</script>
<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
.counter-container {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 20px;
}
.controls {
  display: flex;
  gap: 10px;
}
button {
  padding: 8px 16px;
  background: #42b983;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 16px;
}
button:hover {
  background: #3aa876;
}
.info {
  margin-top: 20px;
  font-size: 18px;
}
</style>

高级定制

让我们添加一些高级功能,比如自定义颜色、大小和动画速度:

vue 复制代码
<template>
  <div class="flip-counter-container" :style="{ perspective: `${perspective}px` }">
    <div class="flip-counter" :style="{ gap: `${gap}px` }">
      <div 
        v-for="(digit, index) in digits" 
        :key="index" 
        class="flip-counter-segment" 
        :style="segmentStyle"
      >
        <div 
          class="flip-counter-digit" 
          :class="{ 'flip': isFlipping[index] }"
          :style="digitStyle"
        >
          <div class="flip-counter-front" :style="frontStyle">{{ digit }}</div>
          <div class="flip-counter-back" :style="backStyle">{{ nextDigits[index] }}</div>
        </div>
      </div>
    </div>
  </div>
</template>
<script setup>
import { ref, computed, watch } from 'vue';
const props = defineProps({
  modelValue: {
    type: Number,
    required: true
  },
  width: {
    type: Number,
    default: 40
  },
  height: {
    type: Number,
    default: 60
  },
  gap: {
    type: Number,
    default: 5
  },
  perspective: {
    type: Number,
    default: 1000
  },
  frontColor: {
    type: String,
    default: '#f0f0f0'
  },
  backColor: {
    type: String,
    default: '#e0e0e0'
  },
  textColor: {
    type: String,
    default: '#333'
  },
  fontSize: {
    type: Number,
    default: 36
  },
  animationDuration: {
    type: Number,
    default: 500
  },
  increment: {
    type: Function,
    default: () => {}
  }
});
const emit = defineEmits(['update:modelValue', 'increment']);
const digits = computed(() => {
  return props.modelValue.toString().padStart(2, '0').split('');
});
const nextDigits = computed(() => {
  const nextValue = props.modelValue + 1;
  return nextValue.toString().padStart(2, '0').split('');
});
const isFlipping = ref(Array(digits.value.length).fill(false));
const segmentStyle = computed(() => ({
  width: `${props.width}px`,
  height: `${props.height}px`,
  backgroundColor: props.frontColor,
  boxShadow: `0 4px 8px rgba(0, 0, 0, 0.1)`,
  borderRadius: '5px',
  overflow: 'hidden'
}));
const digitStyle = computed(() => ({
  width: '100%',
  height: '100%',
  transformStyle: 'preserve-3d',
  transition: `transform ${props.animationDuration}ms`
}));
const frontStyle = computed(() => ({
  backgroundColor: props.frontColor,
  color: props.textColor,
  fontSize: `${props.fontSize}px`
}));
const backStyle = computed(() => ({
  backgroundColor: props.backColor,
  color: props.textColor,
  fontSize: `${props.fontSize}px`,
  transform: 'rotateX(180deg)'
}));
watch(() => props.modelValue, (newValue, oldValue) => {
  if (newValue !== oldValue) {
    isFlipping.value = Array(digits.value.length).fill(true);
    
    // 翻转完成后更新值
    setTimeout(() => {
      isFlipping.value = Array(digits.value.length).fill(false);
      emit('update:modelValue', newValue);
      emit('increment', newValue);
    }, props.animationDuration); // 与CSS过渡时间匹配
  }
}, { immediate: true });
const increment = () => {
  emit('update:modelValue', props.modelValue + 1);
};
</script>
<style scoped>
.flip-counter-container {
  margin: 20px;
}
.flip-counter {
  display: flex;
  justify-content: center;
}
.flip-counter-digit.flip {
  transform: rotateX(-180deg);
}
.flip-counter-front, .flip-counter-back {
  position: absolute;
  width: 100%;
  height: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
  backface-visibility: hidden;
}
.flip-counter-segment::before,
.flip-counter-segment::after {
  content: '';
  position: absolute;
  left: 0;
  width: 100%;
  height: 1px;
  background: #ddd;
}
.flip-counter-segment::before {
  top: 50%;
}
.flip-counter-segment::after {
  bottom: 50%;
}
</style>

完整使用示例

现在让我们看看如何使用这个高级版本的组件:

vue 复制代码
<template>
  <div id="app">
    <h1>高级数字翻转计数器</h1>
    
    <div class="counter-container">
      <DigitalFlipCounter 
        v-model="counter" 
        @increment="handleIncrement"
        :width="50"
        :height="70"
        :gap="10"
        :frontColor="'#4a6baf'"
        :backColor="'#3a5a9f'"
        :textColor="'#ffffff'"
        :fontSize="42"
        :animationDuration="600"
      />
      
      <div class="controls">
        <button @click="decrement">-</button>
        <button @click="increment">+</button>
        <button @click="reset">重置</button>
      </div>
    </div>
    
    <div class="info">
      <p>当前值: {{ counter }}</p>
      <p>已增加次数: {{ incrementCount }}</p>
    </div>
    
    <div class="color-picker">
      <h3>自定义颜色</h3>
      <div class="color-options">
        <button 
          v-for="color in colorOptions" 
          :key="color.name"
          @click="changeColor(color)"
          :style="{ backgroundColor: color.value }"
        >
          {{ color.name }}
        </button>
      </div>
    </div>
  </div>
</template>
<script setup>
import { ref } from 'vue';
import DigitalFlipCounter from './components/DigitalFlipCounter.vue';
const counter = ref(0);
const incrementCount = ref(0);
const colorOptions = [
  { name: '蓝色', value: '#4a6baf' },
  { name: '红色', value: '#d9534f' },
  { name: '绿色', value: '#5cb85c' },
  { name: '紫色', value: '#9b59b6' },
  { name: '橙色', value: '#f0ad4e' }
];
const currentColor = ref(colorOptions[0]);
const increment = () => {
  counter.value++;
  incrementCount.value++;
};
const decrement = () => {
  if (counter.value > 0) {
    counter.value--;
  }
};
const reset = () => {
  counter.value = 0;
};
const handleIncrement = (value) => {
  console.log('计数器增加到:', value);
};
const changeColor = (color) => {
  currentColor.value = color;
  updateCounterStyle();
};
const updateCounterStyle = () => {
  // 这里可以添加动态更新组件样式的逻辑
  // 由于Vue的响应式系统,我们只需要更新props即可
};
</script>
<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
.counter-container {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 20px;
}
.controls {
  display: flex;
  gap: 10px;
}
button {
  padding: 8px 16px;
  background: #42b983;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 16px;
  transition: background 0.3s;
}
button:hover {
  background: #3aa876;
}
.info {
  margin-top: 20px;
  font-size: 18px;
}
.color-picker {
  margin-top: 40px;
}
.color-options {
  display: flex;
  justify-content: center;
  gap: 10px;
  margin-top: 10px;
}
.color-options button {
  width: 40px;
  height: 40px;
  border-radius: 50%;
  border: 2px solid #ddd;
  cursor: pointer;
  transition: transform 0.2s, border 0.2s;
}
.color-options button:hover {
  transform: scale(1.1);
  border: 2px solid #333;
}
.color-options button.active {
  border: 2px solid #333;
  transform: scale(1.1);
}
</style>

总结

通过这个教程,我们创建了一个功能丰富的Vue3数字翻转计数器组件。这个组件不仅实现了基本的数字翻转效果,还提供了高度的可定制性,允许开发者调整颜色、大小、动画速度等参数。 在实际项目中,这种组件可以用于:

  1. 倒计时器
  2. 访问量统计
  3. 产品数量显示
  4. 游戏中的分数显示
  5. 任何需要数字变化的场景 希望这个教程对你有所帮助!如果你有任何问题或建议,欢迎在评论区留言。如果你喜欢这个组件,别忘了点赞和分享哦!
相关推荐
拾光拾趣录6 分钟前
一张 8K 海报差点把首屏拖垮
前端·性能优化
天涯学馆13 分钟前
为什么越来越多开发者偷偷用上了 Svelte?
前端·javascript·svelte
Silver〄line21 分钟前
前端图像视频实时检测
前端·目标检测·canva可画
三月的一天23 分钟前
React+threejs两种3D多场景渲染方案
前端·react.js·前端框架
拾光拾趣录24 分钟前
为什么浏览器那条“假进度”救不了我们?
前端·javascript·浏览器
香菜狗29 分钟前
vue3响应式数据(ref,reactive)详解
前端·javascript·vue.js
拾光拾趣录37 分钟前
老板突然要看“代码当量 KPI”
前端·node.js
拾光拾趣录44 分钟前
为什么我们要亲手“捏”一个 Vue 项目?
前端·vue.js·性能优化
油丶酸萝卜别吃1 小时前
SSE与Websocket有什么区别?
前端·javascript·网络·网络协议
27669582921 小时前
拼多多小程序 anti_content 分析
java·javascript·python·node·拼多多·anti-content·anti_content