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.
- package/gen-ai/build-report/chartStreamRefs.ts +227 -0
- package/gen-ai/build-report/coherence-check/index.vue +559 -0
- package/gen-ai/build-report/components/ReportChapterWorkspace.vue +4571 -0
- package/gen-ai/build-report/components/ReportConfigPanel.vue +727 -0
- package/gen-ai/build-report/components/ReportGenerateSettingsPanel.vue +2361 -0
- package/gen-ai/build-report/components/ReportGeneratingPanel.vue +1765 -0
- package/gen-ai/build-report/components/ReportPromptPanel.vue +625 -0
- package/gen-ai/build-report/components/ReportSourceFileSummary.vue +665 -0
- package/gen-ai/build-report/components/ReportSourcesPanel.vue +1327 -0
- package/gen-ai/build-report/components/ReportTemplateDetail.vue +191 -0
- package/gen-ai/build-report/components/ReportTemplateEditorFormPanel.vue +667 -0
- package/gen-ai/build-report/components/ReportTemplatePicker.vue +302 -0
- package/gen-ai/build-report/components/ReportWorkflowSidebar.vue +148 -0
- package/gen-ai/build-report/components/paramsSetting.vue +886 -0
- package/gen-ai/build-report/components/reportEdit.vue +2283 -0
- package/gen-ai/build-report/index.vue +856 -0
- package/gen-ai/build-report/report-domain.ts +1133 -0
- package/gen-ai/build-report/report-template-data.ts +295 -0
- package/gen-ai/build-report/useReportWorkflow.ts +1386 -0
- package/gen-ai/common/assets/NotoSans-SC.woff2 +0 -0
- package/gen-ai/common/assets/NotoSans-TC.woff2 +0 -0
- package/gen-ai/common/assets/NotoSans.woff2 +0 -0
- package/gen-ai/common/assets/fonts.css +278 -0
- package/gen-ai/common/assets/pagination.css +163 -0
- package/gen-ai/common/directives/btnLoading.ts +455 -0
- package/gen-ai/common/directives/debounce.ts +98 -0
- package/gen-ai/common/directives/index.ts +30 -0
- package/gen-ai/common/en.md +417 -0
- package/gen-ai/common/index.ts +36 -0
- package/gen-ai/common/types/component-expose.ts +9 -0
- package/gen-ai/common/utils/api-res-status.ts +7 -0
- package/gen-ai/common/utils/navigation.ts +59 -0
- package/gen-ai/common/utils/remote-request.ts +45 -0
- package/gen-ai/common/utils/scroll.ts +577 -0
- package/gen-ai/components/AIExport/docx-export.ts +950 -0
- package/gen-ai/components/AIExport/docxExport.ts +2566 -0
- package/gen-ai/components/AIExport/index.ts +52 -0
- package/gen-ai/components/AIExport/parseMarkdownHtml.ts +653 -0
- package/gen-ai/components/CapsuleScrollbar.vue +149 -0
- package/gen-ai/components/ChapterTitleScroll.vue +468 -0
- package/gen-ai/components/EntryHeroNavCards.vue +379 -0
- package/gen-ai/components/GenAiPagination.vue +52 -0
- package/gen-ai/components/ModelCard.vue +1114 -0
- package/gen-ai/components/Preview/FilePreviewPage.vue +949 -0
- package/gen-ai/components/Preview/PreviewCapsuleScroll.vue +205 -0
- package/gen-ai/components/Preview/PreviewOutlineTree.vue +148 -0
- package/gen-ai/components/Preview/index.ts +11 -0
- package/gen-ai/components/Preview/preview.ts +724 -0
- package/gen-ai/components/PromptField.vue +505 -0
- package/gen-ai/components/RecentReportsTable.vue +974 -0
- package/gen-ai/components/ReportCollapsibleSection.vue +172 -0
- package/gen-ai/components/ReportHistoryList.vue +637 -0
- package/gen-ai/components/ReportSectionConfirmPopover.vue +251 -0
- package/gen-ai/components/editor/ChapterTiptapEditor.vue +3718 -0
- package/gen-ai/components/editor/StructureBlockView.vue +441 -0
- package/gen-ai/components/editor/TiptapToolbar.vue +1036 -0
- package/gen-ai/components/editor/TypewriterDocEditor.vue +422 -0
- package/gen-ai/components/editor/_StructureOrgNode.vue +326 -0
- package/gen-ai/components/editor/index.ts +28 -0
- package/gen-ai/components/editor/parseMarkdownHtml.ts +754 -0
- package/gen-ai/components/editor/reviewCitationTip.ts +163 -0
- package/gen-ai/gen-entry/api/index.ts +32 -0
- package/gen-ai/gen-entry/index.vue +287 -0
- package/gen-ai/gen-entry/types/index.ts +4 -0
- package/gen-ai/i.mark +2 -0
- package/gen-ai/management-center/index.vue +1815 -0
- package/gen-ai/management-center/mockData.ts +248 -0
- package/gen-ai/management-center/types/index.ts +37 -0
- package/gen-ai/model-manager/TemplateEditorFormPanel.vue +661 -0
- package/gen-ai/model-manager/api/index.ts +32 -0
- package/gen-ai/model-manager/api/mockApi.ts +33 -0
- package/gen-ai/model-manager/index.vue +1583 -0
- package/gen-ai/model-manager/mockData.ts +177 -0
- package/gen-ai/model-manager/template-detail-shared.ts +154 -0
- package/gen-ai/model-manager/template-editor.vue +345 -0
- package/gen-ai/model-manager/types/index.ts +40 -0
- package/gen-ai/model-manager/utils.ts +25 -0
- 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'
|