# Toast 轻提示

轻提示只通报结果、不打断操作，通常出现在保存成功、请求失败、状态切换之后。它同时提供两种用法：编程式的 `toast` 服务适合在事件回调与请求封装里随手调用；声明式的 `MToast` 组件适合需要跟随 `v-model`、或正文要放复杂节点的场景，两者定位与样式一致。

## 代码演示

### 语义类型（编程式）

`toast.show` / `success` / `error` / `warning` / `info` 对应五种语义，各自带语义色与图标。

```vue
<script setup lang="ts">
import { toast } from 'miao-design'
</script>

<template>
  <div class="m-row-demo">
    <MButton variant="contained" color="success" @click="toast.success('保存成功')">成功</MButton>
    <MButton variant="contained" color="error" @click="toast.error('操作失败，请重试')">失败</MButton>
    <MButton variant="contained" color="warning" @click="toast.warning('空间即将用尽')">警告</MButton>
    <MButton variant="contained" color="info" @click="toast.info('有 3 条新消息')">信息</MButton>
    <MButton variant="outlined" @click="toast.show('一条普通提示')">show</MButton>
  </div>
</template>
```

### 自定义参数

`duration`、`position`、`closable`、`offset`、`title` 都通过第二个参数传入；`duration` 为 `0` 时提示常驻，只能手动关闭。

```vue
<script setup lang="ts">
import { toast } from 'miao-design'

function withOptions() {
  toast.error('请求超时，请检查网络后重试', {
    title: '加载失败',
    duration: 5000,
    position: 'top-right',
    closable: true,
    offset: 24
  })
}

function sticky() {
  toast.show('这条提示不会自动消失，需要手动关闭', { duration: 0, closable: true })
}

function plainSlot() {
  toast.show('没有额外配置的提示，3 秒后自动消失')
}
</script>

<template>
  <div class="m-row-demo">
    <MButton variant="outlined" @click="plainSlot">默认配置</MButton>
    <MButton variant="outlined" @click="withOptions">标题 + 5 秒 + 右上角</MButton>
    <MButton variant="outlined" @click="sticky">常驻（duration: 0）</MButton>
  </div>
</template>
```

### 六个角位

六个角位各自维护一堆提示，互不干扰，同一角位的多条按添加顺序排列。

```vue
<script setup lang="ts">
import { toast } from 'miao-design'

const positions = [
  'top-left',
  'top-center',
  'top-right',
  'bottom-left',
  'bottom-center',
  'bottom-right'
] as const

function show(position: (typeof positions)[number]) {
  toast.info(`position: ${position}`, { position, duration: 2000 })
}
</script>

<template>
  <div class="m-row-demo">
    <MButton
      v-for="position in positions"
      :key="position"
      variant="outlined"
      size="small"
      @click="show(position)">
      {{ position }}
    </MButton>
  </div>
</template>
```

### 关闭单条与清空

`toast.close(id)` 关闭单条，`toast.clearAll()` 清空全部；`toastItems` 是编程式队列的共享响应式数组，可以拿它统计条数或取回 id。

```vue
<script setup lang="ts">
import { computed } from 'vue'
import { toast, toastItems } from 'miao-design'

/** toastItems 是编程式队列的共享响应式数组，可用来统计与取回 id */
const count = computed(() => toastItems.length)

let seed = 0

function createSticky() {
  seed += 1
  toast.show(`第 ${seed} 条常驻提示`, { duration: 0, closable: true, position: 'bottom-right' })
}

function closeLast() {
  const last = toastItems[toastItems.length - 1]
  if (last) toast.close(last.id)
}
</script>

<template>
  <div class="m-row-demo">
    <MButton variant="outlined" @click="createSticky">新建常驻提示</MButton>
    <MButton variant="outlined" :disabled="!count" @click="closeLast">关闭最后一条</MButton>
    <MButton variant="outlined" :disabled="!count" @click="toast.clearAll()">清空全部</MButton>
    <span style="color: var(--m-text-sub)">当前队列 {{ count }} 条</span>
  </div>
</template>
```

### 声明式组件

用 `v-model` 控制显隐，`type`、`position`、`duration`、`closable` 等属性与编程式一致。

**演示说明：** 声明式与编程式的队列互相独立，这条提示不会出现在 toastItems 里。

```vue
<script setup lang="ts">
import { ref } from 'vue'

const visible = ref(false)
const closed = ref(0)
</script>

<template>
  <div class="m-row-demo">
    <MButton variant="contained" color="success" @click="visible = true">显示声明式提示</MButton>

    <MToast
      v-model="visible"
      type="success"
      title="提交成功"
      position="top-right"
      :duration="4000"
      :offset="24"
      closable
      @close="closed += 1">
      正文来自默认插槽，可以放 <strong>任意节点</strong>；不写插槽时回退到 message 属性。
    </MToast>

    <span style="color: var(--m-text-sub)">close 触发次数：{{ closed }}</span>
  </div>
</template>
```

## API

### toast 方法

| 方法 | 签名 | 说明 |
| --- | --- | --- |
| `toast.show` | `(message: string, options?: ToastOptions) => void` | 弹出提示，`type` 取 `options.type`，缺省为 `'default'` |
| `toast.success` | `(message: string, options?: ToastOptions) => void` | 成功提示，`type` 强制为 `'success'` |
| `toast.error` | `(message: string, options?: ToastOptions) => void` | 失败提示，`type` 强制为 `'error'` |
| `toast.warning` | `(message: string, options?: ToastOptions) => void` | 警告提示，`type` 强制为 `'warning'` |
| `toast.info` | `(message: string, options?: ToastOptions) => void` | 信息提示，`type` 强制为 `'info'` |
| `toast.close` | `(id: number) => void` | 按 id 关闭单条 |
| `toast.clearAll` | `() => void` | 清空编程式队列中的全部提示 |

