Vue3 封装优雅的全局状态提示组件(StatusMessage)

前言

在前端项目开发中,全局状态提示(如操作成功、失败提示)是高频交互需求。市面上的 UI 库(Element Plus、Ant Design Vue)虽提供了 Message 组件,但为了适配项目视觉风格、实现更轻量的定制化,手动封装全局提示组件会更灵活。本文基于 Vue3 组合式 API,手把手教你封装一个支持自定义类型、自动关闭、消息堆叠的全局 StatusMessage 组件。

一、组件设计思路

核心功能需求

  • 支持「成功 / 失败」两种基础提示类型,可扩展更多类型
  • 自定义提示显示时长,支持鼠标悬浮暂停自动关闭
  • 多个提示消息自动堆叠,计算偏移量避免重叠
  • 全局调用(无需在组件内注册,直接方法调用)
  • 轻量无冗余,适配项目自定义样式

技术方案

  • 利用 Vue3 的createVNode+render手动创建组件实例,实现全局调用
  • 维护消息队列,管理多消息的显示、关闭与位置计算
  • 封装快捷方法(success/error),简化调用逻辑
  • 结合Teleport将组件挂载到body,避免样式隔离问题

二、实现步骤

1. 编写提示组件本体(StatusMessage.vue)

先实现提示组件的 UI 结构、样式和基础交互,包括类型样式、过渡动画、自动关闭逻辑。

javascript 复制代码
<template>
  <teleport to="body">
    <div 
      v-if="visible" 
      :id="id"
      :class="['status-message', `status-message--${type}`]"
      :style="{ top: `${top}px`, ...transitionStyles }"
      @mouseenter="clearTimer"
      @mouseleave="startTimer"
    >
      <!-- 图标可替换为项目自有图标库 -->
      <i :class="iconClass"></i>
      <span class="status-message__text">{{ text }}</span>
    </div>
  </teleport>
</template>

<script setup>
import { ref, onMounted, onUnmounted, computed } from 'vue'

// 接收外部传入的属性
const props = defineProps({
  id: {
    type: String,
    required: true
  },
  top: {
    type: Number,
    default: 20 // 基础顶部偏移
  }
})

// 触发关闭事件,通知队列移除当前消息
const emit = defineEmits(['close'])

// 组件内部状态
const visible = ref(false)
const text = ref('')
const type = ref('success')
const duration = ref(3000)
let timer = null

// 不同类型的图标类名(可替换为svg/iconfont)
const iconClass = computed(() => {
  return type.value === 'success' 
    ? 'iconfont icon-success' 
    : 'iconfont icon-error'
})

// 过渡动画样式
const transitionStyles = computed(() => {
  return {
    transition: 'all 0.3s ease',
    opacity: visible.value ? 1 : 0,
    transform: visible.value ? 'translateX(-50%)' : 'translateX(-50%) translateY(-10px)',
    zIndex: 9999
  }
})

/**
 * 外部调用的显示方法
 * @param {Object} config 提示配置
 */
const showStatusMessage = (config) => {
  text.value = config.text
  type.value = config.type || 'success'
  duration.value = config.duration || 3000
  visible.value = true
  // 启动自动关闭定时器
  startTimer()
}

// 启动自动关闭定时器
const startTimer = () => {
  clearTimer()
  if (duration.value > 0) {
    timer = setTimeout(() => close(), duration.value)
  }
}

// 清除定时器(鼠标悬浮时调用)
const clearTimer = () => {
  timer && clearTimeout(timer)
}

// 关闭提示(带动画过渡)
const close = () => {
  visible.value = false
  // 动画结束后通知队列移除当前消息
  setTimeout(() => emit('close', props.id), 300)
}

// 暴露方法给外部调用
defineExpose({ showStatusMessage })

// 组件卸载时清除定时器,避免内存泄漏
onUnmounted(() => clearTimer())
</script>

<style scoped>
.status-message {
  position: fixed;
  left: 50%;
  transform: translateX(-50%);
  padding: 8px 16px;
  border-radius: 4px;
  color: #fff;
  display: flex;
  align-items: center;
  gap: 8px;
  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}

/* 成功类型样式 */
.status-message--success {
  background-color: #67c23a;
}

/* 失败类型样式 */
.status-message--error {
  background-color: #f56c6c;
}

.status-message__text {
  font-size: 14px;
  line-height: 1.4;
}
</style>

2. 封装全局调用方法(StatusMessage.js)

核心逻辑:创建组件实例、管理消息队列、计算偏移量、封装快捷调用方法。

javascript 复制代码
import { createVNode, render, nextTick } from 'vue'
import StatusMessageVue from '@renderer/components/StatusMessage.vue'

