vue Transition + animate 实现动画组件封装
css过渡
v-enter-from: 元素进入动画开始前的状态
v-enter-active: 元素进入动画进行中的状态
v-enter-to: 元素进入动画结束后的状态
v-leave-from: 元素离开动画开始前的状态
v-leave-active: 元素离开动画进行中的状态
v-leave-to: 元素离开动画结束后的状态
自定义过渡 class
enter-from-class
enter-active-class
enter-to-class
leave-from-class
leave-active-class
leave-to-class
js过渡
onBeforeEnter(el) // 元素被插入dom前, 设置元素的 "enter-from" 状态
onEnter(el, done) // 元素插入dom下一帧, 开始进入动画, 与 CSS 结合使用done可选
onAfterEnter(el) // 进入过渡完成
onEnterCancelled(el) // 进入过渡在完成之前被取消时
onBeforeLeave(el) // 在 leave 钩子之前调用
onLeave(el, done) // 离开过渡开始时, 开始离开动画, 与 CSS 结合使用done可选
onAfterLeave(el) // 在离开过渡完成, 从 DOM 中移除
onLeaveCancelled(el) // 仅在 v-show 过渡中可用
import { Transition } from 'vue';
import 'animate.css';
// css过渡
// v-enter-from: 元素进入动画开始前的状态
// v-enter-active: 元素进入动画进行中的状态
// v-enter-to: 元素进入动画结束后的状态
// v-leave-from: 元素离开动画开始前的状态
// v-leave-active: 元素离开动画进行中的状态
// v-leave-to: 元素离开动画结束后的状态
// 自定义过渡 class
// enter-from-class
// enter-active-class
// enter-to-class
// leave-from-class
// leave-active-class
// leave-to-class
// js过渡
// onBeforeEnter(el) // 元素被插入dom前, 设置元素的 "enter-from" 状态
// onEnter(el, done) // 元素插入dom下一帧, 开始进入动画, 与 CSS 结合使用done可选
// onAfterEnter(el) // 进入过渡完成
// onEnterCancelled(el) // 进入过渡在完成之前被取消时
// onBeforeLeave(el) // 在 leave 钩子之前调用
// onLeave(el, done) // 离开过渡开始时, 开始离开动画, 与 CSS 结合使用done可选
// onAfterLeave(el) // 在离开过渡完成, 从 DOM 中移除
// onLeaveCancelled(el) // 仅在 v-show 过渡中可用
// 出现时过渡
// <Transition appear><Transition>
// 模式
// <Transition mode="out-in"></Transition>
// mode="out-in" mode="in-out"
// 动态过渡
<Transition :name="transitionName"></Transition>
// 过渡持续时间
// duration?: number | { enter: number; leave: number }
// 过渡事件类型
// type?: 'transition' | 'animation'
// 使用 Key Attribute 过渡, 强制重新渲染 DOM 元素
// <Transition><span :key="count">{{ count }}</span></Transition>
// 过渡组
// <TransitionGroup name="list" tag="ul">
// <li v-for="item in items" :key="item">
// {{ item }}
// </li>
// </TransitionGroup>
interface MyTransitionProps {
// 在这里定义组件 props 的类型
}
export const MyTransitionJS = (props: MyTransitionProps, { slots, emit, attrs }: any) => {
const onEnter = (el: HTMLElement, done: () => void) => {
// 进入动画逻辑
};
const onLeave = (el: HTMLElement, done: () => void) => {
// 离开动画逻辑
};
return (
<Transition name="fade" onEnter={onEnter} onLeave={onLeave}>
{slots.default && slots.default()}
</Transition>
);
}
export const MyTransition = (props: MyTransitionProps, { slots, emit, attrs }: any) => {
return (
<Transition enterFromClass="animate__animated animate__fadeIn" enterActiveClass="animate__animated animate__fadeIn" enterToClass="animate__animated animate__fadeIn"
leaveFromClass="animate__animated animate__fadeOut" leaveActiveClass="animate__animated animate__fadeOut" leaveToClass="animate__animated animate__fadeOut"
>
{slots.default && slots.default()}
</Transition>
);
}
使用transition-group封装列表动画
列表需要逐条显示,所以用了js的动画不用css样式, 并加上setTimeout顺序显示
使用的时候列表要传:data-index过来
<FadeTransition> <div v-for="(item,index) in list" :key="item.id" :data-index="index"></div></FadeTransition>
从右侧滑入的动画 fade-x
从底部滑入的动画 fade-y
从左下滑入的动画 fade-l
从左上滑入的动画 fade-r
<!-- 动画容器 - 淡入淡出 -->
<script setup>
const props = defineProps({
type: {
type: String,
default: 'xl'
},
delay: {
type: String,
default: '200'
}
})
// JavaScript 钩子逻辑...
function onBeforeEnter(el) {
el.style.opacity = 0
}
function onEnter(el, done) {
let delay = el.dataset.index * Number(props.delay)
setTimeout(() => {
el.style.transition = 'all 0.5s '
el.style.opacity = 1
el.style.animation = `fade-${props.type} 0.5s infinite`
el.style.animationIterationCount = 1
done()
}, delay)
// setInterval写法
// let i = 0;
// const interval = setInterval(() => {
// i++;
// el.style.transition = 'all 0.5s '
// el.style.opacity = 1
// el.style.animation = `fade-${props.type} 0.5s infinite`
// el.style.animationIterationCount = 1
// done()
// if (i >= props.length) {
// clearInterval(interval);
// }
// }, props.delay);
}
</script>
<template>
<div class="container">
<!-- 包装内置的 Transition 组件 -->
<TransitionGroup :css="false" @before-enter="onBeforeEnter" @enter="onEnter">
<slot></slot> <!-- 向内传递插槽内容 -->
</TransitionGroup>
</div>
</template>
<style lang="scss">
/*
必要的 CSS...
注意:避免在这里使用 <style scoped>
因为那不会应用到插槽内容上
*/
.container {
overflow: hidden;
}
@keyframes fade-xl {
from {
transform: translateX(-100%);
}
to {
transform: translateX(0);
}
}
@keyframes fade-xr {
from {
transform: translateX(100%);
}
to {
transform: translateX(0);
}
}
@keyframes fade-yt {
from {
transform: translateY(-100%);
}
to {
transform: translateY(0);
}
}
@keyframes fade-yb {
from {
transform: translateY(100%);
}
to {
transform: translateY(0);
}
}
@keyframes fade-lt {
from {
transform: translate3d(-100%, -100px, 0);
}
to {
transform: translate3d(0%, 0px, 0);
}
}
@keyframes fade-lb {
from {
transform: translate3d(-100%, 100px, 0);
}
to {
transform: translate3d(0%, 0px, 0);
}
}
@keyframes fade-rt {
from {
transform: translate3d(100%, -100px, 0);
}
to {
transform: translate3d(0%, 0px, 0);
}
}
@keyframes fade-rb {
from {
transform: translate3d(100%, 100px, 0);
}
to {
transform: translate3d(0%, 0px, 0);
}
}
</style>
tab下拉动画容器
vue 的 transition 组件监测height变化需要给子组件height属性 这里结合component实现的tab下拉动画效果
<template>
<transition name="pack">
<slot></slot>
</transition>
</template>
<style scoped lang='scss'>
.pack-enter-active{
transition: max-height .3s;
}
.pack-leave-active {
transition: max-height .2s;
}
.pack-enter,
.pack-leave-to {
max-height: 0 !important;
}
</style>
使用
不设置固定的height,而是设置一个较大的max-height
<GPackTransition>
<component class="toolbar-com" :is="tabs.find(v=>v.id === activeTab)?.com"
@closePanel="closePanel" />
</GPackTransition>
.toolbar-com {
width: 280px;
// 不设置固定的height,而是设置一个较大的max-height
max-height: 1000px; // 这个值可以根据需要调整,但要确保足够大以容纳组件的最大可能高度
}
步骤条动画
- 原生版
利用active设置选中状态,根据status设置不同颜色 , setInterval实现动画效果, 长度变化加上transition实现动画效果, 因为el-step 的进度条变化没有过渡效果, 所有用原生实现
<script setup>
const props = defineProps({
list: Array
});
const activeStep = ref(0);
const width = ref('0%')
const interval = ref(null)
watch(() => props.list, () => {
animation()
})
/**
* 动画
*/
const animation = () => {
reset()
interval.value = setInterval(() => {
props.list.forEach((item, index) => {
item.active = index <= activeStep.value
})
const w = (activeStep.value / (props.list.length - 1)) * 100
width.value = `${w > 100 ? 100 : w}%`
props.list[activeStep.value].color = getColor(props.list[activeStep.value].status);
activeStep.value++;
if (activeStep.value >= props.list.length) {
clearInterval(interval.value);
}
}, 300);
}
/**
* 重置数据
*/
const reset = () => {
activeStep.value = 0; // 重置状态
width.value = '0%'
props.list.forEach((item) => {
item.active = false;
item.status = null;
item.color = '#e0e0e0';
})
clearInterval(interval.value);
}
/**
* 获取颜色
*/
const getColor = (status) => {
return status === null ? '#A8ABB2' : status ? '#67C23A' : '#F56C6C';
}
</script>
<template>
<div class="progress-container" v-show="list.length">
<div class="progress" :style="{ width }"></div>
<div v-for="(item, index) in list" :key="index" :style="{ color: item.color, 'border-color': item.color }"
:class="['circle', item.active ? 'active' : '']">
<div class="index">{{ item.index }}</div>
<label class="label">{{ item.name }}</label>
</div>
</div>
</template>
<style lang='scss' scoped>
$border-fill: #3498db;
$border-empty: #e0e0e0;
.progress-container {
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
text-align: center;
max-width: 100%;
margin-bottom: 30px;
}
.progress-container::before {
content: '';
position: absolute;
background-color: $border-empty;
top: 50%;
left: 0;
height: 4px;
width: 100%;
z-index: 0;
transform: translateY(-50%);
}
.progress {
position: absolute;
background-color: $border-fill;
top: 50%;
left: 0;
height: 4px;
width: 0%;
z-index: 0;
transform: translateY(-50%);
transition: .4s ease;
}
.circle {
position: relative;
background-color: #fff;
display: flex;
justify-content: center;
align-items: center;
border-radius: 50%;
color: #999;
height: 30px;
width: 30px;
border: 3px solid $border-empty;
transition: .4s ease;
}
.circle.active {
border-color: $border-fill;
color: $border-fill;
}
.circle .label {
position: absolute;
top: 25px;
left: 50%;
width: 60px;
transform: translateX(-50%);
}
</style>
- el-step版
el-step 没有过渡效果, 但是支持图标和线的颜色变化,比较好配置, 这里通过动态改height和加transition试图加动画效果, 但是不太理想, 用的是setTimeout来实现动画间隔, 状态就可以用el-step自带的效果
<script setup>
const props = defineProps({
list: {
type: Array,
default: [
{ name: '第一部', index: 1, status: null },
{ name: '第二部', index: 2, status: null },
{ name: '第三部', index: 3, status: null },
{ name: '第四部', index: 4, status: null }
]
}
});
const active = ref(null)
watch(() => props.list, () => {
animation()
})
/**
* 动画
*/
const animation = () => {
props.list.forEach((item, index) => {
const delay = (index + 1) * 500
setTimeout(() => {
active.value = index
item.stepStatus = getStatus(item.status)
}, delay);
})
}
/**
* 获取状态
* 状态 wait灰色 success绿色 error红色 process黑色 finish蓝色
*/
const getStatus = (status) => {
return status === null ? 'wait' : status ? 'success' : 'error';
}
</script>
<template>
<el-steps style="max-width: 600px" direction="vertical" :space="50" :active="active">
<el-step v-for="(item, index) in list" :key="item.index" :title="item.name" :status="item.stepStatus"
:class="{ 'my-step': active > index }" />
</el-steps>
</template>
<style lang='scss' scoped>
:deep(.el-step__head .el-step__line) {
bottom: unset;
top: unset;
height: 0%;
transition: height 0.5s ease;
}
:deep(.my-step .el-step__line) {
height: 100%;
}
</style>
带收起按钮的卡片
利用vue 的transition组件 封装一个带收起按钮和动画的卡片容器


