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,398 @@
1
+ <template>
2
+ <div ref="rootRef" class="report-typewriter-doc">
3
+ <template v-if="ready && chapters.length">
4
+ <ReportChapterTiptapEditor
5
+ v-for="chapter in revealedChapters"
6
+ :key="chapter.key"
7
+ class="report-typewriter-doc__chapter"
8
+ :data-chapter-key="chapter.key"
9
+ :chapter-title="chapter.title"
10
+ :chapter-intro="chapter.intro"
11
+ :content-identity="chapter.key"
12
+ :typewriter-phase="phaseOf(chapter.key)"
13
+ :editable="false"
14
+ :show-review-markers="false"
15
+ :typewriter-settle-ms="typewriterSettleMs"
16
+ :disable-typewriter="disableTypewriter"
17
+ plain-content
18
+ @typewriter-complete="onChapterComplete(chapter.key)"
19
+ />
20
+ </template>
21
+ <p v-else-if="ready" class="report-typewriter-doc__empty">
22
+ {{ emptyText }}
23
+ </p>
24
+ <div v-else class="report-typewriter-doc__shell" aria-hidden="true">
25
+ <div class="report-typewriter-doc__shell-title" />
26
+ <div class="report-typewriter-doc__shell-line" />
27
+ <div class="report-typewriter-doc__shell-line is-short" />
28
+ <div class="report-typewriter-doc__shell-line" />
29
+ </div>
30
+ </div>
31
+ </template>
32
+
33
+ <script setup lang="ts">
34
+ import { computed, nextTick, ref, watch } from 'vue'
35
+ import ReportChapterTiptapEditor from './ChapterTiptapEditor.vue'
36
+
37
+ export interface TypewriterDocChapter {
38
+ key: string
39
+ title: string
40
+ intro: string
41
+ }
42
+
43
+ const props = withDefaults(
44
+ defineProps<{
45
+ /** 按展示順序的章節(已過濾勾選) */
46
+ chapters: TypewriterDocChapter[]
47
+ /** 內容區是否可掛載編輯器 */
48
+ ready?: boolean
49
+ emptyText?: string
50
+ /** 拖拽排序後重播:從該章起重新列印 */
51
+ replayKey?: string | null
52
+ replayToken?: number
53
+ /**
54
+ * 流式斷續 settle:追平緩衝後等待多久無新內容才進入下一章。
55
+ * 傳給 ChapterTiptapEditor;空緩衝會一直等首包。
56
+ */
57
+ typewriterSettleMs?: number
58
+ /** 需求 2026-08-13 16:58:settings 頁直接滿內容,不打字 */
59
+ disableTypewriter?: boolean
60
+ }>(),
61
+ {
62
+ ready: false,
63
+ emptyText: '請勾選至少一個章節',
64
+ replayKey: null,
65
+ replayToken: 0,
66
+ typewriterSettleMs: 1500,
67
+ disableTypewriter: false,
68
+ }
69
+ )
70
+
71
+ const emit = defineEmits<{
72
+ /** 當前正在打字的章節 key(供外側列表等鬆耦合聯動) */
73
+ 'typing-change': [key: string | null]
74
+ /** 是否仍在整篇列印流程中(含章間切換;全部打完為 false) */
75
+ 'printing-change': [printing: boolean]
76
+ 'chapter-complete': [key: string]
77
+ }>()
78
+
79
+ const rootRef = ref<HTMLElement | null>(null)
80
+ const typedKeys = ref<string[]>([])
81
+ const typingKey = ref<string | null>(null)
82
+
83
+ const chapterKeys = computed(() => props.chapters.map((c) => c.key))
84
+
85
+ const isPrinting = computed(() => {
86
+ if (props.disableTypewriter) return false
87
+ const keys = chapterKeys.value
88
+ if (!keys.length || !props.ready) return false
89
+ return typedKeys.value.length < keys.length || typingKey.value !== null
90
+ })
91
+
92
+ function phaseOf(key: string): 'idle' | 'typing' | 'done' {
93
+ if (props.disableTypewriter) return 'done'
94
+ if (typedKeys.value.includes(key)) return 'done'
95
+ if (typingKey.value === key) return 'typing'
96
+ return 'idle'
97
+ }
98
+
99
+ const revealedChapters = computed(() =>
100
+ props.disableTypewriter
101
+ ? props.chapters
102
+ : props.chapters.filter((chapter) => phaseOf(chapter.key) !== 'idle')
103
+ )
104
+
105
+ function syncQueue() {
106
+ if (props.disableTypewriter) {
107
+ typingKey.value = null
108
+ typedKeys.value = [...chapterKeys.value]
109
+ return
110
+ }
111
+ const keys = chapterKeys.value
112
+ const typed = typedKeys.value.filter((key) => keys.includes(key))
113
+ if (typed.length !== typedKeys.value.length) {
114
+ typedKeys.value = typed
115
+ }
116
+
117
+ if (typingKey.value && !keys.includes(typingKey.value)) {
118
+ typingKey.value = null
119
+ }
120
+
121
+ if (typingKey.value && keys.includes(typingKey.value) && !typed.includes(typingKey.value)) {
122
+ return
123
+ }
124
+
125
+ const next = keys.find((key) => !typed.includes(key)) ?? null
126
+ if (typingKey.value !== next) {
127
+ typingKey.value = next
128
+ }
129
+ }
130
+
131
+ function onChapterComplete(key: string) {
132
+ if (!typedKeys.value.includes(key)) {
133
+ typedKeys.value = [...typedKeys.value, key]
134
+ }
135
+ if (typingKey.value === key) {
136
+ typingKey.value = null
137
+ }
138
+ emit('chapter-complete', key)
139
+ syncQueue()
140
+
141
+ // 全部打完:滾到文檔最底(表格等高塊佈局穩定後)
142
+ const keys = chapterKeys.value
143
+ if (keys.length && keys.every((k) => typedKeys.value.includes(k))) {
144
+ scrollDocToBottom()
145
+ }
146
+ }
147
+
148
+ function findDocScrollParent(from: HTMLElement): HTMLElement | null {
149
+ const col = from.closest('.report-edit-page__col-scroll') as HTMLElement | null
150
+ const wrap = col?.querySelector('.el-scrollbar__wrap') as HTMLElement | null
151
+ if (wrap) return wrap
152
+
153
+ let node: HTMLElement | null = from.parentElement
154
+ let fallback: HTMLElement | null = null
155
+ while (node && node !== document.body) {
156
+ if (node.classList.contains('el-scrollbar__wrap')) fallback = node
157
+ const { overflowY } = getComputedStyle(node)
158
+ const canOverflow =
159
+ overflowY === 'auto' ||
160
+ overflowY === 'scroll' ||
161
+ overflowY === 'overlay' ||
162
+ node.classList.contains('el-scrollbar__wrap')
163
+ if (canOverflow && node.scrollHeight > node.clientHeight + 1) {
164
+ return node
165
+ }
166
+ node = node.parentElement
167
+ }
168
+ return fallback
169
+ }
170
+
171
+ /** 僅滾動本組件所在滾動容器,不查詢左右欄 DOM */
172
+ function scrollTypingChapterIntoView(key: string | null) {
173
+ if (!key || !rootRef.value) return
174
+ void nextTick(() => {
175
+ const el = rootRef.value?.querySelector(
176
+ `.report-typewriter-doc__chapter[data-chapter-key="${CSS.escape(key)}"]`
177
+ ) as HTMLElement | null
178
+ if (!el) return
179
+ // 僅在章節頂部超出視口時輕推,避免與 DeepSeek 跟滾搶控制權
180
+ const scrollParent = findDocScrollParent(el)
181
+ if (!scrollParent) {
182
+ el.scrollIntoView({ block: 'nearest', behavior: 'auto', inline: 'nearest' })
183
+ return
184
+ }
185
+ const parentRect = scrollParent.getBoundingClientRect()
186
+ const rect = el.getBoundingClientRect()
187
+ if (rect.top < parentRect.top + 24) {
188
+ scrollParent.scrollTop += rect.top - parentRect.top - 24
189
+ }
190
+ })
191
+ }
192
+
193
+ function scrollDocToBottom() {
194
+ void nextTick(() => {
195
+ requestAnimationFrame(() => {
196
+ requestAnimationFrame(() => {
197
+ const root = rootRef.value
198
+ if (!root) return
199
+ const scrollParent = findDocScrollParent(root)
200
+ if (!scrollParent) return
201
+ scrollParent.scrollTop = scrollParent.scrollHeight - scrollParent.clientHeight
202
+ })
203
+ })
204
+ })
205
+ }
206
+
207
+ function replayFromKey(key: string) {
208
+ const keys = chapterKeys.value
209
+ const idx = keys.indexOf(key)
210
+ if (idx < 0) return
211
+
212
+ typedKeys.value = keys.slice(0, idx)
213
+ typingKey.value = null
214
+ void nextTick(() => {
215
+ typingKey.value = key
216
+ scrollTypingChapterIntoView(key)
217
+ })
218
+ }
219
+
220
+ function reset() {
221
+ typedKeys.value = []
222
+ typingKey.value = null
223
+ }
224
+
225
+ watch(
226
+ chapterKeys,
227
+ () => {
228
+ syncQueue()
229
+ },
230
+ { immediate: true }
231
+ )
232
+
233
+ /**
234
+ * 接口正文晚到:當前章仍在打 / 已 done 但 intro 從空→有 → 重播該章
235
+ * (子編輯器在 phase=done 時只能靜態灌入,必須由隊列拉回 typing)
236
+ */
237
+ watch(
238
+ () =>
239
+ props.chapters.map((chapter) => ({
240
+ key: chapter.key,
241
+ title: (chapter.title || '').trim(),
242
+ intro: (chapter.intro || '').trim(),
243
+ })),
244
+ (next, prev) => {
245
+ if (props.disableTypewriter) return
246
+ if (!props.ready || !prev?.length) return
247
+ for (const chapter of next) {
248
+ const before = prev.find((item) => item.key === chapter.key)
249
+ if (!before) continue
250
+ const introArrived = !before.intro && !!chapter.intro
251
+ const titleArrived = !before.title && !!chapter.title
252
+ const introGrew = !!before.intro && chapter.intro.startsWith(before.intro) && chapter.intro.length > before.intro.length
253
+ if (!introArrived && !titleArrived && !introGrew) continue
254
+
255
+ const alreadyDone = typedKeys.value.includes(chapter.key)
256
+ const isCurrent = typingKey.value === chapter.key
257
+ // 增長中的當前章:交給子編輯器 onStreamContentUpdated 續打
258
+ if (isCurrent && introGrew && !introArrived) continue
259
+ if (alreadyDone || (isCurrent && (introArrived || titleArrived))) {
260
+ replayFromKey(chapter.key)
261
+ return
262
+ }
263
+ }
264
+ },
265
+ { deep: true }
266
+ )
267
+
268
+ watch(typingKey, (key) => {
269
+ emit('typing-change', key)
270
+ scrollTypingChapterIntoView(key)
271
+ })
272
+
273
+ watch(
274
+ isPrinting,
275
+ (printing) => {
276
+ emit('printing-change', printing)
277
+ },
278
+ { immediate: true }
279
+ )
280
+
281
+ watch(
282
+ () => [props.replayToken ?? 0, props.replayKey ?? null] as const,
283
+ ([token, key]) => {
284
+ if (props.disableTypewriter) return
285
+ if (!token || !key) return
286
+ replayFromKey(key)
287
+ }
288
+ )
289
+
290
+ defineExpose({
291
+ reset,
292
+ replayFromKey,
293
+ typingKey,
294
+ })
295
+ </script>
296
+
297
+ <style scoped>
298
+ .report-typewriter-doc {
299
+ flex: 1 1 auto;
300
+ min-height: 0;
301
+ display: flex;
302
+ flex-direction: column;
303
+ gap: 0;
304
+ padding-bottom: 24px;
305
+ background: #fff;
306
+ box-sizing: border-box;
307
+ }
308
+
309
+ .report-typewriter-doc
310
+ :deep(.ProseMirror > *:first-child:not(h1):not(h2):not(h3):not(h4):not(h5):not(h6)) {
311
+ margin-top: 0 !important;
312
+ }
313
+
314
+ .report-typewriter-doc :deep(.ProseMirror h2) {
315
+ margin: 24px 0 !important;
316
+ }
317
+
318
+ .report-typewriter-doc__chapter + .report-typewriter-doc__chapter :deep(.ProseMirror > h2:first-child) {
319
+ margin-top: 24px !important;
320
+ }
321
+
322
+ .report-typewriter-doc :deep(.report-tiptap-editor__toolbar) {
323
+ display: none;
324
+ }
325
+
326
+ .report-typewriter-doc__chapter {
327
+ flex-shrink: 0;
328
+ }
329
+
330
+ .report-typewriter-doc__chapter :deep(.report-tiptap-editor__content),
331
+ .report-typewriter-doc__chapter :deep(.report-tiptap-editor__body),
332
+ .report-typewriter-doc__chapter :deep(.report-tiptap-editor) {
333
+ min-height: 0 !important;
334
+ height: auto !important;
335
+ }
336
+
337
+ .report-typewriter-doc__chapter :deep(.ProseMirror),
338
+ .report-typewriter-doc__chapter :deep(.report-tiptap-editor__prose) {
339
+ min-height: 0 !important;
340
+ height: auto !important;
341
+ padding-bottom: 0 !important;
342
+ }
343
+
344
+ .report-typewriter-doc :deep(.ProseMirror > p:empty),
345
+ .report-typewriter-doc :deep(.ProseMirror > p:has(> br.ProseMirror-trailingBreak:only-child)) {
346
+ display: none !important;
347
+ margin: 0 !important;
348
+ height: 0 !important;
349
+ }
350
+
351
+ .report-typewriter-doc__empty {
352
+ margin: 48px 0;
353
+ text-align: center;
354
+ color: #606266;
355
+ font-size: 16px;
356
+ font-weight: 500;
357
+ font-family: var(--gen-ai-font-family, 'Noto Sans', 'Noto Sans TC', sans-serif);
358
+ }
359
+
360
+ .report-typewriter-doc__shell {
361
+ display: flex;
362
+ flex-direction: column;
363
+ gap: 14px;
364
+ min-height: 280px;
365
+ padding-top: 8px;
366
+ }
367
+
368
+ .report-typewriter-doc__shell-title {
369
+ width: min(420px, 70%);
370
+ height: 28px;
371
+ border-radius: 4px;
372
+ background: linear-gradient(90deg, #f2f3f5 25%, #e9ebef 37%, #f2f3f5 63%);
373
+ background-size: 400% 100%;
374
+ animation: report-typewriter-doc-shimmer 1.2s ease-in-out infinite;
375
+ }
376
+
377
+ .report-typewriter-doc__shell-line {
378
+ width: 100%;
379
+ height: 14px;
380
+ border-radius: 4px;
381
+ background: linear-gradient(90deg, #f5f6f8 25%, #eceef2 37%, #f5f6f8 63%);
382
+ background-size: 400% 100%;
383
+ animation: report-typewriter-doc-shimmer 1.2s ease-in-out infinite;
384
+ }
385
+
386
+ .report-typewriter-doc__shell-line.is-short {
387
+ width: 62%;
388
+ }
389
+
390
+ @keyframes report-typewriter-doc-shimmer {
391
+ 0% {
392
+ background-position: 100% 0;
393
+ }
394
+ 100% {
395
+ background-position: 0 0;
396
+ }
397
+ }
398
+ </style>
@@ -0,0 +1,326 @@
1
+ <template>
2
+ <div class="org-tree-node" :class="{ 'is-root': isRoot, 'has-children': hasChildren }">
3
+ <div
4
+ class="org-tree-node__box"
5
+ :class="{
6
+ 'is-root': isRoot,
7
+ 'is-selected': selected,
8
+ 'is-editing': editing,
9
+ }"
10
+ @click.stop="onBoxClick"
11
+ >
12
+ <span
13
+ ref="labelRef"
14
+ class="org-tree-node__label"
15
+ :contenteditable="canEdit ? 'true' : 'false'"
16
+ spellcheck="false"
17
+ @focus="editing = true"
18
+ @blur="onLabelBlur"
19
+ @keydown.enter.prevent="blurLabel"
20
+ @keydown.esc.prevent="cancelEdit"
21
+ @mousedown.stop
22
+ @click.stop="onLabelClick"
23
+ >{{ node.label }}</span
24
+ >
25
+ </div>
26
+
27
+ <!-- 連續豎幹:線不斷開,+ 疊在線上 -->
28
+ <div v-if="hasChildren || (selected && !readonly)" class="org-tree-node__trunk">
29
+ <button
30
+ v-if="selected && !readonly"
31
+ type="button"
32
+ class="org-tree-node__plus"
33
+ title="新增子公司"
34
+ @mousedown.prevent
35
+ @click.stop="emit('add', node.id)"
36
+ >
37
+ <Plus :size="12" :stroke-width="3" />
38
+ </button>
39
+ </div>
40
+
41
+ <div v-if="hasChildren" class="org-tree-node__children">
42
+ <div v-for="child in node.children" :key="child.id" class="org-tree-node__branch">
43
+ <span class="org-tree-node__pct">{{ child.pct }}%</span>
44
+ <ReportStructureOrgNode
45
+ :node="child"
46
+ :selected-id="selectedId"
47
+ :readonly="readonly"
48
+ @select="emit('select', $event)"
49
+ @add="emit('add', $event)"
50
+ @rename="onChildRename"
51
+ />
52
+ </div>
53
+ </div>
54
+ </div>
55
+ </template>
56
+
57
+ <script setup lang="ts">
58
+ import { Plus } from 'lucide-vue-next'
59
+ import { computed, nextTick, ref, watch } from 'vue'
60
+
61
+ export type StructureOrgNode = {
62
+ id: string
63
+ label: string
64
+ pct: number
65
+ children: StructureOrgNode[]
66
+ }
67
+
68
+ const props = defineProps<{
69
+ node: StructureOrgNode
70
+ selectedId: string | null
71
+ readonly?: boolean
72
+ }>()
73
+
74
+ const emit = defineEmits<{
75
+ select: [id: string]
76
+ add: [id: string]
77
+ rename: [id: string, label: string]
78
+ }>()
79
+
80
+ const labelRef = ref<HTMLElement | null>(null)
81
+ const editing = ref(false)
82
+
83
+ const isRoot = computed(() => props.node.id === 'root')
84
+ const selected = computed(() => props.selectedId === props.node.id)
85
+ const hasChildren = computed(() => props.node.children.length > 0)
86
+ const canEdit = computed(() => !props.readonly && selected.value)
87
+
88
+ watch(selected, (isSelected) => {
89
+ if (!isSelected && editing.value) {
90
+ commitLabel()
91
+ }
92
+ })
93
+
94
+ function onBoxClick() {
95
+ emit('select', props.node.id)
96
+ }
97
+
98
+ function onLabelClick() {
99
+ emit('select', props.node.id)
100
+ if (props.readonly) return
101
+ nextTick(() => {
102
+ const el = labelRef.value
103
+ if (!el) return
104
+ el.focus()
105
+ const range = document.createRange()
106
+ range.selectNodeContents(el)
107
+ const sel = window.getSelection()
108
+ sel?.removeAllRanges()
109
+ sel?.addRange(range)
110
+ })
111
+ }
112
+
113
+ function blurLabel() {
114
+ labelRef.value?.blur()
115
+ }
116
+
117
+ function commitLabel() {
118
+ editing.value = false
119
+ const text = (labelRef.value?.innerText ?? '').trim()
120
+ if (labelRef.value) labelRef.value.innerText = text
121
+ if (text !== props.node.label) {
122
+ emit('rename', props.node.id, text)
123
+ }
124
+ }
125
+
126
+ function cancelEdit() {
127
+ editing.value = false
128
+ if (labelRef.value) labelRef.value.innerText = props.node.label
129
+ labelRef.value?.blur()
130
+ }
131
+
132
+ function onLabelBlur() {
133
+ commitLabel()
134
+ }
135
+
136
+ function onChildRename(id: string, label: string) {
137
+ emit('rename', id, label)
138
+ }
139
+ </script>
140
+
141
+ <script lang="ts">
142
+ export default {
143
+ name: 'ReportStructureOrgNode',
144
+ }
145
+ </script>
146
+
147
+ <style scoped>
148
+ .org-tree-node {
149
+ position: relative;
150
+ display: flex;
151
+ flex-direction: column;
152
+ align-items: center;
153
+ }
154
+
155
+ .org-tree-node__box {
156
+ position: relative;
157
+ z-index: 2;
158
+ display: flex;
159
+ align-items: center;
160
+ justify-content: center;
161
+ box-sizing: border-box;
162
+ min-width: 88px;
163
+ min-height: 36px;
164
+ padding: 0 14px;
165
+ border: 1px solid transparent;
166
+ border-radius: 2px;
167
+ background: #f4f4f4;
168
+ color: #303133;
169
+ cursor: pointer;
170
+ outline: none;
171
+ box-shadow: none;
172
+ }
173
+
174
+ .org-tree-node__box.is-root {
175
+ background: #c53355;
176
+ color: #fff;
177
+ }
178
+
179
+ .org-tree-node__box:focus,
180
+ .org-tree-node__box:focus-visible,
181
+ .org-tree-node__label:focus,
182
+ .org-tree-node__label:focus-visible {
183
+ outline: none !important;
184
+ }
185
+
186
+ /* 選中:紅色邊框(覆蓋瀏覽器/編輯器藍色 focus) */
187
+ .org-tree-node__box.is-selected {
188
+ border-color: #c53355 !important;
189
+ outline: none !important;
190
+ box-shadow: none !important;
191
+ }
192
+
193
+ .org-tree-node__box.is-root.is-selected {
194
+ border-color: #c53355 !important;
195
+ box-shadow: 0 0 0 2px #c53355 !important;
196
+ }
197
+
198
+ .org-tree-node__label {
199
+ display: inline-block;
200
+ min-width: 2em;
201
+ max-width: 160px;
202
+ outline: none;
203
+ font-family: var(--gen-ai-font-family, 'Noto Sans', 'Noto Sans TC', sans-serif);
204
+ font-size: 13px;
205
+ font-weight: 500;
206
+ line-height: 1.2;
207
+ white-space: nowrap;
208
+ overflow: hidden;
209
+ text-overflow: ellipsis;
210
+ cursor: text;
211
+ color: inherit;
212
+ }
213
+
214
+ .org-tree-node__label[contenteditable='false'] {
215
+ cursor: pointer;
216
+ user-select: none;
217
+ }
218
+
219
+ .org-tree-node__label[contenteditable='true'] {
220
+ cursor: text;
221
+ user-select: text;
222
+ }
223
+
224
+ /* 連續豎幹:整段實線,+ 居中疊加,線不中斷 */
225
+ .org-tree-node__trunk {
226
+ position: relative;
227
+ z-index: 1;
228
+ width: 1px;
229
+ height: 36px;
230
+ flex-shrink: 0;
231
+ background: #d1d5db;
232
+ }
233
+
234
+ .org-tree-node__plus {
235
+ position: absolute;
236
+ top: 50%;
237
+ left: 50%;
238
+ z-index: 2;
239
+ display: flex;
240
+ align-items: center;
241
+ justify-content: center;
242
+ width: 18px;
243
+ height: 18px;
244
+ padding: 0;
245
+ border: none;
246
+ border-radius: 50%;
247
+ background: #c53355;
248
+ color: #fff;
249
+ transform: translate(-50%, -50%);
250
+ cursor: pointer;
251
+ /* 不使用白圈打斷豎線;線從圓形兩側連續可見 */
252
+ box-shadow: none;
253
+ }
254
+
255
+ .org-tree-node__plus:hover {
256
+ background: #a82b48;
257
+ }
258
+
259
+ .org-tree-node__children {
260
+ position: relative;
261
+ display: flex;
262
+ justify-content: center;
263
+ align-items: flex-start;
264
+ gap: 28px;
265
+ width: max-content;
266
+ margin-top: 0;
267
+ padding-top: 0;
268
+ }
269
+
270
+ .org-tree-node__branch {
271
+ position: relative;
272
+ display: flex;
273
+ flex-direction: column;
274
+ align-items: center;
275
+ padding-top: 20px;
276
+ }
277
+
278
+ /*
279
+ * 水平連線:向兩側延伸半個 gap(14px) 以跨過 flex gap,避免斷開
280
+ */
281
+ .org-tree-node__branch::before {
282
+ content: '';
283
+ position: absolute;
284
+ top: 0;
285
+ left: -14px;
286
+ right: -14px;
287
+ height: 1px;
288
+ background: #d1d5db;
289
+ }
290
+
291
+ .org-tree-node__branch:first-child::before {
292
+ left: 50%;
293
+ }
294
+
295
+ .org-tree-node__branch:last-child::before {
296
+ right: 50%;
297
+ }
298
+
299
+ .org-tree-node__branch:only-child::before {
300
+ display: none;
301
+ }
302
+
303
+ /* 落到百分比/節點的豎線(與水平線相接,不斷開) */
304
+ .org-tree-node__branch::after {
305
+ content: '';
306
+ position: absolute;
307
+ top: 0;
308
+ left: 50%;
309
+ width: 1px;
310
+ height: 20px;
311
+ background: #d1d5db;
312
+ transform: translateX(-50%);
313
+ }
314
+
315
+ .org-tree-node__pct {
316
+ position: relative;
317
+ z-index: 1;
318
+ margin-bottom: 6px;
319
+ padding: 0 3px;
320
+ color: #9ca3af;
321
+ font-family: var(--gen-ai-font-family, 'Noto Sans', 'Noto Sans TC', sans-serif);
322
+ font-size: 12px;
323
+ line-height: 1;
324
+ background: #fff;
325
+ }
326
+ </style>