Vue3 修改 Element-Plus 对话框 el-dialog 默认的动画效果 (Ant Design modal 动画风格)

6,065 阅读4分钟

前言

我们先来看一段目前两大 UI 框架中对话框的动画效果

  1. Element-Plus,等于没有动画效果

element-plus.gif

  1. Ant-Design,一段不是那么复杂的动画往往能提升观看体验

ant-design.gif

3. 更改el-dialog动画,效果预览

new-el.gif

一、动画原理

通过阅读ant-design源码,发现他们使用了transform-origin这个属性轻松实现了对话框的缩放位移效果。

  1. 我们可以先简单定义一个block元素

    <template>
        <div class="block"></div>
    </template>
    
    
    <style>
    .block {
      width: 100px;
      height: 100px;
      border-radius: 8px;
      background-color: #409eff;
    }
    </style>
    
    

    b1.png

  2. 我们给这个块块实现一个简单的缩放动画,在样式中添加

    .block {
      width: 100px;
      height: 100px;
      border-radius: 8px;
      background-color: #409eff;
      animation: scale 1s ease infinite; // 定义一个无限循环的动画
    }
    // 新增一个缩放动画
    @keyframes scale {
      0% {
        opacity: 0;
        transform: scale3d(0, 0, 1);
      }
      100% {
        opacity: 1;
        transform: scale3d(1, 1, 1);
      }
    }
    

    此时的效果: b2.gif

  3. 最重要的一步来了,对于transform-origin这个属性,默认值为transform-origin:50% 50% 0三个值分别对应着水平、垂直和z轴,默认值其实就是中心点!我们修改这个默认值为200px 200px

    .block {
      width: 100px;
      height: 100px;
      border-radius: 8px;
      background-color: #409eff;
      animation: scale 1s ease infinite; // 定义一个无限循环的动画
      transform-origin: 200px 200px 0; // 新增代码
    }
    // 新增一个缩放动画
    @keyframes scale {
      0% {
        opacity: 0;
        transform: scale3d(0, 0, 1);
      }
      100% {
        opacity: 1;
        transform: scale3d(1, 1, 1);
      }
    }
    

    这样,你就可以感受到其中的奥妙了 GIF 2022-2-9 16-03-46.gif

二、过程分析

承上所述,我们的步骤为
鼠标点击按钮 -> 对话框从鼠标点击的位置弹出
对话框关闭 -> 对话框回到鼠标点击的位置

画了一张简单的图 图解.png

  1. 首先,当对话框弹出时,我们需要获取到鼠标点击的位置(e.x,e.y),以及对话框距离可视区域的距离offsetLeftoffsetTop
  2. 我们需要的水平垂直偏移量是一个相对距离,相对于当前元素原点的位置,所以存在负值
  3. 计算得出,图中的蓝色线条部分
    const transformLeft = e.x - offsetLeft; // 水平偏移
    const transformTop = e.x - offetTop; // 垂直偏移
    

