全局提示 Message
全局展示操作反馈信息
何时使用
- 可提供成功、警告和错误等反馈信息
- 顶部居中显示并自动消失,是一种不打断用户操作的轻量级提示方式
提示
Message 推荐在组件 setup 内通过 useMessage() 调用,使用前需在应用根节点放置一次 <MessageProvider>。
如果你想在 setup 外使用(例如 axios 拦截器、路由守卫、Pinia action 等),请参考文档末尾的 在 setup 外使用。
使用方式
| 调用方式 | API | 适用位置 |
|---|---|---|
| 组件树内调用 推荐 | useMessage() | 组件 setup 内,需外层存在 <MessageProvider> |
| 脱离组件树调用 无需 MessageProvider | createDiscreteApi() | 任意位置(axios 拦截器、路由守卫、Pinia action 等) |
组件树内使用:useMessage() 推荐
适用于组件内部调用:先在应用根节点放置一次 <MessageProvider>,之后任意层级组件均可通过 useMessage() 取得同一实例
关于 MessageProvider 与 Message
MessageProvider内部渲染一个Message组件,并通过provide/inject向下提供useMessage()所需的 API,自身不渲染任何可见内容- 组件级配置属性(
duration/top/maxCount/keepAliveOnHover/to)会透传给内部的Message,因此直接参考下方 Message Props 设置即可 - 使用
useMessage()时,组件级配置设置在<MessageProvider>上(无法直接接触内部Message);每条消息的个性化配置(content/icon/class/style等)则在调用open/info等方法时作为参数传入,参考 MessageOptions Type
1. 在应用根节点放置 MessageProvider
App.vue
<script setup lang="ts">
import { MessageProvider } from 'vue-amazing-ui'
</script>
<template>
<MessageProvider>
<RouterView />
</MessageProvider>
</template>2. 在任意层级组件中调用 useMessage()
XXX.vue
<script setup lang="ts">
import { useMessage } from 'vue-amazing-ui'
const message = useMessage()
function onClick() {
message.success('点击了按钮')
}
</script>
<template>
<Button @click="onClick">按钮</Button>
</template>本文档网站已在主题层全局包裹
<MessageProvider>,以下演示均通过useMessage()获取实例(与真实项目用法一致);需要不同组件级配置的演示,均通过页面内局部嵌套<MessageProvider>实现。
基本使用
Show Code
<script setup lang="ts">
import { useMessage } from 'vue-amazing-ui'
const message = useMessage()
function onOpen(content: string) {
message.open(content) // open 调用
}
</script>
<template>
<Button type="primary" @click="onOpen('This is a normal message')">Open</Button>
</template>不同类型的全局提示
Show Code
<script setup lang="ts">
import { useMessage } from 'vue-amazing-ui'
const message = useMessage()
function onInfo(content: string) {
message.info(content) // info 调用
}
function onSuccess(content: string) {
message.success(content) // success 调用
}
function onError(content: string) {
message.error(content) // error 调用
}
function onWarning(content: string) {
message.warning(content) // warning 调用
}
function onLoading(content: string) {
message.loading(content) // loading 调用
}
</script>
<template>
<Space>
<Button type="primary" @click="onInfo('This is an info message')">Info</Button>
<Button type="primary" @click="onSuccess('This is a success message')">Success</Button>
<Button type="primary" @click="onError('This is an error message')">Error</Button>
<Button type="primary" @click="onWarning('This is a warning message')">Warning</Button>
<Button type="primary" @click="onLoading('This is a loading message')">Loading</Button>
</Space>
</template>自定义图标
Show Code
<script setup lang="ts">
import { h } from 'vue'
import { SoundFilled, FireFilled } from '@ant-design/icons-vue'
import { useMessage } from 'vue-amazing-ui'
const message = useMessage()
function onInfoIcon() {
message.info({
content: 'This is an info message with a custom icon',
icon: h(SoundFilled)
})
}
function onOpenIcon() {
message.open({
content: 'This is an open message with a custom icon',
icon: h(FireFilled, { style: 'color: gold' })
})
}
// icon 也支持渲染函数形态,每次渲染时动态生成图标
function onIconRenderFn() {
message.success({
content: 'icon 渲染函数:每次渲染时动态生成图标',
icon: () => h(SoundFilled, { style: 'color: #52c41a' })
})
}
</script>
<template>
<Space>
<Button type="primary" @click="onInfoIcon">自定义 Info 图标</Button>
<Button type="primary" @click="onOpenIcon">自定义 Open 图标</Button>
<Button type="primary" @click="onIconRenderFn">图标渲染函数</Button>
</Space>
</template>自定义样式
Show Code
<script setup lang="ts">
import { h } from 'vue'
import { SoundFilled, FireFilled } from '@ant-design/icons-vue'
import { useMessage } from 'vue-amazing-ui'
const message = useMessage()
function onCustomClass(content: string) {
message.info({
content: 'This is a custom class message',
icon: h(SoundFilled),
class: 'custom-message-class'
})
}
function onCustomStyle(content: string) {
message.warning({
content: 'This is a custom style message',
icon: h(FireFilled),
style: {
color: '#f50'
}
})
}
</script>
<template>
<Space>
<Button type="primary" @click="onCustomClass">自定义 class</Button>
<Button type="primary" @click="onCustomStyle">自定义 style</Button>
</Space>
</template>
<style lang="less">
// 消息通过 Teleport 挂载到 body 下,scoped 样式无法命中,需使用全局样式
.custom-message-class {
color: #ff6900;
}
</style>自定义关闭延时
Show Code
<script setup lang="ts">
import { useMessage } from 'vue-amazing-ui'
const message = useMessage()
// 自动关闭
function onAutoClose() {
message.info({
content: 'This message will automatically turn off after 3 seconds.',
onClose: () => {
// onClose 回调:消息关闭后给出可见反馈
message.success('onClose 回调触发: 上一条消息已自动关闭')
}
})
}
// 不自动关闭(duration: null),点击消息时通过句柄手动关闭(onClick 回调)
function onNeverAutoClose() {
const handle = message.info({
content: 'This message will not automatically turn off. Click it to close.',
duration: null,
onClick: () => handle.destroy()
})
}
</script>
<template>
<Space>
<Button type="primary" @click="onAutoClose">3s 后自动关闭</Button>
<Button type="primary" @click="onNeverAutoClose">常驻消息,点击关闭</Button>
</Space>
</template>复杂内容
content 支持三种形态:纯文本、VNode 与返回 VNode 的渲染函数
Show Code
<script setup lang="ts">
import { h } from 'vue'
import { useMessage } from 'vue-amazing-ui'
const message = useMessage()
function onVNodeContent() {
message.info({
content: h('span', { style: 'color: #1677ff; font-weight: 600' }, '这是一条 VNode 渲染的富文本内容')
})
}
function onRenderFnContent() {
message.info({
content: () => h('span', { style: 'color: #52c41a; font-weight: 600' }, '这是一条渲染函数动态生成的内容')
})
}
</script>
<template>
<Space>
<Button type="primary" @click="onVNodeContent">VNode 内容</Button>
<Button type="primary" @click="onRenderFnContent">渲染函数内容</Button>
</Space>
</template>手动关闭
Show Code
<script setup lang="ts">
import { useMessage } from 'vue-amazing-ui'
import type { MessageReactive } from 'vue-amazing-ui'
const message = useMessage()
let messageReactive: MessageReactive | null = null
function onOpenMessage() {
if (!messageReactive) {
messageReactive = message.info({
content: '这是一条消息(duration: null),可通过手动关闭',
duration: null
})
}
}
function onDestroy() {
if (messageReactive) {
messageReactive.destroy()
messageReactive = null
}
}
</script>
<template>
<Space>
<Button type="primary" @click="onOpenMessage">打开</Button>
<Button @click="onDestroy">关闭</Button>
</Space>
</template>原地更新
Show Code
<script setup lang="ts">
import { h, onBeforeUnmount, ref } from 'vue'
import { useMessage } from 'vue-amazing-ui'
import type { MessageUpdate } from 'vue-amazing-ui'
const message = useMessage()
const updateTimers: ReturnType<typeof setInterval>[] = []
function onProgressUpload() {
const percent = ref(0)
const handle = message.loading({
// content 传渲染函数:内部引用响应式 percent,进度变化时消息内容自动更新
content: () =>
h('span', { style: 'white-space: nowrap' }, [
'上传中...',
h(
'span',
{
style: `
display: inline-block;
min-width: 4ch;
text-align: right;
font-variant-numeric: tabular-nums;
`
},
[h('span', { style: 'font-weight: 600; color: #1677ff' }, `${percent.value}%`)]
)
]),
duration: null
})
const timer = setInterval(() => {
percent.value += 2
if (percent.value >= 100) {
clearInterval(timer)
// update 支持 mode:原地把 loading 图标切换为 success,并重设时长自动关闭
const doneOptions: MessageUpdate = { content: '上传成功', mode: 'success', duration: 2000 }
handle.update(doneOptions)
}
}, 100)
updateTimers.push(timer)
}
onBeforeUnmount(() => {
updateTimers.forEach((timer) => {
clearInterval(timer)
})
})
</script>
<template>
<Button type="primary" @click="onProgressUpload">异步上传</Button>
</template>最大消息数
maxCount 为组件级配置,用于限制同时存在的消息数量,超出上限时淘汰最旧的一条(淘汰不触发 onClose)
Show Code
<script setup lang="ts">
import { ref } from 'vue'
import { MessageProvider } from 'vue-amazing-ui'
import type { MessageApi } from 'vue-amazing-ui'
// 组件级配置无法逐条传入,通过局部 <MessageProvider> 演示,@ready 可取到该 Provider 作用域内的 api
const maxCountMessage = ref<MessageApi>()
let maxCountSeed = 0
function onMaxCountMessage() {
maxCountSeed += 1
maxCountMessage.value?.info({
content: `第 ${maxCountSeed} 条消息(最多同时存在 3 条)`,
duration: null
})
}
</script>
<template>
<Space>
<Button type="primary" @click="onMaxCountMessage">打开消息(最多 3 条)</Button>
<Button @click="maxCountMessage?.destroyAll()">全部销毁</Button>
</Space>
<MessageProvider :max-count="3" @ready="maxCountMessage = $event" />
</template>鼠标移入保持显示
Show Code
<script setup lang="ts">
import { ref } from 'vue'
import { MessageProvider } from 'vue-amazing-ui'
import type { MessageApi } from 'vue-amazing-ui'
const hoverMessage = ref<MessageApi>()
const noHoverMessage = ref<MessageApi>()
function onHoverPauseMessage() {
hoverMessage.value?.info({
content: '鼠标移入时暂停计时,移出后重新计时 2s 关闭',
duration: 2000
})
}
function onHoverNoPauseMessage() {
noHoverMessage.value?.info({
content: '鼠标移入不暂停计时,2s 后照常关闭',
duration: 2000
})
}
</script>
<template>
<Space>
<Button type="primary" @click="onHoverPauseMessage">hover 暂停关闭</Button>
<Button type="primary" @click="onHoverNoPauseMessage">hover 不暂停关闭</Button>
</Space>
<!-- 两条消息来自 keepAliveOnHover 不同的 Provider,便于对比 hover 行为 -->
<MessageProvider :keep-alive-on-hover="true" @ready="hoverMessage = $event" />
<MessageProvider :keep-alive-on-hover="false" @ready="noHoverMessage = $event" />
</template>顶部位置
Show Code
<script setup lang="ts">
import { ref } from 'vue'
import { MessageProvider } from 'vue-amazing-ui'
import type { MessageApi } from 'vue-amazing-ui'
const topMessage = ref<MessageApi>()
function onTopMessage() {
topMessage.value?.info({
content: '这条消息距离页面顶部 100px',
duration: 2000
})
}
</script>
<template>
<MessageProvider :top="100" @ready="topMessage = $event" />
<Button type="primary" @click="onTopMessage">消息距顶部 100px</Button>
</template>自定义挂载容器
提示
Teleport 的目标必须已存在于文档中。
当目标与组件位于同一组件树内时,组件挂载瞬间目标尚未插入文档,需用 v-if 将组件延迟到挂载完成后再渲染。
Show Code
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { MessageProvider } from 'vue-amazing-ui'
import type { MessageApi } from 'vue-amazing-ui'
const toMessage = ref<MessageApi>()
const toReady = ref(false)
// 目标与组件位于同一组件树时,需等挂载完成(目标已插入文档)后再渲染组件
onMounted(() => {
toReady.value = true
})
function onToMessage() {
toMessage.value?.info('这条消息被挂载到当前虚线容器中')
}
</script>
<template>
<div id="message-to-container" class="message-to-container"></div>
<MessageProvider v-if="toReady" to="#message-to-container" :top="20" @ready="toMessage = $event" />
<Button type="primary" @click="onToMessage">挂载到指定容器</Button>
</template>
<style lang="less" scoped>
.message-to-container {
position: relative;
transform: translateZ(0); // 建立包含块,使内部 fixed 定位的消息相对该容器定位
height: 240px; // 高度按需设置,保证可容纳多条消息
margin-bottom: 10px;
border: 1px dashed #d9d9d9;
border-radius: 8px;
overflow: hidden; // 超出容器高度的消息被裁切,避免溢出到容器外
}
</style>APIs
Message
组件级配置属性(class / style 亦会透传到消息容器):使用 useMessage() 时设置在 <MessageProvider> 上(会透传给内部 Message),直接使用 <Message> 组件时设置在 <Message> 上,两者等价。
每条消息的个性化配置请参考 MessageOptions Type
| 参数 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| duration | 自动关闭的延时,单位 ms,设置 null 时,不自动关闭 | number | null | 3000 |
| top | 消息距离顶部的位置,单位 px | string | number | 30 |
| maxCount | 可同时存在的最大消息数,超出时淘汰最旧的一条 | number | undefined |
| keepAliveOnHover | 鼠标移入时是否暂停自动关闭 | boolean | true |
| to | 消息容器挂载的节点,可选:元素标签名(例如 'body')或者元素本身 | string | HTMLElement | 'body' |
组件上的
class/style透传到消息容器.message-wrap;单条消息的样式请用单条配置项中的class/style(落在该条消息自己的容器上)。
MessageOptions Type
调用时传入的 MessageOptions 类型(open / info / success / error / warning / loading 的参数),以下属性均具有更高优先级(覆盖组件级配置)
| 名称 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| content? | 提示内容 | string | VNode | (() => VNode) | undefined |
| icon? | 自定义图标 | VNode | (() => VNode) | undefined |
| duration? | 自动关闭的延时时长,单位 ms;设置 null 时,不自动关闭 | number | null | undefined |
| class? | 自定义类名 | string | undefined |
| style? | 自定义样式 | CSSProperties | undefined |
| onClick? | 点击 message 时的回调函数 | () => void | undefined |
| onClose? | 关闭时的回调函数 | () => void | undefined |
Methods
useMessage() 返回的 MessageApi,或通过 <Message> / <MessageProvider> 的 @ready 事件获取:
| 名称 | 说明 | 类型 |
|---|---|---|
| open | 基本全局提示 | (content: string | MessageOptions) => MessageReactive |
| info | 信息全局提示 | (content: string | MessageOptions) => MessageReactive |
| success | 成功全局提示 | (content: string | MessageOptions) => MessageReactive |
| error | 失败全局提示 | (content: string | MessageOptions) => MessageReactive |
| warning | 警告全局提示 | (content: string | MessageOptions) => MessageReactive |
| loading | 加载全局提示 | (content: string | MessageOptions) => MessageReactive |
| destroyAll | 销毁所有全局提示 | () => void |
MessageReactive Type
单条消息的句柄,由 open / info 等方法调用后返回:
| 名称 | 说明 | 类型 |
|---|---|---|
| key | 该条消息的唯一标识 | string |
| destroy | 关闭该条消息 | () => void |
| update | 更新该条消息,mode 可切换内置图标类型 | (options: MessageUpdate) => void |
MessageUpdate Type
update 可更新的字段为 MessageOptions 的全部属性,另支持 mode 用于切换内置图标类型(例如 loading 消息完成后原地切换为 success)。
注意:若该条消息设置了自定义
icon,图标不随mode切换。
| 名称 | 说明 | 类型 |
|---|---|---|
| mode? | 消息类型,决定内置图标(仅 update 可传此字段) | 'open' | 'info' | 'success' | 'error' | 'warning' | 'loading' |
Events
click / close 为 <Message> / <MessageProvider> 组件的原生事件(需通过组件标签监听);使用 useMessage() 时,对应事件请改用每条消息的 onClick / onClose 回调。
| 名称 | 说明 | 类型 |
|---|---|---|
| ready | 实例挂载完成时触发,参数为该实例的 api | (api: MessageApi) => void |
| click | 点击 message 时触发的回调函数 | (e: Event) => void |
| close | 关闭时触发的回调函数,参数为该条消息的 key | (key: string) => void |
在 setup 外使用
选择 1:createDiscreteApi()(脱离组件树)
适用于 axios 拦截器、路由守卫、Pinia action 等任意位置:内部会创建一个独立的应用实例,因此可在任意位置调用,无需外层 MessageProvider
注意
- 主题使用内置默认值;如需自定义,通过第二个参数
configProviderProps(支持Ref/computed响应式)显式传入,详见 全局化配置 ConfigProvider「主题同步到离散 API」 - 每次调用都会创建一套独立实例(独立的容器与消息栈),建议缓存返回值复用,避免重复创建;不再使用时可通过返回的
dispose()销毁该实例 - 内部会访问
document,SSR场景请在客户端(点击回调、onMounted等)中调用 - 不建议与
useMessage()在同一 App 中混用:两者各自持有独立的消息栈,maxCount/top等组件级配置互不共享,同一页面会出现两套消息容器
独立实例的复用与销毁(dispose)
独立实例创建后不会随组件卸载自动销毁,也不会因内部消息全部关闭而回收,需根据使用场景决定是否手动 dispose():
场景 A · 全局单例,缓存复用(推荐):适用于 axios 拦截器、路由守卫、Pinia action 等常驻场景,整个应用生命周期内复用同一实例:
import { createDiscreteApi } from 'vue-amazing-ui'
// 在模块顶层创建一次,全局复用
const { message } = createDiscreteApi(['message'])
message.success('这是一条消息')场景 B · 临时使用后手动 dispose():适用于测试用例、单次任务等按需回收场景,用完即销毁:
import { createDiscreteApi } from 'vue-amazing-ui'
import type { DiscreteApiInstance } from 'vue-amazing-ui'
let discrete: DiscreteApiInstance<'message'> | null = null
function getMessageApi() {
if (!discrete) {
discrete = createDiscreteApi(['message'])
}
return discrete.message
}
getMessageApi().success('这是一条消息')
// 不再需要时手动销毁:卸载内部应用并移除挂载容器
// 重复调用无副作用(内部有 disposed 保护);再次调用 getMessageApi() 会安全重建
discrete?.dispose()
discrete = nullXXX.ts(任意 .ts 文件)
import { createDiscreteApi } from 'vue-amazing-ui'
// 任意位置调用,无需外层 MessageProvider
const { message } = createDiscreteApi(['message'])
// 例:axios 响应拦截器
axios.interceptors.response.use(
(response) => response,
(error) => {
message.error(error.message)
return Promise.reject(error)
}
)选择 2:挂载到 window(复用组件树内实例)
注意
如果你想在 setup 外使用 message,需要在顶层 setup 中把 useMessage() 返回的实例挂载到 window 下然后再调用,调用前需要确保实例已经挂载成功。
App.vue
<script setup lang="ts">
import { MessageProvider } from 'vue-amazing-ui'
</script>
<template>
<MessageProvider>
<Content />
</MessageProvider>
</template>content.vue(<MessageProvider> 内的顶层组件)
<script setup lang="ts">
import { useMessage } from 'vue-amazing-ui'
// 挂载到 window 后,即可在任意非组件环境(工具函数、事件监听等)中调用
window.$message = useMessage()
</script>XXX.ts(任意 .ts 文件)
// 需确保已在顶层 setup 中执行了 window.$message = useMessage()
window.$message?.success('这是一条消息')可选:为 window.$message 补充 TypeScript 类型声明
// types/global.d.ts
import type { MessageApi } from 'vue-amazing-ui'
declare global {
interface Window {
$message?: MessageApi
}
}