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,1036 @@
1
+ <!--
2
+ TipTap 工具欄(格式 + 插入),僅供 ChapterTiptapEditor 使用。
3
+ -->
4
+ <template>
5
+ <div class="report-tiptap-toolbar-merged">
6
+ <div class="report-tiptap-format-toolbar">
7
+ <div class="report-tiptap-editor__group">
8
+ <el-dropdown
9
+ trigger="click"
10
+ :disabled="!editable"
11
+ :popper-options="mainBoundPopperOptions"
12
+ popper-class="report-tiptap-style-popper"
13
+ @command="applyBlockStyle"
14
+ @visible-change="onStyleDropdownVisible"
15
+ >
16
+ <button
17
+ type="button"
18
+ class="report-tiptap-editor__tool report-tiptap-editor__tool--style"
19
+ :disabled="!editable"
20
+ >
21
+ Style
22
+ <ChevronDown :size="14" :stroke-width="2" class="report-tiptap-editor__caret" />
23
+ </button>
24
+ <template #dropdown>
25
+ <el-dropdown-menu>
26
+ <el-dropdown-item
27
+ v-for="(item, index) in styleOptions"
28
+ :key="item.command"
29
+ :command="item.command"
30
+ :divided="index > 0"
31
+ :class="{ 'is-style-active': isStyleActive(item.command) }"
32
+ >
33
+ <span class="report-tiptap-style-item">
34
+ <span class="report-tiptap-style-item__icon" aria-hidden="true">{{
35
+ item.icon
36
+ }}</span>
37
+ <span class="report-tiptap-style-item__label">{{ item.label }}</span>
38
+ </span>
39
+ </el-dropdown-item>
40
+ </el-dropdown-menu>
41
+ </template>
42
+ </el-dropdown>
43
+ </div>
44
+
45
+ <span class="report-tiptap-editor__divider" aria-hidden="true" />
46
+
47
+ <div class="report-tiptap-editor__group">
48
+ <button
49
+ type="button"
50
+ class="report-tiptap-editor__tool report-tiptap-editor__tool--text"
51
+ :class="{ 'is-active': editor?.isActive('bold') }"
52
+ title="粗體"
53
+ :disabled="!editable"
54
+ @click="editor?.chain().focus().toggleBold().run()"
55
+ >
56
+ <Bold :size="18" :stroke-width="2.5" />
57
+ </button>
58
+ <button
59
+ type="button"
60
+ class="report-tiptap-editor__tool report-tiptap-editor__tool--text"
61
+ :class="{ 'is-active': editor?.isActive('italic') }"
62
+ title="斜體"
63
+ :disabled="!editable"
64
+ @click="editor?.chain().focus().toggleItalic().run()"
65
+ >
66
+ <Italic :size="18" :stroke-width="2" />
67
+ </button>
68
+ <button
69
+ type="button"
70
+ class="report-tiptap-editor__tool report-tiptap-editor__tool--text"
71
+ :class="{ 'is-active': editor?.isActive('underline') }"
72
+ title="底線"
73
+ :disabled="!editable"
74
+ @click="editor?.chain().focus().toggleUnderline().run()"
75
+ >
76
+ <UnderlineIcon :size="18" :stroke-width="2" />
77
+ </button>
78
+ </div>
79
+
80
+ <span class="report-tiptap-editor__divider" aria-hidden="true" />
81
+
82
+ <div class="report-tiptap-editor__group">
83
+ <button
84
+ type="button"
85
+ class="report-tiptap-editor__tool"
86
+ :class="{ 'is-active': editor?.isActive('bulletList') }"
87
+ title="無序列表"
88
+ :disabled="!editable"
89
+ @click="editor?.chain().focus().toggleBulletList().run()"
90
+ >
91
+ <List :size="18" :stroke-width="1.75" />
92
+ </button>
93
+ <button
94
+ type="button"
95
+ class="report-tiptap-editor__tool"
96
+ :class="{ 'is-active': editor?.isActive('orderedList') }"
97
+ title="有序列表"
98
+ :disabled="!editable"
99
+ @click="editor?.chain().focus().toggleOrderedList().run()"
100
+ >
101
+ <ListOrdered :size="18" :stroke-width="1.75" />
102
+ </button>
103
+ </div>
104
+ </div>
105
+ <span class="report-tiptap-editor__divider" aria-hidden="true" />
106
+ <div class="report-tiptap-insert-toolbar report-tiptap-editor__group">
107
+ <el-popover
108
+ :visible="tablePickerVisible"
109
+ placement="bottom-start"
110
+ :width="232"
111
+ trigger="click"
112
+ :disabled="!editable"
113
+ :popper-options="mainBoundPopperOptions"
114
+ popper-class="report-tiptap-table-picker-popper"
115
+ @update:visible="onTablePickerVisible"
116
+ >
117
+ <template #reference>
118
+ <button
119
+ type="button"
120
+ class="report-tiptap-editor__tool"
121
+ :class="{ 'is-active': tablePickerVisible }"
122
+ title="插入表格"
123
+ :disabled="!editable"
124
+ >
125
+ <svg
126
+ class="report-tiptap-editor__table-icon"
127
+ viewBox="0 0 18 18"
128
+ width="18"
129
+ height="18"
130
+ aria-hidden="true"
131
+ >
132
+ <rect
133
+ x="1.5"
134
+ y="1.5"
135
+ width="15"
136
+ height="15"
137
+ rx="2.5"
138
+ fill="none"
139
+ stroke="currentColor"
140
+ stroke-width="1.5"
141
+ />
142
+ <path d="M9 1.5v15M1.5 9h15" fill="none" stroke="currentColor" stroke-width="1.5" />
143
+ </svg>
144
+ </button>
145
+ </template>
146
+
147
+ <div class="report-tiptap-table-picker" @mouseleave="resetTableHover">
148
+ <div class="report-tiptap-table-picker__label">
149
+ 插入 {{ tableHoverCols }} x {{ tableHoverRows }} 表格
150
+ </div>
151
+ <div class="report-tiptap-table-picker__grid" role="grid" aria-label="選擇表格尺寸">
152
+ <button
153
+ v-for="cell in tablePickerCells"
154
+ :key="cell.key"
155
+ type="button"
156
+ class="report-tiptap-table-picker__cell"
157
+ :class="{ 'is-selected': cell.selected }"
158
+ :aria-label="`${cell.cols} 欄 ${cell.rows} 列`"
159
+ @mouseenter="setTableHover(cell.cols, cell.rows)"
160
+ @click="confirmInsertTable(cell.cols, cell.rows)"
161
+ />
162
+ </div>
163
+ </div>
164
+ </el-popover>
165
+
166
+ <el-popover
167
+ :visible="linkPopoverVisible"
168
+ placement="bottom-start"
169
+ :width="320"
170
+ trigger="click"
171
+ :disabled="!editable"
172
+ :popper-options="mainBoundPopperOptions"
173
+ popper-class="report-tiptap-link-popper"
174
+ @update:visible="onLinkPopoverVisible"
175
+ >
176
+ <template #reference>
177
+ <button
178
+ type="button"
179
+ class="report-tiptap-editor__tool"
180
+ :class="{ 'is-active': linkPopoverVisible || editor?.isActive('link') }"
181
+ title="插入鏈接"
182
+ :disabled="!editable"
183
+ >
184
+ <LinkIcon :size="18" :stroke-width="1.75" />
185
+ </button>
186
+ </template>
187
+
188
+ <div class="report-tiptap-link-panel">
189
+ <div class="report-tiptap-link-panel__title">插入鏈接</div>
190
+ <div class="report-tiptap-link-panel__field">
191
+ <input
192
+ ref="linkInputRef"
193
+ v-model="linkUrl"
194
+ type="url"
195
+ class="report-tiptap-link-panel__input"
196
+ @keydown.enter.prevent="confirmInsertLink"
197
+ />
198
+ <button
199
+ v-if="linkUrl"
200
+ type="button"
201
+ class="report-tiptap-link-panel__clear"
202
+ title="一鍵清除"
203
+ @click="clearLinkInput"
204
+ >
205
+ ×
206
+ </button>
207
+ </div>
208
+ <div class="report-tiptap-link-panel__actions">
209
+ <button
210
+ type="button"
211
+ class="report-tiptap-link-panel__submit"
212
+ @click="confirmInsertLink"
213
+ >
214
+ 插入
215
+ </button>
216
+ </div>
217
+ </div>
218
+ </el-popover>
219
+
220
+ <el-popover
221
+ :visible="imagePopoverVisible"
222
+ placement="bottom-start"
223
+ :width="360"
224
+ trigger="click"
225
+ :disabled="!editable"
226
+ :popper-options="mainBoundPopperOptions"
227
+ popper-class="report-tiptap-image-popper"
228
+ @update:visible="onImagePopoverVisible"
229
+ >
230
+ <template #reference>
231
+ <button
232
+ type="button"
233
+ class="report-tiptap-editor__tool"
234
+ :class="{ 'is-active': imagePopoverVisible }"
235
+ title="上傳圖片"
236
+ :disabled="!editable"
237
+ >
238
+ <ImageIcon :size="18" :stroke-width="1.75" />
239
+ </button>
240
+ </template>
241
+
242
+ <div
243
+ class="report-tiptap-image-panel"
244
+ :class="{ 'is-dragging': imageDragging }"
245
+ @click="triggerImageFilePick"
246
+ @dragenter.prevent="onImageDragEnter"
247
+ @dragover.prevent="onImageDragEnter"
248
+ @dragleave.prevent="onImageDragLeave"
249
+ @drop.prevent="onImageDrop"
250
+ >
251
+ <div class="report-tiptap-image-panel__icon" aria-hidden="true">
252
+ <FileUp :size="28" :stroke-width="1.75" />
253
+ </div>
254
+ <div class="report-tiptap-image-panel__copy">
255
+ <div class="report-tiptap-image-panel__title">上傳圖片</div>
256
+ <div class="report-tiptap-image-panel__desc">拖動圖片到輸入框,或點擊上傳圖片</div>
257
+ </div>
258
+ <input
259
+ ref="imageFileInputRef"
260
+ type="file"
261
+ class="report-tiptap-image-panel__file"
262
+ accept="image/*"
263
+ @change="onImageFileChange"
264
+ />
265
+ </div>
266
+ </el-popover>
267
+
268
+ <button
269
+ type="button"
270
+ class="report-tiptap-editor__tool"
271
+ title="插入結構區塊"
272
+ :disabled="!editable"
273
+ @click="insertSectionBlock"
274
+ >
275
+ <Network :size="18" :stroke-width="1.75" />
276
+ </button>
277
+ </div>
278
+ </div>
279
+ </template>
280
+ <script setup lang="ts">
281
+ import type { Editor } from '@tiptap/core'
282
+ import { ElDropdown, ElDropdownItem, ElDropdownMenu, ElMessage, ElPopover } from 'element-plus'
283
+ import {
284
+ Bold,
285
+ ChevronDown,
286
+ FileUp,
287
+ Image as ImageIcon,
288
+ Italic,
289
+ Link as LinkIcon,
290
+ List,
291
+ ListOrdered,
292
+ Network,
293
+ Underline as UnderlineIcon,
294
+ } from 'lucide-vue-next'
295
+ import { computed, nextTick, ref } from 'vue'
296
+
297
+ const props = defineProps<{
298
+ editor: Editor | undefined | null
299
+ editable: boolean
300
+ }>()
301
+
302
+ const mainBoundaryEl = ref<HTMLElement | null>(null)
303
+
304
+ function refreshMainBoundary() {
305
+ mainBoundaryEl.value = document.querySelector(
306
+ '.report-edit-page__main--anchored'
307
+ ) as HTMLElement | null
308
+ }
309
+
310
+ const mainBoundPopperOptions = computed(() => ({
311
+ modifiers: [
312
+ {
313
+ name: 'preventOverflow',
314
+ options: {
315
+ boundary: mainBoundaryEl.value ?? 'clippingParents',
316
+ padding: 8,
317
+ altAxis: true,
318
+ },
319
+ },
320
+ ],
321
+ }))
322
+
323
+ function onStyleDropdownVisible(visible: boolean) {
324
+ if (visible) refreshMainBoundary()
325
+ }
326
+
327
+ const styleOptions = [
328
+ { command: 'paragraph', icon: 'T', label: '段落' },
329
+ { command: 'heading1', icon: 'H₁', label: '標題1' },
330
+ { command: 'heading2', icon: 'H₂', label: '標題2' },
331
+ { command: 'heading3', icon: 'H₃', label: '標題3' },
332
+ { command: 'remark', icon: 'a', label: '備註' },
333
+ ] as const
334
+
335
+ function isStyleActive(command: string) {
336
+ if (!props.editor) return false
337
+ switch (command) {
338
+ case 'heading1':
339
+ return props.editor.isActive('heading', { level: 1 })
340
+ case 'heading2':
341
+ return props.editor.isActive('heading', { level: 2 })
342
+ case 'heading3':
343
+ return props.editor.isActive('heading', { level: 3 })
344
+ case 'remark':
345
+ return props.editor.isActive('blockquote')
346
+ case 'paragraph':
347
+ return (
348
+ props.editor.isActive('paragraph') &&
349
+ !props.editor.isActive('heading') &&
350
+ !props.editor.isActive('blockquote')
351
+ )
352
+ default:
353
+ return false
354
+ }
355
+ }
356
+
357
+ function applyBlockStyle(command: string) {
358
+ if (!props.editor || !props.editable) return
359
+ const chain = props.editor.chain().focus()
360
+ switch (command) {
361
+ case 'heading1':
362
+ chain.setHeading({ level: 1 }).run()
363
+ break
364
+ case 'heading2':
365
+ chain.setHeading({ level: 2 }).run()
366
+ break
367
+ case 'heading3':
368
+ chain.setHeading({ level: 3 }).run()
369
+ break
370
+ case 'remark':
371
+ if (props.editor.isActive('heading')) {
372
+ chain.setParagraph()
373
+ }
374
+ if (!props.editor.isActive('blockquote')) {
375
+ chain.toggleBlockquote()
376
+ }
377
+ chain.run()
378
+ break
379
+ default:
380
+ if (props.editor.isActive('blockquote')) {
381
+ chain.toggleBlockquote()
382
+ }
383
+ chain.setParagraph().run()
384
+ }
385
+ }
386
+
387
+ const TABLE_PICKER_MAX = 10
388
+ const tablePickerVisible = ref(false)
389
+ const tableHoverCols = ref(1)
390
+ const tableHoverRows = ref(1)
391
+
392
+ const tablePickerCells = computed(() => {
393
+ const cells: Array<{ key: string; cols: number; rows: number; selected: boolean }> = []
394
+ for (let row = 1; row <= TABLE_PICKER_MAX; row += 1) {
395
+ for (let col = 1; col <= TABLE_PICKER_MAX; col += 1) {
396
+ cells.push({
397
+ key: `${col}-${row}`,
398
+ cols: col,
399
+ rows: row,
400
+ selected: col <= tableHoverCols.value && row <= tableHoverRows.value,
401
+ })
402
+ }
403
+ }
404
+ return cells
405
+ })
406
+
407
+ function onTablePickerVisible(visible: boolean) {
408
+ if (!props.editable) {
409
+ tablePickerVisible.value = false
410
+ return
411
+ }
412
+ if (visible) refreshMainBoundary()
413
+ tablePickerVisible.value = visible
414
+ if (!visible) resetTableHover()
415
+ }
416
+
417
+ function setTableHover(cols: number, rows: number) {
418
+ tableHoverCols.value = cols
419
+ tableHoverRows.value = rows
420
+ }
421
+
422
+ function resetTableHover() {
423
+ tableHoverCols.value = 1
424
+ tableHoverRows.value = 1
425
+ }
426
+
427
+ function confirmInsertTable(cols: number, rows: number) {
428
+ if (!props.editor || !props.editable) return
429
+ props.editor.chain().focus().insertTable({ rows, cols, withHeaderRow: false }).run()
430
+ tablePickerVisible.value = false
431
+ resetTableHover()
432
+ }
433
+
434
+ const linkPopoverVisible = ref(false)
435
+ const linkUrl = ref('')
436
+ const linkInputRef = ref<HTMLInputElement | null>(null)
437
+
438
+ function normalizeLinkUrl(raw: string) {
439
+ const value = raw.trim()
440
+ if (!value) return ''
441
+ if (/^[a-z][a-z0-9+.-]*:/i.test(value) || value.startsWith('/') || value.startsWith('#')) {
442
+ return value
443
+ }
444
+ return `https://${value}`
445
+ }
446
+
447
+ function onLinkPopoverVisible(visible: boolean) {
448
+ if (!props.editable) {
449
+ linkPopoverVisible.value = false
450
+ return
451
+ }
452
+ if (visible) refreshMainBoundary()
453
+ linkPopoverVisible.value = visible
454
+ if (visible) {
455
+ const previous = (props.editor?.getAttributes('link').href as string | undefined) || ''
456
+ linkUrl.value = previous
457
+ void nextTick(() => linkInputRef.value?.focus())
458
+ } else {
459
+ linkUrl.value = ''
460
+ }
461
+ }
462
+
463
+ function clearLinkInput() {
464
+ linkUrl.value = ''
465
+ if (props.editor?.isActive('link')) {
466
+ props.editor.chain().focus().extendMarkRange('link').unsetLink().run()
467
+ }
468
+ void nextTick(() => linkInputRef.value?.focus())
469
+ }
470
+
471
+ function confirmInsertLink() {
472
+ if (!props.editor || !props.editable) return
473
+ const raw = linkUrl.value.trim()
474
+ const href = normalizeLinkUrl(raw)
475
+ if (!href) {
476
+ clearLinkInput()
477
+ return
478
+ }
479
+
480
+ const { empty } = props.editor.state.selection
481
+ if (empty) {
482
+ props.editor
483
+ .chain()
484
+ .focus()
485
+ .insertContent({
486
+ type: 'text',
487
+ text: raw || href,
488
+ marks: [{ type: 'link', attrs: { href, target: '_blank' } }],
489
+ })
490
+ .run()
491
+ } else {
492
+ props.editor.chain().focus().extendMarkRange('link').setLink({ href, target: '_blank' }).run()
493
+ }
494
+
495
+ linkPopoverVisible.value = false
496
+ linkUrl.value = ''
497
+ }
498
+
499
+ const imagePopoverVisible = ref(false)
500
+ const imageDragging = ref(false)
501
+ const imageFileInputRef = ref<HTMLInputElement | null>(null)
502
+ let imageDragDepth = 0
503
+
504
+ function isAllowedImageSource(src: string): boolean {
505
+ try {
506
+ const url = new URL(src, window.location.href)
507
+ const hostname = url.hostname.toLowerCase()
508
+ return hostname.includes('bochk') || hostname.includes('bochkuatcloud')
509
+ } catch {
510
+ return false
511
+ }
512
+ }
513
+
514
+ function rejectDisallowedImageSource() {
515
+ ElMessage.warning('僅允許上傳來源為 bochk / bochkuatcloud 網域的圖片')
516
+ }
517
+
518
+ function onImagePopoverVisible(visible: boolean) {
519
+ if (!props.editable) {
520
+ imagePopoverVisible.value = false
521
+ return
522
+ }
523
+ if (visible) refreshMainBoundary()
524
+ imagePopoverVisible.value = visible
525
+ if (!visible) {
526
+ imageDragging.value = false
527
+ imageDragDepth = 0
528
+ if (imageFileInputRef.value) imageFileInputRef.value.value = ''
529
+ }
530
+ }
531
+
532
+ function triggerImageFilePick() {
533
+ if (!props.editable) return
534
+ imageFileInputRef.value?.click()
535
+ }
536
+
537
+ function onImageDragEnter() {
538
+ imageDragDepth += 1
539
+ imageDragging.value = true
540
+ }
541
+
542
+ function onImageDragLeave() {
543
+ imageDragDepth = Math.max(0, imageDragDepth - 1)
544
+ if (imageDragDepth === 0) imageDragging.value = false
545
+ }
546
+
547
+ function getDroppedImageUrl(event: DragEvent): string | null {
548
+ const uriList = event.dataTransfer?.getData('text/uri-list')?.trim()
549
+ if (uriList) {
550
+ const first = uriList.split('\n').find((line) => line.trim() && !line.startsWith('#'))
551
+ if (first?.trim()) return first.trim()
552
+ }
553
+
554
+ const plain = event.dataTransfer?.getData('text/plain')?.trim()
555
+ if (plain && /^https?:\/\//i.test(plain)) return plain
556
+
557
+ const html = event.dataTransfer?.getData('text/html')
558
+ if (html) {
559
+ const match = html.match(/<img[^>]+src=["']([^"']+)["']/i)
560
+ if (match?.[1]) return match[1].trim()
561
+ }
562
+
563
+ return null
564
+ }
565
+
566
+ function onImageDrop(event: DragEvent) {
567
+ imageDragDepth = 0
568
+ imageDragging.value = false
569
+
570
+ const droppedUrl = getDroppedImageUrl(event)
571
+ if (droppedUrl) {
572
+ insertImageFromSrc(droppedUrl)
573
+ return
574
+ }
575
+
576
+ // 本地檔案無 hostname,無法通過網域白名單
577
+ if (event.dataTransfer?.files?.length) {
578
+ rejectDisallowedImageSource()
579
+ return
580
+ }
581
+
582
+ rejectDisallowedImageSource()
583
+ }
584
+
585
+ function onImageFileChange(event: Event) {
586
+ const input = event.target as HTMLInputElement
587
+ input.value = ''
588
+ // 本地選擇的檔案沒有 bochk / bochkuatcloud 來源 URL
589
+ rejectDisallowedImageSource()
590
+ }
591
+
592
+ function insertImageFromSrc(src: string, alt = '') {
593
+ if (!props.editor || !props.editable) return
594
+ if (!isAllowedImageSource(src)) {
595
+ rejectDisallowedImageSource()
596
+ return
597
+ }
598
+
599
+ props.editor.chain().focus().setImage({ src, alt }).run()
600
+ imagePopoverVisible.value = false
601
+ imageDragging.value = false
602
+ imageDragDepth = 0
603
+ }
604
+
605
+ function insertSectionBlock() {
606
+ if (!props.editor || !props.editable) return
607
+ props.editor.chain().focus().insertStructureBlock().run()
608
+ }
609
+ </script>
610
+
611
+ <style scoped>
612
+ .report-tiptap-toolbar-merged {
613
+ display: contents;
614
+ }
615
+
616
+ .report-tiptap-format-toolbar {
617
+ --report-tiptap-toolbar-fg: oklch(0.145 0 0);
618
+ display: contents;
619
+ color: var(--report-tiptap-toolbar-fg);
620
+ }
621
+
622
+ .report-tiptap-editor__group {
623
+ display: inline-flex;
624
+ align-items: center;
625
+ gap: 0;
626
+ flex-shrink: 0;
627
+ }
628
+
629
+ .report-tiptap-editor__tool {
630
+ display: inline-flex;
631
+ align-items: center;
632
+ justify-content: center;
633
+ gap: 2px;
634
+ box-sizing: border-box;
635
+ min-width: 32px;
636
+ height: 32px;
637
+ padding: 0 6px;
638
+ border: none;
639
+ border-radius: 4px;
640
+ background: transparent;
641
+ color: var(--report-tiptap-toolbar-fg);
642
+ font-family: inherit;
643
+ font-size: 14px;
644
+ line-height: 1;
645
+ cursor: pointer;
646
+ transition:
647
+ background 0.15s ease,
648
+ color 0.15s ease;
649
+ }
650
+
651
+ .report-tiptap-editor__tool :deep(svg) {
652
+ display: block;
653
+ flex-shrink: 0;
654
+ color: inherit;
655
+ stroke: currentColor;
656
+ }
657
+
658
+ .report-tiptap-editor__tool:hover:not(:disabled) {
659
+ background: #f3f4f6;
660
+ color: var(--report-tiptap-toolbar-fg);
661
+ }
662
+
663
+ .report-tiptap-editor__tool.is-active {
664
+ background: #f3f4f6;
665
+ color: var(--report-tiptap-toolbar-fg);
666
+ }
667
+
668
+ .report-tiptap-editor__tool:disabled {
669
+ opacity: 0.35;
670
+ cursor: not-allowed;
671
+ }
672
+
673
+ .report-tiptap-editor__tool--style {
674
+ min-width: auto;
675
+ padding: 0 8px;
676
+ gap: 4px;
677
+ font-family: inherit;
678
+ font-size: 14px;
679
+ font-weight: 700;
680
+ color: var(--report-tiptap-toolbar-fg);
681
+ }
682
+
683
+ .report-tiptap-editor__caret {
684
+ color: inherit;
685
+ opacity: 0.72;
686
+ }
687
+
688
+ .report-tiptap-editor__divider {
689
+ width: 1px;
690
+ height: 16px;
691
+ margin: 0 8px;
692
+ background: #e5e7eb;
693
+ flex-shrink: 0;
694
+ }
695
+ </style>
696
+
697
+ <style>
698
+ .report-tiptap-style-popper.el-dropdown__popper {
699
+ border: 1px solid #e5e7eb;
700
+ border-radius: 10px;
701
+ box-shadow: 0 8px 24px rgba(15, 23, 42, 0.12);
702
+ }
703
+
704
+ .report-tiptap-style-popper .el-dropdown-menu {
705
+ min-width: 168px;
706
+ padding: 6px 0;
707
+ border: none;
708
+ border-radius: 10px;
709
+ }
710
+
711
+ .report-tiptap-style-popper .el-dropdown-menu__item {
712
+ display: flex;
713
+ align-items: center;
714
+ height: 40px;
715
+ padding: 0 14px;
716
+ color: oklch(0.145 0 0);
717
+ font-family: var(--gen-ai-font-family, 'Noto Sans', 'Noto Sans TC', sans-serif);
718
+ font-size: 14px;
719
+ line-height: 1;
720
+ }
721
+
722
+ .report-tiptap-style-popper .el-dropdown-menu__item--divided {
723
+ margin-top: 0;
724
+ border-top: 1px solid #eceff3;
725
+ }
726
+
727
+ .report-tiptap-style-popper .el-dropdown-menu__item--divided::before {
728
+ display: none;
729
+ }
730
+
731
+ .report-tiptap-style-popper .el-dropdown-menu__item:hover,
732
+ .report-tiptap-style-popper .el-dropdown-menu__item:focus,
733
+ .report-tiptap-style-popper .el-dropdown-menu__item.is-style-active {
734
+ background: #f3f4f6;
735
+ color: oklch(0.145 0 0);
736
+ }
737
+
738
+ .report-tiptap-style-item {
739
+ display: inline-flex;
740
+ align-items: center;
741
+ gap: 12px;
742
+ width: 100%;
743
+ }
744
+
745
+ .report-tiptap-style-item__icon {
746
+ flex-shrink: 0;
747
+ width: 28px;
748
+ color: oklch(0.145 0 0);
749
+ font-family: var(--gen-ai-font-family, 'Noto Sans', 'Noto Sans TC', sans-serif);
750
+ font-size: 15px;
751
+ font-weight: 600;
752
+ letter-spacing: -0.02em;
753
+ text-align: left;
754
+ }
755
+
756
+ .report-tiptap-style-item__label {
757
+ color: inherit;
758
+ font-size: 14px;
759
+ font-weight: 400;
760
+ }
761
+ </style>
762
+
763
+ <style scoped>
764
+ .report-tiptap-insert-toolbar {
765
+ --report-tiptap-toolbar-fg: oklch(0.145 0 0);
766
+ display: inline-flex;
767
+ align-items: center;
768
+ gap: 0;
769
+ flex-shrink: 0;
770
+ color: var(--report-tiptap-toolbar-fg);
771
+ }
772
+
773
+ .report-tiptap-editor__tool {
774
+ display: inline-flex;
775
+ align-items: center;
776
+ justify-content: center;
777
+ gap: 2px;
778
+ box-sizing: border-box;
779
+ min-width: 32px;
780
+ height: 32px;
781
+ padding: 0 6px;
782
+ border: none;
783
+ border-radius: 4px;
784
+ background: transparent;
785
+ color: var(--report-tiptap-toolbar-fg);
786
+ font-family: inherit;
787
+ font-size: 14px;
788
+ line-height: 1;
789
+ cursor: pointer;
790
+ transition:
791
+ background 0.15s ease,
792
+ color 0.15s ease;
793
+ }
794
+
795
+ .report-tiptap-editor__tool :deep(svg) {
796
+ display: block;
797
+ flex-shrink: 0;
798
+ color: inherit;
799
+ stroke: currentColor;
800
+ }
801
+
802
+ .report-tiptap-editor__table-icon {
803
+ display: block;
804
+ }
805
+
806
+ .report-tiptap-editor__tool:hover:not(:disabled) {
807
+ background: #f3f4f6;
808
+ color: var(--report-tiptap-toolbar-fg);
809
+ }
810
+
811
+ .report-tiptap-editor__tool.is-active {
812
+ background: #f3f4f6;
813
+ color: var(--report-tiptap-toolbar-fg);
814
+ }
815
+
816
+ .report-tiptap-editor__tool:disabled {
817
+ opacity: 0.35;
818
+ cursor: not-allowed;
819
+ }
820
+ </style>
821
+
822
+ <style>
823
+ .report-tiptap-table-picker-popper.el-popover {
824
+ padding: 12px !important;
825
+ border: 1px solid #e5e7eb !important;
826
+ border-radius: 8px !important;
827
+ box-shadow: 0 8px 24px rgb(0 0 0 / 10%) !important;
828
+ }
829
+
830
+ .report-tiptap-table-picker {
831
+ font-family: var(--gen-ai-font-family, 'Noto Sans', 'Noto Sans TC', sans-serif);
832
+ font-size: 14px;
833
+ color: oklch(0.145 0 0);
834
+ user-select: none;
835
+ }
836
+
837
+ .report-tiptap-table-picker__label {
838
+ margin-bottom: 10px;
839
+ font-size: 14px;
840
+ font-weight: 400;
841
+ line-height: 1.4;
842
+ color: oklch(0.145 0 0);
843
+ }
844
+
845
+ .report-tiptap-table-picker__grid {
846
+ display: grid;
847
+ grid-template-columns: repeat(10, 18px);
848
+ grid-template-rows: repeat(10, 18px);
849
+ gap: 2px;
850
+ width: max-content;
851
+ }
852
+
853
+ .report-tiptap-table-picker__cell {
854
+ box-sizing: border-box;
855
+ width: 18px;
856
+ height: 18px;
857
+ padding: 0;
858
+ border: 1px solid #dcdfe6;
859
+ border-radius: 1px;
860
+ background: #fff;
861
+ cursor: pointer;
862
+ }
863
+
864
+ .report-tiptap-table-picker__cell.is-selected {
865
+ border-color: #c53355;
866
+ background: #c53355;
867
+ }
868
+
869
+ .report-tiptap-link-popper.el-popover {
870
+ padding: 16px !important;
871
+ border: 1px solid #e5e7eb !important;
872
+ border-radius: 10px !important;
873
+ box-shadow: 0 8px 24px rgb(0 0 0 / 10%) !important;
874
+ }
875
+
876
+ .report-tiptap-link-panel {
877
+ font-family: var(--gen-ai-font-family, 'Noto Sans', 'Noto Sans TC', sans-serif);
878
+ font-size: 14px;
879
+ color: oklch(0.145 0 0);
880
+ }
881
+
882
+ .report-tiptap-link-panel__title {
883
+ margin-bottom: 12px;
884
+ font-size: 14px;
885
+ font-weight: 500;
886
+ line-height: 1.4;
887
+ color: oklch(0.145 0 0);
888
+ }
889
+
890
+ .report-tiptap-link-panel__field {
891
+ position: relative;
892
+ margin-bottom: 14px;
893
+ }
894
+
895
+ .report-tiptap-link-panel__input {
896
+ box-sizing: border-box;
897
+ width: 100%;
898
+ height: 40px;
899
+ padding: 0 36px 0 12px;
900
+ border: none;
901
+ border-radius: 8px;
902
+ background: #f4f4f4;
903
+ color: oklch(0.145 0 0);
904
+ font-family: inherit;
905
+ font-size: 14px;
906
+ outline: none;
907
+ }
908
+
909
+ .report-tiptap-link-panel__input:focus {
910
+ box-shadow: 0 0 0 1px #c53355 inset;
911
+ }
912
+
913
+ .report-tiptap-link-panel__clear {
914
+ position: absolute;
915
+ top: 50%;
916
+ right: 8px;
917
+ display: inline-flex;
918
+ align-items: center;
919
+ justify-content: center;
920
+ width: 22px;
921
+ height: 22px;
922
+ padding: 0;
923
+ border: none;
924
+ border-radius: 50%;
925
+ background: #dcdfe6;
926
+ color: #606266;
927
+ font-size: 16px;
928
+ line-height: 1;
929
+ cursor: pointer;
930
+ transform: translateY(-50%);
931
+ }
932
+
933
+ .report-tiptap-link-panel__clear:hover {
934
+ background: #c0c4cc;
935
+ color: #303133;
936
+ }
937
+
938
+ .report-tiptap-link-panel__actions {
939
+ display: flex;
940
+ justify-content: flex-end;
941
+ }
942
+
943
+ .report-tiptap-link-panel__submit {
944
+ display: inline-flex;
945
+ align-items: center;
946
+ justify-content: center;
947
+ min-width: 72px;
948
+ height: 36px;
949
+ padding: 0 16px;
950
+ border: none;
951
+ border-radius: 8px;
952
+ background: #c53355;
953
+ color: #fff;
954
+ font-family: inherit;
955
+ font-size: 14px;
956
+ font-weight: 500;
957
+ cursor: pointer;
958
+ }
959
+
960
+ .report-tiptap-link-panel__submit:hover {
961
+ background: #a82b48;
962
+ }
963
+
964
+ .report-tiptap-image-popper.el-popover {
965
+ padding: 12px;
966
+ border: 1px solid #e5e7eb;
967
+ border-radius: 12px;
968
+ box-shadow: 0 8px 24px rgba(15, 23, 42, 0.12);
969
+ overflow: visible;
970
+ }
971
+
972
+ .report-tiptap-image-popper .el-popover__content {
973
+ overflow: visible;
974
+ }
975
+
976
+ .report-tiptap-image-panel {
977
+ position: relative;
978
+ display: flex;
979
+ align-items: center;
980
+ gap: 14px;
981
+ min-height: 88px;
982
+ padding: 18px 16px;
983
+ border: 1px dashed #d1d5db;
984
+ border-radius: 10px;
985
+ background: #fff;
986
+ cursor: pointer;
987
+ user-select: none;
988
+ transition:
989
+ border-color 0.15s ease,
990
+ background-color 0.15s ease;
991
+ }
992
+
993
+ .report-tiptap-image-panel:hover,
994
+ .report-tiptap-image-panel.is-dragging {
995
+ border-color: #9ca3af;
996
+ background: #fafafa;
997
+ }
998
+
999
+ .report-tiptap-image-panel__icon {
1000
+ flex-shrink: 0;
1001
+ display: flex;
1002
+ align-items: center;
1003
+ justify-content: center;
1004
+ color: oklch(0.145 0 0);
1005
+ }
1006
+
1007
+ .report-tiptap-image-panel__copy {
1008
+ min-width: 0;
1009
+ display: flex;
1010
+ flex-direction: column;
1011
+ gap: 4px;
1012
+ overflow: visible;
1013
+ }
1014
+
1015
+ .report-tiptap-image-panel__title {
1016
+ color: oklch(0.145 0 0);
1017
+ font-family: var(--gen-ai-font-family, 'Noto Sans', 'Noto Sans TC', sans-serif);
1018
+ font-size: 15px;
1019
+ font-weight: 600;
1020
+ line-height: 1.3;
1021
+ }
1022
+
1023
+ .report-tiptap-image-panel__desc {
1024
+ color: #666;
1025
+ font-family: var(--gen-ai-font-family, 'Noto Sans', 'Noto Sans TC', sans-serif);
1026
+ font-size: 13px;
1027
+ font-weight: 400;
1028
+ line-height: 1.4;
1029
+ padding-inline-end: 0.2em;
1030
+ overflow: visible;
1031
+ }
1032
+
1033
+ .report-tiptap-image-panel__file {
1034
+ display: none;
1035
+ }
1036
+ </style>