Skip to content

全局提示 Message

赞助

全局展示操作反馈信息

何时使用

  • 可提供成功、警告和错误等反馈信息
  • 顶部居中显示并自动消失,是一种不打断用户操作的轻量级提示方式

提示

Message 推荐在组件 setup 内通过 useMessage() 调用,使用前需在应用根节点放置一次 <MessageProvider>
如果你想在 setup 外使用(例如 axios 拦截器、路由守卫、Pinia action 等),请参考文档末尾的 在 setup 外使用

使用方式

调用方式API适用位置
组件树内调用
推荐
useMessage()组件 setup 内,需外层存在 <MessageProvider>
脱离组件树调用
无需 MessageProvider
createDiscreteApi()任意位置(axios 拦截器、路由守卫、Pinia action 等)

组件树内使用:useMessage()
推荐


适用于组件内部调用:先在应用根节点放置一次 <MessageProvider>,之后任意层级组件均可通过 useMessage() 取得同一实例

关于 MessageProviderMessage

  • 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

vue
<script setup lang="ts">
import { MessageProvider } from 'vue-amazing-ui'
</script>
<template>
  <MessageProvider>
    <RouterView />
  </MessageProvider>
</template>

2. 在任意层级组件中调用 useMessage()

XXX.vue

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> 实现。

基本使用

Open
Show Code
vue
<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>

不同类型的全局提示

Info
Success
Error
Warning
Loading
Show Code
vue
<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>

自定义图标

自定义 Info 图标
自定义 Open 图标
图标渲染函数
Show Code
vue
<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>

自定义样式

自定义 class
自定义 style
Show Code
vue
<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>

自定义关闭延时

3s 后自动关闭
常驻消息,点击关闭
Show Code
vue
<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 的渲染函数


VNode 内容
渲染函数内容
Show Code
vue
<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
vue
<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
vue
<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


打开消息(最多 3 条)
全部销毁
Show Code
vue
<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>

鼠标移入保持显示

hover 暂停关闭
hover 不暂停关闭
Show Code
vue
<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>

顶部位置

消息距顶部 100px
Show Code
vue
<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
vue
<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 | null3000
top消息距离顶部的位置,单位 pxstring | number30
maxCount可同时存在的最大消息数,超出时淘汰最旧的一条numberundefined
keepAliveOnHover鼠标移入时是否暂停自动关闭booleantrue
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 | nullundefined
class?自定义类名stringundefined
style?自定义样式CSSPropertiesundefined
onClick?点击 message 时的回调函数() => voidundefined
onClose?关闭时的回调函数() => voidundefined

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() 销毁该实例
  • 内部会访问 documentSSR 场景请在客户端(点击回调、onMounted 等)中调用
  • 不建议与 useMessage() 在同一 App 中混用:两者各自持有独立的消息栈,maxCount / top 等组件级配置互不共享,同一页面会出现两套消息容器

独立实例的复用与销毁(dispose)

独立实例创建后不会随组件卸载自动销毁,也不会因内部消息全部关闭而回收,需根据使用场景决定是否手动 dispose()

场景 A · 全局单例,缓存复用(推荐):适用于 axios 拦截器、路由守卫、Pinia action 等常驻场景,整个应用生命周期内复用同一实例:

ts
import { createDiscreteApi } from 'vue-amazing-ui'

// 在模块顶层创建一次,全局复用
const { message } = createDiscreteApi(['message'])

message.success('这是一条消息')

场景 B · 临时使用后手动 dispose():适用于测试用例、单次任务等按需回收场景,用完即销毁:

ts
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 = null

XXX.ts(任意 .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)
  }
)
Discrete Message(脱离组件树调用)

选择 2:挂载到 window(复用组件树内实例)

注意

如果你想在 setup 外使用 message,需要在顶层 setup 中把 useMessage() 返回的实例挂载到 window 下然后再调用,调用前需要确保实例已经挂载成功。

App.vue

vue
<script setup lang="ts">
import { MessageProvider } from 'vue-amazing-ui'
</script>
<template>
  <MessageProvider>
    <Content />
  </MessageProvider>
</template>

content.vue(<MessageProvider> 内的顶层组件)

vue
<script setup lang="ts">
import { useMessage } from 'vue-amazing-ui'

// 挂载到 window 后,即可在任意非组件环境(工具函数、事件监听等)中调用
window.$message = useMessage()
</script>

XXX.ts(任意 .ts 文件)

ts
// 需确保已在顶层 setup 中执行了 window.$message = useMessage()
window.$message?.success('这是一条消息')

可选:为 window.$message 补充 TypeScript 类型声明

ts
// types/global.d.ts
import type { MessageApi } from 'vue-amazing-ui'

declare global {
  interface Window {
    $message?: MessageApi
  }
}

Released under the MIT License.