前言
在前端项目开发中,全局状态提示(如操作成功、失败提示)是高频交互需求。市面上的 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()