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,37 @@
|
|
|
1
|
+
import type { Router } from 'vue-router'
|
|
2
|
+
|
|
3
|
+
function openGenAiPage(
|
|
4
|
+
router: Router,
|
|
5
|
+
envUrl: string | undefined,
|
|
6
|
+
routeName: string,
|
|
7
|
+
query?: Record<string, string>,
|
|
8
|
+
) {
|
|
9
|
+
const configured = envUrl?.trim()
|
|
10
|
+
const url = configured || router.resolve({ name: routeName, query }).href
|
|
11
|
+
window.open(url, '_blank')
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** 開啟模板中心(優先 VITE_TEMPLATE_CENTER_URL,否則同域路由) */
|
|
15
|
+
export function openTemplateCenter(router: Router) {
|
|
16
|
+
openGenAiPage(router, import.meta.env.VITE_TEMPLATE_CENTER_URL, 'TemplateCenter')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** 開啟模板中心(優先 VITE_MODEL_MANAGER_URL,否則同域路由) */
|
|
20
|
+
export function openModelManager(router: Router, query?: Record<string, string>) {
|
|
21
|
+
openGenAiPage(router, import.meta.env.VITE_MODEL_MANAGER_URL, 'model-manager', query)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** 開啟管理中心(優先 VITE_MANAGEMENT_CENTER_URL,否則同域路由) */
|
|
25
|
+
export function openManagementCenter(router: Router, query?: Record<string, string>) {
|
|
26
|
+
openGenAiPage(router, import.meta.env.VITE_MANAGEMENT_CENTER_URL, 'management-center', query)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** 同頁跳轉報告中心 */
|
|
30
|
+
export function navigateToReportCenter(router: Router) {
|
|
31
|
+
void router.push({ name: 'report-center' })
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** 開啟報告中心(優先 VITE_REPORT_CENTER_URL,否則新分頁打開同域路由) */
|
|
35
|
+
export function openReportCenter(router: Router, query?: Record<string, string>) {
|
|
36
|
+
openGenAiPage(router, import.meta.env.VITE_REPORT_CENTER_URL, 'report-center', query)
|
|
37
|
+
}
|
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
import { computed, nextTick, onUnmounted, ref, watch, type ComputedRef, type Ref } from 'vue'
|
|
2
|
+
|
|
3
|
+
export const CAPSULE_SCROLLBAR_STYLE_ID = 'capsule-scrollbar-styles'
|
|
4
|
+
export const CAPSULE_SCROLL_CONTAINER_CLASS = 'capsule-scroll-container'
|
|
5
|
+
|
|
6
|
+
const NATIVE_GUTTER_HIDE_PX = 20
|
|
7
|
+
|
|
8
|
+
export interface CapsuleScrollbarTheme {
|
|
9
|
+
thumbColor?: string
|
|
10
|
+
thumbHoverColor?: string
|
|
11
|
+
thumbActiveColor?: string
|
|
12
|
+
trackColor?: string
|
|
13
|
+
thumbWidthPx?: number
|
|
14
|
+
classPrefix?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface CapsuleScrollbarOptions extends CapsuleScrollbarTheme {
|
|
18
|
+
scrollEl: Ref<HTMLElement | null>
|
|
19
|
+
anchorEl?: Ref<HTMLElement | null>
|
|
20
|
+
active: Ref<boolean>
|
|
21
|
+
gutterPx?: number
|
|
22
|
+
thumbWidthPx?: number
|
|
23
|
+
minThumbPx?: number
|
|
24
|
+
visibleRatio?: number
|
|
25
|
+
alwaysVisible?: boolean
|
|
26
|
+
autoHideAfterMs?: number
|
|
27
|
+
rightOffsetPx?: number
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface CapsuleScrollbarBindings {
|
|
31
|
+
canScroll: Ref<boolean>
|
|
32
|
+
shown: Ref<boolean>
|
|
33
|
+
visible: ComputedRef<boolean>
|
|
34
|
+
rootStyle: ComputedRef<Record<string, string>>
|
|
35
|
+
thumbStyle: ComputedRef<Record<string, string>>
|
|
36
|
+
rootClass: string
|
|
37
|
+
thumbClass: string
|
|
38
|
+
onThumbMouseDown: (event: MouseEvent) => void
|
|
39
|
+
refresh: () => void
|
|
40
|
+
bind: () => void
|
|
41
|
+
unbind: () => void
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const DEFAULT_THEME: Required<Omit<CapsuleScrollbarTheme, 'thumbWidthPx'>> & { thumbWidthPx: number } = {
|
|
45
|
+
thumbColor: '#999',
|
|
46
|
+
thumbHoverColor: '#777',
|
|
47
|
+
thumbActiveColor: '#666',
|
|
48
|
+
trackColor: 'transparent',
|
|
49
|
+
thumbWidthPx: 10,
|
|
50
|
+
classPrefix: 'capsule-scrollbar',
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function injectCapsuleScrollbarStyles(theme: CapsuleScrollbarTheme = {}): () => void {
|
|
54
|
+
const merged = { ...DEFAULT_THEME, ...theme }
|
|
55
|
+
const p = merged.classPrefix
|
|
56
|
+
const thumbW = merged.thumbWidthPx
|
|
57
|
+
const hide = NATIVE_GUTTER_HIDE_PX
|
|
58
|
+
|
|
59
|
+
const css = `
|
|
60
|
+
.${CAPSULE_SCROLL_CONTAINER_CLASS} {
|
|
61
|
+
scrollbar-width: none !important;
|
|
62
|
+
-ms-overflow-style: none !important;
|
|
63
|
+
overflow-y: auto !important;
|
|
64
|
+
box-sizing: border-box !important;
|
|
65
|
+
padding-right: calc(var(--capsule-scroll-padding-right, 0px) + ${hide}px) !important;
|
|
66
|
+
margin-right: -${hide}px !important;
|
|
67
|
+
width: calc(100% + ${hide}px) !important;
|
|
68
|
+
}
|
|
69
|
+
.${CAPSULE_SCROLL_CONTAINER_CLASS}::-webkit-scrollbar,
|
|
70
|
+
.${CAPSULE_SCROLL_CONTAINER_CLASS}::-webkit-scrollbar-thumb,
|
|
71
|
+
.${CAPSULE_SCROLL_CONTAINER_CLASS}::-webkit-scrollbar-track,
|
|
72
|
+
.${CAPSULE_SCROLL_CONTAINER_CLASS}::-webkit-scrollbar-corner {
|
|
73
|
+
-webkit-appearance: none !important;
|
|
74
|
+
appearance: none !important;
|
|
75
|
+
width: 0 !important;
|
|
76
|
+
height: 0 !important;
|
|
77
|
+
display: none !important;
|
|
78
|
+
opacity: 0 !important;
|
|
79
|
+
visibility: hidden !important;
|
|
80
|
+
background: transparent !important;
|
|
81
|
+
}
|
|
82
|
+
.${p} {
|
|
83
|
+
position: fixed;
|
|
84
|
+
z-index: 12;
|
|
85
|
+
box-sizing: border-box;
|
|
86
|
+
padding: 0;
|
|
87
|
+
pointer-events: none;
|
|
88
|
+
transition: opacity 0.25s ease;
|
|
89
|
+
}
|
|
90
|
+
.${p}--hidden {
|
|
91
|
+
opacity: 0;
|
|
92
|
+
pointer-events: none !important;
|
|
93
|
+
}
|
|
94
|
+
.${p}__thumb {
|
|
95
|
+
width: ${thumbW}px;
|
|
96
|
+
height: 100%;
|
|
97
|
+
background: ${merged.thumbColor};
|
|
98
|
+
border-radius: 9999px;
|
|
99
|
+
cursor: grab;
|
|
100
|
+
pointer-events: auto;
|
|
101
|
+
box-shadow: none;
|
|
102
|
+
filter: none;
|
|
103
|
+
outline: none;
|
|
104
|
+
border: none;
|
|
105
|
+
backface-visibility: hidden;
|
|
106
|
+
-webkit-backface-visibility: hidden;
|
|
107
|
+
}
|
|
108
|
+
.${p}__thumb:hover {
|
|
109
|
+
background: ${merged.thumbHoverColor};
|
|
110
|
+
}
|
|
111
|
+
.${p}__thumb:active {
|
|
112
|
+
cursor: grabbing;
|
|
113
|
+
background: ${merged.thumbActiveColor};
|
|
114
|
+
}
|
|
115
|
+
`
|
|
116
|
+
|
|
117
|
+
let style = document.getElementById(CAPSULE_SCROLLBAR_STYLE_ID) as HTMLStyleElement | null
|
|
118
|
+
if (!style) {
|
|
119
|
+
style = document.createElement('style')
|
|
120
|
+
style.id = CAPSULE_SCROLLBAR_STYLE_ID
|
|
121
|
+
document.head.appendChild(style)
|
|
122
|
+
}
|
|
123
|
+
style.textContent = css
|
|
124
|
+
|
|
125
|
+
return () => style?.remove()
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
let globalCapsuleStylesInitialized = false
|
|
129
|
+
|
|
130
|
+
/** 應用啟動時注入一次;全局組件與舊版 useCapsuleScrollbar 手寫模板共用 */
|
|
131
|
+
export function initCapsuleScrollbarGlobal(theme: CapsuleScrollbarTheme = {}) {
|
|
132
|
+
if (globalCapsuleStylesInitialized) return
|
|
133
|
+
injectCapsuleScrollbarStyles(theme)
|
|
134
|
+
globalCapsuleStylesInitialized = true
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
interface NativeHideSnapshot {
|
|
138
|
+
paddingRight: string
|
|
139
|
+
marginRight: string
|
|
140
|
+
width: string
|
|
141
|
+
boxSizing: string
|
|
142
|
+
scrollbarWidth: string
|
|
143
|
+
msOverflowStyle: string
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function markCapsuleScrollContainer(el: HTMLElement): () => void {
|
|
147
|
+
const computed = getComputedStyle(el)
|
|
148
|
+
const snapshot: NativeHideSnapshot = {
|
|
149
|
+
paddingRight: el.style.paddingRight,
|
|
150
|
+
marginRight: el.style.marginRight,
|
|
151
|
+
width: el.style.width,
|
|
152
|
+
boxSizing: el.style.boxSizing,
|
|
153
|
+
scrollbarWidth: el.style.scrollbarWidth,
|
|
154
|
+
msOverflowStyle: (el.style as CSSStyleDeclaration & { msOverflowStyle?: string }).msOverflowStyle ?? '',
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
el.style.setProperty('--capsule-scroll-padding-right', computed.paddingRight)
|
|
158
|
+
el.classList.add(CAPSULE_SCROLL_CONTAINER_CLASS)
|
|
159
|
+
el.style.scrollbarWidth = 'none'
|
|
160
|
+
;(el.style as CSSStyleDeclaration & { msOverflowStyle?: string }).msOverflowStyle = 'none'
|
|
161
|
+
|
|
162
|
+
return () => {
|
|
163
|
+
el.classList.remove(CAPSULE_SCROLL_CONTAINER_CLASS)
|
|
164
|
+
el.style.removeProperty('--capsule-scroll-padding-right')
|
|
165
|
+
el.style.paddingRight = snapshot.paddingRight
|
|
166
|
+
el.style.marginRight = snapshot.marginRight
|
|
167
|
+
el.style.width = snapshot.width
|
|
168
|
+
el.style.boxSizing = snapshot.boxSizing
|
|
169
|
+
el.style.scrollbarWidth = snapshot.scrollbarWidth
|
|
170
|
+
;(el.style as CSSStyleDeclaration & { msOverflowStyle?: string }).msOverflowStyle =
|
|
171
|
+
snapshot.msOverflowStyle
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function useCapsuleScrollbar(options: CapsuleScrollbarOptions): CapsuleScrollbarBindings {
|
|
176
|
+
const thumbWidthPx = options.thumbWidthPx ?? 10
|
|
177
|
+
const gutterPx = options.gutterPx ?? thumbWidthPx
|
|
178
|
+
const minThumbPx = options.minThumbPx ?? 48
|
|
179
|
+
const visibleRatio = options.visibleRatio ?? 1
|
|
180
|
+
const rightOffsetPx = options.rightOffsetPx ?? 1
|
|
181
|
+
const autoHideAfterMs = options.autoHideAfterMs ?? 3000
|
|
182
|
+
const alwaysVisible = options.alwaysVisible ?? autoHideAfterMs <= 0
|
|
183
|
+
const classPrefix = options.classPrefix ?? DEFAULT_THEME.classPrefix
|
|
184
|
+
|
|
185
|
+
const canScroll = ref(false)
|
|
186
|
+
const shown = ref(false)
|
|
187
|
+
const zoneTop = ref(0)
|
|
188
|
+
const zoneRight = ref(rightOffsetPx)
|
|
189
|
+
const zoneHeight = ref(0)
|
|
190
|
+
const thumbHeight = ref(minThumbPx)
|
|
191
|
+
const thumbOffset = ref(0)
|
|
192
|
+
|
|
193
|
+
const visible = computed(() => canScroll.value && (alwaysVisible || shown.value))
|
|
194
|
+
|
|
195
|
+
let resizeObserver: ResizeObserver | undefined
|
|
196
|
+
let thumbDragCleanup: (() => void) | undefined
|
|
197
|
+
let removeContainerMark: (() => void) | undefined
|
|
198
|
+
let hideTimer: ReturnType<typeof setTimeout> | undefined
|
|
199
|
+
let rafId = 0
|
|
200
|
+
|
|
201
|
+
const rootStyle = computed(() => ({
|
|
202
|
+
top: `${zoneTop.value + thumbOffset.value}px`,
|
|
203
|
+
right: `${zoneRight.value}px`,
|
|
204
|
+
width: `${gutterPx}px`,
|
|
205
|
+
height: `${Math.round(thumbHeight.value)}px`,
|
|
206
|
+
}))
|
|
207
|
+
|
|
208
|
+
const thumbStyle = computed(() => ({
|
|
209
|
+
width: `${thumbWidthPx}px`,
|
|
210
|
+
height: '100%',
|
|
211
|
+
}))
|
|
212
|
+
|
|
213
|
+
function clearHideTimer() {
|
|
214
|
+
if (hideTimer !== undefined) {
|
|
215
|
+
clearTimeout(hideTimer)
|
|
216
|
+
hideTimer = undefined
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function scheduleHide() {
|
|
221
|
+
clearHideTimer()
|
|
222
|
+
if (alwaysVisible || autoHideAfterMs <= 0) return
|
|
223
|
+
hideTimer = setTimeout(() => {
|
|
224
|
+
shown.value = false
|
|
225
|
+
hideTimer = undefined
|
|
226
|
+
}, autoHideAfterMs)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function revealBar() {
|
|
230
|
+
if (!canScroll.value) return
|
|
231
|
+
shown.value = true
|
|
232
|
+
scheduleHide()
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function refreshLayout() {
|
|
236
|
+
const scrollEl = options.scrollEl.value
|
|
237
|
+
const anchorEl = options.anchorEl?.value ?? scrollEl
|
|
238
|
+
if (!options.active.value || !scrollEl || !anchorEl) {
|
|
239
|
+
canScroll.value = false
|
|
240
|
+
shown.value = false
|
|
241
|
+
return
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const { scrollTop, scrollHeight, clientHeight } = scrollEl
|
|
245
|
+
const scrollable = scrollHeight > clientHeight + 1
|
|
246
|
+
canScroll.value = scrollable
|
|
247
|
+
if (!scrollable) {
|
|
248
|
+
shown.value = false
|
|
249
|
+
return
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const anchorRect = anchorEl.getBoundingClientRect()
|
|
253
|
+
const shrinkInset = (anchorRect.height * (1 - visibleRatio)) / 2
|
|
254
|
+
|
|
255
|
+
zoneTop.value = Math.round(anchorRect.top + shrinkInset)
|
|
256
|
+
zoneRight.value = rightOffsetPx
|
|
257
|
+
zoneHeight.value = Math.round(anchorRect.height * visibleRatio)
|
|
258
|
+
|
|
259
|
+
const trackH = zoneHeight.value
|
|
260
|
+
thumbHeight.value = Math.max(minThumbPx, (clientHeight / scrollHeight) * trackH)
|
|
261
|
+
const maxThumbTravel = Math.max(0, trackH - thumbHeight.value)
|
|
262
|
+
const scrollMax = scrollHeight - clientHeight
|
|
263
|
+
thumbOffset.value = scrollMax > 0 ? Math.round((scrollTop / scrollMax) * maxThumbTravel) : 0
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function scheduleRefresh() {
|
|
267
|
+
cancelAnimationFrame(rafId)
|
|
268
|
+
rafId = requestAnimationFrame(refreshLayout)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function onScroll() {
|
|
272
|
+
revealBar()
|
|
273
|
+
scheduleRefresh()
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function bind() {
|
|
277
|
+
unbind()
|
|
278
|
+
const scrollEl = options.scrollEl.value
|
|
279
|
+
if (!scrollEl) return
|
|
280
|
+
|
|
281
|
+
removeContainerMark = markCapsuleScrollContainer(scrollEl)
|
|
282
|
+
refreshLayout()
|
|
283
|
+
scrollEl.addEventListener('scroll', onScroll, { passive: true })
|
|
284
|
+
resizeObserver = new ResizeObserver(() => scheduleRefresh())
|
|
285
|
+
resizeObserver.observe(scrollEl)
|
|
286
|
+
if (options.anchorEl?.value) resizeObserver.observe(options.anchorEl.value)
|
|
287
|
+
window.addEventListener('resize', scheduleRefresh, { passive: true })
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function unbind() {
|
|
291
|
+
cancelAnimationFrame(rafId)
|
|
292
|
+
clearHideTimer()
|
|
293
|
+
window.removeEventListener('resize', scheduleRefresh)
|
|
294
|
+
resizeObserver?.disconnect()
|
|
295
|
+
resizeObserver = undefined
|
|
296
|
+
thumbDragCleanup?.()
|
|
297
|
+
thumbDragCleanup = undefined
|
|
298
|
+
options.scrollEl.value?.removeEventListener('scroll', onScroll)
|
|
299
|
+
removeContainerMark?.()
|
|
300
|
+
removeContainerMark = undefined
|
|
301
|
+
canScroll.value = false
|
|
302
|
+
shown.value = false
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function onThumbMouseDown(event: MouseEvent) {
|
|
306
|
+
const scrollEl = options.scrollEl.value
|
|
307
|
+
if (!scrollEl) return
|
|
308
|
+
|
|
309
|
+
event.preventDefault()
|
|
310
|
+
revealBar()
|
|
311
|
+
clearHideTimer()
|
|
312
|
+
|
|
313
|
+
const startY = event.clientY
|
|
314
|
+
const startScrollTop = scrollEl.scrollTop
|
|
315
|
+
const trackH = zoneHeight.value
|
|
316
|
+
const scrollMax = scrollEl.scrollHeight - scrollEl.clientHeight
|
|
317
|
+
const maxThumbTravel = Math.max(0, trackH - thumbHeight.value)
|
|
318
|
+
|
|
319
|
+
const onMove = (ev: MouseEvent) => {
|
|
320
|
+
if (maxThumbTravel <= 0 || scrollMax <= 0) return
|
|
321
|
+
scrollEl.scrollTop = startScrollTop + ((ev.clientY - startY) / maxThumbTravel) * scrollMax
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const onUp = () => {
|
|
325
|
+
document.removeEventListener('mousemove', onMove)
|
|
326
|
+
document.removeEventListener('mouseup', onUp)
|
|
327
|
+
thumbDragCleanup = undefined
|
|
328
|
+
scheduleHide()
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
document.addEventListener('mousemove', onMove)
|
|
332
|
+
document.addEventListener('mouseup', onUp)
|
|
333
|
+
thumbDragCleanup = onUp
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
watch(
|
|
337
|
+
() => options.active.value,
|
|
338
|
+
(active) => {
|
|
339
|
+
if (active) {
|
|
340
|
+
void nextTick(() => {
|
|
341
|
+
bind()
|
|
342
|
+
void nextTick(refreshLayout)
|
|
343
|
+
})
|
|
344
|
+
return
|
|
345
|
+
}
|
|
346
|
+
unbind()
|
|
347
|
+
},
|
|
348
|
+
{ immediate: true },
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
onUnmounted(unbind)
|
|
352
|
+
|
|
353
|
+
return {
|
|
354
|
+
canScroll,
|
|
355
|
+
shown,
|
|
356
|
+
visible,
|
|
357
|
+
rootStyle,
|
|
358
|
+
thumbStyle,
|
|
359
|
+
rootClass: classPrefix,
|
|
360
|
+
thumbClass: `${classPrefix}__thumb`,
|
|
361
|
+
onThumbMouseDown,
|
|
362
|
+
refresh: refreshLayout,
|
|
363
|
+
bind,
|
|
364
|
+
unbind,
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// --- 頁面滿屏滾動鎖定 ---
|
|
369
|
+
|
|
370
|
+
/** 鎖定 document 滿屏滾動,返回 cleanup 恢復原 overflow */
|
|
371
|
+
export function lockDocumentScroll(): () => void {
|
|
372
|
+
const html = document.documentElement
|
|
373
|
+
const body = document.body
|
|
374
|
+
const prevHtml = html.style.overflow
|
|
375
|
+
const prevBody = body.style.overflow
|
|
376
|
+
|
|
377
|
+
html.style.overflow = 'hidden'
|
|
378
|
+
body.style.overflow = 'hidden'
|
|
379
|
+
|
|
380
|
+
return () => {
|
|
381
|
+
html.style.overflow = prevHtml
|
|
382
|
+
body.style.overflow = prevBody
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// --- 視口主內容區高度 ---
|
|
387
|
+
|
|
388
|
+
export interface ViewportBodyHeightOptions {
|
|
389
|
+
/** 從視口高度中扣除的固定像素 */
|
|
390
|
+
subtract?: number
|
|
391
|
+
/** 按選擇器測量需扣除的高度(如 PageHeader) */
|
|
392
|
+
subtractSelector?: string
|
|
393
|
+
/** 測量 subtractSelector 時的父級容器,默認取 element 的 offsetParent 或 document */
|
|
394
|
+
subtractRoot?: HTMLElement
|
|
395
|
+
/** 是否監聽 resize,默認 true */
|
|
396
|
+
listenResize?: boolean
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/** 當前視口高度(px) */
|
|
400
|
+
export function getViewportHeight(): number {
|
|
401
|
+
return window.innerHeight
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function resolveSubtract(el: HTMLElement, options?: ViewportBodyHeightOptions): number {
|
|
405
|
+
if (options?.subtract !== undefined) return options.subtract
|
|
406
|
+
|
|
407
|
+
if (options?.subtractSelector) {
|
|
408
|
+
const root = options.subtractRoot ?? el.parentElement ?? document.body
|
|
409
|
+
const node = root.querySelector<HTMLElement>(options.subtractSelector)
|
|
410
|
+
if (node) return node.getBoundingClientRect().height
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return 0
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/** 計算主內容區可用高度(視口高度 − 扣除項) */
|
|
417
|
+
export function getViewportBodyHeight(
|
|
418
|
+
el: HTMLElement,
|
|
419
|
+
options?: ViewportBodyHeightOptions,
|
|
420
|
+
): number {
|
|
421
|
+
const subtract = resolveSubtract(el, options)
|
|
422
|
+
return Math.max(0, getViewportHeight() - subtract)
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* 將元素高度綁定為當前視口可用高度;返回 cleanup(移除監聽並清空 inline height)
|
|
427
|
+
*/
|
|
428
|
+
export function bindViewportBodyHeight(
|
|
429
|
+
el: HTMLElement,
|
|
430
|
+
options?: ViewportBodyHeightOptions,
|
|
431
|
+
): () => void {
|
|
432
|
+
const listenResize = options?.listenResize ?? true
|
|
433
|
+
|
|
434
|
+
const update = () => {
|
|
435
|
+
const height = getViewportBodyHeight(el, options)
|
|
436
|
+
el.style.height = `${height}px`
|
|
437
|
+
el.style.maxHeight = `${height}px`
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
update()
|
|
441
|
+
|
|
442
|
+
if (!listenResize) {
|
|
443
|
+
return () => {
|
|
444
|
+
el.style.height = ''
|
|
445
|
+
el.style.maxHeight = ''
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
window.addEventListener('resize', update)
|
|
450
|
+
return () => {
|
|
451
|
+
window.removeEventListener('resize', update)
|
|
452
|
+
el.style.height = ''
|
|
453
|
+
el.style.maxHeight = ''
|
|
454
|
+
}
|
|
455
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const EXPORT_WORD_API = '/opendataloader/export/word'
|
|
2
|
+
|
|
3
|
+
export interface OpenDataLoaderWordExportOptions {
|
|
4
|
+
fileName?: string
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export async function exportPdfViaOpenDataLoader(
|
|
8
|
+
pdfBlob: Blob,
|
|
9
|
+
options: OpenDataLoaderWordExportOptions = {},
|
|
10
|
+
): Promise<Blob> {
|
|
11
|
+
const fileName = options.fileName ?? 'report.pdf'
|
|
12
|
+
const response = await fetch(EXPORT_WORD_API, {
|
|
13
|
+
method: 'POST',
|
|
14
|
+
headers: {
|
|
15
|
+
'Content-Type': 'application/pdf',
|
|
16
|
+
'X-File-Name': encodeURIComponent(fileName),
|
|
17
|
+
},
|
|
18
|
+
body: pdfBlob,
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
if (!response.ok) {
|
|
22
|
+
let message = `Word export failed (${response.status})`
|
|
23
|
+
try {
|
|
24
|
+
const payload = (await response.json()) as { message?: string }
|
|
25
|
+
if (payload.message) message = payload.message
|
|
26
|
+
} catch {
|
|
27
|
+
// ignore json parse errors
|
|
28
|
+
}
|
|
29
|
+
throw new Error(message)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return response.blob()
|
|
33
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { lockDocumentScroll } from './gen-ai-scroll'
|
|
2
|
+
|
|
3
|
+
export { lockDocumentScroll }
|
|
4
|
+
|
|
5
|
+
export interface PageScrollbarStyleOptions {
|
|
6
|
+
/** 滚动条宽高,默认 20px */
|
|
7
|
+
size?: number
|
|
8
|
+
/** 挂载到滚动容器的 class,默认 app-page-scrollbar */
|
|
9
|
+
className?: string
|
|
10
|
+
trackColor?: string
|
|
11
|
+
thumbColor?: string
|
|
12
|
+
thumbHoverColor?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface SetupPageScrollOptions extends PageScrollbarStyleOptions {
|
|
16
|
+
/** 需要滚动的容器(元素或选择器) */
|
|
17
|
+
containers: (HTMLElement | string)[]
|
|
18
|
+
/** 进入页面时锁定 html/body 满屏滚动,默认 true */
|
|
19
|
+
lockDocumentScroll?: boolean
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const DEFAULT_CLASS = 'app-page-scrollbar'
|
|
23
|
+
const STYLE_ID_PREFIX = 'app-page-scrollbar-style'
|
|
24
|
+
|
|
25
|
+
function resolveElements(targets: (HTMLElement | string)[]): HTMLElement[] {
|
|
26
|
+
const result: HTMLElement[] = []
|
|
27
|
+
for (const target of targets) {
|
|
28
|
+
if (typeof target === 'string') {
|
|
29
|
+
result.push(...Array.from(document.querySelectorAll<HTMLElement>(target)))
|
|
30
|
+
} else {
|
|
31
|
+
result.push(target)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return result
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** 注入 20px(可配置)滚动条样式,返回 cleanup */
|
|
38
|
+
export function injectPageScrollbarStyle(options?: PageScrollbarStyleOptions): () => void {
|
|
39
|
+
const size = options?.size ?? 20
|
|
40
|
+
const className = options?.className ?? DEFAULT_CLASS
|
|
41
|
+
const track = options?.trackColor ?? '#f4f5f7'
|
|
42
|
+
const thumb = options?.thumbColor ?? '#cbd5e1'
|
|
43
|
+
const thumbHover = options?.thumbHoverColor ?? '#94a3b8'
|
|
44
|
+
const styleId = `${STYLE_ID_PREFIX}-${size}`
|
|
45
|
+
|
|
46
|
+
const css = `
|
|
47
|
+
.${className}::-webkit-scrollbar {
|
|
48
|
+
width: ${size}px !important;
|
|
49
|
+
height: ${size}px !important;
|
|
50
|
+
}
|
|
51
|
+
.${className}::-webkit-scrollbar-track {
|
|
52
|
+
background: ${track} !important;
|
|
53
|
+
border-radius: 9999px !important;
|
|
54
|
+
}
|
|
55
|
+
.${className}::-webkit-scrollbar-thumb {
|
|
56
|
+
background-color: ${thumb} !important;
|
|
57
|
+
border-radius: 9999px !important;
|
|
58
|
+
border: 3px solid transparent !important;
|
|
59
|
+
background-clip: padding-box !important;
|
|
60
|
+
min-height: 40px;
|
|
61
|
+
}
|
|
62
|
+
.${className}::-webkit-scrollbar-thumb:hover {
|
|
63
|
+
background-color: ${thumbHover} !important;
|
|
64
|
+
}
|
|
65
|
+
.${className} {
|
|
66
|
+
scrollbar-color: ${thumb} ${track};
|
|
67
|
+
scrollbar-width: auto;
|
|
68
|
+
}
|
|
69
|
+
`
|
|
70
|
+
|
|
71
|
+
let style = document.getElementById(styleId) as HTMLStyleElement | null
|
|
72
|
+
if (!style) {
|
|
73
|
+
style = document.createElement('style')
|
|
74
|
+
style.id = styleId
|
|
75
|
+
document.head.appendChild(style)
|
|
76
|
+
}
|
|
77
|
+
style.textContent = css
|
|
78
|
+
|
|
79
|
+
return () => {
|
|
80
|
+
style?.remove()
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
/** 为容器标记滚动条 class,返回 cleanup */
|
|
86
|
+
export function markScrollContainers(
|
|
87
|
+
containers: (HTMLElement | string)[],
|
|
88
|
+
className = DEFAULT_CLASS,
|
|
89
|
+
): () => void {
|
|
90
|
+
const elements = resolveElements(containers)
|
|
91
|
+
elements.forEach((el) => el.classList.add(className))
|
|
92
|
+
|
|
93
|
+
return () => {
|
|
94
|
+
elements.forEach((el) => el.classList.remove(className))
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 进入页面:禁用满屏滚动 + 为指定容器应用定制滚动条(默认 20px)
|
|
100
|
+
* 离开页面时调用返回的 cleanup 恢复
|
|
101
|
+
*/
|
|
102
|
+
export function setupPageScroll(options: SetupPageScrollOptions): () => void {
|
|
103
|
+
const className = options.className ?? DEFAULT_CLASS
|
|
104
|
+
const lockDocument = options.lockDocumentScroll ?? true
|
|
105
|
+
|
|
106
|
+
const cleanStyle = injectPageScrollbarStyle(options)
|
|
107
|
+
const cleanLock = lockDocument ? lockDocumentScroll() : () => {}
|
|
108
|
+
const cleanMark = markScrollContainers(options.containers, className)
|
|
109
|
+
|
|
110
|
+
return () => {
|
|
111
|
+
cleanMark()
|
|
112
|
+
cleanLock()
|
|
113
|
+
cleanStyle()
|
|
114
|
+
}
|
|
115
|
+
}
|