Skip to content

Vue.js 动画和过渡

Transition 组件

Vue 提供了 <Transition> 组件来为元素的进入和离开添加动画效果。

基本用法

vue
<script setup>
import { ref } from 'vue'

const show = ref(true)
</script>

<template>
  <button @click="show = !show">切换</button>
  
  <Transition name="fade">
    <p v-if="show">Hello Vue!</p>
  </Transition>
</template>

<style scoped>
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.5s ease;
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}
</style>

过渡类名

Vue 会自动添加以下 CSS 类名:

  • v-enter-from:进入动画的起始状态
  • v-enter-active:进入动画的激活状态
  • v-enter-to:进入动画的结束状态
  • v-leave-from:离开动画的起始状态
  • v-leave-active:离开动画的激活状态
  • v-leave-to:离开动画的结束状态

常见动画效果

淡入淡出

vue
<script setup>
import { ref } from 'vue'
const show = ref(true)
</script>

<template>
  <button @click="show = !show">切换</button>
  <Transition name="fade">
    <div v-if="show" class="box">淡入淡出</div>
  </Transition>
</template>

<style scoped>
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.5s;
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}

.box {
  padding: 20px;
  background-color: #42b983;
  color: white;
  border-radius: 8px;
  margin-top: 10px;
}
</style>

滑动效果

vue
<style scoped>
.slide-enter-active,
.slide-leave-active {
  transition: all 0.3s ease;
}

.slide-enter-from {
  transform: translateX(-100%);
  opacity: 0;
}

.slide-leave-to {
  transform: translateX(100%);
  opacity: 0;
}
</style>

缩放效果

vue
<style scoped>
.zoom-enter-active,
.zoom-leave-active {
  transition: all 0.3s ease;
}

.zoom-enter-from,
.zoom-leave-to {
  transform: scale(0);
  opacity: 0;
}
</style>

旋转效果

vue
<style scoped>
.rotate-enter-active,
.rotate-leave-active {
  transition: all 0.5s ease;
}

.rotate-enter-from {
  transform: rotate(-180deg) scale(0);
  opacity: 0;
}

.rotate-leave-to {
  transform: rotate(180deg) scale(0);
  opacity: 0;
}
</style>

TransitionGroup 列表动画

vue
<script setup>
import { ref } from 'vue'

const items = ref([1, 2, 3, 4, 5])
let nextId = 6

function addItem() {
  items.value.push(nextId++)
}

function removeItem(id) {
  items.value = items.value.filter(item => item !== id)
}

function shuffle() {
  items.value = items.value.sort(() => Math.random() - 0.5)
}
</script>

<template>
  <div class="list-demo">
    <button @click="addItem">添加</button>
    <button @click="shuffle">打乱</button>
    
    <TransitionGroup name="list" tag="ul">
      <li v-for="item in items" :key="item" class="list-item">
        {{ item }}
        <button @click="removeItem(item)" class="remove-btn">×</button>
      </li>
    </TransitionGroup>
  </div>
</template>

<style scoped>
.list-demo {
  max-width: 400px;
  margin: 20px auto;
}

