use-toast.js
3.37 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
/**
* 命令式 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<ToastItem[]>} 受控全局队列。 */
const queue = ref(/** @type {ToastItem[]} */ ([]))
/** @type {Map<string, ReturnType<typeof setTimeout>>} 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<readonly ToastItem[]>}
*/
export function useToastQueue() {
return /** @type {import('vue').Ref<readonly ToastItem[]>} */ (readonly(queue))
}