vue Transition + animate 实现动画组件封装

160 阅读6分钟

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组件 封装一个带收起按钮和动画的卡片容器

image.png

image.png

  <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>

上下位移动画

image.png

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>