// 创建挂载容器,避免重复挂载
const container = document.createElement('div')
document.body.appendChild(container)

// 消息队列:管理所有显示中的提示实例
let messageQueue = []
// 消息ID生成器:确保每个消息唯一
let messageId = 0

/**
 * 计算消息偏移量,实现堆叠效果
 * 每个消息高度30px + 间距16px,基础顶部偏移20px
 */
const calculateOffsets = () => {
  messageQueue.forEach(({ vm }, index) => {
    vm.props.top = index * 46 + 20 // 30(高度) + 16(间距) = 46
  })
}

/**
 * 显示提示消息
 * @param {Object|string} options - 消息配置或纯文本
 * @param {string} options.text - 消息内容
 * @param {string} [options.type='success'] - 类型:success/error
 * @param {number} [options.duration=3000] - 显示时长(ms)
 */
const showMessage = async (options) => {
  // 生成唯一ID
  const id = `message_${messageId++}`

  // 处理参数:支持字符串快捷调用
  const config = typeof options === 'string'
    ? { text: options, type: 'success' }
    : { type: 'success', duration: 3000, ...options }

  // 创建组件虚拟节点
  const vnode = createVNode(StatusMessageVue, {
    id,
    // 关闭时从队列移除并重新计算偏移
    onClose: (closedId) => {
      messageQueue = messageQueue.filter(item => item.id !== closedId)
      calculateOffsets() // 重新计算剩余消息的偏移
    }
  })

  // 渲染组件到容器
  render(vnode, container)
  // 等待DOM渲染完成
  await nextTick()

  // 将实例加入队列
  messageQueue.push({
    id,
    vm: vnode.component,
    close: () => vnode.component.emitter.emit('close', id)
  })

  // 计算当前消息的偏移量
  calculateOffsets()

  // 调用组件内部方法显示消息
  vnode.component.exposed.showStatusMessage(config)
}

// 封装快捷方法:成功提示
showMessage.success = (text, duration) => {
  showMessage({ text, type: 'success', duration })
}

// 封装快捷方法:失败提示
showMessage.error = (text, duration) => {
  showMessage({ text, type: 'error', duration })
}

// 扩展:关闭所有提示
showMessage.closeAll = () => {
  messageQueue.forEach(item => item.close())
  messageQueue = []
}

export default showMessage


//使用方法
// showMessage.success('操作成功')
// showMessage.error('操作失败')
// showMessage({ text: '操作成功', type: 'success', duration: 5000 })
// showMessage({ text: '操作失败', type: 'error', duration: 5000 })

三、使用方法

1. 全局挂载(可选)

main.js中挂载到 Vue 原型,方便所有组件调用:

javascript 复制代码
import { createApp } from 'vue'
import App from './App.vue'
import showMessage from './utils/StatusMessage'

const app = createApp(App)
// 全局挂载
app.config.globalProperties.$message = showMessage
app.mount('#app')

2. 组件内调用

选项式 API
javascript 复制代码
// 成功提示
this.$message.success('操作成功!')

// 失败提示(自定义时长)
this.$message.error('操作失败,请重试', 5000)

// 自定义配置
this.$message({
  text: '自定义提示',
  type: 'error',
  duration: 10000
})
组合式 API
javascript 复制代码
import showMessage from '@/utils/StatusMessage'

// 成功提示
showMessage.success('操作成功!')

// 关闭所有提示
showMessage.closeAll()
相关推荐
leoZ23115 小时前
Claude 驱动的全栈开发:Spring Boot + Vue 的 BS 架构实践
vue.js·spring boot·架构
荒诞英雄19 小时前
从 17MB Vendor 大包到按需加载:Vite 多入口 Vue 组件库首屏优化实践
vue.js·vite
jsonbro19 小时前
手把手带你实现发布订阅模式
前端·vue.js·设计模式
Cobyte21 小时前
23.Vue Vapor 组件 attrs 的实现
前端·javascript·vue.js
斯特凡今天也很帅1 天前
xml页面可以打开,不报错,但是显示数据的地方也不显示,我是怎么解决的
xml·vue.js
柯克七七1 天前
给老项目加了 TypeScript,本来只想自己爽,结果全公司代码审查标准被我抬高了
前端·vue.js·typescript
自然 醒1 天前
前端如何实现在线预览office常见文件功能?
前端·vue.js
肉肉不吃 肉1 天前
前端调试跨域如何解决
前端·vue.js
卓怡学长1 天前
w268基于springboot + vue 物流系统
java·数据库·vue.js·spring boot·spring·intellij-idea
2601_962851741 天前
计算机毕业设计之基于SSM的网络安全课程资源分享平台
vue.js·算法·安全·web安全·yolo·目标检测·课程设计