三、代码实现

  1. 获取鼠标点击位置,分析一下点击事件的处理,点击按钮后弹出对话框,对于对话框我们肯定已经在按钮上绑定了一个click事件用于点击事件处理逻辑,我们可以给window再添加一个点击事件处理
    // 保存鼠标点击位置
    const mousePosition = {
      x: 0,
      y: 0,
    };
    
    /**
     * @description: 点击事件回调
     * @param {MouseEvent} e
     */
    const clickHandler = (e: MouseEvent) => {
      mousePosition.x = e.x;
      mousePosition.y = e.y;
    };
    
    document.documentElement.addEventListener("click", clickHandler, true); // 事件捕获
    
    要点来了,我们要确保这个点击事件比弹出对话框按钮的点击事件先执行,因此,在addEvenetListener这个方法中,第三个参数的意思为,在捕获阶段执行。
    我们通过鼠标点击按钮,那么浏览器对于事件处理的流程为捕获 -> 冒泡,我们为按钮添加的点击事件默认在冒泡阶段执行,那么上面的事件处理我们希望早于按钮事件,也就是捕获阶段就执行。
  2. 在对话框显示的时候,设置transform-origin实现位移动画
    <template>
      <el-button type="primary" @click="visible = true"> element plus</el-button>
      <el-dialog v-model="visible" :title="'对话框'" :width="500" ref="modalRef">
        <h1>Element-Plus 对话框</h1>
        <h1>Element-Plus 对话框</h1>
        <template #footer>
          <el-button @click="visible = false">cancel</el-button>
          <el-button type="primary" @click="visible = false">ok</el-button>
        </template>
      </el-dialog>
    </template>
    
    <script setup lang="ts">
    import { watch, ref } from "vue";
    
    const modalRef =ref<any>();
    const visible = ref(false);
    
    watch(
      () => visible.value,
      (value) => {
        if (value) { 
          const node: HTMLElement = modalRef.value?.dialogRef; // 这里是获取到el-dialog的dom节点
          console.log(modalRef); // 可以自己打印一下这个ref
          const computedStyle = getComputedStyle(node); // 样式集合
          let width; // 对话框宽度
          if (/px/g.test(computedStyle.width)) {
            // 如果宽度是像素类型
            // 正则替换像素
            width = Number(computedStyle.width.replace(/px/g, ""));
          } else {
            // 宽度为百分比类型
            // 正则替换百分比并转化为数字格式
            width = document.documentElement.clientWidth * (Number(computedStyle.width.replace(/%/g, "")) / 100);
          }
          const top = computedStyle.marginTop.replace(/px/g, ""); // 对话框距离顶部的距离
          // 计算变换偏移
          // element-plus 中,对话框默认是居中的
          const transformLeft = triggerPosition.x - (document.documentElement.clientWidth - width) / 2; // 本质上为对话框左上角的 x 距离触发点 x 的距离
          const transformTop = triggerPosition.y - Number(top); // 本质上为对话框左上角的 y 距离触发点 y 的距离
          node.style.transformOrigin = `${transformLeft}px ${transformTop}px`;
        }
      }
    );
    </script>
    

四、覆盖element-plus原有动画

如果仅仅只是想得到一个缩放效果,这里就足够了

  1. 通过阅读 element-plus 源码得知,内部利用vuetransition组件实现动画,类名为dialog-fade

  2. 我们首先定义几个动画,对应对话框的出现消失动画

    /* animation.scss */
    
    @keyframes dialog-open {
      0% {
        opacity: 0;
        transform: scale(0.2);
      }
      100% {
        opacity: 1;
        transform: scale(1);
      }
    }
    
    @keyframes dialog-close {
      0% {
        opacity: 1;
        transform: scale(1);
      }
      100% {
        opacity: 0;
        transform: scale(0.2);
      }
    }
    
    // 遮罩层动画
    @keyframes fade-out {
      0% {
        opacity: 1;
      }
      100% {
        opacity: 0;
      }
    }
    
  3. 然后覆盖element-plus原有的动画,大功告成!

    /* transition.scss */
    
    .dialog-fade-enter-active {
      .el-dialog {
        animation: dialog-open 0.3s cubic-bezier(0.32, 0.14, 0.15, 0.86);
      }
    }
    
    .dialog-fade-leave-active {
      animation: fade-out 0.2s linear;
      .el-dialog {
        animation: dialog-close 0.2s cubic-bezier(0.78, 0.14, 0.15, 0.86);
      }
    }
    
    

功能拓展

1. 组件封装(代码地址)

对话框是中后台应用中常用的业务组件,因此我们将其可以封装成组件。
代码仓库 -> 传送门

2. 拖拽

element-plus 已经发布正式版2.0,他们为对话框添加了拖拽效果,不巧的是他们使用了transform来实现对话框的移动,这会与本文中的transform-origin冲突,因此本文中的设置方法不能将draggble设置为true,我会在下一篇文章中介绍如何用vue3的自定义指令实现拖拽


本帖未经允许禁止转载