<div class="gHideTransition">
<transition name="button">
<div class="gHideTransition-showBtn" v-show="showBtn" @click="toggleCard(true)">{{ $attrs.title }}</div>
</transition>
<transition name="slide">
<el-card class="gHideTransition-card" v-show="showCard">
<!-- 隐藏按钮 -->
<div class="card-hidden" @click="toggleCard(false)">
<el-button size="small" round icon="el-icon-arrow-left" type="primary">隐藏</el-button>
</div>
<slot></slot>
</el-card>
</transition>
</div>
showCard = true
showBtn = false
/**
* 切换卡片显示隐藏
*/
toggleCard(show) {
this.showCard = show
if (show) {
this.showBtn = !show
} else {
setTimeout(() => {
this.showBtn = !show
}, 500);
}
}
<style scoped lang='scss'>
.gHideTransition {
position: relative;
.gHideTransition-showBtn {
position: fixed;
left: 0;
top: 150px;
padding: 10px 20px;
color: #fff;
font-weight: bold;
border-radius: 0 16px 16px 0;
cursor: pointer;
background: $primary;
}
.gHideTransition-card {
position: relative;
.card-hidden {
position: absolute;
right: 10px;
top: 10px;
}
:deep(.container) {
display: flex;
flex-direction: column;
height: 75vh;
.container-main {
flex: 1;
overflow-y: auto;
}
}
}
.button-enter-active,
.button-leave-active {
transition: transform .1s;
}
.button-enter,
.button-leave-to {
transform: translateX(-100%);
}
.slide-enter-active,
.slide-leave-active {
transition: transform .5s;
}
.slide-enter,
.slide-leave-to {
transform: translateX(-100%);
}
}
</style>
- 使用
<GHideTransition :title="title">
...
</GHideTransition>
上下位移动画
translate(-50%, -100%) 是因为原本就有translateX(-50%), transform要合并使用
<!-- 上下位移动画 -->
<template>
<transition name="translate" appear>
<slot></slot>
</transition>
</template>
<script lang="ts" setup>
defineOptions({ name: 'l-transition1' })
</script>
<style lang="scss" scoped>
.translate-enter-active,
.translate-leave-active {
transition: transform 0.5s, opacity 0.5s; /* 添加 opacity 的过渡 */
}
.translate-enter-from,
.translate-leave-to {
transform: translate(-50%, -100%);
opacity: 0; /* 初始状态透明 */
}
.translate-enter-to,
.translate-leave-from {
transform: translate(-50%, 0%);
opacity: 1; /* 最终状态不透明 */
}
</style>