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,64 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="block">
|
|
3
|
+
<span class="demonstration">默认</span>
|
|
4
|
+
<el-date-picker
|
|
5
|
+
v-model="value1"
|
|
6
|
+
type="daterange"
|
|
7
|
+
range-separator="至"
|
|
8
|
+
start-placeholder="开始日期"
|
|
9
|
+
end-placeholder="结束日期"
|
|
10
|
+
:disabled-date="disabledDate"
|
|
11
|
+
@change="handleDateChange"
|
|
12
|
+
>
|
|
13
|
+
</el-date-picker>
|
|
14
|
+
</div>
|
|
15
|
+
</template>
|
|
16
|
+
|
|
17
|
+
<script setup lang="ts">
|
|
18
|
+
import { ref } from "vue";
|
|
19
|
+
import { ElMessage } from "element-plus";
|
|
20
|
+
|
|
21
|
+
type DateRange = [Date | null, Date | null];
|
|
22
|
+
|
|
23
|
+
const value1 = ref<DateRange>([null, null]);
|
|
24
|
+
|
|
25
|
+
const getDateDiffInDays = (date1: Date, date2: Date): number => {
|
|
26
|
+
const timeDiff = Math.abs(date2.getTime() - date1.getTime());
|
|
27
|
+
return Math.ceil(timeDiff / (1000 * 3600 * 24));
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const handleDateChange = (value: DateRange | null) => {
|
|
31
|
+
if (!value || !value[0] || !value[1]) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const [startDate, endDate] = value;
|
|
36
|
+
|
|
37
|
+
if (startDate > endDate) {
|
|
38
|
+
ElMessage.error("开始时间不能晚于结束时间");
|
|
39
|
+
value1.value = [null, null];
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const dayDiff = getDateDiffInDays(startDate, endDate);
|
|
44
|
+
if (dayDiff > 31) {
|
|
45
|
+
ElMessage.error("时间范围不能超过31天");
|
|
46
|
+
value1.value = [null, null];
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const disabledDate = (_time: Date) => false;
|
|
51
|
+
</script>
|
|
52
|
+
|
|
53
|
+
<style scoped>
|
|
54
|
+
.block {
|
|
55
|
+
padding: 20px 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.demonstration {
|
|
59
|
+
display: block;
|
|
60
|
+
color: var(--el-text-color-secondary);
|
|
61
|
+
font-size: 14px;
|
|
62
|
+
margin-bottom: 10px;
|
|
63
|
+
}
|
|
64
|
+
</style>
|
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
import { nextTick, type Directive, type DirectiveBinding } from 'vue'
|
|
2
|
+
|
|
3
|
+
const BTN_LOADING_KEY = Symbol('vBtnLoading')
|
|
4
|
+
const REFRESH_GEN_KEY = Symbol('vBtnLoadingRefreshGen')
|
|
5
|
+
|
|
6
|
+
const LOADING_STYLE_ID = 'v-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-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
|
+
/** closeBtnLoading() 主动关闭;未调用时 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-btn-loading-host,
|
|
127
|
+
button.v-btn-loading-host {
|
|
128
|
+
position: relative !important;
|
|
129
|
+
box-sizing: border-box;
|
|
130
|
+
cursor: not-allowed !important;
|
|
131
|
+
}
|
|
132
|
+
.el-button.v-btn-loading-host {
|
|
133
|
+
display: inline-flex !important;
|
|
134
|
+
align-items: center !important;
|
|
135
|
+
justify-content: center !important;
|
|
136
|
+
}
|
|
137
|
+
.el-button.v-btn-loading-host::after,
|
|
138
|
+
button.v-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-btn-loading-spin 0.75s linear infinite;
|
|
149
|
+
vertical-align: middle;
|
|
150
|
+
opacity: 1 !important;
|
|
151
|
+
}
|
|
152
|
+
@keyframes v-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-btn-loading-host', 'is-disabled')
|
|
179
|
+
host.setAttribute('aria-busy', 'true')
|
|
180
|
+
host.setAttribute('aria-disabled', 'true')
|
|
181
|
+
host.dataset.btnLoading = '1'
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function clearLoadingVisual(host: HTMLButtonElement) {
|
|
185
|
+
host.classList.remove('v-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.btnLoading
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function ensureLoadingVisual(session: BtnLoadingSession) {
|
|
193
|
+
if (!activeSessions.has(session.id)) return
|
|
194
|
+
|
|
195
|
+
const host = rebindSessionHost(session)
|
|
196
|
+
if (!host) return
|
|
197
|
+
|
|
198
|
+
if (!host.classList.contains('v-btn-loading-host')) {
|
|
199
|
+
applyLoadingVisual(host, session.id)
|
|
200
|
+
} else {
|
|
201
|
+
ensureHostId(host, session.id)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function startClassGuard(session: BtnLoadingSession) {
|
|
206
|
+
stopClassGuard(session)
|
|
207
|
+
|
|
208
|
+
const guard = () => {
|
|
209
|
+
if (!activeSessions.has(session.id)) return
|
|
210
|
+
ensureLoadingVisual(session)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const host = rebindSessionHost(session)
|
|
214
|
+
if (host) {
|
|
215
|
+
session.classObserver = new MutationObserver(guard)
|
|
216
|
+
session.classObserver.observe(host, { attributes: true, attributeFilter: ['class'] })
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
session.guardTimer = window.setInterval(guard, 120)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function stopClassGuard(session: BtnLoadingSession) {
|
|
223
|
+
session.classObserver?.disconnect()
|
|
224
|
+
delete session.classObserver
|
|
225
|
+
if (session.guardTimer !== undefined) {
|
|
226
|
+
window.clearInterval(session.guardTimer)
|
|
227
|
+
delete session.guardTimer
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function scheduleLoadingRefresh(session: BtnLoadingSession) {
|
|
232
|
+
const host = rebindSessionHost(session)
|
|
233
|
+
if (!host) return
|
|
234
|
+
|
|
235
|
+
const gen = (host as HTMLElement & { [REFRESH_GEN_KEY]?: number })[REFRESH_GEN_KEY] ?? 0
|
|
236
|
+
void nextTick(() => {
|
|
237
|
+
if (!activeSessions.has(session.id)) return
|
|
238
|
+
if ((host as HTMLElement & { [REFRESH_GEN_KEY]?: number })[REFRESH_GEN_KEY] !== gen) return
|
|
239
|
+
ensureLoadingVisual(session)
|
|
240
|
+
})
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function clearReleaseTimer(session: BtnLoadingSession) {
|
|
244
|
+
if (session.releaseTimer !== undefined) {
|
|
245
|
+
window.clearTimeout(session.releaseTimer)
|
|
246
|
+
session.releaseTimer = undefined
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function clearAutoCloseTimer(session: BtnLoadingSession) {
|
|
251
|
+
if (session.autoCloseTimer !== undefined) {
|
|
252
|
+
window.clearTimeout(session.autoCloseTimer)
|
|
253
|
+
session.autoCloseTimer = undefined
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function releaseSession(session: BtnLoadingSession) {
|
|
258
|
+
if (!activeSessions.has(session.id)) return
|
|
259
|
+
|
|
260
|
+
clearReleaseTimer(session)
|
|
261
|
+
clearAutoCloseTimer(session)
|
|
262
|
+
stopClassGuard(session)
|
|
263
|
+
|
|
264
|
+
const host = rebindSessionHost(session)
|
|
265
|
+
if (host) {
|
|
266
|
+
clearLoadingVisual(host)
|
|
267
|
+
hostToSessionId.delete(host)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
activeSessions.delete(session.id)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** closeBtnLoading() 或超时触发;从点击起至少 minLockMs 后才真正关闭 */
|
|
274
|
+
function scheduleRelease(session: BtnLoadingSession) {
|
|
275
|
+
clearReleaseTimer(session)
|
|
276
|
+
const elapsed = Date.now() - session.startedAt
|
|
277
|
+
const remain = Math.max(0, session.minLockMs - elapsed)
|
|
278
|
+
|
|
279
|
+
session.releaseTimer = window.setTimeout(() => {
|
|
280
|
+
session.releaseTimer = undefined
|
|
281
|
+
session.locked = false
|
|
282
|
+
releaseSession(session)
|
|
283
|
+
}, remain)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** 未调用 closeBtnLoading() 时,点击后 DEFAULT_AUTO_CLOSE_MS 自动关闭 */
|
|
287
|
+
function scheduleAutoClose(session: BtnLoadingSession) {
|
|
288
|
+
clearAutoCloseTimer(session)
|
|
289
|
+
session.autoCloseTimer = window.setTimeout(() => {
|
|
290
|
+
session.autoCloseTimer = undefined
|
|
291
|
+
if (!activeSessions.has(session.id)) return
|
|
292
|
+
requestClose(session)
|
|
293
|
+
}, DEFAULT_AUTO_CLOSE_MS)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function requestClose(session: BtnLoadingSession) {
|
|
297
|
+
if (session.closeRequested) return
|
|
298
|
+
session.closeRequested = true
|
|
299
|
+
session.closeRequestedAt = Date.now()
|
|
300
|
+
clearAutoCloseTimer(session)
|
|
301
|
+
scheduleRelease(session)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function activateLoading(host: HTMLButtonElement, minLockMs: number) {
|
|
305
|
+
const existing = findSessionForHost(host)
|
|
306
|
+
if (existing?.locked && !existing.closeRequested) return
|
|
307
|
+
|
|
308
|
+
const id = existing?.id ?? `bl-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`
|
|
309
|
+
const session: BtnLoadingSession = {
|
|
310
|
+
id,
|
|
311
|
+
host,
|
|
312
|
+
startedAt: Date.now(),
|
|
313
|
+
minLockMs,
|
|
314
|
+
locked: true,
|
|
315
|
+
closeRequested: false,
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
activeSessions.set(id, session)
|
|
319
|
+
hostToSessionId.set(host, id)
|
|
320
|
+
bumpRefreshGen(host)
|
|
321
|
+
applyLoadingVisual(host, id)
|
|
322
|
+
startClassGuard(session)
|
|
323
|
+
scheduleLoadingRefresh(session)
|
|
324
|
+
scheduleAutoClose(session)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** 请求关闭;未调用时 30s 后自动关闭;调用后至少再保持 minLockMs(默认 1s,从点击起算) */
|
|
328
|
+
export function closeBtnLoading(target?: HTMLElement | string) {
|
|
329
|
+
if (target === undefined) {
|
|
330
|
+
;[...activeSessions.values()].forEach((session) => requestClose(session))
|
|
331
|
+
return
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
resolveCloseTargets(target).forEach((host) => {
|
|
335
|
+
const session = findSessionForHost(host)
|
|
336
|
+
if (!session) return
|
|
337
|
+
requestClose(session)
|
|
338
|
+
})
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** 是否存在未关闭的 loading(便于调试/测试) */
|
|
342
|
+
export function hasActiveBtnLoading(target?: HTMLElement | string): boolean {
|
|
343
|
+
if (target === undefined) return activeSessions.size > 0
|
|
344
|
+
|
|
345
|
+
return resolveCloseTargets(target).some((host) => {
|
|
346
|
+
const session = findSessionForHost(host)
|
|
347
|
+
return !!session && !session.closeRequested
|
|
348
|
+
})
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* v-btnLoading — 按钮 loading(与 v-debounce 解耦)
|
|
353
|
+
* - v-btnLoading 默认最短 1s(从点击起算)
|
|
354
|
+
* - v-btnLoading:1500 自定义最短毫秒
|
|
355
|
+
* - 未调用 closeBtnLoading():30s 后自动关闭
|
|
356
|
+
* - closeBtnLoading() 后:若未满 minLockMs 则补满剩余时间再关闭
|
|
357
|
+
*/
|
|
358
|
+
export const vBtnLoading: Directive<HTMLElement> = {
|
|
359
|
+
mounted(el: HTMLElement, binding: DirectiveBinding) {
|
|
360
|
+
const host = resolveButtonHost(el)
|
|
361
|
+
const event = binding.arg && !/^\d+$/.test(binding.arg) ? binding.arg : 'click'
|
|
362
|
+
|
|
363
|
+
const listener = (e: Event) => {
|
|
364
|
+
const meta = (el as HTMLElement & { [BTN_LOADING_KEY]?: BtnLoadingBindingMeta })[BTN_LOADING_KEY]
|
|
365
|
+
const currentBinding = meta?.binding ?? binding
|
|
366
|
+
const minLockMs = resolveMinLockMs(
|
|
367
|
+
currentBinding.arg && /^\d+$/.test(currentBinding.arg) ? currentBinding.arg : undefined,
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
const session = findSessionForHost(host)
|
|
371
|
+
if (session?.locked && !session.closeRequested) {
|
|
372
|
+
e.preventDefault()
|
|
373
|
+
e.stopImmediatePropagation()
|
|
374
|
+
return
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
activateLoading(host, minLockMs)
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
host.addEventListener(event, listener, true)
|
|
381
|
+
;(el as HTMLElement & { [BTN_LOADING_KEY]?: BtnLoadingBindingMeta })[BTN_LOADING_KEY] = {
|
|
382
|
+
listener,
|
|
383
|
+
event,
|
|
384
|
+
binding,
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const existing = findSessionForHost(host)
|
|
388
|
+
if (existing && !existing.closeRequested) {
|
|
389
|
+
existing.host = host
|
|
390
|
+
hostToSessionId.set(host, existing.id)
|
|
391
|
+
ensureLoadingVisual(existing)
|
|
392
|
+
startClassGuard(existing)
|
|
393
|
+
}
|
|
394
|
+
},
|
|
395
|
+
updated(el: HTMLElement, binding: DirectiveBinding) {
|
|
396
|
+
const host = resolveButtonHost(el)
|
|
397
|
+
const meta = (el as HTMLElement & { [BTN_LOADING_KEY]?: BtnLoadingBindingMeta })[BTN_LOADING_KEY]
|
|
398
|
+
if (meta) meta.binding = binding
|
|
399
|
+
|
|
400
|
+
const session = findSessionForHost(host)
|
|
401
|
+
if (!session || session.closeRequested) return
|
|
402
|
+
|
|
403
|
+
session.host = host
|
|
404
|
+
hostToSessionId.set(host, session.id)
|
|
405
|
+
ensureLoadingVisual(session)
|
|
406
|
+
scheduleLoadingRefresh(session)
|
|
407
|
+
},
|
|
408
|
+
unmounted(el: HTMLElement) {
|
|
409
|
+
const host = resolveButtonHost(el)
|
|
410
|
+
const meta = (el as HTMLElement & { [BTN_LOADING_KEY]?: BtnLoadingBindingMeta })[BTN_LOADING_KEY]
|
|
411
|
+
if (meta) {
|
|
412
|
+
host.removeEventListener(meta.event, meta.listener, true)
|
|
413
|
+
delete (el as HTMLElement & { [BTN_LOADING_KEY]?: BtnLoadingBindingMeta })[BTN_LOADING_KEY]
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const session = findSessionForHost(host)
|
|
417
|
+
if (!session) return
|
|
418
|
+
|
|
419
|
+
if (session.closeRequested) {
|
|
420
|
+
releaseSession(session)
|
|
421
|
+
return
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
stopClassGuard(session)
|
|
425
|
+
hostToSessionId.delete(host)
|
|
426
|
+
},
|
|
427
|
+
}
|