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.cssvue
<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 应用的性能。