仅需三分钟,使用Vue3.x版本组件式风格实现一个消息提示组件!

一、前言

在日常的前端项目开发中,我们时常需要使用到"消息提示"(以下简称"消息")这个组件来帮助我们更好的给予用户提示,例如常见的"登录成功"、"操作成功"、"服务器异常"等等提示。

尽管市面上已经有一些组件库提供了这样的组件,例如国产的Element-Plus(如下图)。但他们还是有一个缺点,即如果我仅仅需要消息提示这一组件,那么引入一个大型的组件库是完全多余的,对整个项目来说,也会使其依赖体积过分庞大。

此外,还需要考虑到用户自定义的需求。例如,我可能并不希望我的"消息"组件和别人的千篇一律,那么学习如何定制消息组件显然是很有必要的。

那么,如何使用Vue3.x版本的组件式开发风格配合TypeScript来实现这样的功能呢?以下,我将做一个简单的演示,需要注意依赖的版本问题。

二、分析消息组件的需求

在正式的实现一个消息组件之前,我们先要思考这个组件需要哪些功能,这部分我整理如下:

  1. 在页面顶部中心位置弹出消息通知框,并且能够设置停留在页面上的时间。
  2. 消息通知框有不同的类型,例如警告、错误、、通知、成功。
  3. 当多个消息通知框同时展示时,新出现的消息通知框应该在原有的下方展示,并且当原有的消息通知消失时,能够自动更新位置。

三、消息组件的实现

首先,创建一个Vue3.x的项目。

(一)新建一个Message组件

以下是Message组件代码的一个示例,它支持自定义消息内容,并且能根据props中传入消息类型的不同而展示不同的图标和样式,同时引入了一个名为lucide的UI库,这个库相对轻量级,因此不必担心其占用问题。

唯一需要注意的是,下方的全局样式部分。它定义了两个动画帧,同时定义了一个名为message-fade-out的类,这部分主要是用于给消息组件做入场和出场动画的。

它不能被放入组件私有样式里,因为我们的消息组件后续会作为一个Vue APP挂载到一个HTML Element上,这时由于该元素属于消息组件的父级节点,组件内部样式会对其不起作用。

ts 复制代码
<template>
    <div class="message" :class="type">
        <span class="icon" v-if="showIcon">
            <component :is="iconComponent" />
        </span>
        <span class="content">{{ content }}</span>
    </div>
</template>

<script setup lang="ts">
import { computed } from 'vue';
import { Info, CheckCircle, AlertCircle, XCircle } from 'lucide-vue-next'; // 使用 lucide-vue-next 图标库

interface Props {
    content: string;
    type?: 'info' | 'success' | 'warning' | 'error';
    showIcon?: boolean;
}

const props = withDefaults(defineProps<Props>(), {
    type: 'info',
    showIcon: true,
});

// 根据类型选择图标
const iconComponent = computed(() => {
    switch (props.type) {
        case 'success':
            return CheckCircle;
        case 'warning':
            return AlertCircle;
        case 'error':
            return XCircle;
        default:
            return Info;
    }
});
</script>

<style scoped>
/* 组件的私有样式 */
.message {
    position: relative;
    left: 50%;
    transform: translateX(-50%);
    padding: 12px 20px;
    border-radius: 8px;
    color: white;
    z-index: 1000;
    display: flex;
    align-items: center;
    gap: 10px;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
    opacity: 0;
    animation: fadeIn 0.3s ease forwards;
}

.message.info {
background-color: #3498db;
}

.message.success {
background-color: #2ecc71;
}

.message.warning {
background-color: #f1c40f;
}

.message.error {
background-color: #e74c3c;
}

.icon {
display: flex;
align-items: center;
}

.content {
flex: 1;
word-break: break-word;
}

</style>

<style>
.message-fade-out {
  animation: fadeOut 0.3s ease forwards !important;
}

@keyframes fadeIn {
  from { opacity: 0; transform: translateX(-50%) translateY(-20px); }
  to { opacity: 1; transform: translateX(-50%) translateY(0); }
}

@keyframes fadeOut {
  from { opacity: 1; transform: translateX(-50%) translateY(0); }
  to { opacity: 0; transform: translateX(-50%) translateY(-20px); }
}
</style>

(二)新建一个message.ts的文件

其主要用于实现消息组件的逻辑,同时对外提供消息方法。主要实现思路如下:

1.首先定义一个消息对象和一个消息队列,消息对象由idapp对象和html元素构成。之所以这么做,是因为我们的消息组件需要作为Vue App挂载到一个具体的元素上,他们具备一一对应的关系。这样维护一个消息队列,可以更方便地计算多个消息展示时具体的高度。

2.然后编写展示消息的逻辑:

(1)对于初次使用消息组件的情况,先新建一个外层容器div,这么做可以更好的控制消息组件的样式。

(2)创建一个消息div,添加到上述容器的子节点中,接着消息组件作为APP挂载到该元素上。

