aegon-lv97 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/gen-ai/build-report/chartStreamRefs.ts +227 -0
  2. package/gen-ai/build-report/coherence-check/index.vue +559 -0
  3. package/gen-ai/build-report/components/ReportChapterWorkspace.vue +4571 -0
  4. package/gen-ai/build-report/components/ReportConfigPanel.vue +727 -0
  5. package/gen-ai/build-report/components/ReportGenerateSettingsPanel.vue +2361 -0
  6. package/gen-ai/build-report/components/ReportGeneratingPanel.vue +1765 -0
  7. package/gen-ai/build-report/components/ReportPromptPanel.vue +625 -0
  8. package/gen-ai/build-report/components/ReportSourceFileSummary.vue +665 -0
  9. package/gen-ai/build-report/components/ReportSourcesPanel.vue +1327 -0
  10. package/gen-ai/build-report/components/ReportTemplateDetail.vue +191 -0
  11. package/gen-ai/build-report/components/ReportTemplateEditorFormPanel.vue +667 -0
  12. package/gen-ai/build-report/components/ReportTemplatePicker.vue +302 -0
  13. package/gen-ai/build-report/components/ReportWorkflowSidebar.vue +148 -0
  14. package/gen-ai/build-report/components/paramsSetting.vue +886 -0
  15. package/gen-ai/build-report/components/reportEdit.vue +2283 -0
  16. package/gen-ai/build-report/index.vue +856 -0
  17. package/gen-ai/build-report/report-domain.ts +1133 -0
  18. package/gen-ai/build-report/report-template-data.ts +295 -0
  19. package/gen-ai/build-report/useReportWorkflow.ts +1386 -0
  20. package/gen-ai/common/assets/NotoSans-SC.woff2 +0 -0
  21. package/gen-ai/common/assets/NotoSans-TC.woff2 +0 -0
  22. package/gen-ai/common/assets/NotoSans.woff2 +0 -0
  23. package/gen-ai/common/assets/fonts.css +278 -0
  24. package/gen-ai/common/assets/pagination.css +163 -0
  25. package/gen-ai/common/directives/btnLoading.ts +455 -0
  26. package/gen-ai/common/directives/debounce.ts +98 -0
  27. package/gen-ai/common/directives/index.ts +30 -0
  28. package/gen-ai/common/en.md +417 -0
  29. package/gen-ai/common/index.ts +36 -0
  30. package/gen-ai/common/types/component-expose.ts +9 -0
  31. package/gen-ai/common/utils/api-res-status.ts +7 -0
  32. package/gen-ai/common/utils/navigation.ts +59 -0
  33. package/gen-ai/common/utils/remote-request.ts +45 -0
  34. package/gen-ai/common/utils/scroll.ts +577 -0
  35. package/gen-ai/components/AIExport/docx-export.ts +950 -0
  36. package/gen-ai/components/AIExport/docxExport.ts +2566 -0
  37. package/gen-ai/components/AIExport/index.ts +52 -0
  38. package/gen-ai/components/AIExport/parseMarkdownHtml.ts +653 -0
  39. package/gen-ai/components/CapsuleScrollbar.vue +149 -0
  40. package/gen-ai/components/ChapterTitleScroll.vue +468 -0
  41. package/gen-ai/components/EntryHeroNavCards.vue +379 -0
  42. package/gen-ai/components/GenAiPagination.vue +52 -0
  43. package/gen-ai/components/ModelCard.vue +1114 -0
  44. package/gen-ai/components/Preview/FilePreviewPage.vue +949 -0
  45. package/gen-ai/components/Preview/PreviewCapsuleScroll.vue +205 -0
  46. package/gen-ai/components/Preview/PreviewOutlineTree.vue +148 -0
  47. package/gen-ai/components/Preview/index.ts +11 -0
  48. package/gen-ai/components/Preview/preview.ts +724 -0
  49. package/gen-ai/components/PromptField.vue +505 -0
  50. package/gen-ai/components/RecentReportsTable.vue +974 -0
  51. package/gen-ai/components/ReportCollapsibleSection.vue +172 -0
  52. package/gen-ai/components/ReportHistoryList.vue +637 -0
  53. package/gen-ai/components/ReportSectionConfirmPopover.vue +251 -0
  54. package/gen-ai/components/editor/ChapterTiptapEditor.vue +3718 -0
  55. package/gen-ai/components/editor/StructureBlockView.vue +441 -0
  56. package/gen-ai/components/editor/TiptapToolbar.vue +1036 -0
  57. package/gen-ai/components/editor/TypewriterDocEditor.vue +422 -0
  58. package/gen-ai/components/editor/_StructureOrgNode.vue +326 -0
  59. package/gen-ai/components/editor/index.ts +28 -0
  60. package/gen-ai/components/editor/parseMarkdownHtml.ts +754 -0
  61. package/gen-ai/components/editor/reviewCitationTip.ts +163 -0
  62. package/gen-ai/gen-entry/api/index.ts +32 -0
  63. package/gen-ai/gen-entry/index.vue +287 -0
  64. package/gen-ai/gen-entry/types/index.ts +4 -0
  65. package/gen-ai/i.mark +2 -0
  66. package/gen-ai/management-center/index.vue +1815 -0
  67. package/gen-ai/management-center/mockData.ts +248 -0
  68. package/gen-ai/management-center/types/index.ts +37 -0
  69. package/gen-ai/model-manager/TemplateEditorFormPanel.vue +661 -0
  70. package/gen-ai/model-manager/api/index.ts +32 -0
  71. package/gen-ai/model-manager/api/mockApi.ts +33 -0
  72. package/gen-ai/model-manager/index.vue +1583 -0
  73. package/gen-ai/model-manager/mockData.ts +177 -0
  74. package/gen-ai/model-manager/template-detail-shared.ts +154 -0
  75. package/gen-ai/model-manager/template-editor.vue +345 -0
  76. package/gen-ai/model-manager/types/index.ts +40 -0
  77. package/gen-ai/model-manager/utils.ts +25 -0
  78. package/package.json +12 -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>