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.
- package/package.json +12 -0
- package/src/App.vue +3 -0
- package/src/api/index.ts +19 -0
- package/src/api/modules/gen-ai/gen-entry/index.ts +30 -0
- package/src/api/modules/gen-ai/model-manager/index.ts +42 -0
- package/src/api/modules/gen-ai/model-manager/mockApi.ts +33 -0
- package/src/api/modules/index.ts +98 -0
- package/src/api/modules/user/index.ts +4 -0
- package/src/api/request.ts +102 -0
- package/src/assets/sample-access-icon.png +0 -0
- package/src/assets/sample-pie-chart.png +0 -0
- package/src/assets/vue.svg +1 -0
- package/src/components/CapsuleScrollbar.vue +93 -0
- package/src/components/Export/ExcelExport.vue +592 -0
- package/src/components/Export/ExcelExport2.vue +494 -0
- package/src/components/Export/ExcelExport3.vue +342 -0
- package/src/components/Export/ExcelExport4.vue +665 -0
- package/src/components/Export/excelExport.js +547 -0
- package/src/components/Export/excelExport.ts +551 -0
- package/src/components/GEN-AI/index.vue +142 -0
- package/src/components/GEN-AI/index1.vue +456 -0
- package/src/components/GEN-AI/index10.vue +5 -0
- package/src/components/GEN-AI/index2.vue +568 -0
- package/src/components/GEN-AI/index3.vue +623 -0
- package/src/components/GEN-AI/index4.vue +629 -0
- package/src/components/GEN-AI/index5.vue +578 -0
- package/src/components/GEN-AI/index6.vue +656 -0
- package/src/components/GEN-AI/index7.vue +717 -0
- package/src/components/GEN-AI/index8.vue +405 -0
- package/src/components/GEN-AI/index9.vue +1065 -0
- package/src/components/GEN-AI/types.ts +12 -0
- package/src/components/GEN-AI/utils.ts +42 -0
- package/src/components/HelloWorld.vue +41 -0
- package/src/components/PageCard.vue +7 -0
- package/src/components/PageHeader.vue +32 -0
- package/src/components/backup/index5 copy.vue +556 -0
- package/src/components/backup/index5.vue +620 -0
- package/src/components/backup/index9 copy.vue +1029 -0
- package/src/components/backup/index9-pro.vue +1065 -0
- package/src/components/backup/index9.vue +1057 -0
- package/src/components/el-date-picker.vue +64 -0
- package/src/directives/btnLoading.ts +427 -0
- package/src/directives/debounce copy.ts +670 -0
- package/src/directives/debounce.ts +98 -0
- package/src/directives/index.ts +25 -0
- package/src/layouts/MainLayout.vue +101 -0
- package/src/main.ts +85 -0
- package/src/router/index.ts +76 -0
- package/src/router/menus.ts +28 -0
- package/src/style.css +79 -0
- package/src/styles/_variables.scss +24 -0
- package/src/styles/app-button.css +26 -0
- package/src/styles/element-overrides.css +23 -0
- package/src/styles/global.css +44 -0
- package/src/styles/index.scss +1 -0
- package/src/styles/page-card.css +21 -0
- package/src/styles/variables.css +26 -0
- package/src/test/mock.ts +101 -0
- package/src/test/test1.vue +402 -0
- package/src/test/test2.vue +1689 -0
- package/src/types/gen-ai/gen-entry/index.ts +17 -0
- package/src/types/gen-ai/model-manager/index.ts +19 -0
- package/src/utils/docxExport.ts +1610 -0
- package/src/utils/gen-ai-navigation.ts +37 -0
- package/src/utils/gen-ai-scroll.ts +455 -0
- package/src/utils/openDataLoaderWordExport.ts +33 -0
- package/src/utils/pageScrollbar.ts +115 -0
- package/src/utils/randomTranscode.ts +87 -0
- package/src/utils/reportPdfExport.ts +44 -0
- package/src/views/AdminCenter/index.vue +817 -0
- package/src/views/Blank.vue +68 -0
- package/src/views/Home.vue +29 -0
- package/src/views/ReportCenter/index.vue +1380 -0
- package/src/views/TemplateCenter/Knowledge.ts +83 -0
- package/src/views/TemplateCenter/data.d.ts +10 -0
- package/src/views/TemplateCenter/index.vue +1205 -0
- package/src/views/TemplateCenter/service.ts +69 -0
- package/src/views/gen-ai/components/RecentReportsTable.vue +193 -0
- package/src/views/gen-ai/gen-entry/index.vue +309 -0
- package/src/views/gen-ai/gen-entry/mockData.ts +160 -0
- package/src/views/gen-ai/management-center/index.vue +53 -0
- package/src/views/gen-ai/model-manager/ChapterTitleScroll.vue +275 -0
- package/src/views/gen-ai/model-manager/index.vue +1205 -0
- package/src/views/gen-ai/model-manager/mockData.ts +122 -0
- package/src/views/gen-ai/report-center/index.vue +158 -0
- 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
|
+
}
|