/** * 命令式 Toast —— 组件内受控的轻量全局队列(不读 env、不发请求、无 DOM 依赖)。 * * 队列是一个模块级 Vue `ref`,由 `useToast()` 推入、`ToastHost.vue` 渲染消费。逻辑(入队 / 自动 * 消失定时器 / 手动 dismiss / duration 覆盖)全在此文件,可脱离 DOM 单测(§8)。这是任务文档允许的 * 唯一「全局状态」例外,且严格受控于本包。 * * @typedef {'success'|'error'|'info'} ToastType * * @typedef {Object} ToastItem * @property {string} id 队列内唯一 id * @property {ToastType} type 语义类型,决定 ToastHost 的视觉 * @property {string} message 展示文案(由调用方注入,本包不存文案) * @property {number} duration 自动消失毫秒数;<=0 表示不自动消失 * * @typedef {Object} ToastOptions * @property {number} [duration] 覆盖默认 4000ms;<=0 则不自动消失 * * @typedef {Object} ToastApi * @property {(message: string, opts?: ToastOptions) => string} success * @property {(message: string, opts?: ToastOptions) => string} error * @property {(message: string, opts?: ToastOptions) => string} info * @property {(id: string) => void} dismiss */ import { ref, readonly } from 'vue' /** 默认自动消失时长(ms)。 */ const DEFAULT_DURATION = 4000 /** @type {import('vue').Ref} 受控全局队列。 */ const queue = ref(/** @type {ToastItem[]} */ ([])) /** @type {Map>} id → 自动消失定时器句柄。 */ const timers = new Map() let seq = 0 /** * 生成队列内唯一 id。 * @returns {string} */ function nextId() { seq += 1 return `toast-${seq}` } /** * 从队列移除指定 toast 并清掉其定时器(手动关闭 / 自动消失 / 编程 dismiss 共用)。 * @param {string} id * @returns {void} */ function dismiss(id) { const timer = timers.get(id) if (timer !== undefined) { clearTimeout(timer) timers.delete(id) } queue.value = queue.value.filter((t) => t.id !== id) } /** * 入队一条 toast,按 duration 安排自动消失(排队由队列顺序天然保证)。 * @param {ToastType} type * @param {string} message * @param {ToastOptions} [opts] * @returns {string} 新 toast 的 id(供调用方主动 dismiss) */ function push(type, message, opts = {}) { const id = nextId() const duration = typeof opts.duration === 'number' ? opts.duration : DEFAULT_DURATION /** @type {ToastItem} */ const item = { id, type, message, duration } queue.value = [...queue.value, item] if (duration > 0) { timers.set( id, setTimeout(() => dismiss(id), duration), ) } return id } /** @type {ToastApi} */ const api = Object.freeze({ success: (message, opts) => push('success', message, opts), error: (message, opts) => push('error', message, opts), info: (message, opts) => push('info', message, opts), dismiss, }) /** * 命令式 toast 入口。返回稳定的 api 对象(`success`/`error`/`info` 入队,`dismiss` 关闭)。 * @returns {ToastApi} */ export function useToast() { return api } /** * 供 ToastHost.vue 订阅的只读队列。组件只读不写,写入一律走 `useToast()`,保证队列受控。 * @returns {import('vue').Ref} */ export function useToastQueue() { return /** @type {import('vue').Ref} */ (readonly(queue)) }