仰望星辰工作室

starmusic-pc

starmusic-pc/ frontend/src/uni/ui-state.js 3.7 KB · 125 行 原始文件
1/**
2 * UI 反馈状态(Toast / Loading / Modal)
3 *
4 * uni 兼容层的 showToast 等 API 需要一个渲染出口:这里用 Vue 响应式对象承载,
5 * 由 App.vue 顶层的 <UiToast> / <UiLoading> / <UiModal> 组件渲染。
6 * 这样 common/* 里的业务代码无需任何改动即可在桌面端弹出提示。
7 */
8import { reactive } from 'vue'
9
10let toastSeq = 0
11
12export const uiState = reactive({
13 /** [{ id, title, icon, duration }] */
14 toasts: [],
15 /** { title, mask } | null */
16 loading: null,
17 /** { id, title, content, showCancel, confirmText, cancelText, confirmColor, danger } | null */
18 modal: null,
19})
20
21// ============ Toast ============
22
23/**
24 * 弹出一条轻提示
25 * @param {{title?:string, icon?:'none'|'success'|'error'|'loading', duration?:number, image?:string}} opts
26 */
27export function pushToast(opts = {}) {
28 const title = String(opts.title == null ? '' : opts.title)
29 if (!title) return -1
30 const duration = Number(opts.duration) > 0 ? Number(opts.duration) : 2000
31 const icon = opts.icon || 'none'
32 // 相同文案的提示不重复堆叠,避免连点刷屏
33 const exist = uiState.toasts.find((t) => t.title === title && t.icon === icon)
34 if (exist) {
35 exist.duration = duration
36 exist.createdAt = Date.now()
37 return exist.id
38 }
39 const id = ++toastSeq
40 uiState.toasts.push({ id, title, icon, duration, createdAt: Date.now() })
41 if (uiState.toasts.length > 4) uiState.toasts.shift()
42 return id
43}
44
45/** 关闭指定 toast */
46export function closeToast(id) {
47 const index = uiState.toasts.findIndex((t) => t.id === id)
48 if (index > -1) uiState.toasts.splice(index, 1)
49}
50
51/** 关闭全部 toast */
52export function clearToasts() {
53 uiState.toasts.splice(0, uiState.toasts.length)
54}
55
56// ============ Loading ============
57
58export function showLoading(title = '加载中...', mask = false) {
59 uiState.loading = { title: String(title), mask: !!mask }
60}
61
62export function updateLoading(title) {
63 if (uiState.loading) uiState.loading = { ...uiState.loading, title: String(title) }
64}
65
66export function hideLoading() {
67 uiState.loading = null
68}
69
70// ============ Modal ============
71
72/** 待展示的弹窗队列(uni 同一时刻只展示一个) */
73const modalQueue = []
74let modalSeq = 0
75
76/**
77 * 弹出一个居中模态确认框
78 * @returns {Promise<{confirm:boolean, cancel:boolean}>} 便于 async/await 使用
79 */
80export function openModal(opts = {}) {
81 return new Promise((resolve) => {
82 const item = {
83 id: ++modalSeq,
84 title: String(opts.title == null ? '提示' : opts.title),
85 content: String(opts.content == null ? '' : opts.content),
86 showCancel: opts.showCancel !== false,
87 confirmText: String(opts.confirmText || '确定'),
88 cancelText: String(opts.cancelText || '取消'),
89 confirmColor: opts.confirmColor || '',
90 danger: !!opts.danger,
91 editable: !!opts.editable,
92 placeholderText: String(opts.placeholderText || ''),
93 _resolve: resolve,
94 }
95 modalQueue.push(item)
96 pumpModal()
97 })
98}
99
100/** 队列泵:当前无弹窗时取出下一个展示 */
101function pumpModal() {
102 if (uiState.modal || !modalQueue.length) return
103 uiState.modal = modalQueue.shift()
104}
105
106/**
107 * 关闭当前弹窗
108 * @param {boolean} confirm true 为确定
109 * @param {string} [inputValue] 可编辑弹窗的输入值
110 */
111export function resolveModal(confirm, inputValue) {
112 const current = uiState.modal
113 if (!current) return
114 uiState.modal = null
115 try {
116 current._resolve({ confirm: !!confirm, cancel: !confirm, content: inputValue })
117 } catch (e) {
118 /* 忽略 */
119 }
120 // 让下一个弹窗在下一帧出现,避免动画串帧
121 requestAnimationFrame(pumpModal)
122}
123
124/** 当前是否有弹窗 */
125export const hasModal = () => !!uiState.modal