@h-ai/ui 0.1.0-alpha.26 → 0.1.0-alpha.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -1
- package/auto-import.d.ts +3 -0
- package/auto-import.js +3 -0
- package/dist/components/compounds/Calendar.svelte +1 -1
- package/dist/components/compounds/DatePicker.svelte +1 -1
- package/dist/components/compounds/MessageBoxContainer.svelte +18 -0
- package/dist/components/compounds/MessageBoxContainer.svelte.d.ts +18 -0
- package/dist/components/compounds/MessageBoxDialog.svelte +193 -0
- package/dist/components/compounds/MessageBoxDialog.svelte.d.ts +7 -0
- package/dist/components/compounds/MessageContainer.svelte +200 -0
- package/dist/components/compounds/MessageContainer.svelte.d.ts +3 -0
- package/dist/components/compounds/Modal.svelte +0 -1
- package/dist/components/compounds/ToastContainer.svelte +159 -20
- package/dist/components/compounds/index.d.ts +3 -0
- package/dist/components/compounds/index.js +5 -0
- package/dist/components/scenes/app/ThemeColorPicker.svelte +7 -7
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -0
- package/dist/message.svelte.d.ts +49 -0
- package/dist/message.svelte.js +77 -0
- package/dist/messagebox.svelte.d.ts +82 -0
- package/dist/messagebox.svelte.js +129 -0
- package/dist/messages/en-US.json +1 -0
- package/dist/messages/zh-CN.json +1 -0
- package/dist/types.d.ts +58 -0
- package/package.json +9 -3
- package/vite.d.ts +13 -0
- package/vite.js +95 -0
|
@@ -4,10 +4,17 @@
|
|
|
4
4
|
=============================================================================
|
|
5
5
|
Toast 容器组件,显示所有 Toast 通知
|
|
6
6
|
|
|
7
|
-
使用 Svelte 5 Runes ($derived)
|
|
7
|
+
使用 Svelte 5 Runes ($derived / $effect)
|
|
8
|
+
|
|
9
|
+
叠加策略(关键):原生 <dialog>.showModal() 处于浏览器 top layer,且在 Chromium 中
|
|
10
|
+
始终渲染在 Popover 之上。为保证 Toast 永远可见、不被弹窗遮挡:
|
|
11
|
+
- 当存在已打开的 modal dialog 时,将 Toast 层寄宿到该 dialog 内部(同处 top layer,
|
|
12
|
+
作为最后子节点渲染在其内容之上);
|
|
13
|
+
- 否则寄宿到 <body> 并以 Popover 进入 top layer,叠加在普通内容之上。
|
|
8
14
|
=============================================================================
|
|
9
15
|
-->
|
|
10
16
|
<script lang='ts'>
|
|
17
|
+
import { onMount } from 'svelte'
|
|
11
18
|
import { uiM } from '../../messages.js'
|
|
12
19
|
import { toast } from '../../toast.svelte.js'
|
|
13
20
|
import { cn, getAlertVariantClass } from '../../utils.js'
|
|
@@ -22,6 +29,11 @@
|
|
|
22
29
|
'bottom-center': 'toast-bottom toast-center',
|
|
23
30
|
}
|
|
24
31
|
|
|
32
|
+
// 容器根元素引用。
|
|
33
|
+
let layerElement: HTMLDivElement | undefined = $state()
|
|
34
|
+
|
|
35
|
+
const hasItems = $derived(toast.items.length > 0)
|
|
36
|
+
|
|
25
37
|
// 按位置分组
|
|
26
38
|
const groupedItems = $derived(() => {
|
|
27
39
|
const groups: Record<string, typeof toast.items> = {}
|
|
@@ -36,24 +48,151 @@
|
|
|
36
48
|
|
|
37
49
|
return groups
|
|
38
50
|
})
|
|
51
|
+
|
|
52
|
+
/** 找到最顶层处于打开状态的原生 modal dialog(位于浏览器 top layer)。 */
|
|
53
|
+
function findTopModalDialog(): HTMLDialogElement | null {
|
|
54
|
+
if (typeof document === 'undefined') {
|
|
55
|
+
return null
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const dialogs = document.querySelectorAll<HTMLDialogElement>('dialog')
|
|
59
|
+
let top: HTMLDialogElement | null = null
|
|
60
|
+
for (const dialog of dialogs) {
|
|
61
|
+
if (dialog.open && typeof dialog.matches === 'function' && dialog.matches(':modal')) {
|
|
62
|
+
top = dialog
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return top
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function safeShowPopover(el: HTMLElement) {
|
|
69
|
+
try {
|
|
70
|
+
if (typeof el.showPopover === 'function' && el.isConnected && !el.matches(':popover-open')) {
|
|
71
|
+
el.showPopover()
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
// 元素未连接或重复调用时忽略。
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function safeHidePopover(el: HTMLElement) {
|
|
80
|
+
try {
|
|
81
|
+
if (typeof el.hidePopover === 'function' && el.matches(':popover-open')) {
|
|
82
|
+
el.hidePopover()
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// 同上,忽略异常。
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 将 Toast 层寄宿到正确的容器:
|
|
92
|
+
* - 有打开的 modal dialog 时,去掉 popover 属性并寄宿到该 dialog 内部,渲染在其内容之上;
|
|
93
|
+
* - 否则寄宿到 <body>,恢复 popover 属性并按需进入 / 退出 top layer。
|
|
94
|
+
*/
|
|
95
|
+
function relocate() {
|
|
96
|
+
const el = layerElement
|
|
97
|
+
if (!el) {
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const modal = hasItems ? findTopModalDialog() : null
|
|
102
|
+
|
|
103
|
+
if (modal) {
|
|
104
|
+
// 寄宿到 dialog:popover 元素未显示时会 display:none,故先移除 popover 属性。
|
|
105
|
+
safeHidePopover(el)
|
|
106
|
+
if (el.hasAttribute('popover')) {
|
|
107
|
+
el.removeAttribute('popover')
|
|
108
|
+
}
|
|
109
|
+
if (el.parentElement !== modal) {
|
|
110
|
+
modal.appendChild(el)
|
|
111
|
+
}
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (el.parentElement !== document.body) {
|
|
116
|
+
document.body.appendChild(el)
|
|
117
|
+
}
|
|
118
|
+
if (!el.hasAttribute('popover')) {
|
|
119
|
+
el.setAttribute('popover', 'manual')
|
|
120
|
+
}
|
|
121
|
+
if (hasItems) {
|
|
122
|
+
safeShowPopover(el)
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
safeHidePopover(el)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
onMount(() => {
|
|
130
|
+
relocate()
|
|
131
|
+
|
|
132
|
+
// dialog 通过 showModal()/close() 切换 `open` 属性;据此重新寄宿 Toast 层。
|
|
133
|
+
const observer = new MutationObserver(() => relocate())
|
|
134
|
+
observer.observe(document.body, { subtree: true, attributes: true, attributeFilter: ['open'] })
|
|
135
|
+
|
|
136
|
+
return () => {
|
|
137
|
+
observer.disconnect()
|
|
138
|
+
const el = layerElement
|
|
139
|
+
if (el) {
|
|
140
|
+
safeHidePopover(el)
|
|
141
|
+
el.remove()
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
// Toast 数量变化时重新寄宿(出现 / 消失、以及在 dialog 与 body 之间切换)。
|
|
147
|
+
$effect(() => {
|
|
148
|
+
void hasItems
|
|
149
|
+
relocate()
|
|
150
|
+
})
|
|
39
151
|
</script>
|
|
40
152
|
|
|
41
|
-
{
|
|
42
|
-
|
|
43
|
-
{
|
|
44
|
-
|
|
45
|
-
<
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
153
|
+
<div bind:this={layerElement} popover='manual' class='hai-toast-layer'>
|
|
154
|
+
{#each Object.entries(groupedItems()) as [position, items] (position)}
|
|
155
|
+
<div class={cn('toast', positionMap[position as keyof typeof positionMap])}>
|
|
156
|
+
{#each items as item (item.id)}
|
|
157
|
+
<div class={cn('alert', getAlertVariantClass(item.variant ?? 'info'))}>
|
|
158
|
+
<span>{item.message}</span>
|
|
159
|
+
{#if item.dismissible}
|
|
160
|
+
<IconButton
|
|
161
|
+
variant='ghost'
|
|
162
|
+
size='sm'
|
|
163
|
+
onclick={() => toast.remove(item.id)}
|
|
164
|
+
ariaLabel={uiM('toast_dismiss')}
|
|
165
|
+
>
|
|
166
|
+
✕
|
|
167
|
+
</IconButton>
|
|
168
|
+
{/if}
|
|
169
|
+
</div>
|
|
170
|
+
{/each}
|
|
171
|
+
</div>
|
|
172
|
+
{/each}
|
|
173
|
+
</div>
|
|
174
|
+
|
|
175
|
+
<style>
|
|
176
|
+
/* 容器铺满视口但不拦截交互;仅 toast 本体接收指针事件。 */
|
|
177
|
+
.hai-toast-layer {
|
|
178
|
+
position: fixed;
|
|
179
|
+
inset: 0;
|
|
180
|
+
width: 100%;
|
|
181
|
+
height: 100%;
|
|
182
|
+
max-width: none;
|
|
183
|
+
max-height: none;
|
|
184
|
+
margin: 0;
|
|
185
|
+
padding: 0;
|
|
186
|
+
border: 0;
|
|
187
|
+
background: transparent;
|
|
188
|
+
overflow: visible;
|
|
189
|
+
pointer-events: none;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/* 未支持 Popover API 的浏览器忽略 popover 属性,容器常驻文档流;
|
|
193
|
+
此时无 toast 项则内部不渲染任何内容,容器透明且不拦截交互。 */
|
|
194
|
+
|
|
195
|
+
.hai-toast-layer :global(.toast) {
|
|
196
|
+
pointer-events: auto;
|
|
197
|
+
}
|
|
198
|
+
</style>
|
|
@@ -25,6 +25,9 @@ export { default as Empty } from './Empty.svelte';
|
|
|
25
25
|
export { default as Form } from './Form.svelte';
|
|
26
26
|
export { default as FormField } from './FormField.svelte';
|
|
27
27
|
export { default as InfiniteScroll } from './InfiniteScroll.svelte';
|
|
28
|
+
export { default as MessageBoxContainer } from './MessageBoxContainer.svelte';
|
|
29
|
+
export { default as MessageBoxDialog } from './MessageBoxDialog.svelte';
|
|
30
|
+
export { default as MessageContainer } from './MessageContainer.svelte';
|
|
28
31
|
export { default as Modal } from './Modal.svelte';
|
|
29
32
|
export { default as PageHeader } from './PageHeader.svelte';
|
|
30
33
|
export { default as Pagination } from './Pagination.svelte';
|
|
@@ -33,6 +33,11 @@ export { default as Form } from './Form.svelte';
|
|
|
33
33
|
export { default as FormField } from './FormField.svelte';
|
|
34
34
|
// 无限滚动 / 上拉加载
|
|
35
35
|
export { default as InfiniteScroll } from './InfiniteScroll.svelte';
|
|
36
|
+
// MessageBox 命令式弹框(类似 ElementUI MessageBox)
|
|
37
|
+
export { default as MessageBoxContainer } from './MessageBoxContainer.svelte';
|
|
38
|
+
export { default as MessageBoxDialog } from './MessageBoxDialog.svelte';
|
|
39
|
+
// Message 轻量消息提示(类似 ElementUI Message)
|
|
40
|
+
export { default as MessageContainer } from './MessageContainer.svelte';
|
|
36
41
|
export { default as Modal } from './Modal.svelte';
|
|
37
42
|
// 页面头部
|
|
38
43
|
export { default as PageHeader } from './PageHeader.svelte';
|
|
@@ -97,7 +97,7 @@
|
|
|
97
97
|
}
|
|
98
98
|
</script>
|
|
99
99
|
|
|
100
|
-
<div class={cn('', className)}>
|
|
100
|
+
<div class={cn('theme-color-picker', className)}>
|
|
101
101
|
<div class='flex flex-wrap items-center gap-2'>
|
|
102
102
|
{#each normalizedPresets as preset (preset.value)}
|
|
103
103
|
<button
|
|
@@ -165,7 +165,7 @@
|
|
|
165
165
|
</div>
|
|
166
166
|
|
|
167
167
|
<style>
|
|
168
|
-
button {
|
|
168
|
+
.theme-color-picker button {
|
|
169
169
|
border-color: var(--soft-border, color-mix(in oklab, var(--color-base-content) 12%, transparent));
|
|
170
170
|
background: var(--soft-surface, var(--color-base-100));
|
|
171
171
|
color: var(--soft-text-body, var(--color-base-content));
|
|
@@ -173,7 +173,7 @@
|
|
|
173
173
|
font-weight: 500;
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
-
button:hover:not(:disabled) {
|
|
176
|
+
.theme-color-picker button:hover:not(:disabled) {
|
|
177
177
|
border-color: color-mix(in oklab, var(--color-primary) 28%, var(--soft-border, transparent) 72%);
|
|
178
178
|
background: color-mix(
|
|
179
179
|
in oklab,
|
|
@@ -182,7 +182,7 @@
|
|
|
182
182
|
);
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
-
button[aria-pressed='true'] {
|
|
185
|
+
.theme-color-picker button[aria-pressed='true'] {
|
|
186
186
|
border-color: color-mix(in oklab, var(--color-primary) 42%, var(--soft-border, transparent) 58%);
|
|
187
187
|
background: color-mix(
|
|
188
188
|
in oklab,
|
|
@@ -192,14 +192,14 @@
|
|
|
192
192
|
color: var(--color-primary);
|
|
193
193
|
}
|
|
194
194
|
|
|
195
|
-
button:focus,
|
|
196
|
-
button:focus-visible {
|
|
195
|
+
.theme-color-picker button:focus,
|
|
196
|
+
.theme-color-picker button:focus-visible {
|
|
197
197
|
outline: none;
|
|
198
198
|
border-color: color-mix(in oklab, var(--color-primary) 48%, var(--soft-border, transparent) 52%);
|
|
199
199
|
box-shadow: 0 0 0 4px color-mix(in oklab, var(--color-primary) 12%, transparent 88%);
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
-
button > span:first-child {
|
|
202
|
+
.theme-color-picker button > span:first-child {
|
|
203
203
|
border-color: color-mix(in oklab, var(--color-base-content) 10%, transparent 90%);
|
|
204
204
|
box-shadow: none;
|
|
205
205
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
export * from './components/index.js';
|
|
8
8
|
export * from './i18n.svelte.js';
|
|
9
|
+
export * from './message.svelte.js';
|
|
10
|
+
export * from './messagebox.svelte.js';
|
|
9
11
|
export * from './theme-config.js';
|
|
10
12
|
export * from './toast.svelte.js';
|
|
11
13
|
export * from './types.js';
|
package/dist/index.js
CHANGED
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
export * from './components/index.js';
|
|
9
9
|
// ─── i18n 国际化(Paraglide 辅助工具) ───
|
|
10
10
|
export * from './i18n.svelte.js';
|
|
11
|
+
// ─── Message 轻量消息提示(类似 ElementUI Message) ───
|
|
12
|
+
export * from './message.svelte.js';
|
|
13
|
+
// ─── MessageBox 命令式弹框(类似 ElementUI MessageBox) ───
|
|
14
|
+
export * from './messagebox.svelte.js';
|
|
11
15
|
// ─── 主题配置 ───
|
|
12
16
|
export * from './theme-config.js';
|
|
13
17
|
// ─── Toast 状态管理 ───
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @h-ai/ui — Message 状态管理
|
|
3
|
+
*
|
|
4
|
+
* 命令式轻量消息提示 API,类似 ElementUI Message。
|
|
5
|
+
* 与 Toast 类似但视觉风格不同:顶部居中滑入、带类型图标、可自动关闭。
|
|
6
|
+
* @module message.svelte
|
|
7
|
+
*/
|
|
8
|
+
import type { MessageOptions } from './types.js';
|
|
9
|
+
/**
|
|
10
|
+
* 内部消息条目
|
|
11
|
+
*/
|
|
12
|
+
export interface MessageItem {
|
|
13
|
+
id: string;
|
|
14
|
+
message: string;
|
|
15
|
+
type: 'info' | 'success' | 'warning' | 'error';
|
|
16
|
+
duration: number;
|
|
17
|
+
closable: boolean;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Message 状态
|
|
21
|
+
*/
|
|
22
|
+
declare class MessageState {
|
|
23
|
+
items: MessageItem[];
|
|
24
|
+
private timers;
|
|
25
|
+
/**
|
|
26
|
+
* 添加消息
|
|
27
|
+
*/
|
|
28
|
+
add(options: MessageOptions | string): string;
|
|
29
|
+
/**
|
|
30
|
+
* 移除消息
|
|
31
|
+
*/
|
|
32
|
+
remove(id: string): void;
|
|
33
|
+
/**
|
|
34
|
+
* 清空所有消息
|
|
35
|
+
*/
|
|
36
|
+
clear(): void;
|
|
37
|
+
/**
|
|
38
|
+
* 快捷方法
|
|
39
|
+
*/
|
|
40
|
+
info(message: string, duration?: number): string;
|
|
41
|
+
success(message: string, duration?: number): string;
|
|
42
|
+
warning(message: string, duration?: number): string;
|
|
43
|
+
error(message: string, duration?: number): string;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Message 单例
|
|
47
|
+
*/
|
|
48
|
+
export declare const message: MessageState;
|
|
49
|
+
export {};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @h-ai/ui — Message 状态管理
|
|
3
|
+
*
|
|
4
|
+
* 命令式轻量消息提示 API,类似 ElementUI Message。
|
|
5
|
+
* 与 Toast 类似但视觉风格不同:顶部居中滑入、带类型图标、可自动关闭。
|
|
6
|
+
* @module message.svelte
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Message 状态
|
|
10
|
+
*/
|
|
11
|
+
class MessageState {
|
|
12
|
+
items = $state([]);
|
|
13
|
+
timers = new Map();
|
|
14
|
+
/**
|
|
15
|
+
* 添加消息
|
|
16
|
+
*/
|
|
17
|
+
add(options) {
|
|
18
|
+
const opts = typeof options === 'string' ? { message: options } : options;
|
|
19
|
+
const id = Math.random().toString(36).slice(2, 9);
|
|
20
|
+
const item = {
|
|
21
|
+
id,
|
|
22
|
+
message: opts.message,
|
|
23
|
+
type: opts.type ?? 'info',
|
|
24
|
+
duration: opts.duration ?? 3000,
|
|
25
|
+
closable: opts.closable ?? false,
|
|
26
|
+
};
|
|
27
|
+
this.items = [...this.items, item];
|
|
28
|
+
if (item.duration > 0) {
|
|
29
|
+
const timer = setTimeout(() => {
|
|
30
|
+
this.timers.delete(id);
|
|
31
|
+
this.remove(id);
|
|
32
|
+
}, item.duration);
|
|
33
|
+
this.timers.set(id, timer);
|
|
34
|
+
}
|
|
35
|
+
return id;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* 移除消息
|
|
39
|
+
*/
|
|
40
|
+
remove(id) {
|
|
41
|
+
const timer = this.timers.get(id);
|
|
42
|
+
if (timer) {
|
|
43
|
+
clearTimeout(timer);
|
|
44
|
+
this.timers.delete(id);
|
|
45
|
+
}
|
|
46
|
+
this.items = this.items.filter(i => i.id !== id);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* 清空所有消息
|
|
50
|
+
*/
|
|
51
|
+
clear() {
|
|
52
|
+
for (const timer of this.timers.values()) {
|
|
53
|
+
clearTimeout(timer);
|
|
54
|
+
}
|
|
55
|
+
this.timers.clear();
|
|
56
|
+
this.items = [];
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* 快捷方法
|
|
60
|
+
*/
|
|
61
|
+
info(message, duration) {
|
|
62
|
+
return this.add({ message, type: 'info', duration });
|
|
63
|
+
}
|
|
64
|
+
success(message, duration) {
|
|
65
|
+
return this.add({ message, type: 'success', duration });
|
|
66
|
+
}
|
|
67
|
+
warning(message, duration) {
|
|
68
|
+
return this.add({ message, type: 'warning', duration });
|
|
69
|
+
}
|
|
70
|
+
error(message, duration) {
|
|
71
|
+
return this.add({ message, type: 'error', duration });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Message 单例
|
|
76
|
+
*/
|
|
77
|
+
export const message = new MessageState();
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @h-ai/ui — MessageBox 状态管理
|
|
3
|
+
*
|
|
4
|
+
* 命令式弹框 API,类似 ElementUI MessageBox。
|
|
5
|
+
* 调用 `messageBox.confirm(...)` 或 `messageBox.alert(...)` 返回 Promise<boolean>,
|
|
6
|
+
* 组件内部通过渲染 MessageBoxContainer 消费状态。
|
|
7
|
+
* @module messagebox.svelte
|
|
8
|
+
*/
|
|
9
|
+
import type { MessageBoxIconType, MessageBoxOptions } from './types.js';
|
|
10
|
+
/**
|
|
11
|
+
* 内部弹框条目
|
|
12
|
+
*/
|
|
13
|
+
export interface MessageBoxItem {
|
|
14
|
+
id: string;
|
|
15
|
+
title: string;
|
|
16
|
+
message: string;
|
|
17
|
+
type: 'confirm' | 'alert';
|
|
18
|
+
iconType: MessageBoxIconType;
|
|
19
|
+
confirmText: string;
|
|
20
|
+
cancelText: string;
|
|
21
|
+
confirmVariant: 'default' | 'primary' | 'warning' | 'error';
|
|
22
|
+
showCancel: boolean;
|
|
23
|
+
closeOnClickModal: boolean;
|
|
24
|
+
showClose: boolean;
|
|
25
|
+
loading: boolean;
|
|
26
|
+
/** 外部 resolve 回调 */
|
|
27
|
+
resolve: (value: boolean) => void;
|
|
28
|
+
/** 可选的 beforeClose 钩子 */
|
|
29
|
+
beforeClose?: MessageBoxOptions['beforeClose'];
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* MessageBox 状态
|
|
33
|
+
*/
|
|
34
|
+
declare class MessageBoxState {
|
|
35
|
+
items: MessageBoxItem[];
|
|
36
|
+
private createItem;
|
|
37
|
+
/**
|
|
38
|
+
* 确认弹框(双按钮,返回 Promise<boolean>)
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```ts
|
|
42
|
+
* const ok = await messageBox.confirm({
|
|
43
|
+
* title: '提示',
|
|
44
|
+
* message: '确定要删除吗?',
|
|
45
|
+
* confirmText: '确定',
|
|
46
|
+
* cancelText: '取消',
|
|
47
|
+
* })
|
|
48
|
+
* if (ok) { ... }
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
confirm(options: MessageBoxOptions | string): Promise<boolean>;
|
|
52
|
+
/**
|
|
53
|
+
* 提示弹框(单按钮,返回 Promise<boolean>)
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```ts
|
|
57
|
+
* await messageBox.alert({
|
|
58
|
+
* title: '提示',
|
|
59
|
+
* message: '操作成功',
|
|
60
|
+
* })
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
alert(options: MessageBoxOptions | string): Promise<boolean>;
|
|
64
|
+
/**
|
|
65
|
+
* 内部方法:处理确认
|
|
66
|
+
*/
|
|
67
|
+
handleConfirm(item: MessageBoxItem): Promise<void>;
|
|
68
|
+
/**
|
|
69
|
+
* 内部方法:处理取消
|
|
70
|
+
*/
|
|
71
|
+
handleCancel(item: MessageBoxItem): Promise<void>;
|
|
72
|
+
/**
|
|
73
|
+
* 内部方法:处理关闭(点击关闭按钮或遮罩)
|
|
74
|
+
*/
|
|
75
|
+
handleClose(item: MessageBoxItem): Promise<void>;
|
|
76
|
+
private remove;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* MessageBox 单例
|
|
80
|
+
*/
|
|
81
|
+
export declare const messageBox: MessageBoxState;
|
|
82
|
+
export {};
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @h-ai/ui — MessageBox 状态管理
|
|
3
|
+
*
|
|
4
|
+
* 命令式弹框 API,类似 ElementUI MessageBox。
|
|
5
|
+
* 调用 `messageBox.confirm(...)` 或 `messageBox.alert(...)` 返回 Promise<boolean>,
|
|
6
|
+
* 组件内部通过渲染 MessageBoxContainer 消费状态。
|
|
7
|
+
* @module messagebox.svelte
|
|
8
|
+
*/
|
|
9
|
+
import { uiM } from './messages.js';
|
|
10
|
+
/**
|
|
11
|
+
* MessageBox 状态
|
|
12
|
+
*/
|
|
13
|
+
class MessageBoxState {
|
|
14
|
+
items = $state([]);
|
|
15
|
+
createItem(options) {
|
|
16
|
+
return new Promise((resolve) => {
|
|
17
|
+
const id = Math.random().toString(36).slice(2, 9);
|
|
18
|
+
const isAlert = options.type === 'alert';
|
|
19
|
+
const item = {
|
|
20
|
+
id,
|
|
21
|
+
title: options.title ?? uiM('messagebox_title'),
|
|
22
|
+
message: options.message,
|
|
23
|
+
type: options.type ?? 'confirm',
|
|
24
|
+
iconType: options.iconType ?? (isAlert ? 'info' : 'warning'),
|
|
25
|
+
confirmText: options.confirmText ?? '',
|
|
26
|
+
cancelText: options.cancelText ?? '',
|
|
27
|
+
confirmVariant: options.confirmVariant ?? 'primary',
|
|
28
|
+
showCancel: options.showCancel ?? !isAlert,
|
|
29
|
+
closeOnClickModal: options.closeOnClickModal ?? true,
|
|
30
|
+
showClose: options.showClose ?? true,
|
|
31
|
+
loading: false,
|
|
32
|
+
resolve,
|
|
33
|
+
beforeClose: options.beforeClose,
|
|
34
|
+
};
|
|
35
|
+
this.items = [...this.items, item];
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* 确认弹框(双按钮,返回 Promise<boolean>)
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```ts
|
|
43
|
+
* const ok = await messageBox.confirm({
|
|
44
|
+
* title: '提示',
|
|
45
|
+
* message: '确定要删除吗?',
|
|
46
|
+
* confirmText: '确定',
|
|
47
|
+
* cancelText: '取消',
|
|
48
|
+
* })
|
|
49
|
+
* if (ok) { ... }
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
confirm(options) {
|
|
53
|
+
const opts = typeof options === 'string' ? { message: options } : options;
|
|
54
|
+
return this.createItem({ type: 'confirm', ...opts });
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* 提示弹框(单按钮,返回 Promise<boolean>)
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```ts
|
|
61
|
+
* await messageBox.alert({
|
|
62
|
+
* title: '提示',
|
|
63
|
+
* message: '操作成功',
|
|
64
|
+
* })
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
alert(options) {
|
|
68
|
+
const opts = typeof options === 'string' ? { message: options } : options;
|
|
69
|
+
return this.createItem({ type: 'alert', showCancel: false, ...opts });
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* 内部方法:处理确认
|
|
73
|
+
*/
|
|
74
|
+
async handleConfirm(item) {
|
|
75
|
+
if (item.beforeClose) {
|
|
76
|
+
const canClose = await item.beforeClose('confirm', {
|
|
77
|
+
setLoading: (loading) => {
|
|
78
|
+
item.loading = loading;
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
if (canClose === false) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
this.remove(item.id);
|
|
86
|
+
item.resolve(true);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* 内部方法:处理取消
|
|
90
|
+
*/
|
|
91
|
+
async handleCancel(item) {
|
|
92
|
+
if (item.beforeClose) {
|
|
93
|
+
const canClose = await item.beforeClose('cancel', {
|
|
94
|
+
setLoading: (loading) => {
|
|
95
|
+
item.loading = loading;
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
if (canClose === false) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
this.remove(item.id);
|
|
103
|
+
item.resolve(false);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* 内部方法:处理关闭(点击关闭按钮或遮罩)
|
|
107
|
+
*/
|
|
108
|
+
async handleClose(item) {
|
|
109
|
+
if (item.beforeClose) {
|
|
110
|
+
const canClose = await item.beforeClose('close', {
|
|
111
|
+
setLoading: (loading) => {
|
|
112
|
+
item.loading = loading;
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
if (canClose === false) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
this.remove(item.id);
|
|
120
|
+
item.resolve(false);
|
|
121
|
+
}
|
|
122
|
+
remove(id) {
|
|
123
|
+
this.items = this.items.filter(i => i.id !== id);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* MessageBox 单例
|
|
128
|
+
*/
|
|
129
|
+
export const messageBox = new MessageBoxState();
|
package/dist/messages/en-US.json
CHANGED