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.
Files changed (86) hide show
  1. package/package.json +12 -0
  2. package/src/App.vue +3 -0
  3. package/src/api/index.ts +19 -0
  4. package/src/api/modules/gen-ai/gen-entry/index.ts +30 -0
  5. package/src/api/modules/gen-ai/model-manager/index.ts +42 -0
  6. package/src/api/modules/gen-ai/model-manager/mockApi.ts +33 -0
  7. package/src/api/modules/index.ts +98 -0
  8. package/src/api/modules/user/index.ts +4 -0
  9. package/src/api/request.ts +102 -0
  10. package/src/assets/sample-access-icon.png +0 -0
  11. package/src/assets/sample-pie-chart.png +0 -0
  12. package/src/assets/vue.svg +1 -0
  13. package/src/components/CapsuleScrollbar.vue +93 -0
  14. package/src/components/Export/ExcelExport.vue +592 -0
  15. package/src/components/Export/ExcelExport2.vue +494 -0
  16. package/src/components/Export/ExcelExport3.vue +342 -0
  17. package/src/components/Export/ExcelExport4.vue +665 -0
  18. package/src/components/Export/excelExport.js +547 -0
  19. package/src/components/Export/excelExport.ts +551 -0
  20. package/src/components/GEN-AI/index.vue +142 -0
  21. package/src/components/GEN-AI/index1.vue +456 -0
  22. package/src/components/GEN-AI/index10.vue +5 -0
  23. package/src/components/GEN-AI/index2.vue +568 -0
  24. package/src/components/GEN-AI/index3.vue +623 -0
  25. package/src/components/GEN-AI/index4.vue +629 -0
  26. package/src/components/GEN-AI/index5.vue +578 -0
  27. package/src/components/GEN-AI/index6.vue +656 -0
  28. package/src/components/GEN-AI/index7.vue +717 -0
  29. package/src/components/GEN-AI/index8.vue +405 -0
  30. package/src/components/GEN-AI/index9.vue +1065 -0
  31. package/src/components/GEN-AI/types.ts +12 -0
  32. package/src/components/GEN-AI/utils.ts +42 -0
  33. package/src/components/HelloWorld.vue +41 -0
  34. package/src/components/PageCard.vue +7 -0
  35. package/src/components/PageHeader.vue +32 -0
  36. package/src/components/backup/index5 copy.vue +556 -0
  37. package/src/components/backup/index5.vue +620 -0
  38. package/src/components/backup/index9 copy.vue +1029 -0
  39. package/src/components/backup/index9-pro.vue +1065 -0
  40. package/src/components/backup/index9.vue +1057 -0
  41. package/src/components/el-date-picker.vue +64 -0
  42. package/src/directives/btnLoading.ts +427 -0
  43. package/src/directives/debounce copy.ts +670 -0
  44. package/src/directives/debounce.ts +98 -0
  45. package/src/directives/index.ts +25 -0
  46. package/src/layouts/MainLayout.vue +101 -0
  47. package/src/main.ts +85 -0
  48. package/src/router/index.ts +76 -0
  49. package/src/router/menus.ts +28 -0
  50. package/src/style.css +79 -0
  51. package/src/styles/_variables.scss +24 -0
  52. package/src/styles/app-button.css +26 -0
  53. package/src/styles/element-overrides.css +23 -0
  54. package/src/styles/global.css +44 -0
  55. package/src/styles/index.scss +1 -0
  56. package/src/styles/page-card.css +21 -0
  57. package/src/styles/variables.css +26 -0
  58. package/src/test/mock.ts +101 -0
  59. package/src/test/test1.vue +402 -0
  60. package/src/test/test2.vue +1689 -0
  61. package/src/types/gen-ai/gen-entry/index.ts +17 -0
  62. package/src/types/gen-ai/model-manager/index.ts +19 -0
  63. package/src/utils/docxExport.ts +1610 -0
  64. package/src/utils/gen-ai-navigation.ts +37 -0
  65. package/src/utils/gen-ai-scroll.ts +455 -0
  66. package/src/utils/openDataLoaderWordExport.ts +33 -0
  67. package/src/utils/pageScrollbar.ts +115 -0
  68. package/src/utils/randomTranscode.ts +87 -0
  69. package/src/utils/reportPdfExport.ts +44 -0
  70. package/src/views/AdminCenter/index.vue +817 -0
  71. package/src/views/Blank.vue +68 -0
  72. package/src/views/Home.vue +29 -0
  73. package/src/views/ReportCenter/index.vue +1380 -0
  74. package/src/views/TemplateCenter/Knowledge.ts +83 -0
  75. package/src/views/TemplateCenter/data.d.ts +10 -0
  76. package/src/views/TemplateCenter/index.vue +1205 -0
  77. package/src/views/TemplateCenter/service.ts +69 -0
  78. package/src/views/gen-ai/components/RecentReportsTable.vue +193 -0
  79. package/src/views/gen-ai/gen-entry/index.vue +309 -0
  80. package/src/views/gen-ai/gen-entry/mockData.ts +160 -0
  81. package/src/views/gen-ai/management-center/index.vue +53 -0
  82. package/src/views/gen-ai/model-manager/ChapterTitleScroll.vue +275 -0
  83. package/src/views/gen-ai/model-manager/index.vue +1205 -0
  84. package/src/views/gen-ai/model-manager/mockData.ts +122 -0
  85. package/src/views/gen-ai/report-center/index.vue +158 -0
  86. 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
+ }