先贴下 Element-Plus ElMessage 源码的网址。我们需要实现的效果类似 ElMessage,即能够显示多个消息、上一个消息消失下面的消息会自动往上移动、进入移出动画、自定义消息和持续时间。其他选项这里不考虑。

大体思路
我们的 ElMessage 组件通过函数调用动态显示,无需事先在页面中放入组件,这样能更灵活也更方便。实现的基本思路是使用函数动态渲染组件到页面上。

createVNode 和 render
实际上 createVNodeh 函数的别名,这两个函数的功能是创建一个 VNode,这个 VNode 可以理解为是 DOM 的描述,当我们要渲染 VNode 到真正的页面时,就要用到 render 函数。我们可以封装一个 MessageBox.vue 的组件,然后通过 createVNode 创建对应组件的 VNode,用 render 把组件渲染到页面上。

message.vue

<template>
  <transition name="fade" @before-leave="onClose" @after-leave="onDestroy">
    <div :style="{ top: topOffset + 'px' }" class="message" v-if="visiable">
      {{ message }}
    </div>
  </transition>
</template>

<script>
export default {
  props: {
    topOffset: {
      type: Number,
      required: true,
    },
    message: {
      type: String,
      required: true,
    },
    duration: {
      type: Number,
      default: 3000,
    },
    id: {
      type: Number,
      required: true,
    },
    onDestroy: {
      type: Function,
      required: true,
    },
    onClose: {
      type: Function,
      required: true,
    },
  },
  data() {
    return {
      visiable: false,
    };
  },
  methods: {
    close() {
      this.visiable = false;
    },
  },
  mounted() {
    this.visiable = true;
    setTimeout(() => {
      this.close();
    }, this.duration);
  },
};
</script>

<style scoped>
.message {
  position: fixed;
  left: 50%;
  transform: translate(-50%, 10px);
  background-color: burlywood;
  padding: 0.5rem;
  width: 300px;
  z-index: 10001;
  transition: top 0.4s;
}

.fade-enter-active,
.fade-leave-active {
  transition: transform 0.4s, opacity 0.4s;
}

.fade-enter-from {
  transform: translate(-50%, 0);
  opacity: 0;
}


.fade-leave-to {
  transform: translate(-50%, -10px) !important;
  opacity: 0;
}
</style>

组件的基本样式这里不进行讲解,我们把重点放到 transition 动画上面。

v-enter-active、v-leave-active
这两个属性在整个动画期间都会生效,一般用来写动画的过渡属性 transition

v-enter-to、v-leave-to
在进入过渡触发前、离开过渡被触发后生效。

v-enter-from、v-leave-from
定义进入、离开过渡的初始状态。

在 message 进入时我们想显示一个 message 向下移动的动画,于是给 message 设置了默认的 translateY(10px)。而 v-enter-from 触发后,translateY(0) 生效,下一帧被移除,于是会执行 translateY(0px) -> translateY(10px) 的动画,达到效果。

显示元素 message.js

import {
  remove
} from '@vue/shared';
import {
  createVNode,
  render
} from 'vue'
import MessageBox from '../components/MessageBox.vue'

let seed = 0;
const instance = [];
const appendTo = document.body;

export default function (message, duration) {
  let topOffset = 20;
  const container = document.createElement('div');

  // 计算当前元素距离顶部的偏移量
  instance.forEach(vm => {
    topOffset += (vm.el.offsetHeight || 0) + 16;
  });

  // 保存 id;这行代码是必要的,因为当关闭定时器触发的时候,seed 的值为最后一次增加的值,不保存直接用 seed 会出错
  const id = seed;

  const vm = createVNode(MessageBox, {
    id,
    message,
    duration,
    topOffset,
    // 组件销毁时触发的回调
    onDestroy() {
      render(null, container);
      container.remove();
    },
    // 组件关闭时触发的回调
    // 这个回调用于显示组件的移出动画,和 onDestroy 不冲突
    onClose() {
      close(id)
    }
  })

  // 渲染组件到 container 上
  render(vm, container);
  // 添加 container 到 body
  appendTo.appendChild(container);
  // 保存组件实例,销毁时会用到
  instance.push(vm);

  seed++;
}

const close = (id) => {
  const idx = instance.findIndex(vm => vm.props.id === id);

  if (idx === -1) {
    return;
  }

  const vm = instance[idx];
  const removedHeight = vm.el.offsetHeight;
  instance.splice(idx, 1);

  for (let i = idx; i < instance.length; i++) {
    // 直接赋值组件的 top 为减去移除组件后的高度
    const pos = parseInt(instance[i].el.style['top']) - removedHeight - 16;
    // topOffset = topOffset - removedHeight - 16 同理
    instance[i].component.props.topOffset = pos;
  }
}

App.vue 测试显示组件

<script setup>
import message from "./hooks/useMessageBox";
import { ref } from "vue";

const count = ref(1);
</script>

<template>
  <button @click="message('lala' + count, 2000)">
    show message box
  </button>
</template>

<style></style>```
Logo

前往低代码交流专区

更多推荐