aegon-lv97 1.0.0

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.
Files changed (78) hide show
  1. package/gen-ai/build-report/chartStreamRefs.ts +227 -0
  2. package/gen-ai/build-report/coherence-check/index.vue +559 -0
  3. package/gen-ai/build-report/components/ReportChapterWorkspace.vue +4571 -0
  4. package/gen-ai/build-report/components/ReportConfigPanel.vue +727 -0
  5. package/gen-ai/build-report/components/ReportGenerateSettingsPanel.vue +2361 -0
  6. package/gen-ai/build-report/components/ReportGeneratingPanel.vue +1765 -0
  7. package/gen-ai/build-report/components/ReportPromptPanel.vue +625 -0
  8. package/gen-ai/build-report/components/ReportSourceFileSummary.vue +665 -0
  9. package/gen-ai/build-report/components/ReportSourcesPanel.vue +1327 -0
  10. package/gen-ai/build-report/components/ReportTemplateDetail.vue +191 -0
  11. package/gen-ai/build-report/components/ReportTemplateEditorFormPanel.vue +667 -0
  12. package/gen-ai/build-report/components/ReportTemplatePicker.vue +302 -0
  13. package/gen-ai/build-report/components/ReportWorkflowSidebar.vue +148 -0
  14. package/gen-ai/build-report/components/paramsSetting.vue +886 -0
  15. package/gen-ai/build-report/components/reportEdit.vue +2283 -0
  16. package/gen-ai/build-report/index.vue +856 -0
  17. package/gen-ai/build-report/report-domain.ts +1133 -0
  18. package/gen-ai/build-report/report-template-data.ts +295 -0
  19. package/gen-ai/build-report/useReportWorkflow.ts +1386 -0
  20. package/gen-ai/common/assets/NotoSans-SC.woff2 +0 -0
  21. package/gen-ai/common/assets/NotoSans-TC.woff2 +0 -0
  22. package/gen-ai/common/assets/NotoSans.woff2 +0 -0
  23. package/gen-ai/common/assets/fonts.css +278 -0
  24. package/gen-ai/common/assets/pagination.css +163 -0
  25. package/gen-ai/common/directives/btnLoading.ts +455 -0
  26. package/gen-ai/common/directives/debounce.ts +98 -0
  27. package/gen-ai/common/directives/index.ts +30 -0
  28. package/gen-ai/common/en.md +417 -0
  29. package/gen-ai/common/index.ts +36 -0
  30. package/gen-ai/common/types/component-expose.ts +9 -0
  31. package/gen-ai/common/utils/api-res-status.ts +7 -0
  32. package/gen-ai/common/utils/navigation.ts +59 -0
  33. package/gen-ai/common/utils/remote-request.ts +45 -0
  34. package/gen-ai/common/utils/scroll.ts +577 -0
  35. package/gen-ai/components/AIExport/docx-export.ts +950 -0
  36. package/gen-ai/components/AIExport/docxExport.ts +2566 -0
  37. package/gen-ai/components/AIExport/index.ts +52 -0
  38. package/gen-ai/components/AIExport/parseMarkdownHtml.ts +653 -0
  39. package/gen-ai/components/CapsuleScrollbar.vue +149 -0
  40. package/gen-ai/components/ChapterTitleScroll.vue +468 -0
  41. package/gen-ai/components/EntryHeroNavCards.vue +379 -0
  42. package/gen-ai/components/GenAiPagination.vue +52 -0
  43. package/gen-ai/components/ModelCard.vue +1114 -0
  44. package/gen-ai/components/Preview/FilePreviewPage.vue +949 -0
  45. package/gen-ai/components/Preview/PreviewCapsuleScroll.vue +205 -0
  46. package/gen-ai/components/Preview/PreviewOutlineTree.vue +148 -0
  47. package/gen-ai/components/Preview/index.ts +11 -0
  48. package/gen-ai/components/Preview/preview.ts +724 -0
  49. package/gen-ai/components/PromptField.vue +505 -0
  50. package/gen-ai/components/RecentReportsTable.vue +974 -0
  51. package/gen-ai/components/ReportCollapsibleSection.vue +172 -0
  52. package/gen-ai/components/ReportHistoryList.vue +637 -0
  53. package/gen-ai/components/ReportSectionConfirmPopover.vue +251 -0
  54. package/gen-ai/components/editor/ChapterTiptapEditor.vue +3718 -0
  55. package/gen-ai/components/editor/StructureBlockView.vue +441 -0
  56. package/gen-ai/components/editor/TiptapToolbar.vue +1036 -0
  57. package/gen-ai/components/editor/TypewriterDocEditor.vue +422 -0
  58. package/gen-ai/components/editor/_StructureOrgNode.vue +326 -0
  59. package/gen-ai/components/editor/index.ts +28 -0
  60. package/gen-ai/components/editor/parseMarkdownHtml.ts +754 -0
  61. package/gen-ai/components/editor/reviewCitationTip.ts +163 -0
  62. package/gen-ai/gen-entry/api/index.ts +32 -0
  63. package/gen-ai/gen-entry/index.vue +287 -0
  64. package/gen-ai/gen-entry/types/index.ts +4 -0
  65. package/gen-ai/i.mark +2 -0
  66. package/gen-ai/management-center/index.vue +1815 -0
  67. package/gen-ai/management-center/mockData.ts +248 -0
  68. package/gen-ai/management-center/types/index.ts +37 -0
  69. package/gen-ai/model-manager/TemplateEditorFormPanel.vue +661 -0
  70. package/gen-ai/model-manager/api/index.ts +32 -0
  71. package/gen-ai/model-manager/api/mockApi.ts +33 -0
  72. package/gen-ai/model-manager/index.vue +1583 -0
  73. package/gen-ai/model-manager/mockData.ts +177 -0
  74. package/gen-ai/model-manager/template-detail-shared.ts +154 -0
  75. package/gen-ai/model-manager/template-editor.vue +345 -0
  76. package/gen-ai/model-manager/types/index.ts +40 -0
  77. package/gen-ai/model-manager/utils.ts +25 -0
  78. package/package.json +12 -0
