aegon-gen 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 (86) hide show
  1. package/package.json +12 -0
  2. package/src/App.vue +3 -0
  3. package/src/api/index.ts +19 -0
  4. package/src/api/modules/gen-ai/gen-entry/index.ts +30 -0
  5. package/src/api/modules/gen-ai/model-manager/index.ts +42 -0
  6. package/src/api/modules/gen-ai/model-manager/mockApi.ts +33 -0
  7. package/src/api/modules/index.ts +98 -0
  8. package/src/api/modules/user/index.ts +4 -0
  9. package/src/api/request.ts +102 -0
  10. package/src/assets/sample-access-icon.png +0 -0
  11. package/src/assets/sample-pie-chart.png +0 -0
  12. package/src/assets/vue.svg +1 -0
  13. package/src/components/CapsuleScrollbar.vue +93 -0
  14. package/src/components/Export/ExcelExport.vue +592 -0
  15. package/src/components/Export/ExcelExport2.vue +494 -0
  16. package/src/components/Export/ExcelExport3.vue +342 -0
  17. package/src/components/Export/ExcelExport4.vue +665 -0
  18. package/src/components/Export/excelExport.js +547 -0
  19. package/src/components/Export/excelExport.ts +551 -0
  20. package/src/components/GEN-AI/index.vue +142 -0
  21. package/src/components/GEN-AI/index1.vue +456 -0
  22. package/src/components/GEN-AI/index10.vue +5 -0
  23. package/src/components/GEN-AI/index2.vue +568 -0
  24. package/src/components/GEN-AI/index3.vue +623 -0
  25. package/src/components/GEN-AI/index4.vue +629 -0
  26. package/src/components/GEN-AI/index5.vue +578 -0
  27. package/src/components/GEN-AI/index6.vue +656 -0
  28. package/src/components/GEN-AI/index7.vue +717 -0
  29. package/src/components/GEN-AI/index8.vue +405 -0
  30. package/src/components/GEN-AI/index9.vue +1065 -0
  31. package/src/components/GEN-AI/types.ts +12 -0
  32. package/src/components/GEN-AI/utils.ts +42 -0
  33. package/src/components/HelloWorld.vue +41 -0
  34. package/src/components/PageCard.vue +7 -0
  35. package/src/components/PageHeader.vue +32 -0
  36. package/src/components/backup/index5 copy.vue +556 -0
  37. package/src/components/backup/index5.vue +620 -0
  38. package/src/components/backup/index9 copy.vue +1029 -0
  39. package/src/components/backup/index9-pro.vue +1065 -0
  40. package/src/components/backup/index9.vue +1057 -0
  41. package/src/components/el-date-picker.vue +64 -0
  42. package/src/directives/btnLoading.ts +427 -0
  43. package/src/directives/debounce copy.ts +670 -0
  44. package/src/directives/debounce.ts +98 -0
  45. package/src/directives/index.ts +25 -0
  46. package/src/layouts/MainLayout.vue +101 -0
  47. package/src/main.ts +85 -0
  48. package/src/router/index.ts +76 -0
  49. package/src/router/menus.ts +28 -0
  50. package/src/style.css +79 -0
  51. package/src/styles/_variables.scss +24 -0
  52. package/src/styles/app-button.css +26 -0
  53. package/src/styles/element-overrides.css +23 -0
  54. package/src/styles/global.css +44 -0
  55. package/src/styles/index.scss +1 -0
  56. package/src/styles/page-card.css +21 -0
  57. package/src/styles/variables.css +26 -0
  58. package/src/test/mock.ts +101 -0
  59. package/src/test/test1.vue +402 -0
  60. package/src/test/test2.vue +1689 -0
  61. package/src/types/gen-ai/gen-entry/index.ts +17 -0
  62. package/src/types/gen-ai/model-manager/index.ts +19 -0
  63. package/src/utils/docxExport.ts +1610 -0
  64. package/src/utils/gen-ai-navigation.ts +37 -0
  65. package/src/utils/gen-ai-scroll.ts +455 -0
  66. package/src/utils/openDataLoaderWordExport.ts +33 -0
  67. package/src/utils/pageScrollbar.ts +115 -0
  68. package/src/utils/randomTranscode.ts +87 -0
  69. package/src/utils/reportPdfExport.ts +44 -0
  70. package/src/views/AdminCenter/index.vue +817 -0
  71. package/src/views/Blank.vue +68 -0
  72. package/src/views/Home.vue +29 -0
  73. package/src/views/ReportCenter/index.vue +1380 -0
  74. package/src/views/TemplateCenter/Knowledge.ts +83 -0
  75. package/src/views/TemplateCenter/data.d.ts +10 -0
  76. package/src/views/TemplateCenter/index.vue +1205 -0
  77. package/src/views/TemplateCenter/service.ts +69 -0
  78. package/src/views/gen-ai/components/RecentReportsTable.vue +193 -0
  79. package/src/views/gen-ai/gen-entry/index.vue +309 -0
  80. package/src/views/gen-ai/gen-entry/mockData.ts +160 -0
  81. package/src/views/gen-ai/management-center/index.vue +53 -0
  82. package/src/views/gen-ai/model-manager/ChapterTitleScroll.vue +275 -0
  83. package/src/views/gen-ai/model-manager/index.vue +1205 -0
  84. package/src/views/gen-ai/model-manager/mockData.ts +122 -0
  85. package/src/views/gen-ai/report-center/index.vue +158 -0
  86. package/src/vite-env.d.ts +38 -0
