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,577 @@
|
|
|
1
|
+
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
|
|
2
|
+
|
|
3
|
+
export const CAPSULE_SCROLL_CONTAINER_CLASS = 'capsule-scroll-container'
|
|
4
|
+
|
|
5
|
+
export interface CapsuleScrollbarTheme {
|
|
6
|
+
thumbColor?: string
|
|
7
|
+
thumbHoverColor?: string
|
|
8
|
+
thumbActiveColor?: string
|
|
9
|
+
trackColor?: string
|
|
10
|
+
thumbWidthPx?: number
|
|
11
|
+
classPrefix?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface CapsuleScrollbarOptions extends CapsuleScrollbarTheme {
|
|
15
|
+
scrollEl: { value: HTMLElement | null }
|
|
16
|
+
anchorEl?: { value: HTMLElement | null }
|
|
17
|
+
active: { value: boolean }
|
|
18
|
+
gutterPx?: number
|
|
19
|
+
thumbWidthPx?: number
|
|
20
|
+
minThumbPx?: number
|
|
21
|
+
visibleRatio?: number
|
|
22
|
+
alwaysVisible?: boolean
|
|
23
|
+
autoHideAfterMs?: number
|
|
24
|
+
rightOffsetPx?: number
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface CapsuleScrollbarBindings {
|
|
28
|
+
canScroll: { value: boolean }
|
|
29
|
+
shown: { value: boolean }
|
|
30
|
+
visible: { value: boolean }
|
|
31
|
+
rootStyle: { value: { [key: string]: string } }
|
|
32
|
+
thumbStyle: { value: { [key: string]: string } }
|
|
33
|
+
rootClass: string
|
|
34
|
+
thumbClass: string
|
|
35
|
+
onThumbMouseDown: (event: MouseEvent) => void
|
|
36
|
+
refresh: () => void
|
|
37
|
+
bind: () => void
|
|
38
|
+
unbind: () => void
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const DEFAULT_CLASS_PREFIX = 'capsule-scrollbar'
|
|
42
|
+
|
|
43
|
+
interface NativeHideSnapshot {
|
|
44
|
+
paddingRight: string
|
|
45
|
+
marginRight: string
|
|
46
|
+
width: string
|
|
47
|
+
boxSizing: string
|
|
48
|
+
scrollbarWidth: string
|
|
49
|
+
msOverflowStyle: string
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function markCapsuleScrollContainer(el: HTMLElement): () => void {
|
|
53
|
+
const computed = getComputedStyle(el)
|
|
54
|
+
const snapshot: NativeHideSnapshot = {
|
|
55
|
+
paddingRight: el.style.paddingRight,
|
|
56
|
+
marginRight: el.style.marginRight,
|
|
57
|
+
width: el.style.width,
|
|
58
|
+
boxSizing: el.style.boxSizing,
|
|
59
|
+
scrollbarWidth: el.style.scrollbarWidth,
|
|
60
|
+
msOverflowStyle:
|
|
61
|
+
(el.style as CSSStyleDeclaration & { msOverflowStyle?: string }).msOverflowStyle ?? '',
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
el.style.setProperty('--capsule-scroll-padding-right', computed.paddingRight)
|
|
65
|
+
el.classList.add(CAPSULE_SCROLL_CONTAINER_CLASS)
|
|
66
|
+
el.style.scrollbarWidth = 'none'
|
|
67
|
+
;(el.style as CSSStyleDeclaration & { msOverflowStyle?: string }).msOverflowStyle = 'none'
|
|
68
|
+
|
|
69
|
+
return () => {
|
|
70
|
+
el.classList.remove(CAPSULE_SCROLL_CONTAINER_CLASS)
|
|
71
|
+
el.style.removeProperty('--capsule-scroll-padding-right')
|
|
72
|
+
el.style.paddingRight = snapshot.paddingRight
|
|
73
|
+
el.style.marginRight = snapshot.marginRight
|
|
74
|
+
el.style.width = snapshot.width
|
|
75
|
+
el.style.boxSizing = snapshot.boxSizing
|
|
76
|
+
el.style.scrollbarWidth = snapshot.scrollbarWidth
|
|
77
|
+
;(el.style as CSSStyleDeclaration & { msOverflowStyle?: string }).msOverflowStyle =
|
|
78
|
+
snapshot.msOverflowStyle
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function useCapsuleScrollbar(options: CapsuleScrollbarOptions): CapsuleScrollbarBindings {
|
|
83
|
+
const thumbWidthPx = options.thumbWidthPx ?? 10
|
|
84
|
+
const gutterPx = options.gutterPx ?? thumbWidthPx
|
|
85
|
+
const minThumbPx = options.minThumbPx ?? 48
|
|
86
|
+
const visibleRatio = options.visibleRatio ?? 1
|
|
87
|
+
const rightOffsetPx = options.rightOffsetPx ?? 1
|
|
88
|
+
const autoHideAfterMs = options.autoHideAfterMs ?? 3000
|
|
89
|
+
const alwaysVisible = options.alwaysVisible ?? autoHideAfterMs <= 0
|
|
90
|
+
const classPrefix = options.classPrefix ?? DEFAULT_CLASS_PREFIX
|
|
91
|
+
|
|
92
|
+
const canScroll = ref(false)
|
|
93
|
+
const shown = ref(false)
|
|
94
|
+
const zoneTop = ref(0)
|
|
95
|
+
const zoneRight = ref(rightOffsetPx)
|
|
96
|
+
const zoneHeight = ref(0)
|
|
97
|
+
const thumbHeight = ref(minThumbPx)
|
|
98
|
+
const thumbOffset = ref(0)
|
|
99
|
+
|
|
100
|
+
const visible = computed(() => canScroll.value && (alwaysVisible || shown.value))
|
|
101
|
+
|
|
102
|
+
let resizeObserver: ResizeObserver | undefined
|
|
103
|
+
let thumbDragCleanup: (() => void) | undefined
|
|
104
|
+
let removeContainerMark: (() => void) | undefined
|
|
105
|
+
let hideTimer: number | undefined
|
|
106
|
+
let rafId = 0
|
|
107
|
+
|
|
108
|
+
const rootStyle = computed(() => ({
|
|
109
|
+
top: `${zoneTop.value + thumbOffset.value}px`,
|
|
110
|
+
right: `${zoneRight.value}px`,
|
|
111
|
+
width: `${gutterPx}px`,
|
|
112
|
+
height: `${Math.round(thumbHeight.value)}px`,
|
|
113
|
+
}))
|
|
114
|
+
|
|
115
|
+
const thumbStyle = computed(() => ({
|
|
116
|
+
width: `${thumbWidthPx}px`,
|
|
117
|
+
height: '100%',
|
|
118
|
+
}))
|
|
119
|
+
|
|
120
|
+
function clearHideTimer() {
|
|
121
|
+
if (hideTimer !== undefined) {
|
|
122
|
+
clearTimeout(hideTimer)
|
|
123
|
+
hideTimer = undefined
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function scheduleHide() {
|
|
128
|
+
clearHideTimer()
|
|
129
|
+
if (alwaysVisible || autoHideAfterMs <= 0) return
|
|
130
|
+
hideTimer = window.setTimeout(() => {
|
|
131
|
+
shown.value = false
|
|
132
|
+
hideTimer = undefined
|
|
133
|
+
}, autoHideAfterMs)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function revealBar() {
|
|
137
|
+
if (!canScroll.value) return
|
|
138
|
+
shown.value = true
|
|
139
|
+
scheduleHide()
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function refreshLayout() {
|
|
143
|
+
const scrollEl = options.scrollEl.value
|
|
144
|
+
const anchorEl = options.anchorEl?.value ?? scrollEl
|
|
145
|
+
if (!options.active.value || !scrollEl || !anchorEl) {
|
|
146
|
+
canScroll.value = false
|
|
147
|
+
shown.value = false
|
|
148
|
+
return
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const { scrollTop, scrollHeight, clientHeight } = scrollEl
|
|
152
|
+
const scrollable = scrollHeight > clientHeight + 1
|
|
153
|
+
canScroll.value = scrollable
|
|
154
|
+
if (!scrollable) {
|
|
155
|
+
shown.value = false
|
|
156
|
+
return
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const anchorRect = anchorEl.getBoundingClientRect()
|
|
160
|
+
const shrinkInset = (anchorRect.height * (1 - visibleRatio)) / 2
|
|
161
|
+
|
|
162
|
+
zoneTop.value = Math.round(anchorRect.top + shrinkInset)
|
|
163
|
+
zoneRight.value = rightOffsetPx
|
|
164
|
+
zoneHeight.value = Math.round(anchorRect.height * visibleRatio)
|
|
165
|
+
|
|
166
|
+
const trackH = zoneHeight.value
|
|
167
|
+
thumbHeight.value = Math.max(minThumbPx, (clientHeight / scrollHeight) * trackH)
|
|
168
|
+
const maxThumbTravel = Math.max(0, trackH - thumbHeight.value)
|
|
169
|
+
const scrollMax = scrollHeight - clientHeight
|
|
170
|
+
thumbOffset.value = scrollMax > 0 ? Math.round((scrollTop / scrollMax) * maxThumbTravel) : 0
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function scheduleRefresh() {
|
|
174
|
+
cancelAnimationFrame(rafId)
|
|
175
|
+
rafId = requestAnimationFrame(refreshLayout)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function onScroll() {
|
|
179
|
+
revealBar()
|
|
180
|
+
scheduleRefresh()
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function bind() {
|
|
184
|
+
unbind()
|
|
185
|
+
const scrollEl = options.scrollEl.value
|
|
186
|
+
if (!scrollEl) return
|
|
187
|
+
|
|
188
|
+
removeContainerMark = markCapsuleScrollContainer(scrollEl)
|
|
189
|
+
refreshLayout()
|
|
190
|
+
scrollEl.addEventListener('scroll', onScroll, { passive: true })
|
|
191
|
+
resizeObserver = new ResizeObserver(() => scheduleRefresh())
|
|
192
|
+
resizeObserver.observe(scrollEl)
|
|
193
|
+
if (options.anchorEl?.value) resizeObserver.observe(options.anchorEl.value)
|
|
194
|
+
window.addEventListener('resize', scheduleRefresh, { passive: true })
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function unbind() {
|
|
198
|
+
cancelAnimationFrame(rafId)
|
|
199
|
+
clearHideTimer()
|
|
200
|
+
window.removeEventListener('resize', scheduleRefresh)
|
|
201
|
+
resizeObserver?.disconnect()
|
|
202
|
+
resizeObserver = undefined
|
|
203
|
+
thumbDragCleanup?.()
|
|
204
|
+
thumbDragCleanup = undefined
|
|
205
|
+
options.scrollEl.value?.removeEventListener('scroll', onScroll)
|
|
206
|
+
removeContainerMark?.()
|
|
207
|
+
removeContainerMark = undefined
|
|
208
|
+
canScroll.value = false
|
|
209
|
+
shown.value = false
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function onThumbMouseDown(event: MouseEvent) {
|
|
213
|
+
const scrollEl = options.scrollEl.value
|
|
214
|
+
if (!scrollEl) return
|
|
215
|
+
|
|
216
|
+
event.preventDefault()
|
|
217
|
+
revealBar()
|
|
218
|
+
clearHideTimer()
|
|
219
|
+
|
|
220
|
+
const startY = event.clientY
|
|
221
|
+
const startScrollTop = scrollEl.scrollTop
|
|
222
|
+
const trackH = zoneHeight.value
|
|
223
|
+
const scrollMax = scrollEl.scrollHeight - scrollEl.clientHeight
|
|
224
|
+
const maxThumbTravel = Math.max(0, trackH - thumbHeight.value)
|
|
225
|
+
|
|
226
|
+
const onMove = (ev: MouseEvent) => {
|
|
227
|
+
if (maxThumbTravel <= 0 || scrollMax <= 0) return
|
|
228
|
+
scrollEl.scrollTop = startScrollTop + ((ev.clientY - startY) / maxThumbTravel) * scrollMax
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const onUp = () => {
|
|
232
|
+
document.removeEventListener('mousemove', onMove)
|
|
233
|
+
document.removeEventListener('mouseup', onUp)
|
|
234
|
+
thumbDragCleanup = undefined
|
|
235
|
+
scheduleHide()
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
document.addEventListener('mousemove', onMove)
|
|
239
|
+
document.addEventListener('mouseup', onUp)
|
|
240
|
+
thumbDragCleanup = onUp
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
watch(
|
|
244
|
+
() => options.active.value,
|
|
245
|
+
(active) => {
|
|
246
|
+
if (active) {
|
|
247
|
+
void nextTick(() => {
|
|
248
|
+
bind()
|
|
249
|
+
void nextTick(refreshLayout)
|
|
250
|
+
})
|
|
251
|
+
return
|
|
252
|
+
}
|
|
253
|
+
unbind()
|
|
254
|
+
},
|
|
255
|
+
{ immediate: true }
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
onUnmounted(unbind)
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
canScroll,
|
|
262
|
+
shown,
|
|
263
|
+
visible,
|
|
264
|
+
rootStyle,
|
|
265
|
+
thumbStyle,
|
|
266
|
+
rootClass: classPrefix,
|
|
267
|
+
thumbClass: `${classPrefix}__thumb`,
|
|
268
|
+
onThumbMouseDown,
|
|
269
|
+
refresh: refreshLayout,
|
|
270
|
+
bind,
|
|
271
|
+
unbind,
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// --- 頁面滿屏滾動鎖定 ---
|
|
276
|
+
|
|
277
|
+
const LAYOUT_SCROLL_ROOT_SELECTOR = '.main-layout__main'
|
|
278
|
+
|
|
279
|
+
interface OverflowSnapshot {
|
|
280
|
+
el: HTMLElement
|
|
281
|
+
overflow: string
|
|
282
|
+
overflowX: string
|
|
283
|
+
overflowY: string
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function snapshotOverflow(el: HTMLElement): OverflowSnapshot {
|
|
287
|
+
return {
|
|
288
|
+
el,
|
|
289
|
+
overflow: el.style.overflow,
|
|
290
|
+
overflowX: el.style.overflowX,
|
|
291
|
+
overflowY: el.style.overflowY,
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function lockElementOverflow(el: HTMLElement) {
|
|
296
|
+
el.style.overflow = 'hidden'
|
|
297
|
+
el.style.overflowX = 'hidden'
|
|
298
|
+
el.style.overflowY = 'hidden'
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function restoreElementOverflow(snapshot: OverflowSnapshot) {
|
|
302
|
+
const { el, overflow, overflowX, overflowY } = snapshot
|
|
303
|
+
el.style.overflow = overflow
|
|
304
|
+
el.style.overflowX = overflowX
|
|
305
|
+
el.style.overflowY = overflowY
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** 鎖定 document / 佈局主區滾動,返回 cleanup 恢復原 overflow(保留,新頁面請用 hiddenLayoutScroll) */
|
|
309
|
+
export function lockDocumentScroll(): () => void {
|
|
310
|
+
const roots = [
|
|
311
|
+
document.documentElement,
|
|
312
|
+
document.body,
|
|
313
|
+
...Array.from(document.querySelectorAll<HTMLElement>(LAYOUT_SCROLL_ROOT_SELECTOR)),
|
|
314
|
+
]
|
|
315
|
+
|
|
316
|
+
const snapshots = roots.map(snapshotOverflow)
|
|
317
|
+
snapshots.forEach(({ el }) => lockElementOverflow(el))
|
|
318
|
+
|
|
319
|
+
return () => {
|
|
320
|
+
snapshots.forEach(restoreElementOverflow)
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// --- 隱藏 .layout 佈局滾動(進頁調用,離頁 cleanup)---
|
|
325
|
+
// useHiddenLayoutScroll / hiddenLayoutScroll 及配套
|
|
326
|
+
|
|
327
|
+
/** 默認佈局滾動容器:#app 下的 .layout */
|
|
328
|
+
export const LAYOUT_CONTAINER_SELECTOR = '.layout'
|
|
329
|
+
|
|
330
|
+
/** 鎖定時掛上的 class,resize 後仍靠樣式表固定 */
|
|
331
|
+
export const LAYOUT_SCROLL_HIDDEN_CLASS = 'layout--scroll-hidden'
|
|
332
|
+
|
|
333
|
+
const LAYOUT_SCROLL_HIDDEN_STYLE_ID = 'gen-ai-layout-scroll-hidden-style'
|
|
334
|
+
|
|
335
|
+
interface LayoutScrollSnapshot {
|
|
336
|
+
el: HTMLElement
|
|
337
|
+
height: string
|
|
338
|
+
maxHeight: string
|
|
339
|
+
overflow: string
|
|
340
|
+
overflowX: string
|
|
341
|
+
overflowY: string
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
let layoutScrollLockCount = 0
|
|
345
|
+
let layoutScrollSnapshots: LayoutScrollSnapshot[] | null = null
|
|
346
|
+
let layoutScrollActiveSelector = LAYOUT_CONTAINER_SELECTOR
|
|
347
|
+
let layoutScrollReapplyBound = false
|
|
348
|
+
|
|
349
|
+
function ensureLayoutScrollHiddenStyles() {
|
|
350
|
+
if (typeof document === 'undefined') return
|
|
351
|
+
if (document.getElementById(LAYOUT_SCROLL_HIDDEN_STYLE_ID)) return
|
|
352
|
+
|
|
353
|
+
const style = document.createElement('style')
|
|
354
|
+
style.id = LAYOUT_SCROLL_HIDDEN_STYLE_ID
|
|
355
|
+
style.textContent = `
|
|
356
|
+
.${LAYOUT_SCROLL_HIDDEN_CLASS},
|
|
357
|
+
.layout.${LAYOUT_SCROLL_HIDDEN_CLASS},
|
|
358
|
+
.main-layout__main.${LAYOUT_SCROLL_HIDDEN_CLASS},
|
|
359
|
+
.main-layout__main.layout.${LAYOUT_SCROLL_HIDDEN_CLASS} {
|
|
360
|
+
height: 100% !important;
|
|
361
|
+
max-height: 100% !important;
|
|
362
|
+
overflow: hidden !important;
|
|
363
|
+
overflow-x: hidden !important;
|
|
364
|
+
overflow-y: hidden !important;
|
|
365
|
+
}
|
|
366
|
+
`
|
|
367
|
+
document.head.appendChild(style)
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function snapshotLayoutScroll(el: HTMLElement): LayoutScrollSnapshot {
|
|
371
|
+
return {
|
|
372
|
+
el,
|
|
373
|
+
height: el.style.height,
|
|
374
|
+
maxHeight: el.style.maxHeight,
|
|
375
|
+
overflow: el.style.overflow,
|
|
376
|
+
overflowX: el.style.overflowX,
|
|
377
|
+
overflowY: el.style.overflowY,
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function applyHiddenLayoutScroll(el: HTMLElement) {
|
|
382
|
+
ensureLayoutScrollHiddenStyles()
|
|
383
|
+
el.classList.add(LAYOUT_SCROLL_HIDDEN_CLASS)
|
|
384
|
+
// inline + important:防止業務樣式 / resize 覆寫
|
|
385
|
+
el.style.setProperty('height', '100%', 'important')
|
|
386
|
+
el.style.setProperty('max-height', '100%', 'important')
|
|
387
|
+
el.style.setProperty('overflow', 'hidden', 'important')
|
|
388
|
+
el.style.setProperty('overflow-x', 'hidden', 'important')
|
|
389
|
+
el.style.setProperty('overflow-y', 'hidden', 'important')
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function restoreLayoutScroll(snapshot: LayoutScrollSnapshot) {
|
|
393
|
+
const { el, height, maxHeight, overflow, overflowX, overflowY } = snapshot
|
|
394
|
+
if (!el.isConnected) return
|
|
395
|
+
el.classList.remove(LAYOUT_SCROLL_HIDDEN_CLASS)
|
|
396
|
+
el.style.removeProperty('height')
|
|
397
|
+
el.style.removeProperty('max-height')
|
|
398
|
+
el.style.removeProperty('overflow')
|
|
399
|
+
el.style.removeProperty('overflow-x')
|
|
400
|
+
el.style.removeProperty('overflow-y')
|
|
401
|
+
// 還原進頁前的 inline(若原本有)
|
|
402
|
+
if (height) el.style.height = height
|
|
403
|
+
if (maxHeight) el.style.maxHeight = maxHeight
|
|
404
|
+
if (overflow) el.style.overflow = overflow
|
|
405
|
+
if (overflowX) el.style.overflowX = overflowX
|
|
406
|
+
if (overflowY) el.style.overflowY = overflowY
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function resolveLayoutScrollElements(selector: string): HTMLElement[] {
|
|
410
|
+
const fromSelector = Array.from(document.querySelectorAll<HTMLElement>(selector))
|
|
411
|
+
if (fromSelector.length) return fromSelector
|
|
412
|
+
|
|
413
|
+
// 兼容尚未掛 layout class 的主內容區
|
|
414
|
+
return Array.from(document.querySelectorAll<HTMLElement>(LAYOUT_SCROLL_ROOT_SELECTOR))
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function reapplyHiddenLayoutScroll() {
|
|
418
|
+
if (layoutScrollLockCount <= 0) return
|
|
419
|
+
const elements = resolveLayoutScrollElements(layoutScrollActiveSelector)
|
|
420
|
+
|
|
421
|
+
// DOM 若被重建:先快照原樣再套鎖定,離頁才能正確恢復
|
|
422
|
+
if (layoutScrollSnapshots) {
|
|
423
|
+
const known = new Set(layoutScrollSnapshots.map((s) => s.el))
|
|
424
|
+
for (const el of elements) {
|
|
425
|
+
if (!known.has(el)) {
|
|
426
|
+
layoutScrollSnapshots.push(snapshotLayoutScroll(el))
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
elements.forEach(applyHiddenLayoutScroll)
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function bindLayoutScrollReapply() {
|
|
435
|
+
if (layoutScrollReapplyBound) return
|
|
436
|
+
layoutScrollReapplyBound = true
|
|
437
|
+
window.addEventListener('resize', reapplyHiddenLayoutScroll, { passive: true })
|
|
438
|
+
window.visualViewport?.addEventListener('resize', reapplyHiddenLayoutScroll)
|
|
439
|
+
window.visualViewport?.addEventListener('scroll', reapplyHiddenLayoutScroll)
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function unbindLayoutScrollReapply() {
|
|
443
|
+
if (!layoutScrollReapplyBound) return
|
|
444
|
+
layoutScrollReapplyBound = false
|
|
445
|
+
window.removeEventListener('resize', reapplyHiddenLayoutScroll)
|
|
446
|
+
window.visualViewport?.removeEventListener('resize', reapplyHiddenLayoutScroll)
|
|
447
|
+
window.visualViewport?.removeEventListener('scroll', reapplyHiddenLayoutScroll)
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
export interface HiddenLayoutScrollOptions {
|
|
451
|
+
/** 佈局容器選擇器,默認 `.layout` */
|
|
452
|
+
selector?: string
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* 進頁隱藏佈局滾動:`.layout { height: 100%; overflow: hidden }`
|
|
457
|
+
* 離頁調用返回的 cleanup 恢復原樣。支持多次嵌套調用(引用計數)。
|
|
458
|
+
* resize / 視口變化時會自動重套,保證鎖定一直有效。
|
|
459
|
+
*/
|
|
460
|
+
export function hiddenLayoutScroll(options?: HiddenLayoutScrollOptions): () => void {
|
|
461
|
+
const selector = options?.selector ?? LAYOUT_CONTAINER_SELECTOR
|
|
462
|
+
layoutScrollActiveSelector = selector
|
|
463
|
+
const elements = resolveLayoutScrollElements(selector)
|
|
464
|
+
|
|
465
|
+
if (layoutScrollLockCount === 0) {
|
|
466
|
+
layoutScrollSnapshots = elements.map(snapshotLayoutScroll)
|
|
467
|
+
elements.forEach(applyHiddenLayoutScroll)
|
|
468
|
+
bindLayoutScrollReapply()
|
|
469
|
+
} else {
|
|
470
|
+
// 已鎖定時再調用:立即重套,避免漏網
|
|
471
|
+
elements.forEach(applyHiddenLayoutScroll)
|
|
472
|
+
}
|
|
473
|
+
layoutScrollLockCount += 1
|
|
474
|
+
|
|
475
|
+
// 下一幀再套一次,擋住 mount 後樣式回寫
|
|
476
|
+
requestAnimationFrame(() => reapplyHiddenLayoutScroll())
|
|
477
|
+
|
|
478
|
+
let released = false
|
|
479
|
+
return () => {
|
|
480
|
+
if (released) return
|
|
481
|
+
released = true
|
|
482
|
+
layoutScrollLockCount = Math.max(0, layoutScrollLockCount - 1)
|
|
483
|
+
if (layoutScrollLockCount === 0) {
|
|
484
|
+
unbindLayoutScrollReapply()
|
|
485
|
+
layoutScrollSnapshots?.forEach(restoreLayoutScroll)
|
|
486
|
+
layoutScrollSnapshots = null
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* 頁面 setup 內調用即可:自動 onMounted 鎖定 .layout、onUnmounted 恢復。
|
|
493
|
+
* 業務頁無需再手寫 cleanup;resize 期間鎖定保持有效。
|
|
494
|
+
*/
|
|
495
|
+
export function useHiddenLayoutScroll(options?: HiddenLayoutScrollOptions): void {
|
|
496
|
+
let cleanup: (() => void) | undefined
|
|
497
|
+
|
|
498
|
+
onMounted(() => {
|
|
499
|
+
cleanup = hiddenLayoutScroll(options)
|
|
500
|
+
})
|
|
501
|
+
|
|
502
|
+
onUnmounted(() => {
|
|
503
|
+
cleanup?.()
|
|
504
|
+
cleanup = undefined
|
|
505
|
+
})
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// --- 視口主內容區高度 ---
|
|
509
|
+
|
|
510
|
+
export interface ViewportBodyHeightOptions {
|
|
511
|
+
/** 從視口高度中扣除的固定像素 */
|
|
512
|
+
subtract?: number
|
|
513
|
+
/** 按選擇器測量需扣除的高度(如 PageHeader) */
|
|
514
|
+
subtractSelector?: string
|
|
515
|
+
/** 測量 subtractSelector 時的父級容器,默認取 element 的 offsetParent 或 document */
|
|
516
|
+
subtractRoot?: HTMLElement
|
|
517
|
+
/** 是否監聽 resize,默認 true */
|
|
518
|
+
listenResize?: boolean
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/** 當前視口高度(px) */
|
|
522
|
+
export function getViewportHeight(): number {
|
|
523
|
+
return window.innerHeight
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function resolveSubtract(el: HTMLElement, options?: ViewportBodyHeightOptions): number {
|
|
527
|
+
if (options?.subtract !== undefined) return options.subtract
|
|
528
|
+
|
|
529
|
+
if (options?.subtractSelector) {
|
|
530
|
+
const root = options.subtractRoot ?? el.parentElement ?? document.body
|
|
531
|
+
const node = root.querySelector(options.subtractSelector) as HTMLElement | null
|
|
532
|
+
if (node) return node.getBoundingClientRect().height
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
return 0
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/** 計算主內容區可用高度(視口高度 − 扣除項) */
|
|
539
|
+
export function getViewportBodyHeight(
|
|
540
|
+
el: HTMLElement,
|
|
541
|
+
options?: ViewportBodyHeightOptions
|
|
542
|
+
): number {
|
|
543
|
+
const subtract = resolveSubtract(el, options)
|
|
544
|
+
return Math.max(0, getViewportHeight() - subtract)
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* 將元素高度綁定為當前視口可用高度;返回 cleanup(移除監聽並清空 inline height)
|
|
549
|
+
*/
|
|
550
|
+
export function bindViewportBodyHeight(
|
|
551
|
+
el: HTMLElement,
|
|
552
|
+
options?: ViewportBodyHeightOptions
|
|
553
|
+
): () => void {
|
|
554
|
+
const listenResize = options?.listenResize ?? true
|
|
555
|
+
|
|
556
|
+
const update = () => {
|
|
557
|
+
const height = getViewportBodyHeight(el, options)
|
|
558
|
+
el.style.height = `${height}px`
|
|
559
|
+
el.style.maxHeight = `${height}px`
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
update()
|
|
563
|
+
|
|
564
|
+
if (!listenResize) {
|
|
565
|
+
return () => {
|
|
566
|
+
el.style.height = ''
|
|
567
|
+
el.style.maxHeight = ''
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
window.addEventListener('resize', update)
|
|
572
|
+
return () => {
|
|
573
|
+
window.removeEventListener('resize', update)
|
|
574
|
+
el.style.height = ''
|
|
575
|
+
el.style.maxHeight = ''
|
|
576
|
+
}
|
|
577
|
+
}
|