@@ -0,0 +1,455 @@
1
+ import { nextTick, type Directive, type DirectiveBinding } from 'vue'
2
+
3
+ const BTN_LOADING_KEY = Symbol('vAiBtnLoading')
4
+ const REFRESH_GEN_KEY = Symbol('vAiBtnLoadingRefreshGen')
5
+
6
+ const LOADING_STYLE_ID = 'v-ai-btn-loading-style'
7
+ const DEFAULT_MIN_LOCK_MS = 1000
8
+ const DEFAULT_AUTO_CLOSE_MS = 30_000
9
+ const HOST_ID_ATTR = 'data-v-ai-btn-loading-id'
10
+
11
+ interface BtnLoadingSession {
12
+ id: string
13
+ host: HTMLButtonElement
14
+ startedAt: number
15
+ minLockMs: number
16
+ locked: boolean
17
+ closeRequested: boolean
18
+ closeRequestedAt?: number
19
+ releaseTimer?: number
20
+ autoCloseTimer?: number
21
+ guardTimer?: number
22
+ classObserver?: MutationObserver
23
+ }
24
+
25
+ interface BtnLoadingBindingMeta {
26
+ listener: (event: Event) => void
27
+ event: string
28
+ binding: DirectiveBinding
29
+ }
30
+
31
+ /** closeAiBtnLoading() 主动关闭;未调用时 DEFAULT_AUTO_CLOSE_MS 后自动关闭 */
32
+ const activeSessions = new Map<string, BtnLoadingSession>()
33
+ const hostToSessionId = new Map<HTMLButtonElement, string>()
34
+
35
+ function resolveButtonHost(el: HTMLElement): HTMLButtonElement {
36
+ if (el.tagName === 'BUTTON') return el as HTMLButtonElement
37
+ const nested = el.querySelector<HTMLButtonElement>('button.el-button')
38
+ return (nested ?? el) as HTMLButtonElement
39
+ }
40
+
41
+ function ensureHostId(host: HTMLButtonElement, id: string) {
42
+ host.setAttribute(HOST_ID_ATTR, id)
43
+ }
44
+
45
+ function getSessionById(id: string): BtnLoadingSession | undefined {
46
+ return activeSessions.get(id)
47
+ }
48
+
49
+ /** Vue 重渲染可能替换 button 节点,按 session id 重新绑定 DOM */
50
+ function rebindSessionHost(session: BtnLoadingSession): HTMLButtonElement | null {
51
+ if (session.host.isConnected) {
52
+ ensureHostId(session.host, session.id)
53
+ hostToSessionId.set(session.host, session.id)
54
+ return session.host
55
+ }
56
+
57
+ const byAttr = document.querySelector<HTMLButtonElement>(
58
+ `[${HOST_ID_ATTR}="${CSS.escape(session.id)}"]`,
59
+ )
60
+ if (byAttr) {
61
+ session.host = byAttr
62
+ hostToSessionId.set(byAttr, session.id)
63
+ return byAttr
64
+ }
65
+
66
+ return null
67
+ }
68
+
69
+ function getSessionByHost(host: HTMLButtonElement): BtnLoadingSession | undefined {
70
+ const id = hostToSessionId.get(host) ?? host.getAttribute(HOST_ID_ATTR) ?? undefined
71
+ if (!id) return undefined
72
+
73
+ const session = getSessionById(id)
74
+ if (!session) return undefined
75
+
76
+ session.host = host
77
+ hostToSessionId.set(host, id)
78
+ return session
79
+ }
80
+
81
+ function findSessionForHost(host: HTMLButtonElement): BtnLoadingSession | undefined {
82
+ const direct = getSessionByHost(host)
83
+ if (direct) return direct
84
+
85
+ const id = host.getAttribute(HOST_ID_ATTR)
86
+ if (id) {
87
+ const session = getSessionById(id)
88
+ if (session) {
89
+ session.host = host
90
+ hostToSessionId.set(host, id)
91
+ return session
92
+ }
93
+ }
94
+
95
+ for (const session of activeSessions.values()) {
96
+ const rebound = rebindSessionHost(session)
97
+ if (rebound === host) return session
98
+ }
99
+
100
+ return undefined
101
+ }
102
+
103
+ function resolveCloseTargets(target?: HTMLElement | string): HTMLButtonElement[] {
104
+ if (target === undefined) {
105
+ return [...activeSessions.values()]
106
+ .map((session) => rebindSessionHost(session))
107
+ .filter((host): host is HTMLButtonElement => host !== null)
108
+ }
109
+ if (typeof target === 'string') {
110
+ return Array.from(document.querySelectorAll<HTMLElement>(target)).map((node) =>
111
+ resolveButtonHost(node),
112
+ )
113
+ }
114
+ return [resolveButtonHost(target)]
115
+ }
116
+
117
+ function resolveMinLockMs(arg?: string): number {
118
+ if (arg && /^\d+$/.test(arg)) return Number(arg)
119
+ return DEFAULT_MIN_LOCK_MS
120
+ }
121
+
122
+ function ensureLoadingStyles() {
123
+ if (typeof document === 'undefined') return
124
+
125
+ const css = `
126
+ .el-button.v-ai-btn-loading-host,
127
+ button.v-ai-btn-loading-host {
128
+ position: relative !important;
129
+ box-sizing: border-box;
130
+ cursor: not-allowed !important;
131
+ }
132
+ .el-button.v-ai-btn-loading-host {
133
+ display: inline-flex !important;
134
+ align-items: center !important;
135
+ justify-content: center !important;
136
+ }
137
+ .el-button.v-ai-btn-loading-host::after,
138
+ button.v-ai-btn-loading-host::after {
139
+ content: '';
140
+ display: inline-block;
141
+ width: 14px;
142
+ height: 14px;
143
+ margin-left: 6px;
144
+ flex: 0 0 auto;
145
+ border: 2px solid currentColor;
146
+ border-right-color: transparent;
147
+ border-radius: 50%;
148
+ animation: v-ai-btn-loading-spin 0.75s linear infinite;
149
+ vertical-align: middle;
150
+ opacity: 1 !important;
151
+ }
152
+ @keyframes v-ai-btn-loading-spin {
153
+ to { transform: rotate(360deg); }
154
+ }
155
+ `
156
+
157
+ const existing = document.getElementById(LOADING_STYLE_ID)
158
+ if (existing) {
159
+ existing.textContent = css
160
+ return
161
+ }
162
+
163
+ const style = document.createElement('style')
164
+ style.id = LOADING_STYLE_ID
165
+ style.textContent = css
166
+ document.head.appendChild(style)
167
+ }
168
+
169
+ function bumpRefreshGen(host: HTMLButtonElement): number {
170
+ const next = ((host as HTMLElement & { [REFRESH_GEN_KEY]?: number })[REFRESH_GEN_KEY] ?? 0) + 1
171
+ ;(host as HTMLElement & { [REFRESH_GEN_KEY]?: number })[REFRESH_GEN_KEY] = next
172
+ return next
173
+ }
174
+
175
+ function applyLoadingVisual(host: HTMLButtonElement, sessionId: string) {
176
+ ensureLoadingStyles()
177
+ ensureHostId(host, sessionId)
178
+ host.classList.add('v-ai-btn-loading-host', 'is-disabled')
179
+ host.setAttribute('aria-busy', 'true')
180
+ host.setAttribute('aria-disabled', 'true')
181
+ host.dataset.aiBtnLoading = '1'
182
+ }
183
+
184
+ function clearLoadingVisual(host: HTMLButtonElement) {
185
+ host.classList.remove('v-ai-btn-loading-host', 'is-disabled')
186
+ host.removeAttribute('aria-busy')
187
+ host.removeAttribute('aria-disabled')
188
+ host.removeAttribute(HOST_ID_ATTR)
189
+ delete host.dataset.aiBtnLoading
190
+ }
191
+
192
+ function ensureLoadingVisual(session: BtnLoadingSession) {
193
+ if (!activeSessions.has(session.id)) return
194
+ // 已请求关闭则不再强行补回 class(避免与主动 clear 打架)
195
+ if (session.closeRequested) return
196
+
197
+ const host = rebindSessionHost(session)
198
+ if (!host) return
199
+
200
+ if (!host.classList.contains('v-ai-btn-loading-host')) {
201
+ applyLoadingVisual(host, session.id)
202
+ } else {
203
+ ensureHostId(host, session.id)
204
+ }
205
+ }
206
+
207
+ function startClassGuard(session: BtnLoadingSession) {
208
+ stopClassGuard(session)
209
+
210
+ const guard = () => {
211
+ if (!activeSessions.has(session.id)) return
212
+ ensureLoadingVisual(session)
213
+ }
214
+
215
+ const host = rebindSessionHost(session)
216
+ if (host) {
217
+ session.classObserver = new MutationObserver(guard)
218
+ session.classObserver.observe(host, { attributes: true, attributeFilter: ['class'] })
219
+ }
220
+
221
+ session.guardTimer = window.setInterval(guard, 120)
222
+ }
223
+
224
+ function stopClassGuard(session: BtnLoadingSession) {
225
+ session.classObserver?.disconnect()
226
+ delete session.classObserver
227
+ if (session.guardTimer !== undefined) {
228
+ window.clearInterval(session.guardTimer)
229
+ delete session.guardTimer
230
+ }
231
+ }
232
+
233
+ function scheduleLoadingRefresh(session: BtnLoadingSession) {
234
+ const host = rebindSessionHost(session)
235
+ if (!host) return
236
+
237
+ const gen = (host as HTMLElement & { [REFRESH_GEN_KEY]?: number })[REFRESH_GEN_KEY] ?? 0
238
+ void nextTick(() => {
239
+ if (!activeSessions.has(session.id)) return
240
+ if ((host as HTMLElement & { [REFRESH_GEN_KEY]?: number })[REFRESH_GEN_KEY] !== gen) return
241
+ ensureLoadingVisual(session)
242
+ })
243
+ }
244
+
245
+ function clearReleaseTimer(session: BtnLoadingSession) {
246
+ if (session.releaseTimer !== undefined) {
247
+ window.clearTimeout(session.releaseTimer)
248
+ session.releaseTimer = undefined
249
+ }
250
+ }
251
+
252
+ function clearAutoCloseTimer(session: BtnLoadingSession) {
253
+ if (session.autoCloseTimer !== undefined) {
254
+ window.clearTimeout(session.autoCloseTimer)
255
+ session.autoCloseTimer = undefined
256
+ }
257
+ }
258
+
259
+ function releaseSession(session: BtnLoadingSession) {
260
+ if (!activeSessions.has(session.id)) return
261
+
262
+ clearReleaseTimer(session)
263
+ clearAutoCloseTimer(session)
264
+ stopClassGuard(session)
265
+
266
+ const host = rebindSessionHost(session)
267
+ if (host) {
268
+ clearLoadingVisual(host)
269
+ hostToSessionId.delete(host)
270
+ }
271
+
272
+ activeSessions.delete(session.id)
273
+ }
274
+
275
+ /** closeAiBtnLoading() 或超时触发;从点击起至少 minLockMs 后才真正关闭 */
276
+ function scheduleRelease(session: BtnLoadingSession) {
277
+ clearReleaseTimer(session)
278
+ const elapsed = Date.now() - session.startedAt
279
+ const remain = Math.max(0, session.minLockMs - elapsed)
280
+
281
+ session.releaseTimer = window.setTimeout(() => {
282
+ session.releaseTimer = undefined
283
+ session.locked = false
284
+ releaseSession(session)
285
+ }, remain)
286
+ }
287
+
288
+ /** 未调用 closeAiBtnLoading() 时,点击后 DEFAULT_AUTO_CLOSE_MS 自动关闭 */
289
+ function scheduleAutoClose(session: BtnLoadingSession) {
290
+ clearAutoCloseTimer(session)
291
+ session.autoCloseTimer = window.setTimeout(() => {
292
+ session.autoCloseTimer = undefined
293
+ if (!activeSessions.has(session.id)) return
294
+ requestClose(session)
295
+ }, DEFAULT_AUTO_CLOSE_MS)
296
+ }
297
+
298
+ function requestClose(session: BtnLoadingSession) {
299
+ if (session.closeRequested) return
300
+ session.closeRequested = true
301
+ session.closeRequestedAt = Date.now()
302
+ clearAutoCloseTimer(session)
303
+ scheduleRelease(session)
304
+ }
305
+
306
+ function activateLoading(host: HTMLButtonElement, minLockMs: number) {
307
+ const existing = findSessionForHost(host)
308
+ if (existing?.locked && !existing.closeRequested) return
309
+
310
+ // 上一轮已请求关闭但尚未 release:先清掉,避免旧 releaseTimer 误杀新 session
311
+ if (existing) {
312
+ releaseSession(existing)
313
+ }
314
+
315
+ const id = `bl-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`
316
+ const session: BtnLoadingSession = {
317
+ id,
318
+ host,
319
+ startedAt: Date.now(),
320
+ minLockMs,
321
+ locked: true,
322
+ closeRequested: false,
323
+ }
324
+
325
+ activeSessions.set(id, session)
326
+ hostToSessionId.set(host, id)
327
+ bumpRefreshGen(host)
328
+ applyLoadingVisual(host, id)
329
+ startClassGuard(session)
330
+ scheduleLoadingRefresh(session)
331
+ scheduleAutoClose(session)
332
+ }
333
+
334
+ /** 请求关闭;未调用时 30s 后自动关闭;调用后至少再保持 minLockMs(默认 1s,从点击起算) */
335
+ export function closeAiBtnLoading(target?: HTMLElement | string) {
336
+ if (target === undefined) {
337
+ ;[...activeSessions.values()].forEach((session) => {
338
+ requestClose(session)
339
+ const host = rebindSessionHost(session)
340
+ if (host) clearLoadingVisual(host)
341
+ })
342
+ return
343
+ }
344
+
345
+ const hosts = resolveCloseTargets(target)
346
+ const matched = new Set<BtnLoadingSession>()
347
+
348
+ for (const host of hosts) {
349
+ const session = findSessionForHost(host)
350
+ if (session) matched.add(session)
351
+ }
352
+
353
+ // DOM 重挂后 hostToSessionId 可能失联:按选择器兜底匹配仍存活的 session
354
+ if (typeof target === 'string') {
355
+ for (const session of activeSessions.values()) {
356
+ const host = rebindSessionHost(session)
357
+ if (host?.matches(target)) matched.add(session)
358
+ }
359
+ }
360
+
361
+ if (matched.size > 0) {
362
+ matched.forEach((session) => requestClose(session))
363
+ }
364
+
365
+ // 立即清样式:closeRequested 后 classGuard 不再补回,避免 session 失联时一直转圈
366
+ hosts.forEach((host) => clearLoadingVisual(host))
367
+ }
368
+
369
+ /** 是否存在未关闭的 loading(便于调试/测试) */
370
+ export function hasActiveAiBtnLoading(target?: HTMLElement | string): boolean {
371
+ if (target === undefined) return activeSessions.size > 0
372
+
373
+ return resolveCloseTargets(target).some((host) => {
374
+ const session = findSessionForHost(host)
375
+ return !!session && !session.closeRequested
376
+ })
377
+ }
378
+
379
+ /**
380
+ * v-aiBtnLoading — 按钮 loading(与 v-aiDebounce 解耦)
381
+ * - v-aiBtnLoading 默认最短锁定 1s(session 释放;样式在 closeAiBtnLoading 时立即清除)
382
+ * - v-aiBtnLoading:1500 自定义最短毫秒
383
+ * - 未调用 closeAiBtnLoading():30s 后自动关闭
384
+ * - closeAiBtnLoading():立即去掉转圈样式,并请求释放 session
385
+ */
386
+ export const vAiBtnLoading: Directive<HTMLElement> = {
387
+ mounted(el: HTMLElement, binding: DirectiveBinding) {
388
+ const host = resolveButtonHost(el)
389
+ const event = binding.arg && !/^\d+$/.test(binding.arg) ? binding.arg : 'click'
390
+
391
+ const listener = (e: Event) => {
392
+ const meta = (el as HTMLElement & { [BTN_LOADING_KEY]?: BtnLoadingBindingMeta })[BTN_LOADING_KEY]
393
+ const currentBinding = meta?.binding ?? binding
394
+ const minLockMs = resolveMinLockMs(
395
+ currentBinding.arg && /^\d+$/.test(currentBinding.arg) ? currentBinding.arg : undefined,
396
+ )
397
+
398
+ const session = findSessionForHost(host)
399
+ if (session?.locked && !session.closeRequested) {
400
+ e.preventDefault()
401
+ e.stopImmediatePropagation()
402
+ return
403
+ }
404
+
405
+ activateLoading(host, minLockMs)
406
+ }
407
+
408
+ host.addEventListener(event, listener, true)
409
+ ;(el as HTMLElement & { [BTN_LOADING_KEY]?: BtnLoadingBindingMeta })[BTN_LOADING_KEY] = {
410
+ listener,
411
+ event,
412
+ binding,
413
+ }
414
+
415
+ const existing = findSessionForHost(host)
416
+ if (existing && !existing.closeRequested) {
417
+ existing.host = host
418
+ hostToSessionId.set(host, existing.id)
419
+ ensureLoadingVisual(existing)
420
+ startClassGuard(existing)
421
+ }
422
+ },
423
+ updated(el: HTMLElement, binding: DirectiveBinding) {
424
+ const host = resolveButtonHost(el)
425
+ const meta = (el as HTMLElement & { [BTN_LOADING_KEY]?: BtnLoadingBindingMeta })[BTN_LOADING_KEY]
426
+ if (meta) meta.binding = binding
427
+
428
+ const session = findSessionForHost(host)
429
+ if (!session || session.closeRequested) return
430
+
431
+ session.host = host
432
+ hostToSessionId.set(host, session.id)
433
+ ensureLoadingVisual(session)
434
+ scheduleLoadingRefresh(session)
435
+ },
436
+ unmounted(el: HTMLElement) {
437
+ const host = resolveButtonHost(el)
438
+ const meta = (el as HTMLElement & { [BTN_LOADING_KEY]?: BtnLoadingBindingMeta })[BTN_LOADING_KEY]
439
+ if (meta) {
440
+ host.removeEventListener(meta.event, meta.listener, true)
441
+ delete (el as HTMLElement & { [BTN_LOADING_KEY]?: BtnLoadingBindingMeta })[BTN_LOADING_KEY]
442
+ }
443
+
444
+ const session = findSessionForHost(host)
445
+ if (!session) return
446
+
447
+ if (session.closeRequested) {
448
+ releaseSession(session)
449
+ return
450
+ }
451
+
452
+ stopClassGuard(session)
453
+ hostToSessionId.delete(host)
454
+ },
455
+ }
@@ -0,0 +1,98 @@
1
+ import type { Directive, DirectiveBinding } from 'vue'
2
+
3
+ const DEBOUNCE_KEY = Symbol('vAiDebounce')
4
+
5
+ /** 默认防重间隔 2s;v-aiDebounce:800 自定义毫秒 */
6
+ const DEFAULT_WAIT_MS = 2000
7
+ const DEFAULT_EVENT = 'click'
8
+
9
+ export type DebounceHandler = (event: Event) => unknown
10
+ export type DebounceBindingValue = DebounceHandler | undefined
11
+ export type DebounceDirective = Directive<HTMLElement, DebounceBindingValue>
12
+ export type DebounceDirectiveBinding = DirectiveBinding<DebounceBindingValue>
13
+
14
+ interface DebounceBindingMeta {
15
+ listener: (event: Event) => void
16
+ event: string
17
+ binding: DebounceDirectiveBinding
18
+ }
19
+
20
+ function resolveButtonHost(el: HTMLElement): HTMLElement {
21
+ if (el.tagName === 'BUTTON') return el
22
+ const nested = el.querySelector<HTMLElement>('button.el-button')
23
+ return nested ?? el
24
+ }
25
+
26
+ function resolveWait(arg?: string): number {
27
+ if (arg && /^\d+$/.test(arg)) return Number(arg)
28
+ return DEFAULT_WAIT_MS
29
+ }
30
+
31
+ function resolveEvent(arg?: string): string {
32
+ if (arg && !/^\d+$/.test(arg)) return arg
33
+ return DEFAULT_EVENT
34
+ }
35
+
36
+ /**
37
+ * v-aiDebounce — 纯点击防重,与 loading 解耦
38
+ * - v-aiDebounce @click="fn" 默认 2s
39
+ * - v-aiDebounce:800 @click="fn" 自定义间隔
40
+ * - v-aiDebounce:800="fn" 绑定 handler(拦截原生 @click)
41
+ */
42
+ export const vAiDebounce: DebounceDirective = {
43
+ mounted(el: HTMLElement, binding: DebounceDirectiveBinding) {
44
+ const host = resolveButtonHost(el)
45
+ let locked = false
46
+
47
+ const listener = (e: Event) => {
48
+ const meta = (host as HTMLElement & { [DEBOUNCE_KEY]?: DebounceBindingMeta })[DEBOUNCE_KEY]
49
+ const currentBinding = meta?.binding ?? binding
50
+ const wait = resolveWait(currentBinding.arg)
51
+ const handler =
52
+ typeof currentBinding.value === 'function' ? currentBinding.value : undefined
53
+
54
+ if (locked) {
55
+ e.preventDefault()
56
+ e.stopImmediatePropagation()
57
+ return
58
+ }
59
+
60
+ locked = true
61
+
62
+ if (handler) {
63
+ e.preventDefault()
64
+ e.stopImmediatePropagation()
65
+ Promise.resolve(handler(e)).finally(() => {
66
+ window.setTimeout(() => {
67
+ locked = false
68
+ }, wait)
69
+ })
70
+ return
71
+ }
72
+
73
+ window.setTimeout(() => {
74
+ locked = false
75
+ }, wait)
76
+ }
77
+
78
+ const event = resolveEvent(binding.arg)
79
+ host.addEventListener(event, listener, true)
80
+ ;(host as HTMLElement & { [DEBOUNCE_KEY]?: DebounceBindingMeta })[DEBOUNCE_KEY] = {
81
+ listener,
82
+ event,
83
+ binding,
84
+ }
85
+ },
86
+ updated(el: HTMLElement, binding: DebounceDirectiveBinding) {
87
+ const host = resolveButtonHost(el)
88
+ const meta = (host as HTMLElement & { [DEBOUNCE_KEY]?: DebounceBindingMeta })[DEBOUNCE_KEY]
89
+ if (meta) meta.binding = binding
90
+ },
91
+ unmounted(el: HTMLElement) {
92
+ const host = resolveButtonHost(el)
93
+ const meta = (host as HTMLElement & { [DEBOUNCE_KEY]?: DebounceBindingMeta })[DEBOUNCE_KEY]
94
+ if (!meta) return
95
+ host.removeEventListener(meta.event, meta.listener, true)
96
+ delete (host as HTMLElement & { [DEBOUNCE_KEY]?: DebounceBindingMeta })[DEBOUNCE_KEY]
97
+ },
98
+ }
@@ -0,0 +1,30 @@
1
+ import type { App, Directive } from 'vue'
2
+ import { closeAiBtnLoading, vAiBtnLoading } from './btnLoading'
3
+ import { vAiDebounce } from './debounce'
4
+
5
+ /** GEN-AI 自定义指令(与全局 v-debounce / v-btnLoading 区分命名) */
6
+ const directiveRegistry: ReadonlyArray<{
7
+ name: string
8
+ directive: Directive
9
+ }> = [
10
+ { name: 'aiDebounce', directive: vAiDebounce as Directive },
11
+ { name: 'aiBtnLoading', directive: vAiBtnLoading },
12
+ ]
13
+
14
+ export function registerGenAiDirectives(app: App) {
15
+ directiveRegistry.forEach(({ name, directive }) => {
16
+ app.directive(name, directive)
17
+ })
18
+
19
+ app.config.globalProperties.closeAiBtnLoading = closeAiBtnLoading
20
+ ;(globalThis as typeof globalThis & { closeAiBtnLoading: typeof closeAiBtnLoading }).closeAiBtnLoading =
21
+ closeAiBtnLoading
22
+ }
23
+
24
+ export {
25
+ vAiDebounce,
26
+ type DebounceBindingValue,
27
+ type DebounceDirective,
28
+ type DebounceHandler,
29
+ } from './debounce'
30
+ export { vAiBtnLoading, closeAiBtnLoading, hasActiveAiBtnLoading } from './btnLoading'