仅需三分钟,使用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操作的理解也进一步加深了,接下来可以任意定制想要的内容啦!

相关推荐
崔庆才丨静觅5 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60616 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了6 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅6 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅6 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅6 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment7 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅7 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊7 小时前
jwt介绍
前端
爱敲代码的小鱼7 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax