Toast 轻提示

操作完成后浮出的一条简短反馈。

<script setup lang="ts">
  import { Button, Inline, toast } from '@hina-ui/vue'
</script>

<template>
  <Inline align="center" class="gap-3">
    <Button variant="outline" tone="neutral" @click="toast('草稿已保存')">默认</Button>
    <Button variant="outline" tone="neutral" @click="toast.success('文章已发布')">成功</Button>
    <Button
      variant="outline"
      tone="neutral"
      @click="toast.danger('上传失败', { description: '文件超过 20 MB。' })"
    >
      失败
    </Button>
  </Inline>
</template>
vue

用法

import { Toaster, toast } from '@hina-ui/vue'
ts

在应用的最外层挂载一个 Toaster,然后在任何地方调用 toast()。调用后返回这条提示的 id,可以用它更新或关闭这条提示。

<script setup lang="ts">
  import { Button, toast } from '@hina-ui/vue'
</script>

<template>
  <Button variant="outline" tone="neutral" @click="toast('草稿已保存')">保存草稿</Button>
</template>
vue
<template>
  <AppShell>
    <NuxtPage />
    <Toaster />
  </AppShell>
</template>
vue

示例

语义

五个语义各有自己的图标和颜色。loading 显示转圈图标,并且一直停留,通常再用同一个 id 把它更新为最终结果。

<script setup lang="ts">
  import { Button, Inline, toast } from '@hina-ui/vue'
</script>

<template>
  <Inline align="center" class="gap-3">
    <Button variant="outline" tone="neutral" @click="toast.success('已加入书架')">success</Button>
    <Button variant="outline" tone="neutral" @click="toast.danger('网络连接中断')">danger</Button>
    <Button variant="outline" tone="neutral" @click="toast.warning('还有未保存的改动')">
      warning
    </Button>
    <Button variant="outline" tone="neutral" @click="toast.info('新版本已经可用')">info</Button>
    <Button variant="outline" tone="neutral" @click="toast.loading('正在同步')">loading</Button>
  </Inline>
</template>
vue

说明文字

description 是消息下面的第二行,用来补充细节。

<script setup lang="ts">
  import { Button, toast } from '@hina-ui/vue'
</script>

<template>
  <Button
    variant="outline"
    tone="neutral"
    @click="
      toast.success('导入完成', { description: '共导入 128 本书,其中 3 本因格式不符被跳过。' })
    "
  >
    带说明的提示
  </Button>
</template>
vue

操作按钮

action 是主按钮,cancel 是次按钮,点击其中任何一个都会关闭这条提示。需要用户回应时,把 duration 设为 0。

<script setup lang="ts">
  import { Button, Inline, toast } from '@hina-ui/vue'
</script>

<template>
  <Inline align="center" class="gap-3">
    <Button
      variant="outline"
      tone="neutral"
      @click="
        toast('已移入回收站', {
          action: { label: '撤销', onClick: () => toast.success('已还原') },
        })
      "
    >
      带操作
    </Button>
    <Button
      variant="outline"
      tone="neutral"
      @click="
        toast.warning('确定要清空吗', {
          duration: 0,
          action: { label: '清空', onClick: () => toast.success('已清空') },
          cancel: { label: '取消' },
        })
      "
    >
      两个按钮
    </Button>
  </Inline>
</template>
vue

异步任务

toast.promise 接收一个 Promise,自行在 loading、success 和 error 三种状态之间切换。success 和 error 可以写成函数,用结果拼出消息。

<script setup lang="ts">
  import { Button, toast } from '@hina-ui/vue'

  function upload() {
    const task = new Promise<{ name: string }>(resolve =>
      setTimeout(() => resolve({ name: 'ATRI.epub' }), 2000),
    )
    toast.promise(task, {
      loading: '正在上传',
      success: book => `${book.name} 上传完成`,
      error: '上传失败,请重试',
    })
  }
</script>

<template>
  <Button variant="outline" tone="neutral" @click="upload">上传文件</Button>
</template>
vue

原地更新

传入相同的 id 时,更新已有提示的语义、文字和计时,而不是新增一条。进度类的反馈就是这样实现的。

<script setup lang="ts">
  import { Button, toast } from '@hina-ui/vue'

  function sync() {
    toast.loading('正在同步', { id: 'sync' })
    setTimeout(() => toast.loading('已同步 12 / 30', { id: 'sync' }), 1000)
    setTimeout(() => toast.success('同步完成', { id: 'sync' }), 2000)
  }
</script>

<template>
  <Button variant="outline" tone="neutral" @click="sync">开始同步</Button>
</template>
vue

自定义渲染

toast.custom 用自己的组件渲染提示的正文。props 里的内容原样传给它,组件还会收到一个 toastId。卡片的底色、边框、阴影和内边距仍然由 Toaster 提供。

<script setup lang="ts">
  import { defineComponent, h } from 'vue'
  import { BookOpen } from '@lucide/vue'
  import { Button, Heading, Inline, Stack, Text, toast } from '@hina-ui/vue'

  const ChapterToast = defineComponent({
    props: { title: { type: String, required: true }, chapter: { type: String, required: true } },
    setup(props) {
      return () =>
        h(
          Inline,
          { align: 'center', class: 'gap-3' },
          {
            default: () => [
              h(
                'div',
                { class: 'bg-inset grid size-10 shrink-0 place-items-center rounded-md' },
                h(BookOpen, { class: 'size-5' }),
              ),
              h(
                Stack,
                { gap: 'none', class: 'min-w-0 flex-1' },
                {
                  default: () => [
                    h(Heading, { level: 4, size: 'sm' }, () => props.title),
                    h(Text, { tone: 'muted', size: 'sm' }, () => props.chapter),
                  ],
                },
              ),
              h(
                Button,
                { size: 'sm', onClick: () => toast.dismiss('chapter') },
                { default: () => '去阅读' },
              ),
            ],
          },
        )
    },
  })

  function notify() {
    toast.custom(ChapterToast, {
      id: 'chapter',
      duration: 6000,
      props: { title: 'ATRI', chapter: '第 42 话' },
    })
  }
</script>

<template>
  <Button variant="outline" tone="neutral" @click="notify">新章节上线</Button>
</template>
vue

停留时长

duration 的单位是毫秒,默认为 4000,loading 默认为 0。为 0 表示一直停留,直到用户点击关闭按钮,或者调用 toast.dismiss(id)。

<script setup lang="ts">
  import { Button, Inline, toast } from '@hina-ui/vue'
</script>

<template>
  <Inline align="center" class="gap-3">
    <Button variant="outline" tone="neutral" @click="toast('一秒后消失', { duration: 1000 })">
      一秒
    </Button>
    <Button
      variant="outline"
      tone="neutral"
      @click="toast.info('需要手动关闭', { id: 'sticky', duration: 0 })"
    >
      不自动关闭
    </Button>
    <Button variant="soft" tone="neutral" @click="toast.dismiss('sticky')">关闭上一条</Button>
  </Inline>
</template>
vue

行为

  • 指针悬停在提示区域上,或者焦点进入其中时,计时暂停,离开后继续。
  • 同时最多显示 5 条提示,更早的会退场。
  • 提示按 id 索引,用同一个 id 再次调用是更新,而不是新增。
  • 向右滑动可以关闭一条提示。

无障碍

  • 提示区域是 role="region",默认名称为“通知”,可以用 label 覆盖。
  • 每条提示通过一个独立的 role="alert" 实时区域播报,屏幕阅读器会读出消息和说明文字。
  • 最上面那条提示可以用 Tab 聚焦,关闭按钮带有无障碍名称。

API

toast

方法
说明
toast(message, options?)
中性提示,返回这条提示的 id
toast.success(...)
成功
toast.danger(...)
失败
toast.warning(...)
警告
toast.info(...)
信息
toast.loading(...)
进行中,一直停留
toast.promise(p, messages)
跟随 Promise 的三种状态
toast.custom(component, {})
用自己的组件渲染整条提示
toast.dismiss(id?)
关闭指定的一条,不传时关闭全部

ToastOptions

属性
类型
默认值
说明
id
number | string
—
相同的 id 会更新已有提示
description
string
—
消息下面的第二行
duration
number
4000
停留毫秒数,0 表示一直停留
action
{ label, onClick }
—
主按钮
cancel
{ label, onClick }
—
次按钮
onDismiss
(id) => void
—
提示被关闭时调用
onAutoClose
(id) => void
—
计时结束自动关闭时调用

Toaster

属性
类型
默认值
说明
position
ToasterPosition
—
不设置时窄屏在底部居中,宽屏在右上角
label
string
—
提示区域的无障碍名称
class
string
—
追加到提示区域上的类名

ToasterPosition 的取值为 top-start、top-center、top-end、bottom-start、bottom-center 和 bottom-end。