button {
  margin: 5px;
  padding: 8px 16px;
  background-color: #42b983;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

ul {
  list-style: none;
  padding: 0;
  margin-top: 20px;
}

.list-item {
  padding: 10px;
  margin-bottom: 10px;
  background-color: #f0f0f0;
  border-radius: 4px;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.remove-btn {
  background-color: #f56c6c;
  padding: 4px 8px;
  font-size: 18px;
}

/* 列表动画 */
.list-enter-active,
.list-leave-active {
  transition: all 0.5s ease;
}

.list-enter-from {
  opacity: 0;
  transform: translateX(-30px);
}

.list-leave-to {
  opacity: 0;
  transform: translateX(30px);
}

/* 移动动画 */
.list-move {
  transition: transform 0.5s ease;
}
</style>

JavaScript 钩子

vue
<script setup>
import { ref } from 'vue'

const show = ref(true)

function onBeforeEnter(el) {
  el.style.opacity = 0
  el.style.transform = 'scale(0)'
}

function onEnter(el, done) {
  el.offsetHeight // 触发重排
  el.style.transition = 'all 0.5s'
  el.style.opacity = 1
  el.style.transform = 'scale(1)'
  
  el.addEventListener('transitionend', done)
}

function onLeave(el, done) {
  el.style.transition = 'all 0.5s'
  el.style.opacity = 0
  el.style.transform = 'scale(0)'
  
  el.addEventListener('transitionend', done)
}
</script>

<template>
  <button @click="show = !show">切换</button>
  
  <Transition
    @before-enter="onBeforeEnter"
    @enter="onEnter"
    @leave="onLeave"
  >
    <div v-if="show" class="box">JavaScript 钩子动画</div>
  </Transition>
</template>

实战示例:模态框动画

vue
<script setup>
import { ref } from 'vue'

const showModal = ref(false)
</script>

<template>
  <button @click="showModal = true">打开模态框</button>
  
  <Transition name="modal">
    <div v-if="showModal" class="modal-mask" @click="showModal = false">
      <div class="modal-container" @click.stop>
        <h3>模态框标题</h3>
        <p>这是模态框的内容</p>
        <button @click="showModal = false">关闭</button>
      </div>
    </div>
  </Transition>
</template>

<style scoped>
.modal-mask {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1000;
}

.modal-container {
  background-color: white;
  padding: 30px;
  border-radius: 8px;
  max-width: 500px;
  width: 90%;
}

/* 模态框动画 */
.modal-enter-active,
.modal-leave-active {
  transition: opacity 0.3s ease;
}

.modal-enter-from,
.modal-leave-to {
  opacity: 0;
}

.modal-enter-active .modal-container,
.modal-leave-active .modal-container {
  transition: transform 0.3s ease;
}

.modal-enter-from .modal-container {
  transform: scale(0.8);
}

.modal-leave-to .modal-container {
  transform: scale(0.8);
}
</style>

实战示例:通知消息

vue
<script setup>
import { ref } from 'vue'

const notifications = ref([])
let nextId = 1

function addNotification(type = 'info') {
  const id = nextId++
  notifications.value.push({
    id,
    type,
    message: `这是一条${type}消息`,
    timestamp: new Date().toLocaleTimeString()
  })
  
  // 3秒后自动移除
  setTimeout(() => {
    removeNotification(id)
  }, 3000)
}

function removeNotification(id) {
  notifications.value = notifications.value.filter(n => n.id !== id)
}
</script>

<template>
  <div class="notification-demo">
    <div class="controls">
      <button @click="addNotification('success')">成功消息</button>
      <button @click="addNotification('warning')">警告消息</button>
      <button @click="addNotification('error')">错误消息</button>
    </div>
    
    <div class="notifications">
      <TransitionGroup name="notification">
        <div
          v-for="notif in notifications"
          :key="notif.id"
          :class="['notification', notif.type]"
          @click="removeNotification(notif.id)"
        >
          <span class="icon">
            {{ notif.type === 'success' ? '✓' : notif.type === 'warning' ? '⚠' : '✕' }}
          </span>
          <div class="content">
            <div class="message">{{ notif.message }}</div>
            <div class="time">{{ notif.timestamp }}</div>
          </div>
        </div>
      </TransitionGroup>
    </div>
  </div>
</template>

<style scoped>
.notification-demo {
  padding: 20px;
}

.controls {
  display: flex;
  gap: 10px;
  margin-bottom: 20px;
}

button {
  padding: 10px 20px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  color: white;
  background-color: #42b983;
}

.notifications {
  position: fixed;
  top: 20px;
  right: 20px;
  z-index: 1000;
  width: 300px;
}

.notification {
  display: flex;
  align-items: center;
  gap: 15px;
  padding: 15px;
  margin-bottom: 10px;
  background-color: white;
  border-radius: 8px;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  cursor: pointer;
}

.notification.success {
  border-left: 4px solid #67c23a;
}

.notification.warning {
  border-left: 4px solid #e6a23c;
}

.notification.error {
  border-left: 4px solid #f56c6c;
}

.icon {
  font-size: 24px;
  font-weight: bold;
}

.notification.success .icon {
  color: #67c23a;
}

.notification.warning .icon {
  color: #e6a23c;
}

.notification.error .icon {
  color: #f56c6c;
}

.content {
  flex: 1;
}

.message {
  font-weight: bold;
  margin-bottom: 5px;
}

.time {
  font-size: 12px;
  color: #999;
}

/* 通知动画 */
.notification-enter-active,
.notification-leave-active {
  transition: all 0.3s ease;
}

.notification-enter-from {
  opacity: 0;
  transform: translateX(100%);
}

.notification-leave-to {
  opacity: 0;
  transform: translateX(100%) scale(0.8);
}

.notification-move {
  transition: transform 0.3s ease;
}
</style>

CSS 动画库集成

使用 Animate.css

bash
npm install animate.css
vue
<script setup>
import { ref } from 'vue'
import 'animate.css'

const show = ref(true)
</script>

<template>
  <button @click="show = !show">切换</button>
  
  <Transition
    enter-active-class="animate__animated animate__bounceIn"
    leave-active-class="animate__animated animate__bounceOut"
  >
    <div v-if="show" class="box">Animate.css 动画</div>
  </Transition>
</template>

性能优化

使用 CSS transform 和 opacity

css
/* ✅ 性能好 - 使用 transform 和 opacity */
.fade-enter-active {
  transition: opacity 0.3s, transform 0.3s;
}

.fade-enter-from {
  opacity: 0;
  transform: translateY(20px);
}

/* ❌ 性能差 - 使用 top/left */
.fade-enter-active {
  transition: top 0.3s, left 0.3s;
}

使用 will-change

css
.animated-element {
  will-change: transform, opacity;
}

总结

  • <Transition> 用于单个元素的过渡
  • <TransitionGroup> 用于列表的过渡
  • 可以使用 CSS 类名或 JavaScript 钩子
  • 常见效果:淡入淡出、滑动、缩放、旋转
  • 可以集成第三方动画库如 Animate.css
  • 使用 transform 和 opacity 获得最佳性能

下一步

接下来学习 性能优化,了解如何优化 Vue 应用的性能。