四个语义方法会把 `options.type` 覆盖为对应值。等价底层函数 `showToast(message, options?)` 也可以单独导入使用。

::: warning 方法不返回 id
`toast.show` 与 `toast.success` 等方法的返回值是 `void`，拿不到新建提示的 id。需要精确关闭某一条时，从 `toastItems` 里取（例如最后一条的 `id`），或者干脆改用声明式 `MToast` + `v-model`。
:::

### ToastOptions

| 字段 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `type` | `'default' \| 'success' \| 'error' \| 'warning' \| 'info'` | `'default'` | 语义类型；调用 `toast.success` 等方法时被强制覆盖 |
| `title` | `string` | `undefined` | 标题 |
| `duration` | `number` | `3000` | 自动关闭时长（ms），`0` 表示不自动关闭 |
| `closable` | `boolean` | `false` | 是否显示关闭按钮 |
| `position` | `'top-right' \| 'top-left' \| 'top-center' \| 'bottom-right' \| 'bottom-left' \| 'bottom-center'` | `'bottom-center'` | 显示角位 |
| `offset` | `number` | `16` | 距视口边缘间距（px） |

### MToast Props

| 属性 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `modelValue` | `boolean` | `undefined` | 是否显示，配合 `v-model` 使用 |
| `message` | `string` | `undefined` | 提示内容；默认插槽有内容时优先用插槽 |
| `title` | `string` | `undefined` | 标题 |
| `type` | `'default' \| 'success' \| 'error' \| 'warning' \| 'info'` | `'default'` | 语义类型 |
| `duration` | `number` | `3000` | 自动关闭时长（ms），`0` 表示不自动关闭 |
| `closable` | `boolean` | `false` | 是否显示关闭按钮 |
| `position` | `'top-right' \| 'top-left' \| 'top-center' \| 'bottom-right' \| 'bottom-left' \| 'bottom-center'` | `'bottom-center'` | 显示角位 |
| `offset` | `number` | `16` | 距视口边缘间距（px） |

### MToast Events

| 事件 | 参数 | 说明 |
| --- | --- | --- |
| `update:modelValue` | `value: boolean` | 自动超时或点击关闭按钮时置为 `false` |
| `close` | 无 | 自动超时或点击关闭按钮时触发 |

### MToast Slots

| 插槽 | 参数 | 说明 |
| --- | --- | --- |
| `default` | 无 | 消息正文；无内容时回退到 `message` prop |

### 导出常量

| 常量 | 值 | 说明 |
| --- | --- | --- |
| `TOAST_DEFAULT_DURATION` | `3000` | 默认自动关闭时长（ms） |
| `TOAST_DEFAULT_POSITION` | `'bottom-center'` | 默认角位 |

```ts
export type ToastType = 'default' | 'success' | 'error' | 'warning' | 'info'
export type ToastPosition =
  | 'top-right' | 'top-left' | 'top-center'
  | 'bottom-right' | 'bottom-left' | 'bottom-center'
```

### toastItems 共享状态

`toastItems` 是容器消费的响应式数组，`service` 通过内部的 `addToast` / `removeToast` / `clearToasts` 增删。它只记录编程式提示，声明式 `MToast` 不写入这个数组。

```ts
import { toastItems } from 'miao-design'

export interface ToastItem {
  id: number
  message: string
  title?: string
  type: ToastType
  duration: number
  closable: boolean
  position: ToastPosition
  offset: number
}
```

## 使用建议

::: tip 首次调用时才挂载容器
编程式提示的容器是模块级单例：第一次调用 `toast.*` 时才会把 `ToastContainer` 挂到 `document.body` 上，此后所有调用复用同一个容器。所以没有提示时页面上不存在多余的 DOM 节点，也不需要手动引入任何组件。
:::

::: warning 不要在模块顶层调用
`toast.show()` 内部会创建 DOM 并调用 `render()`，属于浏览器环境操作。SSR 渲染阶段执行模块顶层代码会直接报错，请在事件回调、`onMounted` 或请求返回之后调用；同理，`toastItems` 只做状态读写，在服务端读它是安全的。

声明式 `MToast` 自身会 `Teleport` 到 body，在 SSR 场景下也建议配合 `v-model` 只在客户端展示。
:::

::: warning 计时与堆叠规则
`duration > 0` 时才会渲染进度条，倒计时在提示显示期间运行，提示关闭或组件卸载都会清理计时器。

同一角位的多条提示按添加顺序堆叠，不同角位互不影响，堆叠间距取该角位第一条的 `offset` —— 也就是说同一角位里后加入的 `offset` 不生效。

`type='error'` 的提示使用 `aria-live="assertive"`，其余为 `polite`，屏幕阅读器会优先播报错误。
:::

## 相关组件

- [Alert 警告提示](/components/alert)：需要常驻页面内的提示条时用它，而不是轻提示。
- [Dialog 对话框](/components/dialog)：需要用户确认的关键操作，应当用对话框。
- [Button 按钮](/components/button)：轻提示常由按钮点击触发。