@@ -0,0 +1,670 @@
1
+ import { nextTick, type AppContext, type ComponentPublicInstance, type Directive, type DirectiveBinding } from 'vue'
2
+ import { ElLoading } from 'element-plus'
3
+
4
+ const DEBOUNCE_KEY = Symbol('vDebounce')
5
+ const WATCH_KEY = Symbol('vDebounceWatch')
6
+ const LOADING_INSTANCE_KEY = Symbol('vDebounceElLoading')
7
+ const APP_CTX_KEY = Symbol('vDebounceAppContext')
8
+ const LOCK_STATE_KEY = Symbol('vDebounceLockState')
9
+ const RELEASE_TIMER_KEY = Symbol('vDebounceReleaseTimer')
10
+ const REFRESH_GEN_KEY = Symbol('vDebounceRefreshGen')
11
+
12
+ const INVOKING_FLAG = 'data-debounce-invoking'
13
+ const LOADING_STYLE_ID = 'v-debounce-el-loading-style'
14
+
15
+ type LoadingCloser = { close: () => void }
16
+ type LoadingService = ((
17
+ options?: Record<string, unknown>,
18
+ context?: AppContext | null,
19
+ ) => LoadingCloser | undefined) & {
20
+ _context?: AppContext | null
21
+ }
22
+
23
+ let loadingService: LoadingService | null = null
24
+
25
+ function resolveLoadingService(appContext?: AppContext | null): LoadingService {
26
+ if (loadingService) return loadingService
27
+
28
+ const service = ElLoading.service as LoadingService
29
+ service._context = service._context ?? appContext ?? null
30
+ loadingService = service
31
+ return service
32
+ }
33
+
34
+ export type DebounceHandler = (event: Event) => unknown | false
35
+
36
+ /** 模式二配置;handler 可省略(与 @click 并用) */
37
+ export interface DebounceLockConfig {
38
+ handler?: DebounceHandler
39
+ validate?: () => boolean | Promise<boolean>
40
+ form?: string
41
+ input?: string
42
+ pattern?: string | RegExp
43
+ }
44
+
45
+ export type DebounceBindingValue = DebounceHandler | DebounceLockConfig | undefined
46
+
47
+ export type DebounceDirective = Directive<HTMLElement, DebounceBindingValue>
48
+ export type DebounceDirectiveBinding = DirectiveBinding<DebounceBindingValue>
49
+
50
+ interface DebounceBindingMeta {
51
+ listener: (event: Event) => void
52
+ event: string
53
+ binding: DebounceDirectiveBinding
54
+ }
55
+
56
+ const DEFAULT_WAIT_MS = 1000
57
+ const DEFAULT_EVENT = 'click'
58
+
59
+ function isElButton(el: HTMLElement): boolean {
60
+ return el.classList.contains('el-button')
61
+ }
62
+
63
+ /** 指令挂载点:原生 button 或 el-button 根节点 */
64
+ function resolveButtonHost(el: HTMLElement): HTMLButtonElement {
65
+ if (el.tagName === 'BUTTON') return el as HTMLButtonElement
66
+ const nested = el.querySelector<HTMLButtonElement>('button.el-button')
67
+ return (nested ?? el) as HTMLButtonElement
68
+ }
69
+
70
+ function isButtonDisabled(btn: HTMLButtonElement): boolean {
71
+ return (
72
+ btn.disabled ||
73
+ btn.getAttribute('aria-disabled') === 'true' ||
74
+ btn.classList.contains('is-disabled')
75
+ )
76
+ }
77
+
78
+ function storeAppContext(el: HTMLElement, binding: DebounceDirectiveBinding) {
79
+ const inst = binding.instance as ComponentPublicInstance | null
80
+ ;(el as HTMLElement & { [APP_CTX_KEY]?: AppContext | null })[APP_CTX_KEY] =
81
+ inst?.$?.appContext ?? null
82
+ }
83
+
84
+ /** 使用主包 ElLoading(与 app.use(ElementPlus) 共用 appContext) */
85
+ function prefetchLoadingModule(el: HTMLElement) {
86
+ const appContext = (el as HTMLElement & { [APP_CTX_KEY]?: AppContext | null })[APP_CTX_KEY]
87
+ resolveLoadingService(appContext)
88
+ }
89
+
90
+ function ensureLoadingStyles() {
91
+ if (typeof document === 'undefined') return
92
+
93
+ const css = `
94
+ .el-button.v-debounce-btn-host,
95
+ button.v-debounce-btn-host {
96
+ position: relative !important;
97
+ overflow: hidden !important;
98
+ box-sizing: border-box;
99
+ }
100
+ .el-button.v-debounce-btn-host {
101
+ display: inline-flex !important;
102
+ align-items: center !important;
103
+ justify-content: center !important;
104
+ gap: 6px;
105
+ }
106
+ .v-debounce-el-loading-inline.el-loading-mask {
107
+ display: none !important;
108
+ }
109
+ .v-debounce-btn-label {
110
+ flex: 0 1 auto;
111
+ min-width: 0;
112
+ overflow: hidden;
113
+ text-overflow: ellipsis;
114
+ white-space: nowrap;
115
+ }
116
+ .v-debounce-btn-spinner {
117
+ position: static !important;
118
+ top: auto !important;
119
+ left: auto !important;
120
+ right: auto !important;
121
+ margin: 0 !important;
122
+ width: auto !important;
123
+ height: auto !important;
124
+ flex: 0 0 auto;
125
+ display: inline-flex !important;
126
+ align-items: center;
127
+ justify-content: center;
128
+ line-height: 1;
129
+ text-align: center;
130
+ }
131
+ .v-debounce-btn-spinner .circular {
132
+ width: 14px;
133
+ height: 14px;
134
+ display: block;
135
+ }
136
+ .v-debounce-btn-spinner .path {
137
+ stroke: var(--app-btn-bg, #c53355);
138
+ stroke-width: 4;
139
+ }
140
+ `
141
+
142
+ const existing = document.getElementById(LOADING_STYLE_ID)
143
+ if (existing) {
144
+ existing.textContent = css
145
+ return
146
+ }
147
+
148
+ const style = document.createElement('style')
149
+ style.id = LOADING_STYLE_ID
150
+ style.textContent = css
151
+ document.head.appendChild(style)
152
+ }
153
+
154
+ function bumpRefreshGen(el: HTMLElement): number {
155
+ const host = resolveButtonHost(el)
156
+ const next = ((host as HTMLElement & { [REFRESH_GEN_KEY]?: number })[REFRESH_GEN_KEY] ?? 0) + 1
157
+ ;(host as HTMLElement & { [REFRESH_GEN_KEY]?: number })[REFRESH_GEN_KEY] = next
158
+ return next
159
+ }
160
+
161
+ function clearReleaseTimer(el: HTMLElement) {
162
+ const host = resolveButtonHost(el)
163
+ const timer = (host as HTMLElement & { [RELEASE_TIMER_KEY]?: number })[RELEASE_TIMER_KEY]
164
+ if (timer !== undefined) window.clearTimeout(timer)
165
+ delete (host as HTMLElement & { [RELEASE_TIMER_KEY]?: number })[RELEASE_TIMER_KEY]
166
+ }
167
+
168
+ function forceRemoveLoadingDom(btn: HTMLButtonElement) {
169
+ btn.querySelectorAll('.v-debounce-btn-spinner, .el-loading-mask.v-debounce-el-loading-inline').forEach(
170
+ (node) => node.remove(),
171
+ )
172
+ }
173
+
174
+ function setLockState(el: HTMLElement, locked: boolean) {
175
+ ;(el as HTMLElement & { [LOCK_STATE_KEY]?: boolean })[LOCK_STATE_KEY] = locked
176
+ }
177
+
178
+ function isLockActive(el: HTMLElement): boolean {
179
+ return !!(el as HTMLElement & { [LOCK_STATE_KEY]?: boolean })[LOCK_STATE_KEY]
180
+ }
181
+
182
+ function findButtonLabel(btn: HTMLButtonElement): HTMLElement | null {
183
+ const spanLabel = Array.from(btn.children).find(
184
+ (node): node is HTMLElement =>
185
+ node instanceof HTMLElement &&
186
+ node.tagName === 'SPAN' &&
187
+ !node.classList.contains('el-icon') &&
188
+ !node.classList.contains('v-debounce-btn-spinner'),
189
+ )
190
+ if (spanLabel) return spanLabel
191
+
192
+ if (btn.childNodes.length === 1 && btn.firstChild?.nodeType === Node.TEXT_NODE) {
193
+ const wrap = document.createElement('span')
194
+ wrap.className = 'v-debounce-btn-label'
195
+ wrap.textContent = btn.textContent
196
+ btn.textContent = ''
197
+ btn.appendChild(wrap)
198
+ return wrap
199
+ }
200
+
201
+ return null
202
+ }
203
+
204
+ /** 将 spinner 挪到文字 span 右侧,保持在按钮内部 */
205
+ function formatInlineLoading(btn: HTMLButtonElement) {
206
+ const mask = btn.querySelector('.el-loading-mask.v-debounce-el-loading-inline')
207
+ const spinner =
208
+ (mask?.querySelector('.el-loading-spinner') as HTMLElement | null) ??
209
+ btn.querySelector('.v-debounce-btn-spinner')
210
+
211
+ if (!spinner) return
212
+
213
+ const label = findButtonLabel(btn)
214
+ if (!label) return
215
+
216
+ label.classList.add('v-debounce-btn-label')
217
+ spinner.classList.add('v-debounce-btn-spinner')
218
+ if (label.nextElementSibling !== spinner) {
219
+ label.insertAdjacentElement('afterend', spinner)
220
+ }
221
+
222
+ if (mask instanceof HTMLElement) mask.style.display = 'none'
223
+ }
224
+
225
+ async function showButtonLoading(el: HTMLElement) {
226
+ ensureLoadingStyles()
227
+ const btn = resolveButtonHost(el)
228
+ hideButtonLoading(el)
229
+
230
+ btn.classList.add('v-debounce-btn-host')
231
+ btn.setAttribute('aria-busy', 'true')
232
+
233
+ const appContext = (btn as HTMLElement & { [APP_CTX_KEY]?: AppContext | null })[APP_CTX_KEY]
234
+ const service = resolveLoadingService(appContext)
235
+ const instance = service(
236
+ {
237
+ target: btn,
238
+ lock: false,
239
+ background: 'transparent',
240
+ customClass: 'v-debounce-el-loading-inline',
241
+ },
242
+ appContext ?? service._context ?? undefined,
243
+ )
244
+
245
+ if (instance) {
246
+ ;(btn as HTMLElement & { [LOADING_INSTANCE_KEY]?: LoadingCloser })[LOADING_INSTANCE_KEY] = instance
247
+ formatInlineLoading(btn)
248
+ }
249
+ }
250
+
251
+ async function ensureButtonLoadingVisible(el: HTMLElement) {
252
+ if (!isLockActive(el)) return
253
+ const btn = resolveButtonHost(el)
254
+ if (btn.querySelector('.v-debounce-btn-spinner')) return
255
+
256
+ const mask = btn.querySelector('.el-loading-mask.v-debounce-el-loading-inline')
257
+ if (mask) {
258
+ formatInlineLoading(btn)
259
+ return
260
+ }
261
+
262
+ await showButtonLoading(el)
263
+ }
264
+
265
+ function scheduleLoadingRefresh(el: HTMLElement) {
266
+ const host = resolveButtonHost(el)
267
+ const gen = (host as HTMLElement & { [REFRESH_GEN_KEY]?: number })[REFRESH_GEN_KEY] ?? 0
268
+ void nextTick(() => {
269
+ if (!isLockActive(host)) return
270
+ if ((host as HTMLElement & { [REFRESH_GEN_KEY]?: number })[REFRESH_GEN_KEY] !== gen) return
271
+ if (host.querySelector('.v-debounce-btn-spinner')) {
272
+ formatInlineLoading(host)
273
+ return
274
+ }
275
+ void ensureButtonLoadingVisible(host)
276
+ })
277
+ }
278
+
279
+ function hideButtonLoading(el: HTMLElement) {
280
+ const btn = resolveButtonHost(el)
281
+ const instance = (btn as HTMLElement & { [LOADING_INSTANCE_KEY]?: LoadingCloser })[LOADING_INSTANCE_KEY]
282
+ try {
283
+ instance?.close()
284
+ } catch {
285
+ // spinner 可能被挪到文字旁,close 无法移除 DOM
286
+ }
287
+ delete (btn as HTMLElement & { [LOADING_INSTANCE_KEY]?: LoadingCloser })[LOADING_INSTANCE_KEY]
288
+ forceRemoveLoadingDom(btn)
289
+ btn.querySelectorAll('.v-debounce-btn-label').forEach((node) => {
290
+ node.classList.remove('v-debounce-btn-label')
291
+ })
292
+ btn.classList.remove('v-debounce-btn-host')
293
+ btn.removeAttribute('aria-busy')
294
+ }
295
+
296
+ function resolveWait(arg?: string): number {
297
+ if (arg && /^\d+$/.test(arg)) return Number(arg)
298
+ return DEFAULT_WAIT_MS
299
+ }
300
+
301
+ function resolveEvent(arg?: string): string {
302
+ if (arg && !/^\d+$/.test(arg)) return arg
303
+ return DEFAULT_EVENT
304
+ }
305
+
306
+ function isLockConfigObject(value: DebounceBindingValue): value is DebounceLockConfig {
307
+ return !!value && typeof value === 'object'
308
+ }
309
+
310
+ function resolveValidate(config?: DebounceLockConfig): (() => boolean | Promise<boolean>) | undefined {
311
+ if (!config) return undefined
312
+
313
+ const validators: Array<() => boolean | Promise<boolean>> = []
314
+
315
+ if (config.validate) validators.push(config.validate)
316
+
317
+ if (config.form) {
318
+ const formSelector = config.form
319
+ validators.push(() => {
320
+ const form = document.querySelector<HTMLFormElement>(formSelector)
321
+ if (!form) return false
322
+ const valid = form.checkValidity()
323
+ if (!valid) form.reportValidity()
324
+ return valid
325
+ })
326
+ }
327
+
328
+ if (config.input && config.pattern) {
329
+ const { input, pattern } = config
330
+ validators.push(() => {
331
+ const inputEl = document.querySelector<HTMLInputElement>(input)
332
+ if (!inputEl) return false
333
+ const re = pattern instanceof RegExp ? pattern : new RegExp(pattern)
334
+ const valid = re.test(inputEl.value)
335
+ if (!valid) inputEl.reportValidity?.()
336
+ return valid
337
+ })
338
+ }
339
+
340
+ if (!validators.length) return undefined
341
+
342
+ return async () => {
343
+ for (const run of validators) {
344
+ const ok = await Promise.resolve(run())
345
+ if (!ok) return false
346
+ }
347
+ return true
348
+ }
349
+ }
350
+
351
+ function parseBinding(binding: DebounceDirectiveBinding) {
352
+ const isLock = !!binding.modifiers.lock
353
+ const showLoading = !!binding.modifiers.loading
354
+ const wait = resolveWait(binding.arg)
355
+ const event = resolveEvent(binding.arg)
356
+
357
+ if (typeof binding.value === 'function') {
358
+ return { isLock, showLoading, wait, event, handler: binding.value, validate: undefined }
359
+ }
360
+
361
+ if (isLockConfigObject(binding.value)) {
362
+ return {
363
+ isLock,
364
+ showLoading,
365
+ wait,
366
+ event,
367
+ handler: binding.value.handler,
368
+ validate: resolveValidate(binding.value),
369
+ }
370
+ }
371
+
372
+ return { isLock, showLoading, wait, event, handler: undefined, validate: undefined }
373
+ }
374
+
375
+ function setLockDisabled(el: HTMLElement, locked: boolean) {
376
+ const btn = resolveButtonHost(el)
377
+ if (locked) {
378
+ if (!btn.dataset.debounceLocked) {
379
+ btn.dataset.debouncePrevDisabled = String(isButtonDisabled(btn))
380
+ btn.dataset.debouncePrevAriaDisabled = btn.getAttribute('aria-disabled') ?? ''
381
+ }
382
+ btn.dataset.debounceLocked = 'true'
383
+ btn.disabled = true
384
+ btn.setAttribute('aria-disabled', 'true')
385
+ btn.classList.add('is-disabled')
386
+ return
387
+ }
388
+
389
+ delete btn.dataset.debounceLocked
390
+ const prevDisabled = btn.dataset.debouncePrevDisabled === 'true'
391
+ btn.disabled = prevDisabled
392
+ const prevAria = btn.dataset.debouncePrevAriaDisabled
393
+ if (prevAria) btn.setAttribute('aria-disabled', prevAria)
394
+ else btn.removeAttribute('aria-disabled')
395
+ if (!prevDisabled) btn.classList.remove('is-disabled')
396
+ delete btn.dataset.debouncePrevDisabled
397
+ delete btn.dataset.debouncePrevAriaDisabled
398
+ }
399
+
400
+ async function setLockUi(el: HTMLElement, locked: boolean, showLoading: boolean, touchDisabled = true) {
401
+ if (touchDisabled) setLockDisabled(el, locked)
402
+ if (!showLoading) return
403
+ if (locked) {
404
+ await showButtonLoading(el)
405
+ scheduleLoadingRefresh(el)
406
+ return
407
+ }
408
+ hideButtonLoading(el)
409
+ }
410
+
411
+ function clearDisabledWatch(el: HTMLElement) {
412
+ const stop = (el as HTMLElement & { [WATCH_KEY]?: () => void })[WATCH_KEY]
413
+ stop?.()
414
+ delete (el as HTMLElement & { [WATCH_KEY]?: () => void })[WATCH_KEY]
415
+ }
416
+
417
+ function waitUntilButtonReleased(el: HTMLElement): Promise<void> {
418
+ clearDisabledWatch(el)
419
+ const btn = resolveButtonHost(el)
420
+
421
+ return new Promise((resolve) => {
422
+ let seenDisabled = isButtonDisabled(btn)
423
+ let settled = false
424
+
425
+ const finish = () => {
426
+ if (settled) return
427
+ settled = true
428
+ clearDisabledWatch(el)
429
+ resolve()
430
+ }
431
+
432
+ const syncState = () => {
433
+ if (isButtonDisabled(btn)) seenDisabled = true
434
+ if (seenDisabled && !isButtonDisabled(btn)) finish()
435
+ }
436
+
437
+ const observer = new MutationObserver(syncState)
438
+ observer.observe(btn, {
439
+ attributes: true,
440
+ attributeFilter: ['disabled', 'aria-disabled', 'class'],
441
+ })
442
+ ;(el as HTMLElement & { [WATCH_KEY]?: () => void })[WATCH_KEY] = () => {
443
+ observer.disconnect()
444
+ }
445
+
446
+ requestAnimationFrame(() => {
447
+ requestAnimationFrame(() => {
448
+ syncState()
449
+ if (!seenDisabled) window.setTimeout(finish, 32)
450
+ })
451
+ })
452
+
453
+ window.setTimeout(finish, 120_000)
454
+ })
455
+ }
456
+
457
+ async function runValidate(
458
+ validate: (() => boolean | Promise<boolean>) | undefined,
459
+ ): Promise<boolean> {
460
+ if (!validate) return true
461
+ try {
462
+ return await Promise.resolve(validate())
463
+ } catch {
464
+ return false
465
+ }
466
+ }
467
+
468
+ function shouldAbortHandler(result: unknown): boolean {
469
+ return result === false
470
+ }
471
+
472
+ function normalizeOnClick(handler: unknown): ((event: Event) => unknown) | undefined {
473
+ if (typeof handler === 'function') return handler as (event: Event) => unknown
474
+ if (Array.isArray(handler)) {
475
+ const fns = handler.filter((item) => typeof item === 'function') as Array<(event: Event) => unknown>
476
+ if (!fns.length) return undefined
477
+ return async (event: Event) => {
478
+ for (const fn of fns) await Promise.resolve(fn(event))
479
+ }
480
+ }
481
+ return undefined
482
+ }
483
+
484
+ function getVueOnClick(el: HTMLElement): ((event: Event) => unknown) | undefined {
485
+ const btn = resolveButtonHost(el)
486
+
487
+ if (isElButton(btn)) {
488
+ const parent = (
489
+ btn as unknown as {
490
+ __vueParentComponent?: {
491
+ props?: Record<string, unknown>
492
+ vnode?: { props?: Record<string, unknown> }
493
+ }
494
+ }
495
+ ).__vueParentComponent
496
+ const fromComponent =
497
+ normalizeOnClick(parent?.props?.onClick) ?? normalizeOnClick(parent?.vnode?.props?.onClick)
498
+ if (fromComponent) return fromComponent
499
+ }
500
+
501
+ const props = (btn as unknown as { __vnode?: { props?: Record<string, unknown> } }).__vnode?.props
502
+ return normalizeOnClick(props?.onClick)
503
+ }
504
+
505
+ async function invokeCompanionClick(el: HTMLElement, e: Event): Promise<'awaited' | 'dispatched'> {
506
+ const onClick = getVueOnClick(el)
507
+ if (onClick) {
508
+ await Promise.resolve(onClick(e))
509
+ return 'awaited'
510
+ }
511
+
512
+ el.setAttribute(INVOKING_FLAG, '1')
513
+ el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }))
514
+ el.removeAttribute(INVOKING_FLAG)
515
+ return 'dispatched'
516
+ }
517
+
518
+ async function runLockFlow(
519
+ el: HTMLElement,
520
+ e: Event,
521
+ showLoading: boolean,
522
+ wait: number,
523
+ handler: DebounceHandler | undefined,
524
+ validate: (() => boolean | Promise<boolean>) | undefined,
525
+ setLocked: (value: boolean) => void,
526
+ ) {
527
+ clearReleaseTimer(el)
528
+ bumpRefreshGen(el)
529
+ setLockState(el, true)
530
+ let releaseImmediately = false
531
+
532
+ const release = async (touchDisabled: boolean) => {
533
+ clearReleaseTimer(el)
534
+ bumpRefreshGen(el)
535
+ setLockState(el, false)
536
+ await setLockUi(el, false, showLoading, touchDisabled)
537
+ setLocked(false)
538
+ }
539
+
540
+ const touchDisabled = !!handler
541
+
542
+ try {
543
+ await setLockUi(el, true, showLoading, touchDisabled)
544
+
545
+ const passed = await runValidate(validate)
546
+ if (!passed) {
547
+ releaseImmediately = true
548
+ return
549
+ }
550
+
551
+ if (handler) {
552
+ const result = await Promise.resolve(handler(e))
553
+ if (shouldAbortHandler(result)) {
554
+ releaseImmediately = true
555
+ return
556
+ }
557
+ return
558
+ }
559
+
560
+ const mode = await invokeCompanionClick(el, e)
561
+ scheduleLoadingRefresh(el)
562
+ if (mode === 'dispatched') {
563
+ await waitUntilButtonReleased(el)
564
+ }
565
+ } catch {
566
+ // 事件异常结束,仍按 wait 延迟关闭 loading
567
+ } finally {
568
+ const host = resolveButtonHost(el)
569
+
570
+ if (releaseImmediately) {
571
+ void release(touchDisabled)
572
+ return
573
+ }
574
+
575
+ clearReleaseTimer(host)
576
+ ;(host as HTMLElement & { [RELEASE_TIMER_KEY]?: number })[RELEASE_TIMER_KEY] = window.setTimeout(
577
+ () => {
578
+ delete (host as HTMLElement & { [RELEASE_TIMER_KEY]?: number })[RELEASE_TIMER_KEY]
579
+ void release(touchDisabled)
580
+ },
581
+ wait,
582
+ )
583
+ }
584
+ }
585
+
586
+ /**
587
+ * v-debounce 两种模式:
588
+ * 1. 防重:v-debounce:800 @click="fn"
589
+ * 2. 锁定:v-debounce.lock.loading — 校验失败立即关 loading;事件结束后 delay(DEFAULT_WAIT_MS) 关闭
590
+ */
591
+ export const vDebounce: DebounceDirective = {
592
+ mounted(el: HTMLElement, binding: DebounceDirectiveBinding) {
593
+ const host = resolveButtonHost(el)
594
+ storeAppContext(host, binding)
595
+ if (binding.modifiers.loading) prefetchLoadingModule(host)
596
+
597
+ let locked = false
598
+
599
+ const listener = (e: Event) => {
600
+ if (host.hasAttribute(INVOKING_FLAG)) return
601
+
602
+ const meta = (host as HTMLElement & { [DEBOUNCE_KEY]?: DebounceBindingMeta })[DEBOUNCE_KEY]
603
+ const currentBinding = meta?.binding ?? binding
604
+ const { isLock, showLoading, wait, handler, validate } = parseBinding(currentBinding)
605
+
606
+ if (locked) {
607
+ e.preventDefault()
608
+ e.stopImmediatePropagation()
609
+ return
610
+ }
611
+
612
+ if (isLock) {
613
+ e.preventDefault()
614
+ e.stopImmediatePropagation()
615
+ locked = true
616
+ void runLockFlow(host, e, showLoading, wait, handler, validate, (v) => {
617
+ locked = v
618
+ })
619
+ return
620
+ }
621
+
622
+ locked = true
623
+
624
+ if (handler) {
625
+ e.preventDefault()
626
+ e.stopImmediatePropagation()
627
+ Promise.resolve(handler(e)).finally(() => {
628
+ window.setTimeout(() => {
629
+ locked = false
630
+ }, wait)
631
+ })
632
+ return
633
+ }
634
+
635
+ window.setTimeout(() => {
636
+ locked = false
637
+ }, wait)
638
+ }
639
+
640
+ const event = parseBinding(binding).event
641
+ host.addEventListener(event, listener, true)
642
+ ;(host as HTMLElement & { [DEBOUNCE_KEY]?: DebounceBindingMeta })[DEBOUNCE_KEY] = {
643
+ listener,
644
+ event,
645
+ binding,
646
+ }
647
+ },
648
+ updated(el: HTMLElement, binding: DebounceDirectiveBinding) {
649
+ const host = resolveButtonHost(el)
650
+ const meta = (host as HTMLElement & { [DEBOUNCE_KEY]?: DebounceBindingMeta })[DEBOUNCE_KEY]
651
+ if (meta) meta.binding = binding
652
+ storeAppContext(host, binding)
653
+ if (binding.modifiers.loading && isLockActive(host)) {
654
+ scheduleLoadingRefresh(host)
655
+ }
656
+ },
657
+ unmounted(el: HTMLElement) {
658
+ const host = resolveButtonHost(el)
659
+ setLockState(host, false)
660
+ clearReleaseTimer(host)
661
+ bumpRefreshGen(host)
662
+ const meta = (host as HTMLElement & { [DEBOUNCE_KEY]?: DebounceBindingMeta })[DEBOUNCE_KEY]
663
+ if (!meta) return
664
+ host.removeEventListener(meta.event, meta.listener, true)
665
+ clearDisabledWatch(host)
666
+ hideButtonLoading(host)
667
+ setLockDisabled(host, false)
668
+ delete (host as HTMLElement & { [DEBOUNCE_KEY]?: DebounceBindingMeta })[DEBOUNCE_KEY]
669
+ },
670
+ }