(3)更新消息位置,并设置一个定时器,使其到达指定时间后触发隐藏消息的方法。

3.消息隐藏方法的逻辑较为简单,核心为给消息挂载的el元素上添加渐隐的class,使其触发动画效果,并通过侦听器,让其在动画结束后从DOM中移除。

4.更新消息高度的方法,这部分核心是动态地根据队列中消息的index来给其分配合适的高度。

5.最后,将四种类型的消息方法导出。

ts 复制代码
import { createApp } from 'vue';
import type { App } from 'vue';
import Message from '@/components/Message.vue';

type MessageType = 'info' | 'success' | 'warning' | 'error';

interface MessageInstance {
  id: number;
  app: App<Element>;
  el: HTMLElement;
}

const messageQueue: MessageInstance[] = [];
let messageContainer: HTMLElement | null = null;
let messageId = 0;

// 动态计算消息位置
function updateMessagePositions() {
  let currentTop = 20; // 初始顶部距离
  messageQueue.forEach((msg) => {
    const el = msg.el;
    const height = el.offsetHeight; // 获取实际高度
    el.style.top = `${currentTop}px`;
    currentTop += height + 10; // 累加高度和间隙
  });
}

function showMessage(content: string, type: MessageType = 'info', duration: number = 3000) {
  if (!messageContainer) {
    messageContainer = document.createElement('div');
    messageContainer.style.position = 'fixed';
    messageContainer.style.top = '0';
    messageContainer.style.left = '0';
    messageContainer.style.width = '100%';
    messageContainer.style.pointerEvents = 'none'; // 防止拦截点击事件
    document.body.appendChild(messageContainer);
  }

  const id = messageId++;
  const el = document.createElement('div');
  el.style.position = 'absolute';
  el.style.left = '50%';
  el.style.transform = 'translateX(-50%)';
  el.style.transition = 'top 0.8s ease';//添加过渡,这样当一个消息消失时,其他消息的高度变化会有过渡效果。

  const messageApp = createApp(Message, { content, type });
  const messageInstance: MessageInstance = { id, app: messageApp, el };

  messageQueue.push(messageInstance);
  messageContainer.appendChild(el);
  messageApp.mount(el);

  // 等待 DOM 更新后计算位置
  updateMessagePositions()

  // 自动隐藏
  setTimeout(() => hideMessage(id), duration);
}

function hideMessage(id: number) {
    const index = messageQueue.findIndex((msg) => msg.id === id);
    if (index === -1) return;
  
    const [messageInstance] = messageQueue.splice(index, 1);
    const el = messageInstance.el;
  
    // 添加淡出动画类
    el.classList.add('message-fade-out');
  
    // 动画结束后移除元素
    const onAnimationEnd = () => {
      el.removeEventListener('animationend', onAnimationEnd);
      messageInstance.app.unmount();
      el.remove();
      updateMessagePositions();
    };
  
    el.addEventListener('animationend', onAnimationEnd, { once: true });
  }


export default {
  info(content: string, duration?: number) {
    showMessage(content, 'info', duration);
  },
  success(content: string, duration?: number) {
    showMessage(content, 'success', duration);
  },
  warning(content: string, duration?: number) {
    showMessage(content, 'warning', duration);
  },
  error(content: string, duration?: number) {
    showMessage(content, 'error', duration);
  },
};

(三)测试消息组件

我们可以任意新建一个新的组件,并通过按钮触发消息通知,例如:

ts 复制代码
const handleClick = () => {
  message.info('这是一个比较长的句子字字字字字......', 5000)
}

页面上展示效果如下:

四、总结

恭喜你!顺利看到这里,想必已经掌握了如何自主实现一个消息组件。在这个过程中,相信你对DOM操作的理解也进一步加深了,接下来可以任意定制想要的内容啦!

相关推荐
你的人类朋友24 分钟前
浅谈Object.prototype.hasOwnProperty.call(a, b)
javascript·后端·node.js
Mintopia30 分钟前
深入理解 Three.js 中的 Mesh:构建 3D 世界的基石
前端·javascript·three.js
打瞌睡de喵32 分钟前
JavaScript 空对象检测
javascript
前端太佬34 分钟前
暂时性死区(Temporal Dead Zone, TDZ)
前端·javascript·node.js
Mintopia36 分钟前
Node.js 中 http.createServer API 详解
前端·javascript·node.js
艾克马斯奎普特40 分钟前
Vue.js 3 渐进式实现之响应式系统——第三节:建立副作用函数与被操作字段之间的联系
javascript·vue.js
xRainco41 分钟前
Redux从简单到进阶(Redux、React-redux、Redux-toolkit)
前端
印第安老斑鸠啊42 分钟前
由一次CI流水线失败引发的对各类构建工具的思考
前端
CodePencil44 分钟前
CSS专题之外边距重叠
前端·css
hepherd1 小时前
Flask学习笔记 - 表单
前端·flask