aegon-lv82 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 (148) hide show
  1. package/package.json +12 -0
  2. package/src/App.vue +44 -0
  3. package/src/api/index.ts +19 -0
  4. package/src/api/modules/build-report/reportEditor.ts +526 -0
  5. package/src/api/modules/build-report/reportPermission.ts +64 -0
  6. package/src/api/modules/index.ts +69 -0
  7. package/src/api/modules/industry/green_mortgage.ts +42 -0
  8. package/src/api/modules/user/index.ts +4 -0
  9. package/src/api/request.ts +124 -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 +7 -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 +140 -0
  21. package/src/components/GEN-AI/index1.vue +422 -0
  22. package/src/components/GEN-AI/index10.vue +5 -0
  23. package/src/components/GEN-AI/index2.vue +548 -0
  24. package/src/components/GEN-AI/index3.vue +605 -0
  25. package/src/components/GEN-AI/index4.vue +609 -0
  26. package/src/components/GEN-AI/index5.vue +558 -0
  27. package/src/components/GEN-AI/index6.vue +634 -0
  28. package/src/components/GEN-AI/index7.vue +688 -0
  29. package/src/components/GEN-AI/index8.vue +458 -0
  30. package/src/components/GEN-AI/index9.vue +1217 -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 +124 -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/directives/btnLoading.ts +455 -0
  42. package/src/directives/debounce copy.ts +670 -0
  43. package/src/directives/debounce.ts +98 -0
  44. package/src/directives/index.ts +25 -0
  45. package/src/layouts/MainLayout.vue +129 -0
  46. package/src/main.ts +76 -0
  47. package/src/router/index.ts +142 -0
  48. package/src/router/menus.ts +28 -0
  49. package/src/store/modules/gen-ai/index.ts +1 -0
  50. package/src/store/modules/gen-ai/reportBuild.ts +37 -0
  51. package/src/style.css +79 -0
  52. package/src/styles/report-config-popover.css +39 -0
  53. package/src/styles/report-icon-btn.css +19 -0
  54. package/src/test/mock.ts +101 -0
  55. package/src/test/test1.vue +402 -0
  56. package/src/test/test2.vue +1689 -0
  57. package/src/types/gen-ai/build-report/reportEditor.ts +684 -0
  58. package/src/utils/function.ts +46 -0
  59. package/src/utils/gen-ai-navigation.ts +1 -0
  60. package/src/utils/gen-ai-scroll.ts +1 -0
  61. package/src/utils/openDataLoaderWordExport.ts +33 -0
  62. package/src/utils/pageScrollbar.ts +115 -0
  63. package/src/utils/randomTranscode.ts +87 -0
  64. package/src/utils/reportPdfExport.ts +44 -0
  65. package/src/views/Blank.vue +68 -0
  66. package/src/views/Home.vue +29 -0
  67. package/src/views/TemplateCenter/Knowledge.ts +83 -0
  68. package/src/views/TemplateCenter/data.d.ts +10 -0
  69. package/src/views/TemplateCenter/index.vue +1128 -0
  70. package/src/views/TemplateCenter/service.ts +69 -0
  71. package/src/views/gen-ai/build-report/chartStreamRefs.ts +174 -0
  72. package/src/views/gen-ai/build-report/coherence-check/index.vue +559 -0
  73. package/src/views/gen-ai/build-report/components/ReportChapterWorkspace.vue +4258 -0
  74. package/src/views/gen-ai/build-report/components/ReportConfigPanel.vue +727 -0
  75. package/src/views/gen-ai/build-report/components/ReportGenerateSettingsPanel.vue +2361 -0
  76. package/src/views/gen-ai/build-report/components/ReportGeneratingPanel.vue +1765 -0
  77. package/src/views/gen-ai/build-report/components/ReportPromptPanel.vue +614 -0
  78. package/src/views/gen-ai/build-report/components/ReportSourceFileSummary.vue +656 -0
  79. package/src/views/gen-ai/build-report/components/ReportSourcesPanel.vue +1327 -0
  80. package/src/views/gen-ai/build-report/components/ReportTemplateDetail.vue +191 -0
  81. package/src/views/gen-ai/build-report/components/ReportTemplateEditorFormPanel.vue +667 -0
  82. package/src/views/gen-ai/build-report/components/ReportTemplatePicker.vue +302 -0
  83. package/src/views/gen-ai/build-report/components/ReportWorkflowSidebar.vue +148 -0
  84. package/src/views/gen-ai/build-report/components/paramsSetting.vue +886 -0
  85. package/src/views/gen-ai/build-report/components/reportEdit.vue +2283 -0
  86. package/src/views/gen-ai/build-report/index.vue +856 -0
  87. package/src/views/gen-ai/build-report/report-domain.ts +1133 -0
  88. package/src/views/gen-ai/build-report/report-template-data.ts +295 -0
  89. package/src/views/gen-ai/build-report/useReportWorkflow.ts +1408 -0
  90. package/src/views/gen-ai/common/assets/NotoSans-SC.woff2 +0 -0
  91. package/src/views/gen-ai/common/assets/NotoSans-TC.woff2 +0 -0
  92. package/src/views/gen-ai/common/assets/NotoSans.woff2 +0 -0
  93. package/src/views/gen-ai/common/assets/fonts.css +278 -0
  94. package/src/views/gen-ai/common/assets/pagination.css +163 -0
  95. package/src/views/gen-ai/common/directives/btnLoading.ts +455 -0
  96. package/src/views/gen-ai/common/directives/debounce.ts +98 -0
  97. package/src/views/gen-ai/common/directives/index.ts +30 -0
  98. package/src/views/gen-ai/common/en.md +417 -0
  99. package/src/views/gen-ai/common/index.ts +36 -0
  100. package/src/views/gen-ai/common/types/component-expose.ts +9 -0
  101. package/src/views/gen-ai/common/utils/api-res-status.ts +7 -0
  102. package/src/views/gen-ai/common/utils/navigation.ts +59 -0
  103. package/src/views/gen-ai/common/utils/remote-request.ts +45 -0
  104. package/src/views/gen-ai/common/utils/scroll.ts +577 -0
  105. package/src/views/gen-ai/components/AIExport/docx-export.ts +950 -0
  106. package/src/views/gen-ai/components/AIExport/docxExport.ts +2566 -0
  107. package/src/views/gen-ai/components/AIExport/index.ts +52 -0
  108. package/src/views/gen-ai/components/AIExport/parseMarkdownHtml.ts +649 -0
  109. package/src/views/gen-ai/components/CapsuleScrollbar.vue +149 -0
  110. package/src/views/gen-ai/components/ChapterTitleScroll.vue +468 -0
  111. package/src/views/gen-ai/components/EntryHeroNavCards.vue +379 -0
  112. package/src/views/gen-ai/components/GenAiPagination.vue +52 -0
  113. package/src/views/gen-ai/components/ModelCard.vue +1114 -0
  114. package/src/views/gen-ai/components/Preview/FilePreviewPage.vue +947 -0
  115. package/src/views/gen-ai/components/Preview/PreviewCapsuleScroll.vue +205 -0
  116. package/src/views/gen-ai/components/Preview/PreviewOutlineTree.vue +148 -0
  117. package/src/views/gen-ai/components/Preview/index.ts +11 -0
  118. package/src/views/gen-ai/components/Preview/preview.ts +722 -0
  119. package/src/views/gen-ai/components/PromptField.vue +505 -0
  120. package/src/views/gen-ai/components/RecentReportsTable.vue +974 -0
  121. package/src/views/gen-ai/components/ReportCollapsibleSection.vue +172 -0
  122. package/src/views/gen-ai/components/ReportHistoryList.vue +637 -0
  123. package/src/views/gen-ai/components/ReportSectionConfirmPopover.vue +251 -0
  124. package/src/views/gen-ai/components/editor/ChapterTiptapEditor.vue +3139 -0
  125. package/src/views/gen-ai/components/editor/StructureBlockView.vue +441 -0
  126. package/src/views/gen-ai/components/editor/TiptapToolbar.vue +1036 -0
  127. package/src/views/gen-ai/components/editor/TypewriterDocEditor.vue +398 -0
  128. package/src/views/gen-ai/components/editor/_StructureOrgNode.vue +326 -0
  129. package/src/views/gen-ai/components/editor/index.ts +12 -0
  130. package/src/views/gen-ai/components/editor/parseMarkdownHtml.ts +667 -0
  131. package/src/views/gen-ai/gen-entry/api/index.ts +32 -0
  132. package/src/views/gen-ai/gen-entry/index.vue +287 -0
  133. package/src/views/gen-ai/gen-entry/types/index.ts +4 -0
  134. package/src/views/gen-ai/i.mark +2 -0
  135. package/src/views/gen-ai/management-center/index.vue +1815 -0
  136. package/src/views/gen-ai/management-center/mockData.ts +248 -0
  137. package/src/views/gen-ai/management-center/types/index.ts +37 -0
  138. package/src/views/gen-ai/model-manager/TemplateEditorFormPanel.vue +661 -0
  139. package/src/views/gen-ai/model-manager/api/index.ts +32 -0
  140. package/src/views/gen-ai/model-manager/api/mockApi.ts +33 -0
  141. package/src/views/gen-ai/model-manager/index.vue +1583 -0
  142. package/src/views/gen-ai/model-manager/mockData.ts +177 -0
  143. package/src/views/gen-ai/model-manager/template-detail-shared.ts +154 -0
  144. package/src/views/gen-ai/model-manager/template-editor.vue +345 -0
  145. package/src/views/gen-ai/model-manager/types/index.ts +40 -0
  146. package/src/views/gen-ai/model-manager/utils.ts +25 -0
  147. package/src/views/greenMortgage.vue +1834 -0
  148. package/src/vite-env.d.ts +46 -0
@@ -0,0 +1,947 @@
1
+ <template>
2
+ <div class="file-preview-page" :class="{ 'is-sidebar-collapsed': !sidebarOpen }">
3
+ <header class="file-preview-page__header">
4
+ <div class="file-preview-page__header-left">
5
+ <button
6
+ type="button"
7
+ class="file-preview-page__icon-btn"
8
+ aria-label="切換大綱"
9
+ @click="toggleSidebar"
10
+ >
11
+ <el-icon :size="20"><Operation /></el-icon>
12
+ </button>
13
+ <h1 class="file-preview-page__title" :title="fileName">{{ fileName }}</h1>
14
+ </div>
15
+
16
+ <div class="file-preview-page__header-center">
17
+ <button
18
+ type="button"
19
+ class="file-preview-page__icon-btn"
20
+ aria-label="縮小"
21
+ :disabled="zoomPercent <= 50"
22
+ @click="zoomBy(-10)"
23
+ >
24
+ <el-icon :size="18"><Minus /></el-icon>
25
+ </button>
26
+ <span class="file-preview-page__zoom">{{ zoomPercent }}%</span>
27
+ <button
28
+ type="button"
29
+ class="file-preview-page__icon-btn"
30
+ aria-label="放大"
31
+ :disabled="zoomPercent >= 200"
32
+ @click="zoomBy(10)"
33
+ >
34
+ <el-icon :size="18"><Plus /></el-icon>
35
+ </button>
36
+ <span class="file-preview-page__chars">{{ charCount }} 字元</span>
37
+ </div>
38
+
39
+ <div class="file-preview-page__header-right">
40
+ <button type="button" class="file-preview-page__text-btn" @click="handlePrint">
41
+ <el-icon :size="16"><Printer /></el-icon>
42
+ 列印
43
+ </button>
44
+ <button type="button" class="file-preview-page__text-btn" @click="handleDownload">
45
+ <el-icon :size="16"><Download /></el-icon>
46
+ 下載
47
+ </button>
48
+ <button
49
+ type="button"
50
+ class="file-preview-page__icon-btn"
51
+ aria-label="關閉"
52
+ @click="handleClose"
53
+ >
54
+ <el-icon :size="18"><Close /></el-icon>
55
+ </button>
56
+ </div>
57
+ </header>
58
+
59
+ <div class="file-preview-page__body">
60
+ <!-- 目錄:與正文同一 PreviewCapsuleScroll,高度=視口-頂欄 -->
61
+ <aside v-show="sidebarOpen" class="file-preview-page__sidebar">
62
+ <PreviewCapsuleScroll ref="sidebarScrollRef" class="file-preview-page__panel-scroll">
63
+ <div v-if="!outline.length" class="file-preview-page__outline-empty">暫無大綱</div>
64
+ <div v-else class="file-preview-page__outline-pad">
65
+ <PreviewOutlineTree
66
+ :items="outline"
67
+ :expanded-ids="expandedIds"
68
+ :active-id="activeOutlineId"
69
+ @toggle="toggleOutline"
70
+ @select="scrollToOutline"
71
+ />
72
+ </div>
73
+ </PreviewCapsuleScroll>
74
+ </aside>
75
+
76
+ <!-- 正文 -->
77
+ <main class="file-preview-page__stage" v-loading="loading">
78
+ <PreviewCapsuleScroll ref="stageScrollRef" class="file-preview-page__panel-scroll">
79
+ <div ref="stageRef" class="file-preview-page__stage-inner">
80
+ <div class="file-preview-page__scale" :style="{ zoom: zoomPercent / 100 }">
81
+ <div v-if="errorMessage" class="file-preview-page__error">{{ errorMessage }}</div>
82
+
83
+ <template v-else>
84
+ <!-- PDF → @vue-office/pdf -->
85
+ <div
86
+ v-if="kind === 'pdf' && officeSrc"
87
+ class="file-preview-page__paper file-preview-page__paper--office file-preview-page__paper--pdf"
88
+ >
89
+ <VueOfficePdf
90
+ :src="officeSrc"
91
+ class="file-preview-page__office-viewer"
92
+ @rendered="onOfficeRendered"
93
+ @error="onOfficeError"
94
+ />
95
+ </div>
96
+
97
+ <!-- DOCX → docx-preview;失敗再 mammoth -->
98
+ <div
99
+ v-else-if="kind === 'docx' && !mammothHtml"
100
+ class="file-preview-page__paper file-preview-page__paper--office"
101
+ >
102
+ <div
103
+ ref="docxPreviewHostRef"
104
+ class="file-preview-page__office-viewer file-preview-page__docx-native"
105
+ />
106
+ </div>
107
+
108
+ <!-- DOC / DOCX fallback → mammoth -->
109
+ <article
110
+ v-else-if="mammothHtml"
111
+ ref="printRef"
112
+ class="file-preview-page__paper file-preview-page__paper--html"
113
+ v-html="mammothHtml"
114
+ />
115
+
116
+ <!-- 圖片 -->
117
+ <div
118
+ v-else-if="kind === 'image' && imageUrl"
119
+ class="file-preview-page__paper file-preview-page__paper--image"
120
+ >
121
+ <img :src="imageUrl" :alt="fileName" class="file-preview-page__image" />
122
+ </div>
123
+
124
+ <div v-else-if="!loading" class="file-preview-page__error">暫無可預覽內容</div>
125
+ </template>
126
+ </div>
127
+ </div>
128
+ </PreviewCapsuleScroll>
129
+ </main>
130
+ </div>
131
+ </div>
132
+ </template>
133
+
134
+ <script setup lang="ts">
135
+ import { Close, Download, Minus, Operation, Plus, Printer } from '@element-plus/icons-vue'
136
+ import { ElIcon, ElMessage } from 'element-plus'
137
+ import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
138
+ import { useRoute } from 'vue-router'
139
+ /** postinstall 後可用包入口;類型見 shims / 包內 d.ts */
140
+ import VueOfficePdf from '@vue-office/pdf'
141
+ import '../../common/assets/fonts.css'
142
+ import {
143
+ blobToArrayBuffer,
144
+ buildOutlineFromOrderedLists,
145
+ clampZoomPercent,
146
+ clearDocxPreviewContainer,
147
+ clearFilePreviewPayload,
148
+ convertOfficeBlobToHtml,
149
+ countPlainTextChars,
150
+ decorateHtmlWithOutline,
151
+ downloadBlobAsFile,
152
+ fetchPreviewFileBlob,
153
+ getPreviewFileKind,
154
+ invalidateDocxPreviewRender,
155
+ printPreviewElement,
156
+ readFilePreviewPayload,
157
+ renderDocxWithDocxPreview,
158
+ stripHtmlToText,
159
+ type PreviewFileKind,
160
+ type PreviewOutlineItem,
161
+ } from './preview'
162
+ import PreviewCapsuleScroll from './PreviewCapsuleScroll.vue'
163
+ import PreviewOutlineTree from './PreviewOutlineTree.vue'
164
+
165
+ const route = useRoute()
166
+ const token = computed(() => String(route.query.token || '').trim())
167
+
168
+ const loading = ref(true)
169
+ const errorMessage = ref('')
170
+ const fileName = ref('檔案預覽')
171
+ const kind = ref<PreviewFileKind>('unknown')
172
+ const zoomPercent = ref(100)
173
+ const charCount = ref(0)
174
+ /** 無大綱時默認收起;有大綱時自動展開(用戶手動收起後不再強行打開) */
175
+ const sidebarOpen = ref(false)
176
+ const sidebarManualCollapsed = ref(false)
177
+ const outline = ref<PreviewOutlineItem[]>([])
178
+ const expandedIds = ref<Set<string>>(new Set())
179
+ const activeOutlineId = ref('')
180
+ const officeSrc = ref<string | ArrayBuffer | Blob | undefined>()
181
+ const mammothHtml = ref('')
182
+ const imageUrl = ref('')
183
+ const fileBlob = ref<Blob | null>(null)
184
+ const printRef = ref<HTMLElement | null>(null)
185
+ const docxPreviewHostRef = ref<HTMLElement | null>(null)
186
+ const stageRef = ref<HTMLElement | null>(null)
187
+ const sidebarScrollRef = ref<InstanceType<typeof PreviewCapsuleScroll> | null>(null)
188
+ const stageScrollRef = ref<InstanceType<typeof PreviewCapsuleScroll> | null>(null)
189
+
190
+ let abort: AbortController | null = null
191
+ let imageObjectUrl = ''
192
+
193
+ function refreshSidebarScroll() {
194
+ sidebarScrollRef.value?.refresh()
195
+ }
196
+
197
+ function refreshStageScroll() {
198
+ stageScrollRef.value?.refresh()
199
+ }
200
+
201
+ function getStageScrollWrap() {
202
+ return stageScrollRef.value?.getWrap() ?? null
203
+ }
204
+
205
+ const hasOutline = computed(() => outline.value.length > 0)
206
+
207
+ function toggleSidebar() {
208
+ const next = !sidebarOpen.value
209
+ sidebarOpen.value = next
210
+ // 有大綱時手動收起:記住,避免 outline 重建後又被自動打開
211
+ sidebarManualCollapsed.value = hasOutline.value ? !next : false
212
+ }
213
+
214
+ watch(hasOutline, (has) => {
215
+ if (!has) {
216
+ sidebarOpen.value = false
217
+ sidebarManualCollapsed.value = false
218
+ return
219
+ }
220
+ if (!sidebarManualCollapsed.value) sidebarOpen.value = true
221
+ void nextTick(() => refreshSidebarScroll())
222
+ })
223
+
224
+ watch(sidebarOpen, (open) => {
225
+ if (!open) return
226
+ void nextTick(() => {
227
+ refreshSidebarScroll()
228
+ window.requestAnimationFrame(() => refreshSidebarScroll())
229
+ })
230
+ })
231
+
232
+ watch(
233
+ [outline, expandedIds, sidebarOpen],
234
+ () => {
235
+ if (!sidebarOpen.value) return
236
+ void nextTick(() => refreshSidebarScroll())
237
+ },
238
+ { deep: true }
239
+ )
240
+
241
+ watch([zoomPercent, loading, mammothHtml, officeSrc], () => {
242
+ void nextTick(() => refreshStageScroll())
243
+ })
244
+
245
+ function revokeImageUrl() {
246
+ if (imageObjectUrl) {
247
+ URL.revokeObjectURL(imageObjectUrl)
248
+ imageObjectUrl = ''
249
+ }
250
+ imageUrl.value = ''
251
+ }
252
+
253
+ async function applyMammothHtml(blob: Blob) {
254
+ const converted = await convertOfficeBlobToHtml(blob)
255
+ const decorated = decorateHtmlWithOutline(converted.html)
256
+ if (!decorated.html.trim()) throw new Error('empty')
257
+ invalidateDocxPreviewRender()
258
+ clearDocxPreviewContainer(docxPreviewHostRef.value)
259
+ mammothHtml.value = decorated.html
260
+ outline.value = decorated.outline
261
+ const ids = new Set<string>()
262
+ collectOutlineIds(decorated.outline, ids)
263
+ expandedIds.value = ids
264
+ charCount.value = countPlainTextChars(stripHtmlToText(decorated.html))
265
+ loading.value = false
266
+ officeSrc.value = undefined
267
+ await nextTick()
268
+ if (stageRef.value) buildOutlineFromDom(stageRef.value)
269
+ }
270
+
271
+ function zoomBy(delta: number) {
272
+ zoomPercent.value = clampZoomPercent(zoomPercent.value + delta)
273
+ }
274
+
275
+ function collectOutlineIds(items: PreviewOutlineItem[], into: Set<string>) {
276
+ for (const item of items) {
277
+ into.add(item.id)
278
+ if (item.children?.length) collectOutlineIds(item.children, into)
279
+ }
280
+ }
281
+
282
+ function toggleOutline(id: string) {
283
+ const next = new Set(expandedIds.value)
284
+ if (next.has(id)) next.delete(id)
285
+ else next.add(id)
286
+ expandedIds.value = next
287
+ }
288
+
289
+ function findOutlineTitle(items: PreviewOutlineItem[], id: string): string {
290
+ for (const item of items) {
291
+ if (item.id === id) return item.title
292
+ if (item.children?.length) {
293
+ const found = findOutlineTitle(item.children, id)
294
+ if (found) return found
295
+ }
296
+ }
297
+ return ''
298
+ }
299
+
300
+ function normalizeOutlineText(text: string) {
301
+ return (text || '').replace(/\s+/g, ' ').trim()
302
+ }
303
+
304
+ function scrollElementIntoStage(el: HTMLElement) {
305
+ const stage = getStageScrollWrap()
306
+ if (!stage) {
307
+ el.scrollIntoView({ behavior: 'smooth', block: 'start' })
308
+ return
309
+ }
310
+ // 在可滾動 stage 內定位(兼容 zoom)
311
+ const stageRect = stage.getBoundingClientRect()
312
+ const elRect = el.getBoundingClientRect()
313
+ const nextTop = stage.scrollTop + (elRect.top - stageRect.top) - 12
314
+ stage.scrollTo({ top: Math.max(0, nextTop), behavior: 'smooth' })
315
+ }
316
+
317
+ function scrollToOutline(id: string) {
318
+ activeOutlineId.value = id
319
+
320
+ const byId =
321
+ (document.getElementById(id) as HTMLElement | null) ||
322
+ (stageRef.value?.querySelector(`[data-outline-id="${CSS.escape(id)}"]`) as HTMLElement | null)
323
+ if (byId) {
324
+ scrollElementIntoStage(byId)
325
+ return
326
+ }
327
+
328
+ const title = normalizeOutlineText(findOutlineTitle(outline.value, id))
329
+ if (!title || !stageRef.value) return
330
+
331
+ // 僅在正文標題 / 有序列表錨點中匹配
332
+ const candidates = Array.from(
333
+ stageRef.value.querySelectorAll(
334
+ 'h1, h2, h3, h4, h5, h6, ol > li, [data-outline-id], .docx-wrapper [class*="Heading"], .docx-wrapper [class*="heading"]'
335
+ )
336
+ ) as HTMLElement[]
337
+ const matched =
338
+ candidates.find((node) => normalizeOutlineText(node.textContent || '') === title) ||
339
+ candidates.find((node) => normalizeOutlineText(node.textContent || '').includes(title))
340
+
341
+ if (matched) {
342
+ const target = matched as HTMLElement
343
+ if (!target.id) target.id = id
344
+ target.setAttribute('data-outline-id', id)
345
+ scrollElementIntoStage(target)
346
+ }
347
+ }
348
+
349
+ /** 左側大綱:僅以正文標題 / 有序列表為準 */
350
+ function buildOutlineFromDom(root: HTMLElement) {
351
+ // 清掉舊錨點,避免切檔 / 重渲後殘留非有序項
352
+ root.querySelectorAll('[data-outline-id]').forEach((el) => {
353
+ if (el instanceof HTMLElement) {
354
+ el.removeAttribute('data-outline-id')
355
+ if (el.id?.startsWith('outline-')) el.removeAttribute('id')
356
+ }
357
+ })
358
+
359
+ const roots = buildOutlineFromOrderedLists(root, { stamp: true })
360
+ outline.value = roots
361
+ const ids = new Set<string>()
362
+ collectOutlineIds(roots, ids)
363
+ expandedIds.value = ids
364
+ void nextTick(() => {
365
+ refreshSidebarScroll()
366
+ refreshStageScroll()
367
+ })
368
+ }
369
+
370
+ async function nextTickCountFromDom() {
371
+ await Promise.resolve()
372
+ // 等 vue-office 佈局穩定後再抽大綱並打錨點
373
+ await new Promise<void>((resolve) => {
374
+ window.requestAnimationFrame(() => resolve())
375
+ })
376
+ const root = stageRef.value
377
+ if (!root) return
378
+ charCount.value = countPlainTextChars(root.innerText || '')
379
+ buildOutlineFromDom(root)
380
+ void nextTick(() => refreshStageScroll())
381
+ }
382
+
383
+ function onOfficeRendered() {
384
+ loading.value = false
385
+ void nextTickCountFromDom()
386
+ }
387
+
388
+ function onOfficeError() {
389
+ loading.value = false
390
+ errorMessage.value = '檔案預覽失敗'
391
+ }
392
+
393
+ function handlePrint() {
394
+ if (mammothHtml.value && printRef.value) {
395
+ printPreviewElement(printRef.value)
396
+ return
397
+ }
398
+ window.print()
399
+ }
400
+
401
+ function handleDownload() {
402
+ if (!fileBlob.value) {
403
+ ElMessage.warning('檔案尚未載入完成')
404
+ return
405
+ }
406
+ downloadBlobAsFile(fileBlob.value, fileName.value)
407
+ }
408
+
409
+ function handleClose() {
410
+ window.close()
411
+ window.setTimeout(() => {
412
+ if (!window.closed) ElMessage.info('請手動關閉此分頁')
413
+ }, 200)
414
+ }
415
+
416
+ async function tryRenderDocxNative(blob: Blob) {
417
+ invalidateDocxPreviewRender()
418
+ clearDocxPreviewContainer(docxPreviewHostRef.value)
419
+
420
+ await nextTick()
421
+ const host = docxPreviewHostRef.value
422
+ if (!host) return false
423
+
424
+ try {
425
+ const buffer = await blobToArrayBuffer(blob)
426
+ await renderDocxWithDocxPreview(host, buffer)
427
+ onOfficeRendered()
428
+ return true
429
+ } catch (err) {
430
+ console.warn('[file-preview] docx-preview failed, fallback mammoth', err)
431
+ clearDocxPreviewContainer(host)
432
+ return false
433
+ }
434
+ }
435
+
436
+ async function loadPreview() {
437
+ loading.value = true
438
+ errorMessage.value = ''
439
+ mammothHtml.value = ''
440
+ officeSrc.value = undefined
441
+ invalidateDocxPreviewRender()
442
+ clearDocxPreviewContainer(docxPreviewHostRef.value)
443
+ outline.value = []
444
+ fileBlob.value = null
445
+ revokeImageUrl()
446
+
447
+ const t = token.value
448
+ if (!t) {
449
+ loading.value = false
450
+ errorMessage.value = '缺少預覽參數'
451
+ return
452
+ }
453
+ const payload = readFilePreviewPayload(t)
454
+ if (!payload) {
455
+ loading.value = false
456
+ errorMessage.value = '預覽會話已失效,請返回重新點擊預覽'
457
+ return
458
+ }
459
+
460
+ fileName.value = payload.name
461
+ document.title = payload.name
462
+ kind.value = getPreviewFileKind(payload.name)
463
+
464
+ abort?.abort()
465
+ abort = new AbortController()
466
+
467
+ try {
468
+ const remoteUrl = String(payload.url || '').trim()
469
+ const useRemoteUrl = /^https?:\/\//i.test(remoteUrl)
470
+
471
+ // tip / REF:token 裡已有下載地址,PDF/圖片直接用該 URL 渲染,不走本系統 download 接口
472
+ if (useRemoteUrl && kind.value === 'pdf') {
473
+ officeSrc.value = remoteUrl
474
+ return
475
+ }
476
+ if (useRemoteUrl && kind.value === 'image') {
477
+ imageUrl.value = remoteUrl
478
+ loading.value = false
479
+ return
480
+ }
481
+
482
+ const blob = await fetchPreviewFileBlob(payload, abort.signal)
483
+ fileBlob.value = blob
484
+
485
+ if (kind.value === 'pdf') {
486
+ officeSrc.value = await blobToArrayBuffer(blob)
487
+ return
488
+ }
489
+
490
+ if (kind.value === 'docx') {
491
+ const nativeOk = await tryRenderDocxNative(blob)
492
+ if (nativeOk) {
493
+ // 並行 mammoth 僅預填大綱 / 字數(不替換正文)
494
+ void convertOfficeBlobToHtml(blob)
495
+ .then((converted) => {
496
+ if (!outline.value.length) {
497
+ const decorated = decorateHtmlWithOutline(converted.html)
498
+ outline.value = decorated.outline
499
+ const ids = new Set<string>()
500
+ collectOutlineIds(decorated.outline, ids)
501
+ expandedIds.value = ids
502
+ }
503
+ if (!charCount.value) {
504
+ charCount.value = countPlainTextChars(stripHtmlToText(converted.html))
505
+ }
506
+ })
507
+ .catch(() => undefined)
508
+ return
509
+ }
510
+ try {
511
+ await applyMammothHtml(blob)
512
+ } catch {
513
+ errorMessage.value = 'DOCX 預覽失敗'
514
+ loading.value = false
515
+ }
516
+ return
517
+ }
518
+
519
+ if (kind.value === 'doc') {
520
+ try {
521
+ await applyMammothHtml(blob)
522
+ } catch (err) {
523
+ console.error(err)
524
+ errorMessage.value = '舊版 DOC 預覽失敗,請轉為 DOCX 後再試'
525
+ loading.value = false
526
+ }
527
+ return
528
+ }
529
+
530
+ if (kind.value === 'image') {
531
+ revokeImageUrl()
532
+ imageObjectUrl = URL.createObjectURL(blob)
533
+ imageUrl.value = imageObjectUrl
534
+ charCount.value = 0
535
+ loading.value = false
536
+ return
537
+ }
538
+
539
+ errorMessage.value = '暫僅支持預覽 PDF / DOCX / DOC / 圖片'
540
+ loading.value = false
541
+ } catch (err) {
542
+ console.error(err)
543
+ errorMessage.value = err instanceof Error && err.message.trim() ? err.message : '檔案載入失敗'
544
+ loading.value = false
545
+ }
546
+ }
547
+
548
+ function onWindowResize() {
549
+ refreshSidebarScroll()
550
+ refreshStageScroll()
551
+ }
552
+
553
+ onMounted(() => {
554
+ void loadPreview()
555
+ window.addEventListener('resize', onWindowResize, { passive: true })
556
+ void nextTick(() => {
557
+ refreshSidebarScroll()
558
+ refreshStageScroll()
559
+ })
560
+ })
561
+
562
+ onBeforeUnmount(() => {
563
+ window.removeEventListener('resize', onWindowResize)
564
+ abort?.abort()
565
+ invalidateDocxPreviewRender()
566
+ clearDocxPreviewContainer(docxPreviewHostRef.value)
567
+ revokeImageUrl()
568
+ if (token.value) clearFilePreviewPayload(token.value)
569
+ })
570
+ </script>
571
+
572
+ <style scoped>
573
+ .file-preview-page {
574
+ --fp-header-h: 72px;
575
+ --fp-font: var(--gen-ai-font-family, 'Noto Sans', 'Noto Sans TC', sans-serif);
576
+ display: flex;
577
+ flex-direction: column;
578
+ height: 100vh;
579
+ max-height: 100vh;
580
+ min-height: 100vh;
581
+ overflow: hidden;
582
+ background: #e8e8e8;
583
+ color: #303133;
584
+ font-family: var(--fp-font);
585
+ font-size: 15px;
586
+ font-weight: 500;
587
+ }
588
+
589
+ .file-preview-page__header {
590
+ flex-shrink: 0;
591
+ display: grid;
592
+ grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
593
+ align-items: center;
594
+ gap: 12px;
595
+ height: var(--fp-header-h);
596
+ padding: 0 12px;
597
+ background: #fff;
598
+ border-bottom: 1px solid #e5e7eb;
599
+ z-index: 10;
600
+ font-family: var(--fp-font);
601
+ font-size: 15px;
602
+ font-weight: 500;
603
+ color: #303133;
604
+ }
605
+
606
+ .file-preview-page__header-left,
607
+ .file-preview-page__header-right {
608
+ display: flex;
609
+ align-items: center;
610
+ gap: 8px;
611
+ min-width: 0;
612
+ }
613
+
614
+ .file-preview-page__header-right {
615
+ justify-content: flex-end;
616
+ }
617
+
618
+ .file-preview-page__header-center {
619
+ display: inline-flex;
620
+ align-items: center;
621
+ gap: 8px;
622
+ }
623
+
624
+ .file-preview-page__title {
625
+ margin: 0;
626
+ font-family: var(--fp-font);
627
+ font-size: 18px;
628
+ font-weight: 500;
629
+ color: #303133;
630
+ overflow: hidden;
631
+ text-overflow: ellipsis;
632
+ white-space: nowrap;
633
+ }
634
+
635
+ .file-preview-page__zoom {
636
+ box-sizing: border-box;
637
+ display: inline-flex;
638
+ align-items: center;
639
+ justify-content: center;
640
+ min-width: 64px;
641
+ height: 28px;
642
+ padding: 0 10px;
643
+ border: 1px solid #dcdfe6;
644
+ border-radius: 4px;
645
+ background: #fff;
646
+ font-family: var(--fp-font);
647
+ font-size: 15px;
648
+ font-weight: 500;
649
+ color: #303133;
650
+ line-height: 1;
651
+ text-align: center;
652
+ }
653
+
654
+ .file-preview-page__chars {
655
+ margin-left: 8px;
656
+ font-family: var(--fp-font);
657
+ font-size: 15px;
658
+ font-weight: 500;
659
+ color: #303133;
660
+ text-align: center;
661
+ }
662
+
663
+ .file-preview-page__icon-btn,
664
+ .file-preview-page__text-btn {
665
+ display: inline-flex;
666
+ align-items: center;
667
+ justify-content: center;
668
+ gap: 4px;
669
+ height: 32px;
670
+ padding: 0 8px;
671
+ border: none;
672
+ border-radius: 4px;
673
+ background: transparent;
674
+ color: #303133;
675
+ cursor: pointer;
676
+ font-family: var(--fp-font);
677
+ font-size: 15px;
678
+ font-weight: 500;
679
+ }
680
+
681
+ .file-preview-page__icon-btn {
682
+ width: 32px;
683
+ padding: 0;
684
+ }
685
+
686
+ .file-preview-page__icon-btn:hover,
687
+ .file-preview-page__text-btn:hover {
688
+ background: #f5f5f5;
689
+ }
690
+
691
+ .file-preview-page__icon-btn:disabled {
692
+ opacity: 0.4;
693
+ cursor: not-allowed;
694
+ }
695
+
696
+ .file-preview-page__body {
697
+ flex: 1;
698
+ display: flex;
699
+ min-height: 0;
700
+ /* 正文 / 目錄共用視口高度 = 整頁 - 頂欄 */
701
+ height: calc(100vh - var(--fp-header-h));
702
+ max-height: calc(100vh - var(--fp-header-h));
703
+ overflow: hidden;
704
+ }
705
+
706
+ .file-preview-page__sidebar {
707
+ flex-shrink: 0;
708
+ width: 385px;
709
+ height: 100%;
710
+ max-height: 100%;
711
+ min-height: 0;
712
+ display: flex;
713
+ flex-direction: column;
714
+ overflow: hidden;
715
+ background: #fff;
716
+ border-right: 1px solid #e5e7eb;
717
+ box-sizing: border-box;
718
+ font-family: var(--fp-font);
719
+ font-size: 16px;
720
+ font-weight: 500;
721
+ color: #303133;
722
+ }
723
+
724
+ /* 目錄 / 正文滾動宿主:同一 class,同一高度鏈 */
725
+ .file-preview-page__panel-scroll {
726
+ flex: 1 1 auto;
727
+ min-height: 0 !important;
728
+ height: 100% !important;
729
+ max-height: 100% !important;
730
+ width: 100%;
731
+ }
732
+
733
+ .file-preview-page__outline-pad {
734
+ box-sizing: border-box;
735
+ padding: 12px 16px 12px 12px;
736
+ font-family: var(--fp-font);
737
+ font-size: 16px;
738
+ font-weight: 500;
739
+ color: #303133;
740
+ }
741
+
742
+ .file-preview-page__outline-empty {
743
+ box-sizing: border-box;
744
+ display: flex;
745
+ align-items: center;
746
+ justify-content: center;
747
+ min-height: 240px;
748
+ padding: 16px 12px;
749
+ font-family: var(--fp-font);
750
+ font-size: 16px;
751
+ font-weight: 500;
752
+ color: #606266;
753
+ text-align: center;
754
+ }
755
+
756
+ .file-preview-page__stage {
757
+ flex: 1;
758
+ min-width: 0;
759
+ height: 100%;
760
+ max-height: 100%;
761
+ min-height: 0;
762
+ display: flex;
763
+ flex-direction: column;
764
+ overflow: hidden;
765
+ }
766
+
767
+ .file-preview-page__stage-inner {
768
+ box-sizing: border-box;
769
+ padding: 24px 16px 48px;
770
+ }
771
+
772
+ .file-preview-page__scale {
773
+ width: 210mm;
774
+ max-width: 100%;
775
+ margin: 0 auto;
776
+ }
777
+
778
+ .file-preview-page__paper {
779
+ box-sizing: border-box;
780
+ width: 100%;
781
+ min-height: 297mm;
782
+ background: #fff;
783
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.12);
784
+ }
785
+
786
+ .file-preview-page__paper--html {
787
+ padding: 48px 56px;
788
+ font-family: var(--fp-font);
789
+ font-size: 15px;
790
+ font-weight: 400;
791
+ line-height: 1.7;
792
+ color: #303133;
793
+ }
794
+
795
+ .file-preview-page__paper--html :deep(h1),
796
+ .file-preview-page__paper--html :deep(h2),
797
+ .file-preview-page__paper--html :deep(h3),
798
+ .file-preview-page__paper--html :deep(h4) {
799
+ margin: 1.2em 0 0.6em;
800
+ font-weight: 700;
801
+ color: #303133;
802
+ scroll-margin-top: 12px;
803
+ }
804
+
805
+ .file-preview-page__paper--html :deep(h1) {
806
+ font-size: 22px;
807
+ }
808
+
809
+ .file-preview-page__paper--html :deep(h2) {
810
+ font-size: 18px;
811
+ }
812
+
813
+ .file-preview-page__paper--html :deep(p) {
814
+ margin: 0.6em 0;
815
+ }
816
+
817
+ .file-preview-page__paper--office {
818
+ overflow: visible;
819
+ padding: 0;
820
+ }
821
+
822
+ /* PDF:去掉 A4 紙內 vue-office 默認灰底 / 內邊距造成的左右暗色帶 */
823
+ .file-preview-page__paper--pdf {
824
+ box-shadow: none;
825
+ }
826
+
827
+ .file-preview-page__paper--pdf :deep(.vue-office-pdf) {
828
+ overflow: visible !important;
829
+ text-align: left !important;
830
+ }
831
+
832
+ .file-preview-page__paper--pdf :deep(.vue-office-pdf-wrapper) {
833
+ background: #fff !important;
834
+ padding: 0 !important;
835
+ }
836
+
837
+ .file-preview-page__paper--pdf :deep(canvas) {
838
+ display: block;
839
+ width: 100% !important;
840
+ box-shadow: none !important;
841
+ }
842
+
843
+ /* vue-office 默認 height:100% 在僅有 min-height 的父級會塌成頂部窄條 */
844
+ .file-preview-page__office-viewer {
845
+ display: block;
846
+ width: 100%;
847
+ min-height: 297mm;
848
+ height: auto !important;
849
+ }
850
+
851
+ .file-preview-page__paper--office :deep(.vue-office-pdf) {
852
+ background: #fff !important;
853
+ width: 100% !important;
854
+ height: auto !important;
855
+ min-height: 297mm !important;
856
+ overflow: visible !important;
857
+ }
858
+
859
+ .file-preview-page__paper--office :deep(.docx-wrapper) {
860
+ background: #fff !important;
861
+ padding: 24px 32px !important;
862
+ width: 100% !important;
863
+ box-sizing: border-box;
864
+ }
865
+
866
+ .file-preview-page__paper--office :deep(.docx-wrapper > section.docx) {
867
+ box-shadow: none !important;
868
+ margin: 0 auto 16px !important;
869
+ /* 保留 docx-preview 计算的页宽,避免 width:100% 导致分页失效 */
870
+ width: auto !important;
871
+ max-width: 100%;
872
+ box-sizing: border-box;
873
+ /* 禁止页盒裁切正文:滚到底仍缺内容多半是 overflow:hidden + 固定页高 */
874
+ overflow: visible !important;
875
+ height: auto !important;
876
+ max-height: none !important;
877
+ }
878
+
879
+ .file-preview-page__docx-native :deep(.docx-wrapper) {
880
+ background: #fff !important;
881
+ padding: 24px 32px !important;
882
+ width: 100% !important;
883
+ box-sizing: border-box;
884
+ overflow: visible !important;
885
+ }
886
+
887
+ .file-preview-page__docx-native :deep(section.docx) {
888
+ overflow: visible !important;
889
+ height: auto !important;
890
+ max-height: none !important;
891
+ }
892
+
893
+ .file-preview-page__paper--image {
894
+ display: flex;
895
+ align-items: center;
896
+ justify-content: center;
897
+ padding: 24px;
898
+ }
899
+
900
+ .file-preview-page__image {
901
+ max-width: 100%;
902
+ height: auto;
903
+ object-fit: contain;
904
+ }
905
+
906
+ .file-preview-page__error {
907
+ box-sizing: border-box;
908
+ width: 210mm;
909
+ max-width: 100%;
910
+ min-height: 297mm;
911
+ margin: 0 auto;
912
+ padding: 48px 56px;
913
+ display: flex;
914
+ align-items: center;
915
+ justify-content: center;
916
+ background: #fff;
917
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.12);
918
+ text-align: center;
919
+ color: #c53355;
920
+ font-family: var(--fp-font);
921
+ font-size: 15px;
922
+ font-weight: 500;
923
+ line-height: 1.6;
924
+ }
925
+
926
+ @media print {
927
+ .file-preview-page__header,
928
+ .file-preview-page__sidebar {
929
+ display: none !important;
930
+ }
931
+
932
+ .file-preview-page__stage {
933
+ padding: 0;
934
+ background: #fff;
935
+ overflow: visible;
936
+ }
937
+
938
+ .file-preview-page__scale {
939
+ zoom: 1 !important;
940
+ width: 100%;
941
+ }
942
+
943
+ .file-preview-page__paper {
944
+ box-shadow: none;
945
+ }
946
+ }
947
+ </style>