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,4258 @@
1
+ <template>
2
+ <section
3
+ class="report-chapter-workspace"
4
+ v-loading="chapterContentAwaitingFirstChunk"
5
+ element-loading-text="章節內容生成中..."
6
+ element-loading-background="rgba(255, 255, 255, 0.72)"
7
+ >
8
+ <header class="report-chapter-workspace__toolbar">
9
+ <div v-if="showFinalizeToolbar" class="report-chapter-workspace__chapter-box">
10
+ <el-button class="chatbot-report-btn-outline" @click="handleBackToChapterEdit">
11
+ 返回章節編輯
12
+ </el-button>
13
+ </div>
14
+
15
+ <!--
16
+ // new prompt sections
17
+ 章節下拉(本組件內實現,不拆新組件):
18
+ - 無章節時觸發器空白(不顯示 0);空列表僅 footer「新增章節」
19
+ - >1024 彈層最小 500px + 長標題換行(樣式見本文件 style 底部同標記區塊)
20
+ - 整行可拖排序;拖後顯示「確認排序」;最多 14 章可見
21
+ -->
22
+ <div v-else-if="showChapterEditToolbar" class="report-chapter-workspace__chapter-box">
23
+ <el-select
24
+ ref="chapterSelectRef"
25
+ :model-value="chapterSelectModelValue"
26
+ class="report-chapter-workspace__chapter-select"
27
+ teleported
28
+ placement="bottom-start"
29
+ :fallback-placements="['bottom-start', 'top-start', 'bottom-end', 'top-end']"
30
+ :offset="10"
31
+ :popper-options="chapterSelectPopperOptions"
32
+ popper-class="report-chapter-workspace__chapter-select-popper"
33
+ placeholder=""
34
+ :disabled="chapterEditMode"
35
+ @update:model-value="onChapterSelectModelUpdate"
36
+ @visible-change="onChapterSelectVisibleChange"
37
+ >
38
+ <el-option
39
+ v-for="(title, index) in chapterTitles"
40
+ :key="genAiSectionItems[index]?.sectionId ?? `${title}-${index}`"
41
+ :label="title"
42
+ :value="index"
43
+ :class="{
44
+ 'is-dragging': selectDragSortIndex === index,
45
+ 'is-drag-over': selectDragOverSortIndex === index,
46
+ }"
47
+ >
48
+ <div class="report-chapter-workspace__chapter-option">
49
+ <span class="report-chapter-workspace__sort-handle" aria-hidden="true">
50
+ <el-icon>
51
+ <Sort />
52
+ </el-icon>
53
+ </span>
54
+ <span
55
+ class="report-chapter-workspace__chapter-option-title"
56
+ :class="{ 'is-active': index === activeChapterIndex }"
57
+ >
58
+ {{ title }}
59
+ </span>
60
+ <span class="report-chapter-workspace__chapter-option-status">{{
61
+ getChapterStatus(index)
62
+ }}</span>
63
+ </div>
64
+ </el-option>
65
+ <template #empty>
66
+ <div class="report-chapter-workspace__chapter-select-empty" aria-hidden="true" />
67
+ </template>
68
+ <template #footer>
69
+ <div class="report-chapter-workspace__chapter-select-footer">
70
+ <button
71
+ type="button"
72
+ class="template-center-toolbar-btn gen-ai-action-btn template-center-toolbar-btn--primary report-chapter-workspace__chapter-select-footer-btn"
73
+ @click="openAddChapterDialog"
74
+ >
75
+ 新增章節
76
+ </button>
77
+ <!-- 默認隱藏;下拉內拖動重排後才顯示 -->
78
+ <button
79
+ v-if="showConfirmSortButton"
80
+ type="button"
81
+ class="template-center-toolbar-btn gen-ai-action-btn template-center-toolbar-btn--primary report-chapter-workspace__chapter-select-footer-btn"
82
+ @click="handleConfirmSort"
83
+ >
84
+ 確認排序
85
+ </button>
86
+ </div>
87
+ </template>
88
+ </el-select>
89
+ </div>
90
+ <!-- // new prompt sections end -->
91
+
92
+ <div class="report-chapter-workspace__toolbar-right">
93
+ <template v-if="showFinalizeToolbar">
94
+ <el-button class="chatbot-report-btn-primary" @click="handleApplyCreateTemplate">
95
+ 申請創建模板
96
+ </el-button>
97
+ <el-button
98
+ class="chatbot-report-btn-primary"
99
+ :loading="isExportingWord"
100
+ @click="handleExportWord1"
101
+ >
102
+ 導出(Word-AI)
103
+ </el-button>
104
+ <el-button
105
+ class="chatbot-report-btn-primary"
106
+ :loading="isExportingWord"
107
+ @click="handleExportWord2"
108
+ >
109
+ 導出(Word-Transform)
110
+ </el-button>
111
+ <template v-if="showCoherenceResultActions">
112
+ <el-button
113
+ class="chatbot-report-btn-primary chatbot-report-btn-primary--wide"
114
+ @click="handleViewCoherenceCheck"
115
+ >
116
+ 查看報告連貫性檢查結果
117
+ </el-button>
118
+ <!-- <el-button
119
+ class="chatbot-report-btn-primary"
120
+ :loading="isExportingWord"
121
+ @click="handleExportWord"
122
+ >
123
+ 導出(Word)
124
+ </el-button> -->
125
+ </template>
126
+ </template>
127
+
128
+ <template v-else>
129
+ <!-- 編輯態:取消 | 存為草稿 | 儲存 | 生成報告(max-width 100px、16px) -->
130
+ <button
131
+ v-if="chapterEditMode"
132
+ type="button"
133
+ class="template-center-toolbar-btn gen-ai-action-btn template-center-toolbar-btn--outline report-chapter-workspace__action-btn"
134
+ @click="handleCancelEdit"
135
+ >
136
+ 取消
137
+ </button>
138
+ <button
139
+ v-if="chapterEditMode"
140
+ type="button"
141
+ class="template-center-toolbar-btn gen-ai-action-btn template-center-toolbar-btn--primary report-chapter-workspace__action-btn"
142
+ @click="handleSaveDraft"
143
+ >
144
+ 存為草稿
145
+ </button>
146
+ <button
147
+ type="button"
148
+ class="template-center-toolbar-btn gen-ai-action-btn template-center-toolbar-btn--primary report-chapter-workspace__action-btn"
149
+ @click="handleEditSave"
150
+ >
151
+ {{ chapterEditMode ? '儲存' : '編輯/儲存' }}
152
+ </button>
153
+ <button
154
+ type="button"
155
+ class="template-center-toolbar-btn gen-ai-action-btn template-center-toolbar-btn--primary report-chapter-workspace__action-btn"
156
+ @click="handleGenerateReport"
157
+ >
158
+ 生成報告
159
+ </button>
160
+ </template>
161
+ </div>
162
+ </header>
163
+
164
+ <div
165
+ v-if="isReportSettingsMode"
166
+ ref="wordExportRootRef"
167
+ class="report-chapter-workspace__settings-doc"
168
+ >
169
+ <ReportTypewriterDocEditor
170
+ :chapters="settingsDocChapters"
171
+ :ready="editorContentReady"
172
+ empty-text="請在右側「生成報告設置」勾選至少一個章節"
173
+ :replay-key="settingsTypewriterReplayKey"
174
+ :replay-token="settingsTypewriterReplayToken"
175
+ :disable-typewriter="!reportGenerateStreamActive"
176
+ :typewriter-settle-ms="reportGenerateStreamEnded ? 0 : 1500"
177
+ @typing-change="setSettingsTypingChapterKey"
178
+ @printing-change="setSettingsTypewriterPrinting"
179
+ />
180
+ </div>
181
+
182
+ <div v-else ref="wordExportRootRef" class="report-chapter-workspace__editor">
183
+ <ReportChapterTiptapEditor
184
+ v-if="editorContentReady"
185
+ :key="editorKey"
186
+ :chapter-title="previewChapterTitle"
187
+ :chapter-intro="previewChapterIntro"
188
+ :editable="false"
189
+ :disable-typewriter="!chapterTypewriterActive"
190
+ :show-review-markers="false"
191
+ plain-content
192
+ @typewriter-complete="onChapterTypewriterComplete"
193
+ />
194
+ <div v-else class="report-chapter-workspace__editor-shell" aria-hidden="true">
195
+ <div class="report-chapter-workspace__editor-shell-title" />
196
+ <div class="report-chapter-workspace__editor-shell-line" />
197
+ <div class="report-chapter-workspace__editor-shell-line is-short" />
198
+ <div class="report-chapter-workspace__editor-shell-line" />
199
+ </div>
200
+ </div>
201
+
202
+ <el-dialog
203
+ v-model="chapterDialogVisible"
204
+ width="60%"
205
+ append-to-body
206
+ :show-close="false"
207
+ modal-class="report-workflow-dialog-overlay"
208
+ class="report-workflow-dialog report-chapter-dialog"
209
+ header-class="report-workflow-dialog__header"
210
+ @closed="handleChapterDialogClosed"
211
+ >
212
+ <template #header="{ close }">
213
+ <span class="report-workflow-dialog__title">新增章節</span>
214
+ <button
215
+ type="button"
216
+ class="report-workflow-dialog__close"
217
+ aria-label="關閉"
218
+ @click="close"
219
+ >
220
+ ×
221
+ </button>
222
+ </template>
223
+
224
+ <div class="report-chapter-dialog__form">
225
+ <label class="report-chapter-dialog__field report-chapter-dialog__field--row">
226
+ <span class="report-chapter-dialog__label">
227
+ 章節標題<span class="report-chapter-dialog__required">*</span>
228
+ </span>
229
+ <div class="report-chapter-dialog__control">
230
+ <el-input
231
+ v-model="newChapterForm.title"
232
+ :maxlength="CHAPTER_TITLE_MAX"
233
+ show-word-limit
234
+ class="report-chapter-dialog__input"
235
+ />
236
+ </div>
237
+ </label>
238
+
239
+ <label
240
+ class="report-chapter-dialog__field report-chapter-dialog__field--row report-chapter-dialog__field--textarea"
241
+ >
242
+ <span class="report-chapter-dialog__label">
243
+ 章節描述<span class="report-chapter-dialog__required">*</span>
244
+ </span>
245
+ <div class="report-chapter-dialog__control">
246
+ <el-input
247
+ v-model="newChapterForm.description"
248
+ type="textarea"
249
+ :rows="4"
250
+ :maxlength="CHAPTER_DESC_MAX"
251
+ show-word-limit
252
+ resize="none"
253
+ class="report-chapter-dialog__textarea"
254
+ />
255
+ </div>
256
+ </label>
257
+
258
+ <!-- rows 加大,避免字數統計被 footer 裁切 -->
259
+ <ReportTemplatePromptField
260
+ v-model="newChapterForm.prompt"
261
+ :maxlength="CHAPTER_PROMPT_MAX"
262
+ :rows="8"
263
+ label-width="100px"
264
+ field-gap="16px"
265
+ layout="row"
266
+ @ai-assist="openAiPromptDialog"
267
+ />
268
+
269
+ <!-- Prompt 下方:資訊源範圍(全部 / 部分豎排 + 選擇按鈕) -->
270
+ <div
271
+ class="report-chapter-dialog__field report-chapter-dialog__field--row report-chapter-dialog__field--source"
272
+ >
273
+ <span class="report-chapter-dialog__label">
274
+ 資訊源<span class="report-chapter-dialog__required">*</span>
275
+ </span>
276
+ <div class="report-chapter-dialog__control report-chapter-dialog__source-control">
277
+ <el-radio-group
278
+ v-model="newChapterForm.sourceMode"
279
+ class="report-chapter-dialog__source-radios"
280
+ >
281
+ <el-radio value="all">所有資訊源檔案</el-radio>
282
+ <div class="report-chapter-dialog__source-partial-row">
283
+ <el-radio value="partial">部分資訊源檔案</el-radio>
284
+ <a
285
+ v-if="newChapterForm.sourceMode === 'partial'"
286
+ href="javascript:void(0)"
287
+ class="report-chapter-dialog__source-link"
288
+ @click.prevent="openChapterSourcePicker"
289
+ >
290
+ 選擇資訊源
291
+ </a>
292
+ <span
293
+ v-if="
294
+ newChapterForm.sourceMode === 'partial' &&
295
+ newChapterForm.selectedSourceIds.length
296
+ "
297
+ class="report-chapter-dialog__source-hint"
298
+ >
299
+ 已選 {{ newChapterForm.selectedSourceIds.length }} 個
300
+ </span>
301
+ </div>
302
+ </el-radio-group>
303
+ </div>
304
+ </div>
305
+ </div>
306
+
307
+ <template #footer>
308
+ <div class="report-chapter-dialog__footer">
309
+ <button
310
+ type="button"
311
+ class="template-center-toolbar-btn gen-ai-action-btn template-center-toolbar-btn--outline report-chapter-dialog__footer-btn"
312
+ @click="closeChapterDialog"
313
+ >
314
+ 取消
315
+ </button>
316
+ <button
317
+ type="button"
318
+ class="template-center-toolbar-btn gen-ai-action-btn template-center-toolbar-btn--primary report-chapter-dialog__footer-btn"
319
+ @click="handleAddChapterSubmit"
320
+ >
321
+ 提交並生成章節內容
322
+ </button>
323
+ </div>
324
+ </template>
325
+ </el-dialog>
326
+
327
+ <!-- 新增章節:部分資訊源時選擇檔案 -->
328
+ <el-dialog
329
+ v-model="chapterSourcePickerVisible"
330
+ width="520px"
331
+ append-to-body
332
+ :show-close="false"
333
+ class="report-chapter-source-picker"
334
+ header-class="report-workflow-dialog__header"
335
+ >
336
+ <template #header="{ close }">
337
+ <span class="report-workflow-dialog__title">選擇資訊源</span>
338
+ <button
339
+ type="button"
340
+ class="report-workflow-dialog__close"
341
+ aria-label="關閉"
342
+ @click="close"
343
+ >
344
+ ×
345
+ </button>
346
+ </template>
347
+
348
+ <div class="report-chapter-source-picker__body">
349
+ <el-checkbox
350
+ v-model="chapterSourcePickerCheckAll"
351
+ :indeterminate="chapterSourcePickerIndeterminate"
352
+ @change="onChapterSourceCheckAll"
353
+ >
354
+ 全選
355
+ </el-checkbox>
356
+ <el-checkbox-group
357
+ v-model="chapterSourcePickerDraft"
358
+ class="report-chapter-source-picker__list"
359
+ >
360
+ <el-checkbox
361
+ v-for="file in sourceFiles"
362
+ :key="file.id"
363
+ :value="file.id"
364
+ class="report-chapter-source-picker__item"
365
+ >
366
+ {{ file.name }}
367
+ </el-checkbox>
368
+ </el-checkbox-group>
369
+ <p v-if="!sourceFiles.length" class="report-chapter-source-picker__empty">暫無資訊源檔案</p>
370
+ </div>
371
+
372
+ <template #footer>
373
+ <div class="report-chapter-dialog__footer">
374
+ <button
375
+ type="button"
376
+ class="template-center-toolbar-btn gen-ai-action-btn template-center-toolbar-btn--outline report-chapter-dialog__footer-btn"
377
+ @click="chapterSourcePickerVisible = false"
378
+ >
379
+ 取消
380
+ </button>
381
+ <button
382
+ type="button"
383
+ class="template-center-toolbar-btn gen-ai-action-btn template-center-toolbar-btn--primary report-chapter-dialog__footer-btn"
384
+ @click="confirmChapterSourcePicker"
385
+ >
386
+ 確定
387
+ </button>
388
+ </div>
389
+ </template>
390
+ </el-dialog>
391
+
392
+ <el-dialog
393
+ v-model="applyTemplateDialogVisible"
394
+ width="80%"
395
+ append-to-body
396
+ :z-index="10000"
397
+ :show-close="false"
398
+ modal-class="apply-template-overlay"
399
+ class="apply-template-dialog"
400
+ header-class="apply-template-dialog__header"
401
+ @closed="resetApplyTemplateForm"
402
+ >
403
+ <template #header="{ close, titleId, titleClass }">
404
+ <span :id="titleId" :class="['apply-template-dialog__title', titleClass]"
405
+ >申請創建模板</span
406
+ >
407
+ <button type="button" class="apply-template-dialog__close" aria-label="關閉" @click="close">
408
+ ×
409
+ </button>
410
+ </template>
411
+
412
+ <el-scrollbar v-if="applyTemplateDetail" class="apply-template-dialog__scroll">
413
+ <div class="apply-template-form">
414
+ <div class="apply-template-meta">
415
+ <label class="apply-template-field apply-template-field--row">
416
+ <span class="apply-template-label">來源報告</span>
417
+ <div class="apply-template-field__control">
418
+ <span class="apply-template-source">{{ applyTemplateDetail.sourceReport }}</span>
419
+ </div>
420
+ </label>
421
+
422
+ <label class="apply-template-field apply-template-field--row">
423
+ <span class="apply-template-label"
424
+ >主要語言<span class="apply-template-label__required">*</span></span
425
+ >
426
+ <div class="apply-template-field__control apply-template-field__control--lang">
427
+ <el-input
428
+ :model-value="applyTemplateLanguageDisplay"
429
+ disabled
430
+ class="apply-template-field__input"
431
+ />
432
+ </div>
433
+ </label>
434
+
435
+ <label class="apply-template-field apply-template-field--row">
436
+ <span class="apply-template-label"
437
+ >模板標題<span class="apply-template-label__required">*</span></span
438
+ >
439
+ <div class="apply-template-field__control">
440
+ <el-input
441
+ v-model="applyTemplateTitle"
442
+ :maxlength="100"
443
+ show-word-limit
444
+ class="apply-template-field__input"
445
+ />
446
+ </div>
447
+ </label>
448
+
449
+ <label
450
+ class="apply-template-field apply-template-field--row apply-template-field--textarea-row"
451
+ >
452
+ <span class="apply-template-label"
453
+ >模板描述<span class="apply-template-label__required">*</span></span
454
+ >
455
+ <div class="apply-template-field__control">
456
+ <el-input
457
+ v-model="applyTemplateDescription"
458
+ type="textarea"
459
+ :rows="4"
460
+ :maxlength="3000"
461
+ show-word-limit
462
+ resize="none"
463
+ class="apply-template-field__textarea"
464
+ />
465
+ </div>
466
+ </label>
467
+ </div>
468
+
469
+ <div
470
+ v-for="chapter in applyTemplateDisplayChapters"
471
+ :key="chapter.id"
472
+ class="apply-template-chapter"
473
+ >
474
+ <h4 class="apply-template-chapter__title">{{ chapter.title || '章節標題' }}</h4>
475
+
476
+ <label
477
+ class="apply-template-field apply-template-field--row apply-template-field--textarea-row"
478
+ >
479
+ <span class="apply-template-label">章節Prompt</span>
480
+ <div class="apply-template-field__control">
481
+ <el-input
482
+ :model-value="chapter.prompt"
483
+ type="textarea"
484
+ :rows="8"
485
+ :maxlength="1300"
486
+ show-word-limit
487
+ resize="none"
488
+ disabled
489
+ class="apply-template-field__textarea"
490
+ />
491
+ </div>
492
+ </label>
493
+
494
+ <label
495
+ class="apply-template-field apply-template-field--row apply-template-field--textarea-row"
496
+ >
497
+ <span class="apply-template-label">章節描述</span>
498
+ <div class="apply-template-field__control">
499
+ <el-input
500
+ :model-value="chapter.description"
501
+ type="textarea"
502
+ :rows="8"
503
+ :maxlength="1000"
504
+ show-word-limit
505
+ resize="none"
506
+ disabled
507
+ class="apply-template-field__textarea"
508
+ />
509
+ </div>
510
+ </label>
511
+ </div>
512
+
513
+ <section class="apply-template-process">
514
+ <label
515
+ class="apply-template-field apply-template-field--row apply-template-process__row"
516
+ >
517
+ <span class="apply-template-label"
518
+ >處理事項<span class="apply-template-label__required">*</span></span
519
+ >
520
+ <div class="apply-template-field__control">
521
+ <el-radio-group
522
+ v-model="applyTemplateAction"
523
+ class="apply-template-process__radios"
524
+ >
525
+ <el-radio value="reject">拒絕</el-radio>
526
+ <el-radio value="approve">同意</el-radio>
527
+ </el-radio-group>
528
+ </div>
529
+ </label>
530
+
531
+ <label
532
+ class="apply-template-field apply-template-field--row apply-template-field--textarea-row"
533
+ >
534
+ <span class="apply-template-label">意見</span>
535
+ <div class="apply-template-field__control">
536
+ <el-input
537
+ v-model="applyTemplateOpinion"
538
+ type="textarea"
539
+ :rows="4"
540
+ :maxlength="250"
541
+ show-word-limit
542
+ resize="none"
543
+ class="apply-template-field__textarea"
544
+ />
545
+ </div>
546
+ </label>
547
+
548
+ <div class="apply-template-process__actions">
549
+ <button
550
+ type="button"
551
+ class="apply-template-btn apply-template-btn--ghost"
552
+ @click="applyTemplateDialogVisible = false"
553
+ >
554
+ 取消
555
+ </button>
556
+ <button
557
+ type="button"
558
+ class="apply-template-btn apply-template-btn--primary"
559
+ @click="handleApplyTemplateSubmit"
560
+ >
561
+ 提交
562
+ </button>
563
+ </div>
564
+ </section>
565
+
566
+ <section class="apply-template-approval">
567
+ <h4 class="apply-template-approval__title">審批明細</h4>
568
+ <el-table
569
+ :data="applyTemplateDetail.approvalRecords"
570
+ border
571
+ class="apply-template-approval-table"
572
+ >
573
+ <el-table-column label="操作人員" min-width="180" prop="operator" />
574
+ <el-table-column label="處理狀態" width="120" prop="status" />
575
+ <el-table-column label="處理完成時間" min-width="180" prop="completedAt" />
576
+ <el-table-column label="詳情" min-width="200" prop="detail" show-overflow-tooltip />
577
+ </el-table>
578
+ </section>
579
+ </div>
580
+ </el-scrollbar>
581
+ </el-dialog>
582
+
583
+ <el-dialog
584
+ v-model="aiPromptDialogVisible"
585
+ width="60%"
586
+ append-to-body
587
+ :show-close="false"
588
+ modal-class="report-workflow-dialog-overlay"
589
+ class="report-workflow-dialog ai-prompt-generate-dialog"
590
+ header-class="report-workflow-dialog__header"
591
+ @closed="handleAiPromptDialogClosed"
592
+ >
593
+ <template #header="{ close }">
594
+ <span class="report-workflow-dialog__title">{{ aiPromptDialogTitle }}</span>
595
+ <button
596
+ type="button"
597
+ class="report-workflow-dialog__close"
598
+ aria-label="關閉"
599
+ @click="close"
600
+ >
601
+ ×
602
+ </button>
603
+ </template>
604
+
605
+ <div v-if="aiPromptDialogMode === 'input'" class="ai-prompt-generate-dialog__form">
606
+ <label class="ai-prompt-generate-dialog__field ai-prompt-generate-dialog__field--row">
607
+ <span class="ai-prompt-generate-dialog__label">章節標題</span>
608
+ <div class="ai-prompt-generate-dialog__control">
609
+ <el-input
610
+ v-model="aiPromptForm.title"
611
+ :maxlength="AI_PROMPT_TITLE_MAX"
612
+ show-word-limit
613
+ class="ai-prompt-generate-dialog__input"
614
+ />
615
+ </div>
616
+ </label>
617
+
618
+ <label
619
+ class="ai-prompt-generate-dialog__field ai-prompt-generate-dialog__field--row ai-prompt-generate-dialog__field--textarea"
620
+ >
621
+ <span class="ai-prompt-generate-dialog__label">章節描述</span>
622
+ <div class="ai-prompt-generate-dialog__control">
623
+ <el-input
624
+ v-model="aiPromptForm.description"
625
+ type="textarea"
626
+ :rows="4"
627
+ :maxlength="AI_PROMPT_DESC_MAX"
628
+ show-word-limit
629
+ resize="none"
630
+ class="ai-prompt-generate-dialog__textarea"
631
+ />
632
+ </div>
633
+ </label>
634
+ </div>
635
+
636
+ <div v-else class="ai-prompt-generate-dialog__preview">
637
+ <div class="ai-prompt-generate-dialog__preview-row">
638
+ <span class="ai-prompt-generate-dialog__label">章節標題</span>
639
+ <p class="ai-prompt-generate-dialog__preview-text">{{ aiPromptForm.title }}</p>
640
+ </div>
641
+
642
+ <div class="ai-prompt-generate-dialog__preview-row">
643
+ <span class="ai-prompt-generate-dialog__label">章節描述</span>
644
+ <p
645
+ class="ai-prompt-generate-dialog__preview-text ai-prompt-generate-dialog__preview-text--multiline"
646
+ >
647
+ {{ aiPromptForm.description }}
648
+ </p>
649
+ </div>
650
+
651
+ <div class="ai-prompt-generate-dialog__divider" aria-hidden="true" />
652
+
653
+ <div
654
+ class="ai-prompt-generate-dialog__preview-row ai-prompt-generate-dialog__preview-row--prompt"
655
+ >
656
+ <span class="ai-prompt-generate-dialog__label">生成的 prompt</span>
657
+ <p
658
+ class="ai-prompt-generate-dialog__preview-text ai-prompt-generate-dialog__preview-text--multiline"
659
+ >
660
+ {{ aiPromptGeneratedText }}
661
+ </p>
662
+ </div>
663
+ </div>
664
+
665
+ <template #footer>
666
+ <div class="ai-prompt-generate-dialog__footer">
667
+ <button
668
+ v-if="aiPromptDialogMode === 'input'"
669
+ type="button"
670
+ class="template-center-toolbar-btn gen-ai-action-btn template-center-toolbar-btn--outline ai-prompt-generate-dialog__footer-btn"
671
+ @click="closeAiPromptDialog"
672
+ >
673
+ 取消
674
+ </button>
675
+ <button
676
+ v-else
677
+ type="button"
678
+ class="template-center-toolbar-btn gen-ai-action-btn template-center-toolbar-btn--outline ai-prompt-generate-dialog__footer-btn"
679
+ @click="aiPromptDialogMode = 'input'"
680
+ >
681
+ 退回
682
+ </button>
683
+
684
+ <button
685
+ v-if="aiPromptDialogMode === 'input'"
686
+ type="button"
687
+ class="template-center-toolbar-btn gen-ai-action-btn template-center-toolbar-btn--primary ai-prompt-generate-dialog__footer-btn"
688
+ :disabled="aiPromptGenerating"
689
+ @click="generateAiPrompt"
690
+ >
691
+ {{ aiPromptGenerating ? '生成中' : '生成 Prompt' }}
692
+ </button>
693
+ <button
694
+ v-else
695
+ type="button"
696
+ class="template-center-toolbar-btn gen-ai-action-btn template-center-toolbar-btn--primary ai-prompt-generate-dialog__footer-btn"
697
+ @click="confirmAiPrompt"
698
+ >
699
+ 確認
700
+ </button>
701
+ </div>
702
+ </template>
703
+ </el-dialog>
704
+ </section>
705
+ </template>
706
+
707
+ <script setup lang="ts">
708
+ import genAiReportEditorApi, {
709
+ pickAdjustInstanceText,
710
+ pickExistsInstanceVersionNo,
711
+ } from '@/api/modules/build-report/reportEditor'
712
+ import genAiReportPermissionApi from '@/api/modules/build-report/reportPermission'
713
+ import { useGenAiBuildReportStore } from '@/store/modules/gen-ai'
714
+ import type {
715
+ GenAiAdjustInstanceContent,
716
+ GenAiAdjustInstanceData,
717
+ GenAiAdjustInstanceFileItem,
718
+ GenAiAdjustInstanceSection,
719
+ GenAiGetSectionListParams,
720
+ GenAiReportSectionItem,
721
+ GenAiSectionLatestStatus,
722
+ GenAiSectionVersionItem,
723
+ } from '@/types/gen-ai/build-report/reportEditor'
724
+ import {
725
+ DOCX_HEADER_CLASSIFICATION,
726
+ exportMarkdownToDocxWord,
727
+ } from '../../components/AIExport'
728
+ import { Sort } from '@element-plus/icons-vue'
729
+ import type { SelectInstance } from 'element-plus'
730
+ import {
731
+ ElButton,
732
+ ElCheckbox,
733
+ ElCheckboxGroup,
734
+ ElDialog,
735
+ ElIcon,
736
+ ElInput,
737
+ ElMessage,
738
+ ElMessageBox,
739
+ ElOption,
740
+ ElRadio,
741
+ ElRadioGroup,
742
+ ElScrollbar,
743
+ ElSelect,
744
+ ElTable,
745
+ ElTableColumn,
746
+ } from 'element-plus'
747
+ import Sortable from 'sortablejs'
748
+ import { computed, nextTick, onMounted, onUnmounted, reactive, ref, watch } from 'vue'
749
+ import { useRoute, useRouter } from 'vue-router'
750
+ import {
751
+ ReportChapterTiptapEditor,
752
+ PromptField as ReportTemplatePromptField,
753
+ ReportTypewriterDocEditor,
754
+ } from '../../common'
755
+ import { isGenAiResponseOk } from '../../common/utils/api-res-status'
756
+ import {
757
+ buildApplyTemplateDetailFromReport,
758
+ findFinalizedReportById,
759
+ getApplyTemplateDisplayChapters,
760
+ getApplyTemplateLanguageDisplay,
761
+ getReportLanguageLabel,
762
+ loadCoherenceCheckSession,
763
+ openReportCoherenceCheckResult,
764
+ stripChapterNumberPrefix,
765
+ translateChapterLabel,
766
+ translateChapterTitles,
767
+ type ApplyTemplateDetail,
768
+ type ApplyTemplateProcessAction,
769
+ } from '../report-domain'
770
+ import {
771
+ CHAPTER_DRAFT_HISTORY_TITLE,
772
+ createDefaultPromptChapterForm,
773
+ registerChapterHistoryReloader,
774
+ useReportWorkflow,
775
+ type PromptChapterForm,
776
+ type ReportHistoryItem,
777
+ } from '../useReportWorkflow'
778
+
779
+ const genAiBuildReportStore = useGenAiBuildReportStore()
780
+ const route = useRoute()
781
+
782
+ function resolveInstanceId() {
783
+ return String(genAiBuildReportStore.instanceId ?? '').trim()
784
+ }
785
+
786
+ const CHAPTER_PANEL_MAX_ROWS = 14
787
+ const CHAPTER_PANEL_ROW_HEIGHT = 40
788
+ const CHAPTER_TITLE_MAX = 100
789
+ const CHAPTER_DESC_MAX = 1000
790
+ const CHAPTER_PROMPT_MAX = 1300
791
+ const AI_PROMPT_TITLE_MAX = 100
792
+ const AI_PROMPT_DESC_MAX = 1000
793
+
794
+ type AiPromptDialogMode = 'input' | 'preview'
795
+
796
+ const aiPromptDialogVisible = ref(false)
797
+ const aiPromptDialogMode = ref<AiPromptDialogMode>('input')
798
+ const aiPromptGeneratedText = ref('')
799
+ const aiPromptGenerating = ref(false)
800
+ const aiPromptForm = reactive({
801
+ title: '',
802
+ description: '',
803
+ })
804
+
805
+ const aiPromptDialogTitle = computed(() =>
806
+ aiPromptDialogMode.value === 'input' ? '智能化生成 Prompt' : '智能化生成 Prompt 預覽'
807
+ )
808
+
809
+ function resetAiPromptDialogForm(initialTitle = '', initialDescription = '') {
810
+ aiPromptForm.title = initialTitle.trim()
811
+ aiPromptForm.description = initialDescription.trim()
812
+ aiPromptGeneratedText.value = ''
813
+ aiPromptDialogMode.value = 'input'
814
+ }
815
+
816
+ function openAiPromptDialog() {
817
+ requestOpenAiPromptDialog('add-chapter')
818
+ }
819
+
820
+ function closeAiPromptDialog() {
821
+ aiPromptDialogVisible.value = false
822
+ }
823
+
824
+ function handleAiPromptDialogClosed() {
825
+ aiPromptGenerating.value = false
826
+ resetAiPromptDialogForm()
827
+ }
828
+
829
+ const {
830
+ draft,
831
+ selectedTemplate,
832
+ generatedChapterBodies,
833
+ patchGeneratedChapterBody,
834
+ generatedChapterTitles,
835
+ sourceFiles,
836
+ uiShowFinalizeToolbar,
837
+ uiShowChapterEditToolbar,
838
+ uiShowSettingsDoc,
839
+ chapterEditMode,
840
+ historyPreviewDirty,
841
+ historyPreviewRecordId,
842
+ historyPreviewApplyToken,
843
+ historyPreviewChapterBodies,
844
+ resetHistoryPreviewState,
845
+ resetWorkspaceHistoryOnEnter,
846
+ chapterHistoryItems,
847
+ chapterHistoryOpen,
848
+ chapterHistoryPreviewBody,
849
+ chapterHistoryApplyToken,
850
+ activeChapterHistoryKey,
851
+ enterChapterEditMode,
852
+ commitChapterEdit,
853
+ exitChapterEditMode,
854
+ isChapterEditDirty,
855
+ promptChapterForm,
856
+ saveDraftToSession,
857
+ aiPromptDialogRequestToken,
858
+ aiPromptDialogSource,
859
+ requestOpenAiPromptDialog,
860
+ settingsChapterPresets,
861
+ settingsSelectedChapters,
862
+ settingsChapterOrder,
863
+ settingsChapterVersions,
864
+ settingsTypewriterReplayKey,
865
+ settingsTypewriterReplayToken,
866
+ setSettingsTypingChapterKey,
867
+ setSettingsTypewriterPrinting,
868
+ chapterTypewriterActive,
869
+ chapterContentAwaitingFirstChunk,
870
+ stopChapterContentTypewriter,
871
+ requestSettingsTypewriterReplay,
872
+ reportGenerateStreamText,
873
+ reportGenerateStreamActive,
874
+ reportGenerateStreamEnded,
875
+ clearReportGenerateStream,
876
+ ensureSettingsChapterLayout,
877
+ applyAdjustInstanceSections,
878
+ applyAdjustInstanceFiles,
879
+ clearAdjustInstanceBindings,
880
+ adjustInstanceContents,
881
+ setAdjustInstanceContents,
882
+ setExistsInstanceVersionNo,
883
+ coherenceResultReady,
884
+ setCoherenceCheckResultReady,
885
+ markAdjustInstanceSettingSynced,
886
+ beginSuppressAdjustInstanceSettingWatch,
887
+ endSuppressAdjustInstanceSettingWatch,
888
+ existsInstanceVersionNo,
889
+ resolveChapterChartAuditEventId,
890
+ upsertChapterChartStreamRefsFromSources,
891
+ } = useReportWorkflow()
892
+
893
+ /** 先渲染工具列,再掛載 TipTap,避免進頁中欄空白 */
894
+ const editorContentReady = ref(false)
895
+
896
+ const router = useRouter()
897
+
898
+ /** 接口章節下拉(getSectionList);有數據時優先驅動下拉,覆蓋 mock/模板 */
899
+ const genAiSectionItems = ref<GenAiReportSectionItem[]>([])
900
+ const chapterTitles = ref<string[]>([])
901
+ const activeChapterIndex = ref(0)
902
+ /** 進頁拉列表時抑制 watch(activeChapterIndex) 重複打版本接口 */
903
+ let suppressSectionVersionReload = false
904
+
905
+ watch(aiPromptDialogRequestToken, (token) => {
906
+ if (!token) return
907
+ if (aiPromptDialogSource.value === 'prompt-panel') {
908
+ const section = genAiSectionItems.value[activeChapterIndex.value]
909
+ resetAiPromptDialogForm(
910
+ section?.sectionName ||
911
+ stripChapterNumberPrefix(chapterTitles.value[activeChapterIndex.value] || ''),
912
+ section?.sectionDesc || ''
913
+ )
914
+ } else {
915
+ resetAiPromptDialogForm(newChapterForm.title, newChapterForm.description)
916
+ }
917
+ aiPromptDialogVisible.value = true
918
+ })
919
+
920
+ async function generateAiPrompt() {
921
+ const title = aiPromptForm.title.trim()
922
+ const description = aiPromptForm.description.trim()
923
+
924
+ if (!title) {
925
+ ElMessage.warning('請輸入章節標題')
926
+ return
927
+ }
928
+ if (!description) {
929
+ ElMessage.warning('請輸入章節描述')
930
+ return
931
+ }
932
+
933
+ aiPromptGenerating.value = true
934
+ const res: any = await genAiReportEditorApi
935
+ .adjustGenAiInstance({
936
+ instanceId: resolveInstanceId(),
937
+ promptType: 'AI_PROMPT',
938
+ sectionTitle: title,
939
+ sectionDesc: description,
940
+ })
941
+ .catch((err) => {
942
+ console.error('接口异常', err)
943
+ return null
944
+ })
945
+ aiPromptGenerating.value = false
946
+
947
+ try {
948
+ if (!isGenAiResponseOk(res)) {
949
+ ElMessage.warning(res?.message || '接口异常')
950
+ return
951
+ }
952
+ const text = pickAdjustInstanceText(res?.data)
953
+ if (!text) {
954
+ ElMessage.warning('接口异常')
955
+ return
956
+ }
957
+ aiPromptGeneratedText.value = text
958
+ aiPromptDialogMode.value = 'preview'
959
+ } catch (err) {
960
+ console.error('接口异常', err)
961
+ ElMessage.warning('接口异常')
962
+ }
963
+ }
964
+
965
+ function confirmAiPrompt() {
966
+ if (aiPromptDialogSource.value === 'prompt-panel') {
967
+ promptChapterForm.prompt = aiPromptGeneratedText.value
968
+ } else {
969
+ newChapterForm.prompt = aiPromptGeneratedText.value
970
+ }
971
+ closeAiPromptDialog()
972
+ }
973
+
974
+ const SECTION_STATUS_LABEL: Record<GenAiSectionLatestStatus, string> = {
975
+ PENDING: '未生成',
976
+ EDITING: '編輯中',
977
+ CREATED: '已生成',
978
+ }
979
+
980
+ function formatGenAiSectionTitle(item: GenAiReportSectionItem) {
981
+ // 圖中展示:「1. 摘要與本年總結」= orderSeq + sectionName
982
+ return `${item.orderSeq}. ${item.sectionName}`
983
+ }
984
+
985
+ function applyGenAiSectionList(list: GenAiReportSectionItem[]) {
986
+ const sorted = [...list].sort((a, b) => a.orderSeq - b.orderSeq)
987
+ genAiSectionItems.value = sorted
988
+ // 以接口為準完整覆蓋下拉標題
989
+ chapterTitles.value = sorted.map(formatGenAiSectionTitle)
990
+ activeChapterIndex.value = 0
991
+ }
992
+
993
+ /** 進頁 / 失敗:清空 mock 章節歷史與 Prompt,避免殘留覆蓋接口 */
994
+ function clearChapterHistoryAndPromptForApi() {
995
+ genAiSectionVersionItems.value = []
996
+ chapterHistoryItems.value = []
997
+ activeChapterHistoryKey.value = ''
998
+ chapterHistoryOpen.value = false
999
+ Object.assign(promptChapterForm, createDefaultPromptChapterForm())
1000
+ chapterHistoryPreviewBody.value = null
1001
+ chapterHistoryApplyToken.value += 1
1002
+ }
1003
+
1004
+ /**
1005
+ * 進頁拉取章節下拉;成功則用接口數據替換 chapterTitles
1006
+ * instanceId 取自 genAiBuildReportStore(當前頁正確來源)
1007
+ * 無 instanceId / 接口失敗 / 空列表:界面置空,不阻斷主流程、不回落 mock
1008
+ */
1009
+ async function loadGenAiSectionListOnEnter() {
1010
+ const instanceId = String(genAiBuildReportStore.instanceId ?? '').trim()
1011
+ if (!instanceId) {
1012
+ console.warn('[report editor] 無 instanceId,章節下拉置空')
1013
+ genAiSectionItems.value = []
1014
+ chapterTitles.value = []
1015
+ clearChapterHistoryAndPromptForApi()
1016
+ return false
1017
+ }
1018
+ const result = await genAiSectionList({ instanceId })
1019
+ if (!result.ok) {
1020
+ console.warn('[report editor] getSectionList 失敗,章節下拉置空')
1021
+ genAiSectionItems.value = []
1022
+ chapterTitles.value = []
1023
+ clearChapterHistoryAndPromptForApi()
1024
+ return false
1025
+ }
1026
+ if (!result.sectionList.length) {
1027
+ genAiSectionItems.value = []
1028
+ chapterTitles.value = []
1029
+ clearChapterHistoryAndPromptForApi()
1030
+ return true
1031
+ }
1032
+ applyGenAiSectionList(result.sectionList)
1033
+ suppressSectionVersionReload = true
1034
+ try {
1035
+ await loadGenAiSectionVersionListForActiveChapter()
1036
+ } finally {
1037
+ suppressSectionVersionReload = false
1038
+ }
1039
+ return true
1040
+ }
1041
+
1042
+ onMounted(() => {
1043
+ registerChapterHistoryReloader(() => loadGenAiSectionVersionListForActiveChapter())
1044
+
1045
+ // 每次進入:清預覽態,再清空 mock,隨後以接口完整覆蓋
1046
+ resetWorkspaceHistoryOnEnter()
1047
+ activeChapterIndex.value = 0
1048
+ void init()
1049
+
1050
+ void (async () => {
1051
+ if (isReportSettingsMode.value) {
1052
+ syncLocalizedChapterTitles()
1053
+ // 報告頁:AdjustInstance 由 watch(isReportSettingsMode) 觸發
1054
+ return
1055
+ }
1056
+ clearChapterHistoryAndPromptForApi()
1057
+ chapterTitles.value = []
1058
+ genAiSectionItems.value = []
1059
+ await loadGenAiSectionListOnEnter()
1060
+ })()
1061
+
1062
+ requestAnimationFrame(() => {
1063
+ requestAnimationFrame(() => {
1064
+ editorContentReady.value = true
1065
+ })
1066
+ })
1067
+ })
1068
+
1069
+ const isReportSettingsMode = computed(() => uiShowSettingsDoc.value)
1070
+
1071
+ const isViewingReportHistory = computed(
1072
+ () => !!historyPreviewRecordId.value && !historyPreviewDirty.value
1073
+ )
1074
+
1075
+ function normalizeChapterMatchTitle(value: string) {
1076
+ return stripChapterNumberPrefix(String(value || ''))
1077
+ .replace(/\s+/g, '')
1078
+ .toLowerCase()
1079
+ }
1080
+
1081
+ /** 多候選中優先取有正文的 instanceContent,避免空殼占位擋住真實數據 */
1082
+ function pickAdjustInstanceContent(
1083
+ candidates: Array<{ sectionTitle: string; sectionContent: string } | undefined>
1084
+ ) {
1085
+ const list = candidates.filter(
1086
+ (item): item is { sectionTitle: string; sectionContent: string } => !!item
1087
+ )
1088
+ if (!list.length) return undefined
1089
+ return list.find((item) => String(item.sectionContent || '').trim()) ?? list[0]
1090
+ }
1091
+
1092
+ const SETTINGS_UNAVAILABLE_LABELS = new Set(['附件與數'])
1093
+
1094
+ /** 設置模式:僅 AdjustInstance / AdjustInstanceSetting 正文;無接口則空 */
1095
+ const settingsPreviewChapters = computed(() => {
1096
+ const ordered = settingsChapterOrder.value
1097
+ .map((key) => settingsChapterPresets.value.find((preset) => preset.key === key))
1098
+ .filter((preset): preset is { key: string; label: string } => !!preset)
1099
+
1100
+ const visible = ordered.filter(
1101
+ (preset) =>
1102
+ !!settingsSelectedChapters.value[preset.key] && !SETTINGS_UNAVAILABLE_LABELS.has(preset.label)
1103
+ )
1104
+
1105
+ return visible.map((preset, visibleIndex) => {
1106
+ const label = translateChapterLabel(preset.label, draft.language, 'zh-Hant')
1107
+ const contents = adjustInstanceContents.value
1108
+ const presetsLen = settingsChapterPresets.value.length
1109
+ const sourceIndex = Number(preset.key.replace(/^chapter-/, ''))
1110
+ const apiDefaultOrder = settingsChapterPresets.value.map((item) => item.key)
1111
+ const orderIsApiDefault =
1112
+ settingsChapterOrder.value.length === apiDefaultOrder.length &&
1113
+ settingsChapterOrder.value.every((key, i) => key === apiDefaultOrder[i])
1114
+
1115
+ // 1) byTitle:標題對齊(首選)
1116
+ // 2) byOrder:僅當 contents 與可見列表等長,且為「僅勾選 subset」或「已拖拽重排後的全量列表序」
1117
+ // —— AdjustInstanceSetting 常按 versionIdList / 拖拽序回寫,此時 visibleIndex 才安全
1118
+ // 3) byKey:僅全量平行 + 順序仍是 chapter-0..n(首包 AdjustInstance)→ chapter-N → contents[N]
1119
+ // —— 禁止在拖拽後對「按列表序回寫的全量 contents」用 byKey(會搶在 byOrder 前錯配)
1120
+ const want = normalizeChapterMatchTitle(preset.label)
1121
+ const byTitle = contents.find(
1122
+ (item) => normalizeChapterMatchTitle(item.sectionTitle || '') === want
1123
+ )
1124
+ const byOrder =
1125
+ contents.length === visible.length &&
1126
+ (contents.length < presetsLen || !orderIsApiDefault)
1127
+ ? contents[visibleIndex]
1128
+ : undefined
1129
+ const byKey =
1130
+ contents.length === presetsLen &&
1131
+ orderIsApiDefault &&
1132
+ Number.isFinite(sourceIndex)
1133
+ ? contents[sourceIndex]
1134
+ : undefined
1135
+ const content = pickAdjustInstanceContent([byTitle, byOrder, byKey])
1136
+
1137
+ return {
1138
+ key: preset.key,
1139
+ displayTitle: stripChapterNumberPrefix(content?.sectionTitle?.trim() || label),
1140
+ // 原文交給 editor 做 Markdown + HTML 解析,此處不剝標籤
1141
+ intro: content ? String(content.sectionContent || '') : '',
1142
+ }
1143
+ })
1144
+ })
1145
+
1146
+ /** 供獨立打字機組件:僅傳章節內容,不耦合打字/滾動狀態 */
1147
+ const settingsTypewriterChapters = computed(() =>
1148
+ settingsPreviewChapters.value.map((chapter, index) => {
1149
+ const bareTitle = stripChapterNumberPrefix(String(chapter.displayTitle || ''))
1150
+ // 接口 sectionContent 常以 **標題** / <strong>標題</strong> / ## 標題 開頭,
1151
+ // 與下方 buildPlainContentHtml 注入的 <h2>N. 標題</h2> 重複 → 剝掉這一行
1152
+ let intro = String(chapter.intro || '')
1153
+ const lead = intro.match(
1154
+ /^\s*(?:<p[^>]*>\s*)?(?:#{1,6}\s+([^\n<#]+)|(?:\*\*|__|<strong[^>]*>)\s*([\s\S]*?)\s*(?:\*\*|__|<\/strong>))\s*(?:<\/p>)?\s*(?:\n+|$)/i
1155
+ )
1156
+ const leadTitle = (lead?.[1] || lead?.[2] || '').trim()
1157
+ if (
1158
+ lead &&
1159
+ leadTitle &&
1160
+ normalizeChapterMatchTitle(leadTitle) === normalizeChapterMatchTitle(bareTitle)
1161
+ ) {
1162
+ intro = intro.slice(lead[0].length)
1163
+ }
1164
+ return {
1165
+ key: chapter.key,
1166
+ title: `${index + 1}. ${bareTitle}`,
1167
+ intro,
1168
+ }
1169
+ })
1170
+ )
1171
+
1172
+ const settingsStreamChapters = computed(() => [
1173
+ {
1174
+ key: 'report-generate-stream',
1175
+ title: draft.reportName?.trim() || '報告',
1176
+ intro: reportGenerateStreamText.value,
1177
+ },
1178
+ ])
1179
+
1180
+ const settingsDocChapters = computed(() =>
1181
+ reportGenerateStreamActive.value ? settingsStreamChapters.value : settingsTypewriterChapters.value
1182
+ )
1183
+
1184
+ watch(
1185
+ isReportSettingsMode,
1186
+ (enabled) => {
1187
+ if (!enabled) {
1188
+ setSettingsTypingChapterKey(null)
1189
+ setSettingsTypewriterPrinting(false)
1190
+ clearAdjustInstanceBindings()
1191
+ clearReportGenerateStream()
1192
+ return
1193
+ }
1194
+ stopChapterContentTypewriter()
1195
+ // 章節 / 正文 / 資訊源由 AdjustInstance 回寫;不預填模板 mock
1196
+ // 從 session / existsInstanceVersionNo 恢復:顯示「查看結果 / 導出 Word」
1197
+ const session = loadCoherenceCheckSession()
1198
+ if (String(session?.beenFinalizedFlag ?? '').toUpperCase() === 'Y') {
1199
+ setCoherenceCheckResultReady(true, session?.coherenceCheckContentText ?? '')
1200
+ } else if (String(existsInstanceVersionNo.value ?? '').trim()) {
1201
+ setCoherenceCheckResultReady(true, '')
1202
+ }
1203
+ },
1204
+ { immediate: true }
1205
+ )
1206
+
1207
+ const showFinalizeToolbar = computed(() => uiShowFinalizeToolbar.value)
1208
+ const showChapterEditToolbar = computed(() => uiShowChapterEditToolbar.value)
1209
+ /** existsInstanceVersionNo 有值,或 CoherenceCheck 返回 Y → 顯示結果 / 導出 Word */
1210
+ const showCoherenceResultActions = computed(
1211
+ () => coherenceResultReady.value || !!String(existsInstanceVersionNo.value ?? '').trim()
1212
+ )
1213
+
1214
+ const settingsHistoryPreviewChapterIndex = ref(0)
1215
+
1216
+ const emit = defineEmits<{
1217
+ 'back-to-chapter-edit': []
1218
+ 'generate-report': []
1219
+ }>()
1220
+
1221
+ const applyTemplateDialogVisible = ref(false)
1222
+ const applyTemplateDetail = ref<ApplyTemplateDetail | null>(null)
1223
+ const applyTemplateTitle = ref('')
1224
+ const applyTemplateDescription = ref('')
1225
+ const applyTemplateAction = ref<'' | ApplyTemplateProcessAction>('')
1226
+ const applyTemplateOpinion = ref('')
1227
+
1228
+ const applyTemplateLanguageDisplay = computed(() =>
1229
+ applyTemplateDetail.value
1230
+ ? getApplyTemplateLanguageDisplay(applyTemplateDetail.value.language)
1231
+ : ''
1232
+ )
1233
+ const applyTemplateDisplayChapters = computed(() =>
1234
+ getApplyTemplateDisplayChapters(applyTemplateDetail.value)
1235
+ )
1236
+
1237
+ function mapReportLanguageToTemplateLanguage(language: string) {
1238
+ if (language === 'en') return 'en'
1239
+ return 'zh'
1240
+ }
1241
+
1242
+ function resetApplyTemplateForm() {
1243
+ applyTemplateTitle.value = applyTemplateDetail.value?.title ?? ''
1244
+ applyTemplateDescription.value = applyTemplateDetail.value?.description ?? ''
1245
+ applyTemplateAction.value = ''
1246
+ applyTemplateOpinion.value = ''
1247
+ }
1248
+
1249
+ function buildApplyTemplateDetail() {
1250
+ const previewRecord = historyPreviewRecordId.value
1251
+ ? findFinalizedReportById(historyPreviewRecordId.value)
1252
+ : null
1253
+
1254
+ const sourceReport =
1255
+ previewRecord?.displayTitle ?? `${draft.reportName || '未命名報告'} ${draft.year}`.trim()
1256
+
1257
+ const chapterSource = chapterTitles.value.length
1258
+ ? chapterTitles.value
1259
+ : (selectedTemplate.value?.chapters ?? [])
1260
+
1261
+ return buildApplyTemplateDetailFromReport({
1262
+ sourceReport,
1263
+ language: mapReportLanguageToTemplateLanguage(draft.language),
1264
+ title: draft.reportName?.trim() || '',
1265
+ description: '',
1266
+ chapterTitles: chapterSource,
1267
+ chapterPrompt: promptChapterForm.prompt,
1268
+ // 審批明細以接口為準;無數據則空表,不填本地假操作人
1269
+ approvalRecords: [],
1270
+ })
1271
+ }
1272
+
1273
+ // report editor api
1274
+ /**
1275
+ * 查詢章節下拉列表
1276
+ * 調試:data[] 含 instanceId/sectionId/orderSeq/sectionName/sectionDesc/latestStatus
1277
+ * latestStatus:PENDING 未生成 / EDITING 編輯中 / CREATED 已生成
1278
+ */
1279
+ const genAiSectionList = async (params: GenAiGetSectionListParams) => {
1280
+ const res: any = await genAiReportEditorApi
1281
+ .fetchGenAiSectionList({
1282
+ instanceId: params.instanceId,
1283
+ })
1284
+ .catch((err) => {
1285
+ console.error('接口异常', err)
1286
+ return null
1287
+ })
1288
+
1289
+ try {
1290
+ if (!isGenAiResponseOk(res)) {
1291
+ console.warn('[report editor] getSectionList', res?.message || '接口异常')
1292
+ return {
1293
+ ok: false as const,
1294
+ sectionList: [] as GenAiReportSectionItem[],
1295
+ }
1296
+ }
1297
+
1298
+ const sectionList = (Array.isArray(res.data) ? res.data : []) as GenAiReportSectionItem[]
1299
+ console.log('章节下拉列表', sectionList)
1300
+ return { ok: true as const, sectionList }
1301
+ } catch (err) {
1302
+ console.error('接口异常', err)
1303
+ return {
1304
+ ok: false as const,
1305
+ sectionList: [] as GenAiReportSectionItem[],
1306
+ }
1307
+ }
1308
+ }
1309
+
1310
+ /**
1311
+ * 更新章節順序
1312
+ * 調試:傳 sections[{ instanceId, sectionId, orderSeq }],orderSeq 為調整後新順序
1313
+ */
1314
+ const updateGenAiSectionOrder = async () => {
1315
+ const sections = genAiSectionItems.value.map((item) => ({
1316
+ instanceId: item.instanceId,
1317
+ sectionId: item.sectionId,
1318
+ orderSeq: item.orderSeq,
1319
+ }))
1320
+ if (!sections.length) {
1321
+ return { ok: false as const }
1322
+ }
1323
+
1324
+ const res: any = await genAiReportEditorApi.updateGenAiSectionOrder({ sections }).catch((err) => {
1325
+ console.error('接口异常', err)
1326
+ return null
1327
+ })
1328
+
1329
+ try {
1330
+ if (!isGenAiResponseOk(res)) {
1331
+ ElMessage.warning(res?.message || '接口异常')
1332
+ return { ok: false as const }
1333
+ }
1334
+ console.log('章节顺序已更新', sections)
1335
+ return { ok: true as const }
1336
+ } catch (err) {
1337
+ console.error('接口异常', err)
1338
+ ElMessage.warning('接口异常')
1339
+ return { ok: false as const }
1340
+ }
1341
+ }
1342
+
1343
+ /** 寫死調試參數(與接口文檔一致;後續換登錄用戶 / 當前章節) */
1344
+ const GEN_AI_HARDCODE_SECTION = {
1345
+ categoryId: 1,
1346
+ sectionId: '87E7D6DD-B6E2-4E37-9D38-DC816BD37809',
1347
+ userId: '8740004',
1348
+ userName: 'LOCAL ADMIN',
1349
+ userUnitSeq: '1',
1350
+ } as const
1351
+
1352
+ /**
1353
+ * 更新章節狀態(點「編輯/儲存」進入編輯時)
1354
+ * 調試:action "EDIT" → 後臺狀態改為 EDITING;參數暫用接口文檔寫死數據
1355
+ */
1356
+ const changeGenAiSectionStatus = async () => {
1357
+ const data = {
1358
+ categoryId: GEN_AI_HARDCODE_SECTION.categoryId,
1359
+ sectionId: GEN_AI_HARDCODE_SECTION.sectionId,
1360
+ action: 'EDIT' as const,
1361
+ updateUserId: GEN_AI_HARDCODE_SECTION.userId,
1362
+ updateUserName: GEN_AI_HARDCODE_SECTION.userName,
1363
+ updateUserUnitSeq: GEN_AI_HARDCODE_SECTION.userUnitSeq,
1364
+ }
1365
+
1366
+ const res: any = await genAiReportEditorApi.changeGenAiSectionStatus(data).catch((err) => {
1367
+ console.error('接口异常', err)
1368
+ return null
1369
+ })
1370
+
1371
+ try {
1372
+ if (!isGenAiResponseOk(res)) {
1373
+ ElMessage.warning(res?.message || '接口异常')
1374
+ return { ok: false as const }
1375
+ }
1376
+ console.log('章节状态已更新为 EDITING', data)
1377
+ return { ok: true as const }
1378
+ } catch (err) {
1379
+ console.error('接口异常', err)
1380
+ ElMessage.warning('接口异常')
1381
+ return { ok: false as const }
1382
+ }
1383
+ }
1384
+
1385
+ /**
1386
+ * 版本[保存]/[存為草稿] saveSectionVersion
1387
+ * draftFlag "N"=保存 / "Y"=存為草稿
1388
+ * instanceId/versionId/versionNo/hasOldAiContent:優先取當前選中版本原值
1389
+ */
1390
+ const saveGenAiSectionVersion = async (draftFlag: 'Y' | 'N') => {
1391
+ const versionId = activeChapterHistoryKey.value
1392
+ const version =
1393
+ genAiSectionVersionItems.value.find((item) => item.versionId === versionId) ??
1394
+ genAiSectionVersionItems.value[0]
1395
+
1396
+ const data = {
1397
+ categoryId: GEN_AI_HARDCODE_SECTION.categoryId,
1398
+ instanceId:
1399
+ version?.instanceId ||
1400
+ String(genAiBuildReportStore.instanceId ?? '') ||
1401
+ '62D6B74B-13C8-4113-9F97-54117FEBA9A6',
1402
+ sectionId: GEN_AI_HARDCODE_SECTION.sectionId,
1403
+ versionId: version?.versionId || versionId || '2DE1F24F-61A2-4539-BA66-CC5B75C79CC5',
1404
+ versionNo: version?.versionNo ?? 1,
1405
+ draftFlag,
1406
+ promptDetail: promptChapterForm.prompt,
1407
+ promptByGen: (version?.promptByGen ?? 'N') as 'Y' | 'N',
1408
+ contentLengthLimit: promptChapterForm.enableBodyContent
1409
+ ? promptChapterForm.bodyWordLimit || 500
1410
+ : (null as unknown as number),
1411
+ // 存草稿時正文可空;後臺不存該字段
1412
+ contentText: draftFlag === 'Y' ? '' : chapterHistoryPreviewBody.value || '',
1413
+ fileIndexIdList: version?.fileIndexIdList?.length
1414
+ ? [...version.fileIndexIdList]
1415
+ : sourceFiles.value.filter((f) => f.selected).map((f) => f.id),
1416
+ createdUserId: GEN_AI_HARDCODE_SECTION.userId,
1417
+ createdUserName: GEN_AI_HARDCODE_SECTION.userName,
1418
+ createdUserUnitSeq: GEN_AI_HARDCODE_SECTION.userUnitSeq,
1419
+ tableInfo: promptChapterForm.enableTable
1420
+ ? {
1421
+ tableTitle: promptChapterForm.tableTitle,
1422
+ columnLabel: promptChapterForm.tableRowLabels,
1423
+ barLabel: promptChapterForm.tableColumnLabels,
1424
+ content: promptChapterForm.tableContent,
1425
+ }
1426
+ : null,
1427
+ chartInfo: promptChapterForm.enableSourceChart
1428
+ ? { chartDesc: promptChapterForm.chartDescription }
1429
+ : null,
1430
+ // 須原值回傳
1431
+ hasOldAiContent: (version?.hasOldAiContent ?? 'N') as 'Y' | 'N',
1432
+ auditEventId: resolveChapterChartAuditEventId(),
1433
+ }
1434
+
1435
+ const res: any = await genAiReportEditorApi.saveGenAiSectionVersion(data).catch((err) => {
1436
+ console.error('接口异常', err)
1437
+ return null
1438
+ })
1439
+
1440
+ try {
1441
+ if (!isGenAiResponseOk(res)) {
1442
+ ElMessage.warning(res?.message || '接口异常')
1443
+ return { ok: false as const }
1444
+ }
1445
+ console.log(draftFlag === 'Y' ? '已存为草稿' : '已保存版本', data)
1446
+ return { ok: true as const }
1447
+ } catch (err) {
1448
+ console.error('接口异常', err)
1449
+ ElMessage.warning('接口异常')
1450
+ return { ok: false as const }
1451
+ }
1452
+ }
1453
+
1454
+ /**
1455
+ * —— 章節歷史 / Prompt:接口 GenAiSectionVersionItem ↔ 組件字段一一對應 ——
1456
+ *
1457
+ * 【章節歷史 card】
1458
+ * key / recordId ← versionId(點選後按 versionId 回填 Prompt)
1459
+ * kind ← draftFlag==="Y" → 'draft'(走草稿 card UI:繼續編輯/刪除)
1460
+ * ← 否則 'history'(普通版本 card)
1461
+ * title ← draftFlag==="Y" → CHAPTER_DRAFT_HISTORY_TITLE
1462
+ * 「您有未生成章節內容的草稿...」
1463
+ * ← finalizedFlag==="Y" → objectName ||「定稿版本」
1464
+ * ← 否則 objectName || `V${versionNo}`
1465
+ * author ← updateUserName || createdUserName
1466
+ * finalizedAt ← updateDatetime || createdDatetime
1467
+ * 列表順序 ← 草稿(draftFlag=Y)置頂,其餘按 versionNo 降序
1468
+ *
1469
+ * 【Prompt / 正文】
1470
+ * prompt ← promptDetail
1471
+ * enableBodyContent ← hasOldAiContent==="Y"(後臺標識,保存時須原值回傳,見 version 緩存)
1472
+ * bodyWordLimit ← contentLengthLimit
1473
+ * enableTable + table* ← tableInfo(null 關閉;columnLabel→列,barLabel→欄/行)
1474
+ * enableSourceChart+desc ← chartInfo(null 關閉;chartDesc)
1475
+ * 中欄正文 ← contentText
1476
+ *
1477
+ * 【資訊源反顯】僅用戶點選章節歷史 / 繼續編輯時
1478
+ * fileIndexIdList → sourceFiles.selected;正文靜態反顯(不啟動打字機)
1479
+ *
1480
+ * 【僅緩存在 genAiSectionVersionItems,UI 暫不單獨展示】
1481
+ * backupFlag / promptByGen / promptDetailExt(忽略) / fileIndexIdList /
1482
+ * created* / update* / versionNo(保存須原值回傳)
1483
+ */
1484
+ const genAiSectionVersionItems = ref<GenAiSectionVersionItem[]>([])
1485
+
1486
+ function mapVersionToHistoryItem(version: GenAiSectionVersionItem): ReportHistoryItem {
1487
+ const isDraft = version.draftFlag === 'Y'
1488
+ const isFinalized = version.finalizedFlag === 'Y'
1489
+ return {
1490
+ key: version.versionId,
1491
+ // draftFlag=Y:必須用組件約定的草稿文案,觸發 ReportHistoryList 草稿 card
1492
+ title: isDraft
1493
+ ? CHAPTER_DRAFT_HISTORY_TITLE
1494
+ : isFinalized
1495
+ ? version.objectName?.trim() || '定稿版本'
1496
+ : version.objectName?.trim() || `V${version.versionNo}`,
1497
+ author: version.updateUserName || version.createdUserName || '',
1498
+ finalizedAt: version.updateDatetime || version.createdDatetime || '',
1499
+ kind: isDraft ? 'draft' : 'history',
1500
+ recordId: version.versionId,
1501
+ }
1502
+ }
1503
+
1504
+ function mapVersionToPromptForm(version: GenAiSectionVersionItem): PromptChapterForm {
1505
+ const table = version.tableInfo
1506
+ const chart = version.chartInfo
1507
+ // Prompt 全量反顯:checkbox / input / textarea 與接口字段一一對齊
1508
+ const limitRaw = version.contentLengthLimit
1509
+ const hasBodyLimit = limitRaw != null && Number(limitRaw) > 0
1510
+ const tableEnabled =
1511
+ table != null && String((table as { enableTable?: string }).enableTable ?? 'Y') !== 'N'
1512
+ const chartEnabled =
1513
+ chart != null && String((chart as { enableChart?: string }).enableChart ?? 'Y') !== 'N'
1514
+ return {
1515
+ ...createDefaultPromptChapterForm(),
1516
+ prompt: String(version.promptDetail ?? ''),
1517
+ // 正文內容:有有效字數限制則勾選(與 chartAsStream contentLengthLimit null=關 對齊)
1518
+ enableBodyContent: hasBodyLimit,
1519
+ bodyWordLimit: hasBodyLimit ? Number(limitRaw) : 500,
1520
+ enableTable: tableEnabled,
1521
+ tableTitle: String(table?.tableTitle ?? ''),
1522
+ // 列標籤 ← columnLabel;欄標籤 ← barLabel(與類型註釋 / UI 文案一致)
1523
+ tableRowLabels: String(table?.columnLabel ?? ''),
1524
+ tableColumnLabels: String(table?.barLabel ?? ''),
1525
+ tableContent: String(table?.content ?? ''),
1526
+ enableSourceChart: chartEnabled,
1527
+ chartDescription: String(chart?.chartDesc ?? ''),
1528
+ }
1529
+ }
1530
+
1531
+ /** 程式內更新 activeChapterHistoryKey(切章拉列表等)時跳過 watch,避免誤觸 reverseUi */
1532
+ let suppressChapterHistoryKeyWatch = false
1533
+
1534
+ /** 將當前選中版本寫入 Prompt 表單 + 中欄正文預覽(覆蓋組件內 mock / session) */
1535
+ function applyGenAiVersionToPromptAndBody(versionId: string, reverseUi = false) {
1536
+ const version = genAiSectionVersionItems.value.find((item) => item.versionId === versionId)
1537
+ if (!version) return false
1538
+ // 靜態預覽也要先備 refList,才能把 contentText 裡 [ref:uuid] 解析成 tip
1539
+ upsertChapterChartStreamRefsFromSources(version)
1540
+ // reverseUi=true:用戶點選歷史 / 繼續編輯 → 停流、反顯資訊源、正文靜態展示(不啟動打字機)
1541
+ // reverseUi=false:切章 / 刷新列表 → 保持原行為,不改資訊源、不額外停流
1542
+ if (reverseUi) {
1543
+ stopChapterContentTypewriter({ cancelStream: true })
1544
+ }
1545
+ const nextPrompt = mapVersionToPromptForm(version)
1546
+ // Prompt 全量覆蓋寫入(checkbox / input / textarea),保證歷史切換時 UI 立即反顯
1547
+ Object.assign(promptChapterForm, nextPrompt)
1548
+ if (reverseUi) {
1549
+ const selectedSet = new Set(
1550
+ (Array.isArray(version.fileIndexIdList) ? version.fileIndexIdList : []).map((id) =>
1551
+ String(id)
1552
+ )
1553
+ )
1554
+ for (const file of sourceFiles.value) {
1555
+ file.selected = selectedSet.has(String(file.id))
1556
+ }
1557
+ // 按接口原文靜態反顯;空正文也寫入,避免回落到舊 generated 緩存導致數據不准
1558
+ const exactBody = String(version.contentText ?? '')
1559
+ chapterHistoryPreviewBody.value = exactBody
1560
+ chapterHistoryApplyToken.value += 1
1561
+ patchGeneratedChapterBody(activeChapterIndex.value, exactBody)
1562
+ return true
1563
+ }
1564
+ chapterHistoryPreviewBody.value = version.contentText || null
1565
+ chapterHistoryApplyToken.value += 1
1566
+ // 寫入本地緩存,切章時可先清空再秒開,無需乾等版本接口
1567
+ const body = String(version.contentText || '')
1568
+ if (body.trim()) {
1569
+ patchGeneratedChapterBody(activeChapterIndex.value, body)
1570
+ }
1571
+ return true
1572
+ }
1573
+
1574
+ function applyGenAiSectionVersionList(list: GenAiSectionVersionItem[]) {
1575
+ // 列表內各版本 refList 先備齊,切歷史 / 靜態預覽即可匹配 tip
1576
+ upsertChapterChartStreamRefsFromSources(list)
1577
+ // 草稿置頂,其餘 versionNo 降序
1578
+ const sorted = [...list].sort((a, b) => {
1579
+ const aDraft = a.draftFlag === 'Y' ? 1 : 0
1580
+ const bDraft = b.draftFlag === 'Y' ? 1 : 0
1581
+ if (aDraft !== bDraft) return bDraft - aDraft
1582
+ return b.versionNo - a.versionNo
1583
+ })
1584
+ genAiSectionVersionItems.value = sorted
1585
+ chapterHistoryItems.value = sorted.map(mapVersionToHistoryItem)
1586
+ // 有內容才展開;空列表保持收起
1587
+ chapterHistoryOpen.value = chapterHistoryItems.value.length > 0
1588
+ const firstKey = chapterHistoryItems.value[0]?.key ?? ''
1589
+ // 程式刷新列表時抑制 watch,避免誤走 reverseUi(改資訊源 / 停流)
1590
+ suppressChapterHistoryKeyWatch = true
1591
+ activeChapterHistoryKey.value = firstKey
1592
+ suppressChapterHistoryKeyWatch = false
1593
+ if (firstKey) {
1594
+ applyGenAiVersionToPromptAndBody(firstKey)
1595
+ } else {
1596
+ Object.assign(promptChapterForm, createDefaultPromptChapterForm())
1597
+ chapterHistoryPreviewBody.value = null
1598
+ chapterHistoryApplyToken.value += 1
1599
+ }
1600
+ }
1601
+
1602
+ /**
1603
+ * 查詢版本列表 getSectionVersionList
1604
+ * 調試:保存/存草稿須原值回傳 versionNo、hasOldAiContent;draftFlag 等為 Y/N
1605
+ */
1606
+ const genAiSectionVersionList = async (params: { instanceId: string; sectionId: string }) => {
1607
+ const res: any = await genAiReportEditorApi
1608
+ .fetchGenAiSectionVersionList({
1609
+ instanceId: params.instanceId,
1610
+ sectionId: params.sectionId,
1611
+ })
1612
+ .catch((err) => {
1613
+ console.error('接口异常', err)
1614
+ return null
1615
+ })
1616
+
1617
+ try {
1618
+ if (!isGenAiResponseOk(res)) {
1619
+ console.warn('[report editor] getSectionVersionList', res?.message || '接口异常')
1620
+ return {
1621
+ ok: false as const,
1622
+ versionList: [] as GenAiSectionVersionItem[],
1623
+ }
1624
+ }
1625
+ const versionList = (Array.isArray(res.data) ? res.data : []) as GenAiSectionVersionItem[]
1626
+ console.log('章节版本列表', versionList)
1627
+ return { ok: true as const, versionList }
1628
+ } catch (err) {
1629
+ console.error('接口异常', err)
1630
+ return {
1631
+ ok: false as const,
1632
+ versionList: [] as GenAiSectionVersionItem[],
1633
+ }
1634
+ }
1635
+ }
1636
+
1637
+ /** 按當前下拉選中章節拉取版本歷史 → 完整覆蓋章節歷史列表 + Prompt */
1638
+ async function loadGenAiSectionVersionListForActiveChapter() {
1639
+ const instanceId = String(genAiBuildReportStore.instanceId ?? '').trim()
1640
+ const section = genAiSectionItems.value[activeChapterIndex.value]
1641
+ if (!instanceId || !section?.sectionId) {
1642
+ console.warn('[report editor] 無 instanceId/sectionId,章節歷史置空')
1643
+ clearChapterHistoryAndPromptForApi()
1644
+ return false
1645
+ }
1646
+
1647
+ const result = await genAiSectionVersionList({
1648
+ instanceId,
1649
+ sectionId: section.sectionId,
1650
+ })
1651
+ if (!result.ok) {
1652
+ // 版本列表失敗不影響章節下拉;歷史 / Prompt / 正文置空
1653
+ console.warn('[report editor] getSectionVersionList 失敗,章節歷史置空')
1654
+ clearChapterHistoryAndPromptForApi()
1655
+ return false
1656
+ }
1657
+
1658
+ // 即使空列表也覆蓋(清空 mock)
1659
+ applyGenAiSectionVersionList(result.versionList)
1660
+ return true
1661
+ }
1662
+
1663
+ /**
1664
+ * 生成報告 AdjustInstance
1665
+ * 點「生成報告」→ 先跳報告頁(settings)→ 綁定
1666
+ * instanceContents / instanceSections / instanceFileList;
1667
+ * existsInstanceVersionNo 有值 = 已通過連貫性檢查
1668
+ */
1669
+ const adjustGenAiInstance = async (params: { instanceId: string }) => {
1670
+ const res: any = await genAiReportEditorApi
1671
+ .adjustGenAiInstance({
1672
+ instanceId: params.instanceId,
1673
+ })
1674
+ .catch((err) => {
1675
+ console.error('接口异常', err)
1676
+ return null
1677
+ })
1678
+
1679
+ try {
1680
+ if (!isGenAiResponseOk(res)) {
1681
+ console.warn('[report editor] AdjustInstance', res?.message || '接口异常')
1682
+ return {
1683
+ ok: false as const,
1684
+ data: null as GenAiAdjustInstanceData | null,
1685
+ }
1686
+ }
1687
+ const raw = (res?.data ?? {}) as Partial<GenAiAdjustInstanceData>
1688
+ const data: GenAiAdjustInstanceData = {
1689
+ instanceContents: (Array.isArray(raw.instanceContents)
1690
+ ? raw.instanceContents
1691
+ : []) as GenAiAdjustInstanceContent[],
1692
+ instanceSections: (Array.isArray(raw.instanceSections)
1693
+ ? raw.instanceSections
1694
+ : []) as GenAiAdjustInstanceSection[],
1695
+ instanceFileList: (Array.isArray(raw.instanceFileList)
1696
+ ? raw.instanceFileList
1697
+ : []) as GenAiAdjustInstanceFileItem[],
1698
+ existsInstanceVersionNo: pickExistsInstanceVersionNo(raw),
1699
+ }
1700
+ console.log('生成报告 AdjustInstance', data)
1701
+ return { ok: true as const, data }
1702
+ } catch (err) {
1703
+ console.error('接口异常', err)
1704
+ return {
1705
+ ok: false as const,
1706
+ data: null as GenAiAdjustInstanceData | null,
1707
+ }
1708
+ }
1709
+ }
1710
+
1711
+ /** 報告頁:調 AdjustInstance 並綁定中欄 / 右側章節 / 資訊源;無 instanceId / 失敗則清空綁定 */
1712
+ async function loadAdjustInstanceForReportPage() {
1713
+ const instanceId = resolveInstanceId()
1714
+ if (!instanceId) {
1715
+ console.warn('[report editor] 無 instanceId,跳過 AdjustInstance 並清空綁定')
1716
+ setAdjustInstanceContents([])
1717
+ clearAdjustInstanceBindings()
1718
+ return false
1719
+ }
1720
+
1721
+ const result = await adjustGenAiInstance({ instanceId })
1722
+ if (!result.ok || !result.data) {
1723
+ setAdjustInstanceContents([])
1724
+ clearAdjustInstanceBindings()
1725
+ return false
1726
+ }
1727
+
1728
+ const { instanceContents, instanceSections, instanceFileList, existsInstanceVersionNo } =
1729
+ result.data
1730
+
1731
+ beginSuppressAdjustInstanceSettingWatch()
1732
+ try {
1733
+ // 中欄報告內容
1734
+ setAdjustInstanceContents(instanceContents)
1735
+ // 已存在報告版本號 → 報告歷史 card 選中對照
1736
+ setExistsInstanceVersionNo(existsInstanceVersionNo)
1737
+ // 右側「生成報告設置」章節列表 + 版本下拉(text 如 V3)
1738
+ if (instanceSections.length) {
1739
+ applyAdjustInstanceSections(instanceSections)
1740
+ } else {
1741
+ const titles = instanceContents
1742
+ .map((item) => item.sectionTitle?.trim())
1743
+ .filter((title): title is string => !!title)
1744
+ if (titles.length) ensureSettingsChapterLayout(titles)
1745
+ else clearAdjustInstanceBindings()
1746
+ }
1747
+ // 資訊源
1748
+ applyAdjustInstanceFiles(instanceFileList)
1749
+ // 標記當前版本組合已同步,避免 Settings watch 立刻再打 AdjustInstanceSetting 造成中欄閃爍
1750
+ markAdjustInstanceSettingSynced(
1751
+ settingsChapterOrder.value
1752
+ .map((key) => String(settingsChapterVersions.value[key] ?? '').trim())
1753
+ .filter(Boolean)
1754
+ )
1755
+ // existsInstanceVersionNo 有值 = 已通過連貫性檢查 → 顯示結果 / 導出 Word
1756
+ if (String(existsInstanceVersionNo ?? '').trim()) {
1757
+ setCoherenceCheckResultReady(true, '')
1758
+ }
1759
+
1760
+ // 正文就緒後重播中欄打字機(避免空內容先 complete → 接口晚到只能靜態灌入)
1761
+ const firstKey =
1762
+ settingsChapterOrder.value.find((key) => settingsSelectedChapters.value[key]) ??
1763
+ settingsChapterOrder.value[0]
1764
+ if (firstKey && instanceContents.length) {
1765
+ void nextTick(() => requestSettingsTypewriterReplay(firstKey))
1766
+ }
1767
+ } finally {
1768
+ endSuppressAdjustInstanceSettingWatch()
1769
+ }
1770
+ return true
1771
+ }
1772
+
1773
+ // —— report editor api end ——
1774
+
1775
+ /** 進入報告頁(settings)後清空本地佈局再調 AdjustInstance */
1776
+ watch(
1777
+ isReportSettingsMode,
1778
+ (enabled) => {
1779
+ if (!enabled) return
1780
+ clearAdjustInstanceBindings()
1781
+ void loadAdjustInstanceForReportPage()
1782
+ },
1783
+ { immediate: true }
1784
+ )
1785
+
1786
+ /**
1787
+ * 點選章節歷史 / 繼續編輯 → 反顯 Prompt + 正文 + 資訊源(靜態,不啟動打字機)
1788
+ * 切章拉列表等程式寫 key 時由 suppressChapterHistoryKeyWatch 跳過
1789
+ */
1790
+ watch(activeChapterHistoryKey, (key) => {
1791
+ if (suppressChapterHistoryKeyWatch) return
1792
+ if (!key || !genAiSectionVersionItems.value.length) return
1793
+ applyGenAiVersionToPromptAndBody(key, true)
1794
+ })
1795
+
1796
+ /** 重命名成功後 chapterHistoryItems.title 回寫 → 同步接口緩存 objectName */
1797
+ watch(
1798
+ chapterHistoryItems,
1799
+ (items) => {
1800
+ for (const item of items) {
1801
+ if (item.kind === 'draft') continue
1802
+ const versionId = item.recordId || item.key
1803
+ const version = genAiSectionVersionItems.value.find((entry) => entry.versionId === versionId)
1804
+ if (version && item.title && version.objectName !== item.title) {
1805
+ version.objectName = item.title
1806
+ }
1807
+ }
1808
+ },
1809
+ { deep: true }
1810
+ )
1811
+
1812
+ /** 切章時先壓空正文,避免舊章在接口返回前殘留 ~1s */
1813
+ const chapterBodyCleared = ref(false)
1814
+
1815
+ /** 切換章節下拉:先清空屏幕 → 再展示新章(緩存秒開 / 接口覆蓋) */
1816
+ watch(activeChapterIndex, async (index, prevIndex) => {
1817
+ if (suppressSectionVersionReload) return
1818
+ if (isReportSettingsMode.value) return
1819
+
1820
+ const isSwitch = prevIndex !== undefined && index !== prevIndex
1821
+ if (isSwitch) {
1822
+ stopChapterContentTypewriter({ cancelStream: true })
1823
+ // 1) 立刻清空正文(標題隨 index 更新)
1824
+ chapterHistoryPreviewBody.value = ''
1825
+ chapterBodyCleared.value = true
1826
+ await nextTick()
1827
+ const wrap = document.querySelector(
1828
+ '.report-edit-page__main--anchored .report-edit-page__col-scroll .el-scrollbar__wrap'
1829
+ ) as HTMLElement | null
1830
+ if (wrap) wrap.scrollTop = 0
1831
+ // 2) 有本地緩存則秒開,減少等接口的空白
1832
+ const cached = String(generatedChapterBodies.value[index] ?? '')
1833
+ if (cached.trim()) {
1834
+ chapterHistoryPreviewBody.value = cached
1835
+ }
1836
+ chapterBodyCleared.value = false
1837
+ }
1838
+
1839
+ if (!genAiSectionItems.value.length) return
1840
+ void loadGenAiSectionVersionListForActiveChapter()
1841
+ })
1842
+
1843
+ function syncLocalizedChapterTitles() {
1844
+ // 章節編輯:下拉僅由 getSectionList 驅動,不回落本地假標題
1845
+ if (!isReportSettingsMode.value) {
1846
+ if (genAiSectionItems.value.length) {
1847
+ chapterTitles.value = genAiSectionItems.value.map(formatGenAiSectionTitle)
1848
+ } else {
1849
+ chapterTitles.value = []
1850
+ }
1851
+ return
1852
+ }
1853
+
1854
+ // 優先:生成接口寫入的章節標題
1855
+ if (generatedChapterTitles.value.length) {
1856
+ chapterTitles.value = translateChapterTitles(
1857
+ generatedChapterTitles.value,
1858
+ draft.language,
1859
+ isReportSettingsMode.value ? 'zh-Hant' : draft.chapterContentLanguage
1860
+ )
1861
+ return
1862
+ }
1863
+
1864
+ if (historyPreviewRecordId.value && !historyPreviewDirty.value) {
1865
+ syncHistoryPreviewChapterTitles()
1866
+ return
1867
+ }
1868
+
1869
+ // 僅用已選模板章節(接口/選模結果);無則空
1870
+ const sourceChapters = selectedTemplate.value?.chapters?.length
1871
+ ? selectedTemplate.value.chapters
1872
+ : []
1873
+ chapterTitles.value = sourceChapters.length
1874
+ ? translateChapterTitles(
1875
+ sourceChapters,
1876
+ draft.language,
1877
+ isReportSettingsMode.value ? 'zh-Hant' : draft.chapterContentLanguage
1878
+ )
1879
+ : []
1880
+ }
1881
+
1882
+ watch(
1883
+ () =>
1884
+ [
1885
+ selectedTemplate.value?.chapters,
1886
+ draft.language,
1887
+ draft.chapterContentLanguage,
1888
+ generatedChapterTitles.value,
1889
+ ] as const,
1890
+ () => {
1891
+ syncLocalizedChapterTitles()
1892
+ },
1893
+ { immediate: true, deep: true }
1894
+ )
1895
+
1896
+ watch(
1897
+ () => historyPreviewApplyToken.value,
1898
+ () => {
1899
+ if (isReportSettingsMode.value) {
1900
+ settingsHistoryPreviewChapterIndex.value = 0
1901
+ return
1902
+ }
1903
+ syncHistoryPreviewChapterTitles()
1904
+ }
1905
+ )
1906
+
1907
+ function syncHistoryPreviewChapterTitles() {
1908
+ const recordId = historyPreviewRecordId.value
1909
+ if (!recordId) return
1910
+
1911
+ const record = findFinalizedReportById(recordId)
1912
+ if (!record) return
1913
+
1914
+ const titles = record.chapters.map((chapter, index) => `${index + 1}. ${chapter.label}`)
1915
+ chapterTitles.value = translateChapterTitles(titles, draft.language, draft.chapterContentLanguage)
1916
+ activeChapterIndex.value = 0
1917
+ }
1918
+
1919
+ const chapterSelectRef = ref<SelectInstance>()
1920
+ const mainBoundaryEl = ref<HTMLElement | null>(null)
1921
+
1922
+ function refreshMainBoundary() {
1923
+ mainBoundaryEl.value = document.querySelector(
1924
+ '.report-edit-page__main--anchored'
1925
+ ) as HTMLElement | null
1926
+ }
1927
+
1928
+ /** 章節下拉浮層限制在中間 main card 內,避免超出 report-edit-page__main--anchored */
1929
+ const chapterSelectPopperOptions = computed(() => {
1930
+ const boundary = mainBoundaryEl.value ?? 'clippingParents'
1931
+ return {
1932
+ modifiers: [
1933
+ {
1934
+ name: 'preventOverflow',
1935
+ options: {
1936
+ boundary,
1937
+ padding: 8,
1938
+ altAxis: true,
1939
+ },
1940
+ },
1941
+ {
1942
+ name: 'flip',
1943
+ options: {
1944
+ boundary,
1945
+ padding: 8,
1946
+ },
1947
+ },
1948
+ ],
1949
+ }
1950
+ })
1951
+
1952
+ const chapterDialogVisible = ref(false)
1953
+ /** 拖動重排後顯示「確認排序」;點擊確認後隱藏 */
1954
+ const showConfirmSortButton = ref(false)
1955
+ const selectDragSortIndex = ref<number | null>(null)
1956
+ const selectDragOverSortIndex = ref<number | null>(null)
1957
+ const newChapterForm = reactive({
1958
+ title: '',
1959
+ description: '',
1960
+ prompt: '',
1961
+ /** all = 所有資訊源檔案;partial = 部分資訊源檔案 */
1962
+ sourceMode: 'all' as 'all' | 'partial',
1963
+ selectedSourceIds: [] as string[],
1964
+ })
1965
+
1966
+ const chapterSourcePickerVisible = ref(false)
1967
+ const chapterSourcePickerDraft = ref<string[]>([])
1968
+
1969
+ const chapterSourcePickerCheckAll = computed({
1970
+ get: () =>
1971
+ sourceFiles.value.length > 0 &&
1972
+ chapterSourcePickerDraft.value.length === sourceFiles.value.length,
1973
+ set: (checked: boolean) => {
1974
+ chapterSourcePickerDraft.value = checked ? sourceFiles.value.map((f) => f.id) : []
1975
+ },
1976
+ })
1977
+
1978
+ const chapterSourcePickerIndeterminate = computed(
1979
+ () =>
1980
+ chapterSourcePickerDraft.value.length > 0 &&
1981
+ chapterSourcePickerDraft.value.length < sourceFiles.value.length
1982
+ )
1983
+
1984
+ function onChapterSourceCheckAll(checked: boolean | string | number) {
1985
+ chapterSourcePickerCheckAll.value = Boolean(checked)
1986
+ }
1987
+
1988
+ function openChapterSourcePicker() {
1989
+ if (!sourceFiles.value.length) {
1990
+ ElMessage.warning('暫無資訊源檔案,請先在資訊源面板添加')
1991
+ return
1992
+ }
1993
+ chapterSourcePickerDraft.value = [...newChapterForm.selectedSourceIds]
1994
+ chapterSourcePickerVisible.value = true
1995
+ }
1996
+
1997
+ function confirmChapterSourcePicker() {
1998
+ if (!chapterSourcePickerDraft.value.length) {
1999
+ ElMessage.warning('請至少選擇一個資訊源檔案')
2000
+ return
2001
+ }
2002
+ newChapterForm.selectedSourceIds = [...chapterSourcePickerDraft.value]
2003
+ chapterSourcePickerVisible.value = false
2004
+ ElMessage.success(`已選擇 ${newChapterForm.selectedSourceIds.length} 個資訊源`)
2005
+ }
2006
+
2007
+ watch(
2008
+ () => newChapterForm.sourceMode,
2009
+ (mode) => {
2010
+ if (mode === 'all') {
2011
+ newChapterForm.selectedSourceIds = []
2012
+ }
2013
+ }
2014
+ )
2015
+ /** 下拉內 Sortable 實例(彈層打開時綁定,關閉時銷毀) */
2016
+ let selectSortableInstance: Sortable | null = null
2017
+ let selectListClickGuard: ((event: Event) => void) | null = null
2018
+ let selectDragMoved = false
2019
+
2020
+ function closeChapterSelect() {
2021
+ chapterSelectRef.value?.blur()
2022
+ }
2023
+
2024
+ /** 確認當前下拉拖動後的章節順序(調用 updateSectionOrder) */
2025
+ async function handleConfirmSort() {
2026
+ if (!genAiSectionItems.value.length) {
2027
+ return
2028
+ }
2029
+ const result = await updateGenAiSectionOrder()
2030
+ if (!result.ok) return
2031
+ showConfirmSortButton.value = false
2032
+ closeChapterSelect()
2033
+ ElMessage.success('章節順序已更新')
2034
+ }
2035
+
2036
+ function moveActiveIndexAfterReorder(activeIndex: number, oldIndex: number, newIndex: number) {
2037
+ if (activeIndex === oldIndex) return newIndex
2038
+ if (oldIndex < activeIndex && newIndex >= activeIndex) return activeIndex - 1
2039
+ if (oldIndex > activeIndex && newIndex <= activeIndex) return activeIndex + 1
2040
+ return activeIndex
2041
+ }
2042
+
2043
+ function reorderStringList(list: string[], oldIndex: number, newIndex: number) {
2044
+ const next = [...list]
2045
+ const [moved] = next.splice(oldIndex, 1)
2046
+ if (moved == null) return list
2047
+ next.splice(newIndex, 0, moved)
2048
+ return next
2049
+ }
2050
+
2051
+ function reorderList<T>(list: T[], oldIndex: number, newIndex: number): T[] {
2052
+ const next = [...list]
2053
+ const [moved] = next.splice(oldIndex, 1)
2054
+ if (moved === undefined) return list
2055
+ next.splice(newIndex, 0, moved)
2056
+ return next
2057
+ }
2058
+
2059
+ function renumberChapterTitles(titles: string[]) {
2060
+ return titles.map((title, index) => {
2061
+ const label = stripChapterNumberPrefix(title)
2062
+ return label ? `${index + 1}. ${label}` : `${index + 1}.`
2063
+ })
2064
+ }
2065
+
2066
+ watch(
2067
+ () => chapterTitles.value.length,
2068
+ (length) => {
2069
+ if (activeChapterIndex.value >= length) {
2070
+ activeChapterIndex.value = Math.max(0, length - 1)
2071
+ }
2072
+ }
2073
+ )
2074
+
2075
+ const activeChapterTitle = computed(() => chapterTitles.value[activeChapterIndex.value] ?? '')
2076
+
2077
+ // new prompt sections
2078
+ /** 無章節時不綁 0,觸發器顯示空白 */
2079
+ const chapterSelectModelValue = computed(() =>
2080
+ chapterTitles.value.length ? activeChapterIndex.value : undefined
2081
+ )
2082
+
2083
+ function onChapterSelectModelUpdate(value: unknown) {
2084
+ if (value == null || value === '') return
2085
+ if (typeof value !== 'number' || Number.isNaN(value)) return
2086
+ if (value < 0 || value >= chapterTitles.value.length) return
2087
+ if (value === activeChapterIndex.value) return
2088
+ // 編輯中禁止切章
2089
+ if (chapterEditMode.value) return
2090
+ // 清空 / 停打字 / 拉版本列表由 watch(activeChapterIndex) 統一處理
2091
+ activeChapterIndex.value = value
2092
+ }
2093
+
2094
+ function onChapterTypewriterComplete() {
2095
+ const body = String(chapterHistoryPreviewBody.value ?? '')
2096
+ if (body.trim()) {
2097
+ patchGeneratedChapterBody(activeChapterIndex.value, body)
2098
+ }
2099
+ stopChapterContentTypewriter()
2100
+ }
2101
+ // new prompt sections end
2102
+
2103
+ const activeChapterIntro = computed(() => {
2104
+ // 切章瞬間:強制空白,避免舊章正文殘留
2105
+ if (chapterBodyCleared.value) return ''
2106
+ // 打字機進行中只認 preview body,避免 '' 被當成空而回落到舊 generated 正文
2107
+ if (chapterTypewriterActive.value) {
2108
+ return String(chapterHistoryPreviewBody.value ?? '')
2109
+ }
2110
+ // 原文交給 editor 解析(Markdown / HTML / tip),此處不剝標籤
2111
+ if (!isReportSettingsMode.value) {
2112
+ if (chapterHistoryPreviewBody.value) return String(chapterHistoryPreviewBody.value)
2113
+ const generated = generatedChapterBodies.value[activeChapterIndex.value]
2114
+ if (generated) return String(generated)
2115
+ return ''
2116
+ }
2117
+
2118
+ if (chapterHistoryPreviewBody.value) return String(chapterHistoryPreviewBody.value)
2119
+
2120
+ const generated = generatedChapterBodies.value[activeChapterIndex.value]
2121
+ if (generated) return String(generated)
2122
+
2123
+ return ''
2124
+ })
2125
+
2126
+ const previewChapterTitle = computed(() => {
2127
+ if (isReportSettingsMode.value && isViewingReportHistory.value) {
2128
+ const record = findFinalizedReportById(historyPreviewRecordId.value!)
2129
+ const chapter = record?.chapters[settingsHistoryPreviewChapterIndex.value]
2130
+ if (!chapter) return '報告預覽'
2131
+ const label = translateChapterLabel(chapter.label, draft.language, 'zh-Hant')
2132
+ return `${settingsHistoryPreviewChapterIndex.value + 1}. ${label}`
2133
+ }
2134
+ return activeChapterTitle.value
2135
+ })
2136
+
2137
+ const previewChapterIntro = computed(() => {
2138
+ if (isReportSettingsMode.value && isViewingReportHistory.value) {
2139
+ const historyBody = historyPreviewChapterBodies.value[settingsHistoryPreviewChapterIndex.value]
2140
+ if (historyBody) return String(historyBody)
2141
+ return '暫無報告歷史預覽內容。'
2142
+ }
2143
+ return activeChapterIntro.value
2144
+ })
2145
+
2146
+ const editorKey = computed(() => {
2147
+ if (isReportSettingsMode.value) {
2148
+ return `settings-${historyPreviewRecordId.value ?? 'draft'}-${historyPreviewApplyToken.value}`
2149
+ }
2150
+ // 不含 chapterHistoryApplyToken:點「生成章節內容」只更新 intro,不整棵重掛打字機面板
2151
+ // 切章仍靠 activeChapterIndex / title / historyKey 換實例
2152
+ return `${activeChapterIndex.value}-${activeChapterTitle.value}-${activeChapterHistoryKey.value}`
2153
+ })
2154
+
2155
+ function destroySelectSortable() {
2156
+ const list = getSelectDropdownListEl()
2157
+ if (list && selectListClickGuard) {
2158
+ list.removeEventListener('click', selectListClickGuard, true)
2159
+ }
2160
+ selectListClickGuard = null
2161
+ selectSortableInstance?.destroy()
2162
+ selectSortableInstance = null
2163
+ selectDragSortIndex.value = null
2164
+ selectDragOverSortIndex.value = null
2165
+ }
2166
+
2167
+ function getSelectDropdownListEl() {
2168
+ return document.querySelector(
2169
+ '.report-chapter-workspace__chapter-select-popper .el-select-dropdown__list'
2170
+ ) as HTMLElement | null
2171
+ }
2172
+
2173
+ function initSelectSortable() {
2174
+ destroySelectSortable()
2175
+ const list = getSelectDropdownListEl()
2176
+ if (!list) return
2177
+
2178
+ selectListClickGuard = (event: Event) => {
2179
+ if (!selectDragMoved) return
2180
+ event.preventDefault()
2181
+ event.stopImmediatePropagation()
2182
+ }
2183
+ list.addEventListener('click', selectListClickGuard, true)
2184
+
2185
+ selectSortableInstance = Sortable.create(list, {
2186
+ animation: 150,
2187
+ draggable: '.el-select-dropdown__item',
2188
+ forceFallback: true,
2189
+ fallbackOnBody: true,
2190
+ fallbackClass: 'report-chapter-workspace__select-sortable-fallback',
2191
+ // Sortable 新版類型已無 distance;用 touchStartThreshold + fallbackTolerance 等效防誤拖
2192
+ fallbackTolerance: 4,
2193
+ touchStartThreshold: 4,
2194
+ ghostClass: 'is-drag-over',
2195
+ chosenClass: 'is-dragging',
2196
+ dragClass: 'is-dragging',
2197
+ onStart({ oldIndex }) {
2198
+ selectDragMoved = true
2199
+ selectDragSortIndex.value = oldIndex ?? null
2200
+ },
2201
+ onMove(evt) {
2202
+ const related = evt.related
2203
+ selectDragOverSortIndex.value = related
2204
+ ? Array.from(list.children).indexOf(related as HTMLElement)
2205
+ : null
2206
+ return true
2207
+ },
2208
+ onEnd({ oldIndex, newIndex }) {
2209
+ selectDragSortIndex.value = null
2210
+ selectDragOverSortIndex.value = null
2211
+
2212
+ const shouldReorder = oldIndex != null && newIndex != null && oldIndex !== newIndex
2213
+
2214
+ if (shouldReorder) {
2215
+ if (genAiSectionItems.value.length) {
2216
+ genAiSectionItems.value = reorderList(genAiSectionItems.value, oldIndex, newIndex).map(
2217
+ (item, i) => ({
2218
+ ...item,
2219
+ orderSeq: i + 1,
2220
+ })
2221
+ )
2222
+ chapterTitles.value = genAiSectionItems.value.map(formatGenAiSectionTitle)
2223
+ } else {
2224
+ chapterTitles.value = renumberChapterTitles(
2225
+ reorderStringList(chapterTitles.value, oldIndex, newIndex)
2226
+ )
2227
+ }
2228
+ activeChapterIndex.value = moveActiveIndexAfterReorder(
2229
+ activeChapterIndex.value,
2230
+ oldIndex,
2231
+ newIndex
2232
+ )
2233
+ showConfirmSortButton.value = true
2234
+ }
2235
+
2236
+ window.setTimeout(() => {
2237
+ selectDragMoved = false
2238
+ }, 0)
2239
+
2240
+ if (shouldReorder) {
2241
+ void nextTick(() => {
2242
+ initSelectSortable()
2243
+ })
2244
+ }
2245
+ },
2246
+ })
2247
+ }
2248
+
2249
+ async function onChapterSelectVisibleChange(visible: boolean) {
2250
+ if (!visible) {
2251
+ destroySelectSortable()
2252
+ selectDragMoved = false
2253
+ return
2254
+ }
2255
+ refreshMainBoundary()
2256
+ await nextTick()
2257
+ requestAnimationFrame(() => {
2258
+ const preferred = CHAPTER_PANEL_MAX_ROWS * CHAPTER_PANEL_ROW_HEIGHT
2259
+ const main = document.querySelector('.report-edit-page__main--anchored') as HTMLElement | null
2260
+ const selectEl = chapterSelectRef.value?.$el as HTMLElement | undefined
2261
+ let available = preferred
2262
+ if (main && selectEl) {
2263
+ const mainRect = main.getBoundingClientRect()
2264
+ const selectRect = selectEl.getBoundingClientRect()
2265
+ const footerReserve = 56
2266
+ const gap = 18
2267
+ const spaceBelow = mainRect.bottom - selectRect.bottom - gap - footerReserve
2268
+ const spaceAbove = selectRect.top - mainRect.top - gap - footerReserve
2269
+ available = Math.max(
2270
+ CHAPTER_PANEL_ROW_HEIGHT * 3,
2271
+ Math.min(preferred, Math.max(spaceBelow, spaceAbove))
2272
+ )
2273
+ }
2274
+ const listMaxHeight = `${Math.floor(available)}px`
2275
+ const wrap = document.querySelector(
2276
+ '.report-chapter-workspace__chapter-select-popper .el-select-dropdown__wrap'
2277
+ ) as HTMLElement | null
2278
+ if (wrap) {
2279
+ wrap.style.setProperty('max-height', listMaxHeight, 'important')
2280
+ wrap.style.setProperty('height', 'auto', 'important')
2281
+ wrap.style.setProperty('overflow-y', 'auto', 'important')
2282
+ wrap.style.setProperty('scrollbar-width', 'none', 'important')
2283
+ }
2284
+ const scrollbar = document.querySelector(
2285
+ '.report-chapter-workspace__chapter-select-popper .el-select-dropdown > .el-scrollbar'
2286
+ ) as HTMLElement | null
2287
+ if (scrollbar) {
2288
+ scrollbar.style.setProperty('max-height', listMaxHeight, 'important')
2289
+ }
2290
+
2291
+ const needsScroll = !!wrap && wrap.scrollHeight > wrap.clientHeight + 1
2292
+ const bar = document.querySelector(
2293
+ '.report-chapter-workspace__chapter-select-popper .el-scrollbar__bar.is-vertical'
2294
+ ) as HTMLElement | null
2295
+ const thumb = bar?.querySelector('.el-scrollbar__thumb') as HTMLElement | null
2296
+ if (bar) {
2297
+ bar.style.setProperty('display', needsScroll ? 'block' : 'none', 'important')
2298
+ bar.style.setProperty('width', '8px', 'important')
2299
+ bar.style.setProperty('opacity', '1', 'important')
2300
+ }
2301
+ if (thumb && needsScroll) {
2302
+ thumb.style.setProperty('width', '8px', 'important')
2303
+ thumb.style.setProperty('height', '50px', 'important')
2304
+ thumb.style.setProperty('min-height', '50px', 'important')
2305
+ thumb.style.setProperty('opacity', '1', 'important')
2306
+ thumb.style.setProperty('background-color', '#999', 'important')
2307
+ }
2308
+ initSelectSortable()
2309
+ })
2310
+ }
2311
+
2312
+ onUnmounted(() => {
2313
+ registerChapterHistoryReloader(null)
2314
+ destroySelectSortable()
2315
+ selectDragMoved = false
2316
+ })
2317
+
2318
+ function getChapterStatus(index: number) {
2319
+ if (index === activeChapterIndex.value && chapterEditMode.value) {
2320
+ return '編輯中'
2321
+ }
2322
+ const apiItem = genAiSectionItems.value[index]
2323
+ if (apiItem?.latestStatus) {
2324
+ return SECTION_STATUS_LABEL[apiItem.latestStatus] ?? apiItem.latestStatus
2325
+ }
2326
+ return genAiSectionItems.value.length ? '未生成' : ''
2327
+ }
2328
+
2329
+ function resetNewChapterForm() {
2330
+ newChapterForm.title = ''
2331
+ newChapterForm.description = ''
2332
+ newChapterForm.prompt = ''
2333
+ newChapterForm.sourceMode = 'all'
2334
+ newChapterForm.selectedSourceIds = []
2335
+ chapterSourcePickerDraft.value = []
2336
+ chapterSourcePickerVisible.value = false
2337
+ }
2338
+
2339
+ // new prompt sections
2340
+ function openAddChapterDialog() {
2341
+ closeChapterSelect()
2342
+ resetNewChapterForm()
2343
+ chapterDialogVisible.value = true
2344
+ }
2345
+ // new prompt sections end
2346
+
2347
+ function closeChapterDialog() {
2348
+ chapterDialogVisible.value = false
2349
+ }
2350
+
2351
+ function handleChapterDialogClosed() {
2352
+ resetNewChapterForm()
2353
+ }
2354
+
2355
+ function handleAddChapterSubmit() {
2356
+ const title = newChapterForm.title.trim()
2357
+ const description = newChapterForm.description.trim()
2358
+ const prompt = newChapterForm.prompt.trim()
2359
+
2360
+ if (!title) {
2361
+ ElMessage.warning('請輸入章節標題')
2362
+ return
2363
+ }
2364
+ if (!description) {
2365
+ ElMessage.warning('請輸入章節描述')
2366
+ return
2367
+ }
2368
+ if (!prompt) {
2369
+ ElMessage.warning('請輸入 Prompt')
2370
+ return
2371
+ }
2372
+ if (newChapterForm.sourceMode === 'partial' && !newChapterForm.selectedSourceIds.length) {
2373
+ ElMessage.warning('請選擇資訊源檔案')
2374
+ return
2375
+ }
2376
+
2377
+ chapterTitles.value = [...chapterTitles.value, title]
2378
+ activeChapterIndex.value = chapterTitles.value.length - 1
2379
+ closeChapterDialog()
2380
+ ElMessage.success('章節已提交並生成內容')
2381
+ }
2382
+
2383
+ function handleGenerateReport() {
2384
+ // if (!sourceFiles.value.length) {
2385
+ // ElMessage.warning('請至少添加一個資訊源檔案')
2386
+ // return
2387
+ // }
2388
+ stopChapterContentTypewriter()
2389
+ // 先跳轉報告頁(settings);進頁後 watch 調 AdjustInstance
2390
+ emit('generate-report')
2391
+ }
2392
+
2393
+ function handleBackToChapterEdit() {
2394
+ resetHistoryPreviewState()
2395
+ emit('back-to-chapter-edit')
2396
+ }
2397
+
2398
+ function handleApplyCreateTemplate() {
2399
+ applyTemplateDetail.value = buildApplyTemplateDetail()
2400
+ resetApplyTemplateForm()
2401
+ applyTemplateDialogVisible.value = true
2402
+ }
2403
+
2404
+ async function handleApplyTemplateSubmit() {
2405
+ if (!applyTemplateTitle.value.trim()) {
2406
+ ElMessage.warning('請輸入模板標題')
2407
+ return
2408
+ }
2409
+ if (!applyTemplateDescription.value.trim()) {
2410
+ ElMessage.warning('請輸入模板描述')
2411
+ return
2412
+ }
2413
+ if (!applyTemplateAction.value) {
2414
+ ElMessage.warning('請選擇處理事項')
2415
+ return
2416
+ }
2417
+ if (applyTemplateAction.value === 'reject' && !applyTemplateOpinion.value.trim()) {
2418
+ ElMessage.warning('拒絕時請填寫意見')
2419
+ return
2420
+ }
2421
+
2422
+ const isReject = applyTemplateAction.value === 'reject'
2423
+ try {
2424
+ await ElMessageBox.confirm(
2425
+ isReject ? '確定拒絕嗎?' : '確定通過嗎?',
2426
+ isReject ? '拒絕' : '通過',
2427
+ {
2428
+ confirmButtonText: '確定',
2429
+ cancelButtonText: '取消',
2430
+ type: 'warning',
2431
+ }
2432
+ )
2433
+ } catch {
2434
+ return
2435
+ }
2436
+
2437
+ applyTemplateDialogVisible.value = false
2438
+ if (isReject) {
2439
+ ElMessage.success('已拒絕該模板申請')
2440
+ return
2441
+ }
2442
+ ElMessage.success('已提交模板申請,待審批')
2443
+ }
2444
+
2445
+ function handleViewCoherenceCheck() {
2446
+ const session = loadCoherenceCheckSession()
2447
+ if (session) {
2448
+ openReportCoherenceCheckResult(router, session)
2449
+ return
2450
+ }
2451
+
2452
+ const recordId = historyPreviewRecordId.value
2453
+ if (recordId) {
2454
+ const record = findFinalizedReportById(recordId)
2455
+ if (record) {
2456
+ openReportCoherenceCheckResult(router, {
2457
+ templateId: selectedTemplate.value?.id ?? record.templateId,
2458
+ reportName: draft.reportName,
2459
+ year: draft.year,
2460
+ chapters: record.chapters,
2461
+ chapterContentLanguage: record.chapterContentLanguage,
2462
+ reportLanguage: record.reportLanguage,
2463
+ })
2464
+ return
2465
+ }
2466
+ }
2467
+
2468
+ ElMessage.warning('請先進行報告連貫性檢查')
2469
+ }
2470
+
2471
+ const wordExportRootRef = ref<HTMLElement | null>(null)
2472
+ const isExportingWord = ref(false)
2473
+
2474
+ const instanceId = ref('')
2475
+ const editors = ref<any[]>([])
2476
+ const editorList = ref<any[]>([])
2477
+ /** 以接口 res.data 為準:有 data 才視為可用來源 */
2478
+ const editorsHasData = ref(false)
2479
+ const editorListHasData = ref(false)
2480
+
2481
+ const userGenAiBuildReport = genAiBuildReportStore
2482
+
2483
+ const init = async () => {
2484
+ await userGenAiBuildReport.initFromStorage()
2485
+ const instanceIdRoute: any = route.query.instanceId
2486
+ instanceId.value = route.query.instanceId
2487
+ ? instanceIdRoute
2488
+ : (userGenAiBuildReport.instanceId as any) || ''
2489
+ if (instanceId.value) {
2490
+ userGenAiBuildReport.setInstanceId(String(instanceId.value))
2491
+ }
2492
+ getReportRoles({
2493
+ categoryId: 1,
2494
+ instanceId: route.query.instanceId ? instanceIdRoute : userGenAiBuildReport.instanceId,
2495
+ })
2496
+ getEditorList()
2497
+ }
2498
+
2499
+ // report所有编辑者
2500
+ const getReportRoles = async (params: any) => {
2501
+ const res: any = await genAiReportPermissionApi.fetchGenAiReportPermissionList(params)
2502
+ try {
2503
+ if (res.data) {
2504
+ console.log('res.data + xx :', res.data)
2505
+
2506
+ const realSortCreator = res.data.find((item: any) => item.userRole === 'CREATOR')
2507
+ const realSortEditor = res.data.filter((item: any) => item.userRole !== 'CREATOR')
2508
+ editors.value = realSortCreator ? [realSortCreator, ...realSortEditor] : realSortEditor
2509
+ editorsHasData.value = true
2510
+ } else {
2511
+ editors.value = []
2512
+ editorsHasData.value = false
2513
+ }
2514
+ } catch {
2515
+ editorsHasData.value = false
2516
+ }
2517
+ }
2518
+
2519
+ const getEditorList = async () => {
2520
+ const res: any = await genAiReportPermissionApi.fetchGenAiReportPermissionEditorList({})
2521
+ try {
2522
+ if (res.data) {
2523
+ editorList.value = res.data
2524
+ editorListHasData.value = true
2525
+ } else {
2526
+ editorList.value = []
2527
+ editorListHasData.value = false
2528
+ }
2529
+ } catch {
2530
+ editorListHasData.value = false
2531
+ }
2532
+ }
2533
+
2534
+ /** 兩條導出共用:始終取權威正文(非打字機半成品 / 非流式佔位) */
2535
+ function getWordExportChapters() {
2536
+ if (isReportSettingsMode.value) {
2537
+ // 用 settingsTypewriterChapters(AdjustInstance / 歷史回寫後的完整 Markdown)
2538
+ // 禁止 settingsDocChapters:流式生成時會變成單條 stream stub
2539
+ return settingsTypewriterChapters.value.map((chapter) => ({
2540
+ title: chapter.title,
2541
+ content: chapter.intro,
2542
+ }))
2543
+ }
2544
+
2545
+ // Prompt 模式:當前章取最新(打字機目標正文 / 預覽 / 已生成緩存)
2546
+ // 打字機進行動畫時 preview 已是完整目標串,不是 DOM 上已打出的片段
2547
+ return [
2548
+ {
2549
+ title: previewChapterTitle.value,
2550
+ content: previewChapterIntro.value,
2551
+ },
2552
+ ]
2553
+ }
2554
+
2555
+ /** 導出前等 Vue 刷完(歷史切換 / 章節勾選回寫後的 computed) */
2556
+ async function settleWordExportSource() {
2557
+ await nextTick()
2558
+ await nextTick()
2559
+ }
2560
+
2561
+ /** 按 res.data 可用性合併 getReportRoles / getEditorList,映射為導出 members */
2562
+ function mapEditorListToExportMembers() {
2563
+ const rows: any[] = []
2564
+ const seen = new Set<string>()
2565
+
2566
+ const pushAll = (list: any[]) => {
2567
+ for (const item of list || []) {
2568
+ const department = String(item?.userUnitSeqDesc ?? '').trim()
2569
+ const name = String(item?.userName ?? '').trim()
2570
+ if (!department && !name) continue
2571
+ const key = String(item?.userId ?? `${department}\0${name}`).trim()
2572
+ if (!key || seen.has(key)) continue
2573
+ seen.add(key)
2574
+ rows.push({ department, name })
2575
+ }
2576
+ }
2577
+
2578
+ // 以 res.data 為判斷:有 data 的來源才納入;兩者都有則合併(roles 在前,保 CREATOR 序)
2579
+ if (editorsHasData.value) pushAll(editors.value)
2580
+ if (editorListHasData.value) pushAll(editorList.value)
2581
+
2582
+ return rows
2583
+ }
2584
+
2585
+ /** 報告配置「名稱」→ 導出封面 / 檔名同源 */
2586
+ function getReportConfigNameForExport() {
2587
+ return (draft.reportName || '').trim() || '行業調研報告'
2588
+ }
2589
+
2590
+ /** 對齊行業調研導出模板:封面 / 目錄 / 小組成員 / 參考文獻 */
2591
+ function getWordExportReportMeta() {
2592
+ const chapters = getWordExportChapters()
2593
+
2594
+ return {
2595
+ reportName: getReportConfigNameForExport(),
2596
+ year: String(draft.year || '').trim(),
2597
+ members: mapEditorListToExportMembers(),
2598
+ sourceFileNames: sourceFiles.value
2599
+ .filter((file) => file.selected || sourceFiles.value.every((f) => !f.selected))
2600
+ .map((file) => file.name)
2601
+ .filter(Boolean),
2602
+ tocTitles: chapters.map((chapter) => String(chapter.title ?? '').trim()).filter(Boolean),
2603
+ }
2604
+ }
2605
+
2606
+ function assertWordExportChaptersReady() {
2607
+ if (isReportSettingsMode.value && !settingsPreviewChapters.value.length) {
2608
+ ElMessage.warning('請先勾選至少一個章節')
2609
+ return null
2610
+ }
2611
+ const chapters = getWordExportChapters()
2612
+ const hasContent = chapters.some(
2613
+ (chapter) => String(chapter.title ?? '').trim() || String(chapter.content ?? '').trim()
2614
+ )
2615
+ if (!hasContent) {
2616
+ ElMessage.warning('暫無可導出內容')
2617
+ return null
2618
+ }
2619
+ return chapters
2620
+ }
2621
+
2622
+ /**
2623
+ * 兩條導出按鈕共用:權威章節 Markdown → AIExport 打包。
2624
+ * 不走 DOM/altChunk 半成品路徑,避免遷移環境出現 --------tDocumentpart-- 殘片。
2625
+ */
2626
+ async function runWordExport(filename: string) {
2627
+ if (isExportingWord.value) return
2628
+ await settleWordExportSource()
2629
+ const chapters = assertWordExportChaptersReady()
2630
+ if (!chapters) return
2631
+
2632
+ try {
2633
+ isExportingWord.value = true
2634
+ await nextTick()
2635
+ await exportMarkdownToDocxWord(chapters, {
2636
+ filename,
2637
+ headerText: DOCX_HEADER_CLASSIFICATION,
2638
+ showPageNumbers: true,
2639
+ reportMeta: getWordExportReportMeta(),
2640
+ })
2641
+ } catch (error) {
2642
+ console.error('Word 導出失敗:', error)
2643
+ ElMessage.error(
2644
+ error instanceof Error && error.message.trim()
2645
+ ? error.message
2646
+ : 'Word 導出失敗,請查看瀏覽器控制檯'
2647
+ )
2648
+ } finally {
2649
+ isExportingWord.value = false
2650
+ }
2651
+ }
2652
+
2653
+ /** 版本號統一為 Vn(對齊生成報告設置 / existsInstanceVersionNo) */
2654
+ function normalizeExportVersionNo(raw: unknown): string {
2655
+ if (raw == null || raw === '') return 'V1'
2656
+ let text = String(raw).trim()
2657
+ if (!text) return 'V1'
2658
+ text = text.replace(/^報告版本\s*/i, '').trim()
2659
+ text = text.toUpperCase()
2660
+ if (!text) return 'V1'
2661
+ return text.startsWith('V') ? text : `V${text}`
2662
+ }
2663
+
2664
+ function sanitizeExportFilenamePart(value: string) {
2665
+ return value.replace(/[\\/:*?"<>|]/g, '_').replace(/\s+/g, ' ').trim()
2666
+ }
2667
+
2668
+ /**
2669
+ * 檔名:[GenAI草稿] [報告名稱] [版本號] [語言] ([年份]年).docx
2670
+ * 報告名稱 / 語言 / 年份 ← 報告配置;版本號 ← 生成報告設置 existsInstanceVersionNo
2671
+ */
2672
+ function buildWordExportFilename() {
2673
+ const name = sanitizeExportFilenamePart(getReportConfigNameForExport()) || '行業調研報告'
2674
+ const version = normalizeExportVersionNo(existsInstanceVersionNo.value)
2675
+ const language = sanitizeExportFilenamePart(
2676
+ getReportLanguageLabel(String(draft.language || 'zh-Hant'))
2677
+ )
2678
+ const year =
2679
+ String(draft.year || '').trim() || String(new Date().getFullYear())
2680
+ return `[GenAI草稿] ${name} ${version} ${language} (${year}年).docx`
2681
+ }
2682
+
2683
+ async function handleExportWord1() {
2684
+ await runWordExport(buildWordExportFilename())
2685
+ }
2686
+
2687
+ async function handleExportWord2() {
2688
+ await runWordExport(buildWordExportFilename())
2689
+ }
2690
+
2691
+ async function handleCancelEdit() {
2692
+ if (!chapterEditMode.value) return
2693
+
2694
+ if (!isChapterEditDirty()) {
2695
+ exitChapterEditMode(true)
2696
+ return
2697
+ }
2698
+
2699
+ try {
2700
+ await ElMessageBox.confirm('您已修改部分內容且未儲存,確定要進行「取消」的操作嗎?', '提示', {
2701
+ confirmButtonText: '確定',
2702
+ cancelButtonText: '取消',
2703
+ type: 'warning',
2704
+ })
2705
+ exitChapterEditMode(true)
2706
+ } catch {
2707
+ // 用戶取消操作,保持編輯狀態
2708
+ }
2709
+ }
2710
+
2711
+ async function handleEditSave() {
2712
+ if (!chapterEditMode.value) {
2713
+ // 點「編輯」:嘗試 changeSectionStatus;失敗不阻斷進入編輯
2714
+ void changeGenAiSectionStatus()
2715
+ enterChapterEditMode()
2716
+ return
2717
+ }
2718
+ // 點「儲存」:draftFlag=N
2719
+ const result = await saveGenAiSectionVersion('N')
2720
+ if (!result.ok) return
2721
+ commitChapterEdit()
2722
+ ElMessage.success('章節內容已儲存')
2723
+ // 刷新章節歷史(失敗則保持當前,不阻斷)
2724
+ void loadGenAiSectionVersionListForActiveChapter()
2725
+ }
2726
+
2727
+ /** 編輯態「存為草稿」:draftFlag=Y 調 saveSectionVersion */
2728
+ async function handleSaveDraft() {
2729
+ const result = await saveGenAiSectionVersion('Y')
2730
+ if (!result.ok) return
2731
+ saveDraftToSession()
2732
+ ElMessage.success('已存為草稿')
2733
+ void loadGenAiSectionVersionListForActiveChapter()
2734
+ }
2735
+ </script>
2736
+
2737
+ <style scoped>
2738
+ .report-chapter-workspace {
2739
+ --report-accent: #c53355;
2740
+ --report-accent-soft: #fdf0f2;
2741
+ --report-card-bg: #f5f5f5;
2742
+ --report-table-row-hover-bg: #f5f7fa;
2743
+ position: relative;
2744
+ display: flex;
2745
+ flex-direction: column;
2746
+ gap: 0;
2747
+ min-width: 0;
2748
+ flex: 1 0 auto;
2749
+ min-height: 0;
2750
+ height: auto;
2751
+ }
2752
+
2753
+ .report-chapter-workspace :deep(.el-loading-spinner .circular) {
2754
+ width: 56px;
2755
+ height: 56px;
2756
+ }
2757
+
2758
+ .report-chapter-workspace :deep(.el-loading-spinner .el-loading-text) {
2759
+ margin-top: 12px;
2760
+ font-family: var(--gen-ai-font-family, 'Noto Sans', 'Noto Sans TC', sans-serif);
2761
+ font-size: 16px;
2762
+ font-weight: 500;
2763
+ color: var(--report-accent);
2764
+ }
2765
+
2766
+ .report-chapter-workspace__toolbar {
2767
+ flex-shrink: 0;
2768
+ display: flex;
2769
+ align-items: flex-start;
2770
+ justify-content: space-between;
2771
+ gap: 12px;
2772
+ box-sizing: border-box;
2773
+ /* 隨中欄滾動時吸頂,工具列本身不滾走 */
2774
+ position: sticky;
2775
+ top: 0;
2776
+ z-index: 20;
2777
+ margin: 0;
2778
+ /* 下邊距與中欄 card 上邊距(16px)一致 */
2779
+ padding-bottom: 16px;
2780
+ background: #fff;
2781
+ }
2782
+
2783
+ .report-chapter-workspace__editor-shell {
2784
+ display: flex;
2785
+ flex-direction: column;
2786
+ gap: 14px;
2787
+ min-height: 280px;
2788
+ padding-top: 8px;
2789
+ }
2790
+
2791
+ .report-chapter-workspace__editor-shell-title {
2792
+ width: min(420px, 70%);
2793
+ height: 28px;
2794
+ border-radius: 4px;
2795
+ background: linear-gradient(90deg, #f2f3f5 25%, #e9ebef 37%, #f2f3f5 63%);
2796
+ background-size: 400% 100%;
2797
+ animation: report-chapter-editor-shell-shimmer 1.2s ease-in-out infinite;
2798
+ }
2799
+
2800
+ .report-chapter-workspace__editor-shell-line {
2801
+ width: 100%;
2802
+ height: 14px;
2803
+ border-radius: 4px;
2804
+ background: linear-gradient(90deg, #f5f6f8 25%, #eceef2 37%, #f5f6f8 63%);
2805
+ background-size: 400% 100%;
2806
+ animation: report-chapter-editor-shell-shimmer 1.2s ease-in-out infinite;
2807
+ }
2808
+
2809
+ .report-chapter-workspace__editor-shell-line.is-short {
2810
+ width: 55%;
2811
+ }
2812
+
2813
+ @keyframes report-chapter-editor-shell-shimmer {
2814
+ 0% {
2815
+ background-position: 100% 0;
2816
+ }
2817
+
2818
+ 100% {
2819
+ background-position: 0 0;
2820
+ }
2821
+ }
2822
+
2823
+ .report-chapter-workspace__chapter-box {
2824
+ position: relative;
2825
+ min-width: 0;
2826
+ }
2827
+
2828
+ .report-chapter-workspace__chapter-select {
2829
+ width: 220px;
2830
+ min-width: 180px;
2831
+ }
2832
+
2833
+ .report-chapter-workspace__chapter-select :deep(.el-select__wrappe r) {
2834
+ min-height: 39px;
2835
+ padding: 0 12px;
2836
+ border-radius: 4px;
2837
+ box-shadow: 0 0 0 1px #dcdfe6 inset;
2838
+ transition: box-shadow 0.2s ease;
2839
+ }
2840
+
2841
+ .report-chapter-workspace__chapter-select :deep(.el-select__wrapper:hover:not(.is-disabled)),
2842
+ .report-chapter-workspace__chapter-select :deep(.el-select__wrapper.is-focused:not(.is-disabled)) {
2843
+ box-shadow: 0 0 0 1px var(--report-accent) inset;
2844
+ }
2845
+
2846
+ .report-chapter-workspace__chapter-select :deep(.el-select__wrapper.is-disabled) {
2847
+ cursor: not-allowed;
2848
+ background-color: #f5f5f5;
2849
+ box-shadow: 0 0 0 1px #dcdfe6 inset;
2850
+ }
2851
+
2852
+ .report-chapter-workspace__chapter-select
2853
+ :deep(.el-select__wrapper.is-disabled .el-select__selected-item) {
2854
+ color: #a8abb2;
2855
+ }
2856
+
2857
+ .report-chapter-workspace__chapter-select :deep(.el-select__selected-item) {
2858
+ font-size: 15px;
2859
+ font-weight: 400;
2860
+ font-family: var(--gen-ai-font-family, 'Noto Sans', 'Noto Sans TC', sans-serif);
2861
+ color: #303133;
2862
+ }
2863
+
2864
+ .report-chapter-workspace__chapter-select
2865
+ :deep(.el-select__wrapper:hover .el-select__selected-item),
2866
+ .report-chapter-workspace__chapter-select
2867
+ :deep(.el-select__wrapper.is-focused .el-select__selected-item) {
2868
+ color: var(--report-accent);
2869
+ }
2870
+
2871
+ .report-chapter-workspace__chapter-select :deep(.el-select__caret) {
2872
+ color: #dcdfe6;
2873
+ font-size: 15px;
2874
+ transition: color 0.2s ease;
2875
+ }
2876
+
2877
+ .report-chapter-workspace__chapter-select :deep(.el-select__caret svg) {
2878
+ color: inherit;
2879
+ fill: currentColor;
2880
+ }
2881
+
2882
+ .report-chapter-workspace__chapter-select :deep(.el-select__wrapper:hover .el-select__caret),
2883
+ .report-chapter-workspace__chapter-select :deep(.el-select__wrapper.is-focused .el-select__caret) {
2884
+ color: var(--report-accent);
2885
+ }
2886
+
2887
+ .report-chapter-workspace__chapter-option {
2888
+ display: flex;
2889
+ align-items: center;
2890
+ justify-content: space-between;
2891
+ gap: 8px;
2892
+ width: 100%;
2893
+ min-width: 0;
2894
+ font-size: 15px;
2895
+ font-weight: 400;
2896
+ font-family: var(--gen-ai-font-family, 'Noto Sans', 'Noto Sans TC', sans-serif);
2897
+ }
2898
+
2899
+ .report-chapter-workspace__chapter-option .report-chapter-workspace__sort-handle {
2900
+ margin-top: 0;
2901
+ }
2902
+
2903
+ .report-chapter-workspace__chapter-option-title {
2904
+ flex: 1;
2905
+ min-width: 0;
2906
+ overflow: hidden;
2907
+ text-overflow: ellipsis;
2908
+ white-space: nowrap;
2909
+ font-size: 15px;
2910
+ font-weight: 400;
2911
+ font-family: var(--gen-ai-font-family, 'Noto Sans', 'Noto Sans TC', sans-serif);
2912
+ color: #303133;
2913
+ }
2914
+
2915
+ .report-chapter-workspace__chapter-option-title.is-active {
2916
+ color: var(--report-accent);
2917
+ font-weight: 400;
2918
+ }
2919
+
2920
+ .report-chapter-workspace__chapter-option-status {
2921
+ flex-shrink: 0;
2922
+ font-size: 15px;
2923
+ font-weight: 400;
2924
+ font-family: var(--gen-ai-font-family, 'Noto Sans', 'Noto Sans TC', sans-serif);
2925
+ color: #606266;
2926
+ white-space: nowrap;
2927
+ }
2928
+
2929
+ /* // new prompt sections — toolbar 側 footer 佈局(彈層內按鈕樣式見下方同標記區塊) */
2930
+ .report-chapter-workspace__chapter-select-footer {
2931
+ display: flex;
2932
+ align-items: center;
2933
+ justify-content: center;
2934
+ gap: 12px;
2935
+ padding: 10px 12px;
2936
+ }
2937
+
2938
+ .report-chapter-workspace__chapter-select-footer-btn {
2939
+ min-width: 120px;
2940
+ }
2941
+ /* // new prompt sections end */
2942
+
2943
+ .report-chapter-workspace__toolbar-right {
2944
+ display: flex;
2945
+ align-items: center;
2946
+ gap: 12px;
2947
+ flex-shrink: 0;
2948
+ }
2949
+
2950
+ /* 覆蓋 Element Plus .el-button+.el-button 默認 margin-left:12px,避免與 gap 疊加;按鈕間距統一為 12px */
2951
+ .report-chapter-workspace__toolbar-right :deep(.el-button) {
2952
+ margin-left: 0 !important;
2953
+ margin-right: 0 !important;
2954
+ }
2955
+
2956
+ /* 取消 / 存為草稿 / 儲存 / 編輯/儲存 / 生成報告 */
2957
+ .report-chapter-workspace__action-btn {
2958
+ width: 100px;
2959
+ min-width: 100px;
2960
+ max-width: 100px;
2961
+ padding: 0 8px;
2962
+ font-size: 16px;
2963
+ font-weight: 500;
2964
+ font-family: var(--gen-ai-font-family, 'Noto Sans', 'Noto Sans TC', sans-serif);
2965
+ }
2966
+
2967
+ @media (max-width: 1024px) {
2968
+ .report-chapter-workspace__toolbar {
2969
+ flex-wrap: wrap;
2970
+ align-items: stretch;
2971
+ gap: 10px;
2972
+ }
2973
+
2974
+ /* 章節 select:隨容器等比例縮小 */
2975
+ .report-chapter-workspace__chapter-box {
2976
+ flex: 1 1 auto;
2977
+ min-width: 0;
2978
+ max-width: 100%;
2979
+ width: 100%;
2980
+ }
2981
+
2982
+ .report-chapter-workspace__chapter-select {
2983
+ width: 100%;
2984
+ min-width: 0;
2985
+ max-width: 100%;
2986
+ }
2987
+
2988
+ /* 操作按鈕整組換行 */
2989
+ .report-chapter-workspace__toolbar-right {
2990
+ flex: 1 1 100%;
2991
+ width: 100%;
2992
+ flex-wrap: wrap;
2993
+ justify-content: flex-start;
2994
+ }
2995
+
2996
+ .report-chapter-workspace__action-btn {
2997
+ width: 100px;
2998
+ min-width: 0;
2999
+ max-width: 100px;
3000
+ flex: 0 1 auto;
3001
+ }
3002
+ }
3003
+
3004
+ .report-chapter-workspace__sort-handle {
3005
+ display: inline-flex;
3006
+ align-items: center;
3007
+ justify-content: center;
3008
+ flex-shrink: 0;
3009
+ width: 16px;
3010
+ height: 16px;
3011
+ margin-top: 3px;
3012
+ color: #86909c;
3013
+ cursor: grab;
3014
+ }
3015
+
3016
+ .report-chapter-workspace__sort-handle:active {
3017
+ cursor: grabbing;
3018
+ }
3019
+
3020
+ .report-chapter-workspace__editor {
3021
+ flex: 1 0 auto;
3022
+ min-height: 420px;
3023
+ border-radius: 8px;
3024
+ overflow: visible;
3025
+ display: flex;
3026
+ flex-direction: column;
3027
+ background: #fff;
3028
+ }
3029
+
3030
+ .report-chapter-workspace__settings-doc {
3031
+ flex: 1 0 auto;
3032
+ min-height: 0;
3033
+ overflow: visible;
3034
+ display: flex;
3035
+ flex-direction: column;
3036
+ background: #fff;
3037
+ }
3038
+
3039
+ .report-chapter-workspace__settings-doc
3040
+ :deep(.ProseMirror > *:first-child:not(h1):not(h2):not(h3):not(h4):not(h5):not(h6)),
3041
+ .report-chapter-workspace__editor
3042
+ :deep(.ProseMirror > *:first-child:not(h1):not(h2):not(h3):not(h4):not(h5):not(h6)) {
3043
+ margin-top: 0 !important;
3044
+ }
3045
+
3046
+ .report-chapter-workspace__settings-doc :deep(.ProseMirror h2),
3047
+ .report-chapter-workspace__editor :deep(.ProseMirror h2) {
3048
+ margin: 24px 0 !important;
3049
+ }
3050
+
3051
+ .gen-ai-action-btn,
3052
+ .template-center-toolbar-btn {
3053
+ font-size: 15px;
3054
+ font-family: var(--gen-ai-font-family, 'Noto Sans', 'Noto Sans TC', sans-serif);
3055
+ font-weight: 500;
3056
+ }
3057
+
3058
+ .template-center-toolbar-btn {
3059
+ display: inline-flex;
3060
+ align-items: center;
3061
+ justify-content: center;
3062
+ gap: 6px;
3063
+ box-sizing: border-box;
3064
+ height: 32px;
3065
+ min-width: 120px;
3066
+ padding: 0 14px;
3067
+ border-radius: 4px;
3068
+ line-height: 1;
3069
+ white-space: nowrap;
3070
+ cursor: pointer;
3071
+ transition: all 0.2s ease;
3072
+ }
3073
+
3074
+ .template-center-toolbar-btn--wide {
3075
+ min-width: 160px;
3076
+ }
3077
+
3078
+ .template-center-toolbar-btn:disabled {
3079
+ opacity: 0.45;
3080
+ cursor: not-allowed;
3081
+ }
3082
+
3083
+ .template-center-toolbar-btn--primary {
3084
+ border: 1px solid var(--report-accent);
3085
+ background: var(--report-accent);
3086
+ color: #fff;
3087
+ }
3088
+
3089
+ .template-center-toolbar-btn--primary:hover:not(:disabled) {
3090
+ background: #fff;
3091
+ border-color: var(--report-accent);
3092
+ color: var(--report-accent);
3093
+ }
3094
+
3095
+ .template-center-toolbar-btn--outline {
3096
+ border: 1px solid var(--report-accent);
3097
+ color: var(--report-accent);
3098
+ background: #fff;
3099
+ }
3100
+
3101
+ .template-center-toolbar-btn--outline:hover:not(:disabled) {
3102
+ border-color: var(--report-accent);
3103
+ color: var(--report-accent);
3104
+ background: var(--report-accent-soft);
3105
+ }
3106
+
3107
+ .chatbot-report-btn-outline.el-button,
3108
+ .chatbot-report-btn-primary.el-button {
3109
+ height: 32px !important;
3110
+ padding-inline: 14px !important;
3111
+ border-radius: 4px !important;
3112
+ font-size: 16px !important;
3113
+ font-family: var(--gen-ai-font-family, 'Noto Sans', 'Noto Sans TC', sans-serif) !important;
3114
+ font-weight: 500 !important;
3115
+ }
3116
+
3117
+ .chatbot-report-btn-outline.el-button {
3118
+ border: 1px solid var(--report-accent) !important;
3119
+ color: var(--report-accent) !important;
3120
+ background: #fff !important;
3121
+ }
3122
+
3123
+ .chatbot-report-btn-outline.el-button:hover {
3124
+ background: var(--report-accent-soft) !important;
3125
+ color: var(--report-accent) !important;
3126
+ border-color: var(--report-accent) !important;
3127
+ }
3128
+
3129
+ .chatbot-report-btn-outline--wide.el-button {
3130
+ min-width: 0;
3131
+ max-width: 220px;
3132
+ white-space: normal;
3133
+ line-height: 1.3;
3134
+ height: auto !important;
3135
+ min-height: 32px;
3136
+ padding-block: 6px !important;
3137
+ }
3138
+
3139
+ .chatbot-report-btn-primary.el-button {
3140
+ border: 1px solid var(--report-accent) !important;
3141
+ background: var(--report-accent) !important;
3142
+ color: #fff !important;
3143
+ }
3144
+
3145
+ .chatbot-report-btn-primary.el-button:hover {
3146
+ background: #fff !important;
3147
+ color: var(--report-accent) !important;
3148
+ border-color: var(--report-accent) !important;
3149
+ }
3150
+
3151
+ .chatbot-report-btn-primary--wide.el-button {
3152
+ min-width: 0;
3153
+ max-width: 220px;
3154
+ white-space: normal;
3155
+ line-height: 1.3;
3156
+ height: auto !important;
3157
+ min-height: 32px;
3158
+ padding-block: 6px !important;
3159
+ }
3160
+ </style>
3161
+
3162
+ <style>
3163
+ /* 新增章節彈窗:尺寸 / 位置由 reportEdit 頁 CSS 變量對齊中間卡片 */
3164
+ .report-chapter-dialog.report-workflow-dialog.el-dialog {
3165
+ --el-dialog-padding-primary: 0;
3166
+ --report-accent: #c53355;
3167
+ --report-accent-soft: #fdf0f2;
3168
+ border-radius: 4px;
3169
+ overflow: hidden;
3170
+ display: flex;
3171
+ flex-direction: column;
3172
+ }
3173
+
3174
+ .report-workflow-dialog .el-dialog__header,
3175
+ .report-workflow-dialog__header {
3176
+ display: flex;
3177
+ align-items: center;
3178
+ justify-content: space-between;
3179
+ gap: 16px;
3180
+ margin: 0;
3181
+ padding: 14px 16px 14px 20px;
3182
+ /* 標題欄樣式見 fonts.css(全局 dialog) */
3183
+ background: transparent;
3184
+ border-bottom: 1px solid #ebeef5;
3185
+ flex-shrink: 0;
3186
+ }
3187
+
3188
+ /* 1. 新增章節:去掉標題欄下劃線 */
3189
+ .report-chapter-dialog .el-dialog__header,
3190
+ .report-chapter-dialog .report-workflow-dialog__header {
3191
+ border-bottom: none !important;
3192
+ }
3193
+
3194
+ .report-workflow-dialog__title {
3195
+ flex: 1;
3196
+ min-width: 0;
3197
+ color: #303133;
3198
+ font-size: 18px;
3199
+ font-weight: 600;
3200
+ font-family: var(--gen-ai-font-family, 'Noto Sans', 'Noto Sans TC', sans-serif);
3201
+ line-height: 1.4;
3202
+ }
3203
+
3204
+ .report-workflow-dialog__close {
3205
+ flex-shrink: 0;
3206
+ width: 32px;
3207
+ height: 32px;
3208
+ margin: 0;
3209
+ padding: 0;
3210
+ border: none;
3211
+ background: transparent;
3212
+ color: #303133;
3213
+ font-size: 18px;
3214
+ font-weight: 400;
3215
+ line-height: 1;
3216
+ cursor: pointer;
3217
+ }
3218
+
3219
+ .report-workflow-dialog__close:hover {
3220
+ color: #606266;
3221
+ }
3222
+
3223
+ .report-chapter-dialog .template-center-toolbar-btn {
3224
+ display: inline-flex;
3225
+ align-items: center;
3226
+ justify-content: center;
3227
+ gap: 6px;
3228
+ box-sizing: border-box;
3229
+ height: 32px;
3230
+ min-width: 120px;
3231
+ padding: 0 14px;
3232
+ border-radius: 4px;
3233
+ line-height: 1;
3234
+ white-space: nowrap;
3235
+ cursor: pointer;
3236
+ font-size: 15px;
3237
+ font-family: var(--gen-ai-font-family, 'Noto Sans', 'Noto Sans TC', sans-serif);
3238
+ font-weight: 400;
3239
+ transition: all 0.2s ease;
3240
+ }
3241
+
3242
+ .report-chapter-dialog .template-center-toolbar-btn--primary {
3243
+ border: 1px solid var(--report-accent);
3244
+ background: var(--report-accent);
3245
+ color: #fff;
3246
+ }
3247
+
3248
+ .report-chapter-dialog .template-center-toolbar-btn--primary:hover:not(:disabled) {
3249
+ background: #fff;
3250
+ border-color: var(--report-accent);
3251
+ color: var(--report-accent);
3252
+ }
3253
+
3254
+ .report-chapter-dialog .template-center-toolbar-btn--outline {
3255
+ border: 1px solid var(--report-accent);
3256
+ color: var(--report-accent);
3257
+ background: #fff;
3258
+ }
3259
+
3260
+ .report-chapter-dialog .template-center-toolbar-btn--outline:hover:not(:disabled) {
3261
+ border-color: var(--report-accent);
3262
+ color: var(--report-accent);
3263
+ background: var(--report-accent-soft);
3264
+ }
3265
+
3266
+ .report-chapter-dialog .el-dialog__body {
3267
+ flex: 1 1 auto;
3268
+ min-height: 0;
3269
+ /* 3. 加大內容區,給 Prompt 字數統計留空 */
3270
+ padding: 24px 20px 32px;
3271
+ background: #fff;
3272
+ overflow-y: auto;
3273
+ }
3274
+
3275
+ .report-chapter-dialog .el-dialog__footer {
3276
+ /* 4. 原下邊距 16px × 1.5 = 24px */
3277
+ padding: 12px 16px 24px;
3278
+ background: #fff;
3279
+ display: flex;
3280
+ justify-content: center;
3281
+ flex-shrink: 0;
3282
+ }
3283
+
3284
+ .report-chapter-dialog__form {
3285
+ display: flex;
3286
+ flex-direction: column;
3287
+ gap: 18px;
3288
+ }
3289
+
3290
+ .report-chapter-dialog__field {
3291
+ margin: 0;
3292
+ }
3293
+
3294
+ .report-chapter-dialog__field--row {
3295
+ display: grid;
3296
+ grid-template-columns: 100px minmax(0, 1fr);
3297
+ gap: 16px;
3298
+ align-items: start;
3299
+ }
3300
+
3301
+ .report-chapter-dialog__field--textarea {
3302
+ align-items: start;
3303
+ }
3304
+
3305
+ .report-chapter-dialog__label {
3306
+ padding-top: 8px;
3307
+ font-size: 15px;
3308
+ font-weight: 500;
3309
+ font-family: var(--gen-ai-font-family, 'Noto Sans', 'Noto Sans TC', sans-serif);
3310
+ color: #303133;
3311
+ line-height: 1.4;
3312
+ white-space: nowrap;
3313
+ }
3314
+
3315
+ .report-chapter-dialog__required {
3316
+ color: #c53355;
3317
+ }
3318
+
3319
+ .report-chapter-dialog__control {
3320
+ min-width: 0;
3321
+ }
3322
+
3323
+ .report-chapter-dialog__input :deep(.el-input__wrapper),
3324
+ .report-chapter-dialog__textarea :deep(.el-textarea__inner) {
3325
+ border-radius: 2px;
3326
+ }
3327
+
3328
+ .report-chapter-dialog__textarea :deep(.el-textarea__inner) {
3329
+ min-height: 250px;
3330
+ }
3331
+
3332
+ .report-chapter-dialog__footer {
3333
+ display: flex;
3334
+ align-items: center;
3335
+ justify-content: center;
3336
+ gap: 16px;
3337
+ width: 100%;
3338
+ }
3339
+
3340
+ .report-chapter-dialog__field--source {
3341
+ align-items: flex-start;
3342
+ }
3343
+
3344
+ .report-chapter-dialog__source-control {
3345
+ min-height: 32px;
3346
+ }
3347
+
3348
+ .report-chapter-dialog__source-radios {
3349
+ display: flex;
3350
+ flex-direction: column;
3351
+ align-items: flex-start;
3352
+ gap: 10px;
3353
+ }
3354
+
3355
+ .report-chapter-dialog__source-partial-row {
3356
+ display: flex;
3357
+ flex-wrap: wrap;
3358
+ align-items: center;
3359
+ gap: 12px;
3360
+ }
3361
+
3362
+ .report-chapter-dialog__source-radios :deep(.el-radio) {
3363
+ margin-right: 0;
3364
+ height: auto;
3365
+ font-size: 15px;
3366
+ font-weight: 400;
3367
+ font-family: var(--gen-ai-font-family, 'Noto Sans', 'Noto Sans TC', sans-serif);
3368
+ color: var(--report-accent, #c53355);
3369
+ }
3370
+
3371
+ .report-chapter-dialog__source-radios :deep(.el-radio__label) {
3372
+ font-size: 15px;
3373
+ font-weight: 400;
3374
+ font-family: var(--gen-ai-font-family, 'Noto Sans', 'Noto Sans TC', sans-serif);
3375
+ color: var(--report-accent, #c53355) !important;
3376
+ padding-left: 8px;
3377
+ }
3378
+
3379
+ .report-chapter-dialog__source-link {
3380
+ font-size: 15px;
3381
+ font-weight: 500;
3382
+ font-family: var(--gen-ai-font-family, 'Noto Sans', 'Noto Sans TC', sans-serif);
3383
+ color: var(--report-accent, #c53355);
3384
+ text-decoration: underline;
3385
+ cursor: pointer;
3386
+ line-height: 1.4;
3387
+ }
3388
+
3389
+ .report-chapter-dialog__source-link:hover {
3390
+ color: #a82845;
3391
+ }
3392
+
3393
+ .report-chapter-dialog__source-hint {
3394
+ font-size: 13px;
3395
+ color: #909399;
3396
+ font-family: var(--gen-ai-font-family, 'Noto Sans', 'Noto Sans TC', sans-serif);
3397
+ }
3398
+
3399
+ .report-chapter-dialog__footer-btn {
3400
+ min-width: 140px;
3401
+ }
3402
+
3403
+ .report-chapter-source-picker.el-dialog {
3404
+ --el-dialog-padding-primary: 0;
3405
+ border-radius: 4px;
3406
+ overflow: hidden;
3407
+ }
3408
+
3409
+ .report-chapter-source-picker .el-dialog__header {
3410
+ border-bottom: none !important;
3411
+ }
3412
+
3413
+ .report-chapter-source-picker .el-dialog__body {
3414
+ padding: 8px 20px 20px;
3415
+ }
3416
+
3417
+ .report-chapter-source-picker .el-dialog__footer {
3418
+ padding: 12px 16px 24px;
3419
+ }
3420
+
3421
+ .report-chapter-source-picker__body {
3422
+ display: flex;
3423
+ flex-direction: column;
3424
+ gap: 12px;
3425
+ }
3426
+
3427
+ .report-chapter-source-picker__list {
3428
+ display: flex;
3429
+ flex-direction: column;
3430
+ gap: 8px;
3431
+ max-height: 320px;
3432
+ overflow-y: auto;
3433
+ padding: 8px 12px;
3434
+ border: 1px solid #ebeef5;
3435
+ border-radius: 4px;
3436
+ }
3437
+
3438
+ .report-chapter-source-picker__item {
3439
+ margin-right: 0;
3440
+ height: auto;
3441
+ white-space: normal;
3442
+ }
3443
+
3444
+ .report-chapter-source-picker__empty {
3445
+ margin: 0;
3446
+ padding: 24px 0;
3447
+ text-align: center;
3448
+ color: #909399;
3449
+ font-size: 14px;
3450
+ }
3451
+
3452
+ .report-workflow-dialog-overlay {
3453
+ overflow: auto;
3454
+ }
3455
+
3456
+ .report-workflow-dialog.el-dialog {
3457
+ --el-dialog-padding-primary: 0;
3458
+ /* 寬高 / 對齊由 reportEdit 頁 --report-edit-main-* 變量控制;此處僅作無錨點時的兜底 */
3459
+ width: 60% !important;
3460
+ max-width: 80%;
3461
+ min-height: 60vh;
3462
+ height: auto;
3463
+ max-height: 90vh;
3464
+ margin-top: 5% !important;
3465
+ margin-left: 21% !important;
3466
+ margin-right: auto !important;
3467
+ border-radius: 4px;
3468
+ overflow: hidden;
3469
+ display: flex;
3470
+ flex-direction: column;
3471
+ }
3472
+
3473
+ .report-workflow-dialog .el-dialog__header {
3474
+ flex-shrink: 0;
3475
+ }
3476
+
3477
+ .report-workflow-dialog .el-dialog__body {
3478
+ flex: 1;
3479
+ min-height: 0;
3480
+ overflow-y: auto;
3481
+ }
3482
+
3483
+ .report-workflow-dialog .el-dialog__footer {
3484
+ flex-shrink: 0;
3485
+ }
3486
+
3487
+ .report-chapter-workspace__chapter-select-popper.el-popper {
3488
+ --report-accent: #c53355;
3489
+ --report-accent-soft: #fdf0f2;
3490
+ width: 450px !important;
3491
+ min-width: 450px !important;
3492
+ }
3493
+
3494
+ /* 箭頭對齊 select 右側 caret 正下方(select 寬 220px) */
3495
+ .report-chapter-workspace__chapter-select-popper.el-popper .el-popper__arrow {
3496
+ left: 200px !important;
3497
+ right: auto !important;
3498
+ transform: translateX(-50%) !important;
3499
+ }
3500
+
3501
+ .report-chapter-workspace__chapter-select-popper.el-popper[data-popper-placement^='bottom']
3502
+ .el-popper__arrow {
3503
+ top: -5px !important;
3504
+ }
3505
+
3506
+ .report-chapter-workspace__chapter-select-popper.el-popper[data-popper-placement^='top']
3507
+ .el-popper__arrow {
3508
+ bottom: -5px !important;
3509
+ }
3510
+
3511
+ .report-chapter-workspace__chapter-select-popper .el-select-dropdown {
3512
+ width: 450px;
3513
+ min-width: 450px;
3514
+ }
3515
+
3516
+ /* 最多顯示 14 章(14 × 40px),超出出現滾動條;只保留一條 EP 自定義條 */
3517
+ .report-chapter-workspace__chapter-select-popper .el-select-dropdown > .el-scrollbar {
3518
+ max-height: 560px !important;
3519
+ height: auto !important;
3520
+ overflow: hidden !important;
3521
+ }
3522
+
3523
+ .report-chapter-workspace__chapter-select-popper .el-select-dropdown__wrap,
3524
+ .report-chapter-workspace__chapter-select-popper .el-scrollbar__wrap,
3525
+ .report-chapter-workspace__chapter-select-popper .el-scrollbar__wrap--hidden-default {
3526
+ max-height: 560px !important;
3527
+ height: auto !important;
3528
+ max-width: 100% !important;
3529
+ overflow-x: hidden !important;
3530
+ overflow-y: auto !important;
3531
+ /* 徹底隱藏原生滾動條,避免雙條 */
3532
+ scrollbar-width: none !important;
3533
+ -ms-overflow-style: none !important;
3534
+ }
3535
+
3536
+ .report-chapter-workspace__chapter-select-popper .el-select-dropdown__wrap::-webkit-scrollbar,
3537
+ .report-chapter-workspace__chapter-select-popper .el-scrollbar__wrap::-webkit-scrollbar,
3538
+ .report-chapter-workspace__chapter-select-popper
3539
+ .el-scrollbar__wrap--hidden-default::-webkit-scrollbar {
3540
+ width: 0 !important;
3541
+ height: 0 !important;
3542
+ display: none !important;
3543
+ }
3544
+
3545
+ .report-chapter-workspace__chapter-select-popper .el-scrollbar__view {
3546
+ display: block !important;
3547
+ }
3548
+
3549
+ .report-chapter-workspace__chapter-select-popper .el-scrollbar__bar.is-horizontal {
3550
+ display: none !important;
3551
+ }
3552
+
3553
+ .report-chapter-workspace__chapter-select-popper .el-scrollbar__bar.is-vertical {
3554
+ width: 8px !important;
3555
+ right: 4px !important;
3556
+ top: 2px !important;
3557
+ bottom: 2px !important;
3558
+ height: auto !important;
3559
+ opacity: 1 !important;
3560
+ z-index: 10;
3561
+ }
3562
+
3563
+ .report-chapter-workspace__chapter-select-popper
3564
+ .el-scrollbar__bar.is-vertical
3565
+ .el-scrollbar__thumb {
3566
+ display: block !important;
3567
+ width: 8px !important;
3568
+ height: 50px !important;
3569
+ min-height: 50px !important;
3570
+ border-radius: 4px !important;
3571
+ background-color: #999 !important;
3572
+ opacity: 1 !important;
3573
+ }
3574
+
3575
+ .report-chapter-workspace__chapter-select-popper .el-select-dropdown__list {
3576
+ /* 首尾章節向內 5px,避免貼邊 */
3577
+ padding: 5px 0 !important;
3578
+ margin: 0 !important;
3579
+ }
3580
+
3581
+ .report-chapter-workspace__chapter-select-popper .el-select-dropdown__item {
3582
+ display: flex;
3583
+ align-items: center;
3584
+ height: 40px !important;
3585
+ min-height: 40px !important;
3586
+ max-height: 40px !important;
3587
+ margin: 0 5px;
3588
+ padding: 0 12px !important;
3589
+ line-height: 1.4;
3590
+ border-radius: 8px;
3591
+ border: 1px solid transparent;
3592
+ box-sizing: border-box;
3593
+ cursor: grab;
3594
+ user-select: none;
3595
+ font-size: 15px;
3596
+ font-weight: 400;
3597
+ font-family: var(--gen-ai-font-family, 'Noto Sans', 'Noto Sans TC', sans-serif);
3598
+ color: #303133;
3599
+ transition:
3600
+ background 0.15s ease,
3601
+ box-shadow 0.15s ease,
3602
+ opacity 0.15s ease;
3603
+ }
3604
+
3605
+ .report-chapter-workspace__chapter-select-popper .el-select-dropdown__item.is-selected {
3606
+ color: var(--report-accent);
3607
+ font-weight: 400;
3608
+ }
3609
+
3610
+ .report-chapter-workspace__chapter-select-popper .el-select-dropdown__item.is-hovering {
3611
+ background-color: #f5f7fa;
3612
+ }
3613
+
3614
+ .report-chapter-workspace__chapter-select-popper .el-select-dropdown__item.is-dragging {
3615
+ opacity: 0.5;
3616
+ cursor: grabbing;
3617
+ background: #fff;
3618
+ border-color: var(--report-accent);
3619
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08);
3620
+ }
3621
+
3622
+ .report-chapter-workspace__chapter-select-popper .el-select-dropdown__item.is-drag-over {
3623
+ background: var(--report-accent-soft);
3624
+ box-shadow: inset 0 0 0 1px rgba(197, 51, 85, 0.35);
3625
+ }
3626
+
3627
+ .report-chapter-workspace__select-sortable-fallback {
3628
+ opacity: 0.5;
3629
+ cursor: grabbing;
3630
+ background: #fff !important;
3631
+ border: 1px solid #c53355 !important;
3632
+ border-radius: 8px !important;
3633
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08);
3634
+ list-style: none;
3635
+ z-index: 10001;
3636
+ }
3637
+
3638
+ /* Element Plus 內置 .el-select-dropdown__footer(border-top + padding),不在業務源碼裏,需在此覆盖 */
3639
+ /* // new prompt sections — footer「新增章節」按鈕區 */
3640
+ .report-chapter-workspace__chapter-select-popper .el-select-dropdown__footer {
3641
+ border-top: none;
3642
+ padding: 0;
3643
+ }
3644
+
3645
+ .report-chapter-workspace__chapter-select-popper .report-chapter-workspace__chapter-select-footer {
3646
+ display: flex;
3647
+ align-items: center;
3648
+ justify-content: center;
3649
+ gap: 12px;
3650
+ padding: 10px 12px;
3651
+ }
3652
+
3653
+ .report-chapter-workspace__chapter-select-popper
3654
+ .report-chapter-workspace__chapter-select-footer-btn {
3655
+ --report-accent: #c53355;
3656
+ --report-accent-soft: #fdf0f2;
3657
+ display: inline-flex;
3658
+ align-items: center;
3659
+ justify-content: center;
3660
+ box-sizing: border-box;
3661
+ min-width: 120px;
3662
+ height: 32px;
3663
+ padding: 0 14px;
3664
+ border-radius: 4px;
3665
+ border: 1px solid var(--report-accent);
3666
+ background: var(--report-accent);
3667
+ color: #fff;
3668
+ font-size: 15px;
3669
+ font-family: var(--gen-ai-font-family, 'Noto Sans', 'Noto Sans TC', sans-serif);
3670
+ font-weight: 400;
3671
+ line-height: 1;
3672
+ white-space: nowrap;
3673
+ cursor: pointer;
3674
+ transition: all 0.2s ease;
3675
+ }
3676
+
3677
+ .report-chapter-workspace__chapter-select-popper
3678
+ .report-chapter-workspace__chapter-select-footer-btn:hover {
3679
+ background: #fff;
3680
+ border-color: var(--report-accent);
3681
+ color: var(--report-accent);
3682
+ }
3683
+ /* // new prompt sections end */
3684
+
3685
+ .apply-template-form {
3686
+ --apply-template-label-w: 100px;
3687
+ --apply-template-field-gap: 16px;
3688
+ /* 與章節 card 內邊距一致,保證頂部標籤與下方「章節Prompt」左對齊 */
3689
+ --apply-template-content-inset: 16px;
3690
+ display: flex;
3691
+ flex-direction: column;
3692
+ gap: 20px;
3693
+ }
3694
+
3695
+ .apply-template-meta {
3696
+ display: flex;
3697
+ flex-direction: column;
3698
+ gap: 20px;
3699
+ padding: 0 var(--apply-template-content-inset);
3700
+ }
3701
+
3702
+ .apply-template-field {
3703
+ margin: 0;
3704
+ }
3705
+
3706
+ .apply-template-field--row {
3707
+ display: grid;
3708
+ grid-template-columns: var(--apply-template-label-w) minmax(0, 1fr);
3709
+ column-gap: var(--apply-template-field-gap);
3710
+ align-items: center;
3711
+ width: 100%;
3712
+ }
3713
+
3714
+ .apply-template-field--textarea-row {
3715
+ align-items: start;
3716
+ }
3717
+
3718
+ .apply-template-field--textarea-row .apply-template-label {
3719
+ padding-top: 8px;
3720
+ }
3721
+
3722
+ .apply-template-label {
3723
+ flex-shrink: 0;
3724
+ font-size: 14px;
3725
+ font-weight: 500;
3726
+ font-family: var(--gen-ai-font-family, 'Noto Sans', 'Noto Sans TC', sans-serif);
3727
+ color: #303133;
3728
+ white-space: nowrap;
3729
+ }
3730
+
3731
+ .apply-template-label__required {
3732
+ margin-left: 2px;
3733
+ color: #f56c6c;
3734
+ }
3735
+
3736
+ .apply-template-field__control {
3737
+ min-width: 0;
3738
+ width: 100%;
3739
+ }
3740
+
3741
+ .apply-template-field__control--lang {
3742
+ width: auto;
3743
+ max-width: 270px;
3744
+ }
3745
+
3746
+ .apply-template-source {
3747
+ font-size: 14px;
3748
+ color: #303133;
3749
+ }
3750
+
3751
+ .apply-template-field__input,
3752
+ .apply-template-field__textarea {
3753
+ width: 100%;
3754
+ }
3755
+
3756
+ .apply-template-field--textarea-row .apply-template-field__control {
3757
+ position: relative;
3758
+ padding-bottom: 20px;
3759
+ }
3760
+
3761
+ .apply-template-field__input :deep(.el-input__wrapper),
3762
+ .apply-template-field__textarea :deep(.el-textarea__inner) {
3763
+ font-size: clamp(13px, calc(14 * 100vw / 1920), 14px);
3764
+ }
3765
+
3766
+ /* textarea 字數統計移出框外独占一行,避免遮挡文字 */
3767
+ .apply-template-field__textarea :deep(.el-textarea) {
3768
+ position: relative;
3769
+ overflow: visible;
3770
+ }
3771
+
3772
+ .apply-template-field__textarea :deep(.el-input__count) {
3773
+ position: absolute !important;
3774
+ top: auto !important;
3775
+ right: 0 !important;
3776
+ bottom: -18px !important;
3777
+ left: auto !important;
3778
+ z-index: 1;
3779
+ display: block;
3780
+ width: auto;
3781
+ max-width: 100%;
3782
+ margin: 0;
3783
+ padding: 0;
3784
+ line-height: 18px;
3785
+ background: transparent !important;
3786
+ font-size: 12px;
3787
+ color: #909399;
3788
+ text-align: right;
3789
+ pointer-events: none;
3790
+ }
3791
+
3792
+ .apply-template-field__input :deep(.el-input__count) {
3793
+ font-size: 12px;
3794
+ color: #909399;
3795
+ background: transparent;
3796
+ }
3797
+
3798
+ .apply-template-chapter {
3799
+ padding: var(--apply-template-content-inset, 16px);
3800
+ border: 1px solid #ebeef5;
3801
+ border-radius: 4px;
3802
+ }
3803
+
3804
+ .apply-template-chapter__title {
3805
+ margin: 0 0 16px;
3806
+ font-size: 16px;
3807
+ font-weight: 500;
3808
+ font-family: var(--gen-ai-font-family, 'Noto Sans', 'Noto Sans TC', sans-serif);
3809
+ color: #606266;
3810
+ }
3811
+
3812
+ .apply-template-process {
3813
+ padding: 20px 16px 16px;
3814
+ border: 1px solid #ebeef5;
3815
+ border-radius: 4px;
3816
+ }
3817
+
3818
+ .apply-template-process__row {
3819
+ align-items: center;
3820
+ }
3821
+
3822
+ .apply-template-process__radios {
3823
+ display: flex;
3824
+ align-items: center;
3825
+ gap: 24px;
3826
+ }
3827
+
3828
+ .apply-template-process__actions {
3829
+ display: flex;
3830
+ align-items: center;
3831
+ justify-content: center;
3832
+ gap: 16px;
3833
+ margin-top: 8px;
3834
+ }
3835
+
3836
+ .apply-template-btn {
3837
+ display: inline-flex;
3838
+ align-items: center;
3839
+ justify-content: center;
3840
+ box-sizing: border-box;
3841
+ min-width: 120px;
3842
+ height: 32px;
3843
+ padding: 0 20px;
3844
+ border-radius: 4px;
3845
+ font-size: 14px;
3846
+ line-height: 1;
3847
+ cursor: pointer;
3848
+ transition: all 0.2s ease;
3849
+ }
3850
+
3851
+ .apply-template-btn--primary {
3852
+ border: 1px solid #c53355;
3853
+ background: #c53355;
3854
+ color: #fff;
3855
+ font-weight: 500;
3856
+ }
3857
+
3858
+ .apply-template-btn--primary:hover {
3859
+ background: #fff;
3860
+ color: #c53355;
3861
+ }
3862
+
3863
+ .apply-template-btn--ghost {
3864
+ border: 1px solid #dcdfe6;
3865
+ background: #fff;
3866
+ color: #606266;
3867
+ }
3868
+
3869
+ .apply-template-btn--ghost:hover {
3870
+ border-color: #c53355;
3871
+ color: #c53355;
3872
+ background: #fdf0f2;
3873
+ }
3874
+
3875
+ .apply-template-approval {
3876
+ margin-top: 24px;
3877
+ border: 1px solid #dcdfe6;
3878
+ border-radius: 4px;
3879
+ overflow: hidden;
3880
+ }
3881
+
3882
+ .apply-template-approval__title {
3883
+ margin: 0;
3884
+ padding: 12px 16px;
3885
+ font-size: 16px;
3886
+ font-weight: 500;
3887
+ font-family: var(--gen-ai-font-family, 'Noto Sans', 'Noto Sans TC', sans-serif);
3888
+ color: #606266;
3889
+ background: transparent;
3890
+ border-bottom: 1px solid #dcdfe6;
3891
+ }
3892
+
3893
+ .apply-template-approval-table {
3894
+ width: 100%;
3895
+ }
3896
+
3897
+ .apply-template-approval-table :deep(.el-table--border) {
3898
+ border: none;
3899
+ }
3900
+
3901
+ .apply-template-approval-table :deep(.el-table--border::before),
3902
+ .apply-template-approval-table :deep(.el-table--border::after),
3903
+ .apply-template-approval-table :deep(.el-table__inner-wrapper::before),
3904
+ .apply-template-approval-table :deep(.el-table__inner-wrapper::after) {
3905
+ display: none;
3906
+ }
3907
+
3908
+ .apply-template-approval-table :deep(.el-table__border-left-patch) {
3909
+ display: none;
3910
+ }
3911
+
3912
+ .apply-template-approval-table :deep(.el-table__header th.el-table__cell) {
3913
+ background: rgb(249, 250, 251);
3914
+ color: #606266;
3915
+ font-weight: 500;
3916
+ font-size: 16px;
3917
+ font-family: var(--gen-ai-font-family, 'Noto Sans', 'Noto Sans TC', sans-serif);
3918
+ }
3919
+
3920
+ .apply-template-approval-table :deep(.el-table__body tr.el-table__row) {
3921
+ color: #303133;
3922
+ font-size: 15px;
3923
+ font-family: var(--gen-ai-font-family, 'Noto Sans', 'Noto Sans TC', sans-serif);
3924
+ }
3925
+ </style>
3926
+
3927
+ <style>
3928
+ .apply-template-overlay {
3929
+ overflow: hidden !important;
3930
+ }
3931
+
3932
+ .apply-template-dialog.el-dialog {
3933
+ --el-dialog-padding-primary: 0;
3934
+ width: 80% !important;
3935
+ max-width: 80%;
3936
+ margin-top: 5vh;
3937
+ border-radius: 4px;
3938
+ overflow: hidden;
3939
+ display: flex;
3940
+ flex-direction: column;
3941
+ max-height: 90vh;
3942
+ /* 給 body / scrollbar 明確高度,避免 max-height + height:auto 導致滾動失效 */
3943
+ height: min(90vh, 900px);
3944
+ }
3945
+
3946
+ .apply-template-dialog .el-dialog__header,
3947
+ .apply-template-dialog__header {
3948
+ display: flex;
3949
+ align-items: center;
3950
+ justify-content: space-between;
3951
+ gap: 16px;
3952
+ margin: 0;
3953
+ padding: 14px 16px 14px 20px;
3954
+ background: transparent;
3955
+ border-bottom: none;
3956
+ flex-shrink: 0;
3957
+ }
3958
+
3959
+ .apply-template-dialog__title,
3960
+ .apply-template-dialog .el-dialog__title {
3961
+ flex: 1;
3962
+ min-width: 0;
3963
+ color: #303133;
3964
+ font-size: 18px;
3965
+ font-weight: 600;
3966
+ font-family: var(--gen-ai-font-family, 'Noto Sans', 'Noto Sans TC', sans-serif);
3967
+ line-height: 1.4;
3968
+ text-decoration: none;
3969
+ border-bottom: none;
3970
+ box-shadow: none;
3971
+ }
3972
+
3973
+ .apply-template-dialog__close {
3974
+ flex-shrink: 0;
3975
+ width: 32px;
3976
+ height: 32px;
3977
+ margin: 0;
3978
+ padding: 0;
3979
+ border: none;
3980
+ background: transparent;
3981
+ color: #303133;
3982
+ font-size: 18px;
3983
+ line-height: 1;
3984
+ cursor: pointer;
3985
+ }
3986
+
3987
+ .apply-template-dialog__close:hover {
3988
+ color: #606266;
3989
+ }
3990
+
3991
+ .apply-template-dialog .el-dialog__body {
3992
+ flex: 1 1 auto;
3993
+ min-height: 0;
3994
+ height: 100%;
3995
+ padding: 0;
3996
+ background: #fff;
3997
+ overflow: hidden;
3998
+ display: flex;
3999
+ flex-direction: column;
4000
+ }
4001
+
4002
+ /* 與編輯器 / 生成頁同款:el-scrollbar,滑塊按比例計算(僅 min-height,勿鎖死 height) */
4003
+ .apply-template-dialog__scroll {
4004
+ flex: 1 1 auto;
4005
+ min-height: 0;
4006
+ height: 100%;
4007
+ width: 100%;
4008
+ }
4009
+
4010
+ .apply-template-dialog__scroll .el-scrollbar__view {
4011
+ padding: 24px 20px;
4012
+ box-sizing: border-box;
4013
+ }
4014
+
4015
+ .apply-template-dialog__scroll .el-scrollbar__wrap {
4016
+ overflow-x: hidden !important;
4017
+ overflow-y: auto !important;
4018
+ max-height: 100% !important;
4019
+ scrollbar-width: none !important;
4020
+ -ms-overflow-style: none !important;
4021
+ }
4022
+
4023
+ .apply-template-dialog__scroll .el-scrollbar__wrap::-webkit-scrollbar {
4024
+ width: 0 !important;
4025
+ height: 0 !important;
4026
+ display: none !important;
4027
+ }
4028
+
4029
+ .apply-template-dialog__scroll .el-scrollbar__bar.is-horizontal {
4030
+ display: none !important;
4031
+ }
4032
+
4033
+ .apply-template-dialog__scroll .el-scrollbar__bar.is-vertical {
4034
+ width: 8px !important;
4035
+ right: 4px !important;
4036
+ top: 2px !important;
4037
+ bottom: 2px !important;
4038
+ height: auto !important;
4039
+ opacity: 1 !important;
4040
+ z-index: 10;
4041
+ }
4042
+
4043
+ .apply-template-dialog__scroll .el-scrollbar__bar.is-vertical .el-scrollbar__thumb {
4044
+ display: block !important;
4045
+ width: 8px !important;
4046
+ min-height: 50px !important;
4047
+ border-radius: 4px !important;
4048
+ background-color: #999 !important;
4049
+ opacity: 1 !important;
4050
+ }
4051
+
4052
+ .ai-prompt-generate-dialog__form,
4053
+ .ai-prompt-generate-dialog__preview {
4054
+ display: flex;
4055
+ flex-direction: column;
4056
+ gap: 18px;
4057
+ }
4058
+
4059
+ .ai-prompt-generate-dialog__field {
4060
+ margin: 0;
4061
+ }
4062
+
4063
+ .ai-prompt-generate-dialog__field--row {
4064
+ display: grid;
4065
+ grid-template-columns: 100px minmax(0, 1fr);
4066
+ gap: 16px;
4067
+ align-items: start;
4068
+ }
4069
+
4070
+ .ai-prompt-generate-dialog__field--textarea {
4071
+ align-items: start;
4072
+ }
4073
+
4074
+ .ai-prompt-generate-dialog__label {
4075
+ padding-top: 8px;
4076
+ font-size: 15px;
4077
+ font-weight: 500;
4078
+ font-family: var(--gen-ai-font-family, 'Noto Sans', 'Noto Sans TC', sans-serif);
4079
+ color: #303133;
4080
+ line-height: 1.4;
4081
+ white-space: nowrap;
4082
+ }
4083
+
4084
+ .ai-prompt-generate-dialog__control {
4085
+ min-width: 0;
4086
+ }
4087
+
4088
+ .ai-prompt-generate-dialog__preview-row {
4089
+ display: grid;
4090
+ grid-template-columns: 100px minmax(0, 1fr);
4091
+ gap: 16px;
4092
+ align-items: start;
4093
+ }
4094
+
4095
+ .ai-prompt-generate-dialog__preview-row--prompt .ai-prompt-generate-dialog__label {
4096
+ padding-top: 0;
4097
+ }
4098
+
4099
+ .ai-prompt-generate-dialog__preview-text {
4100
+ margin: 0;
4101
+ font-size: 14px;
4102
+ line-height: 1.6;
4103
+ color: #303133;
4104
+ word-break: break-word;
4105
+ }
4106
+
4107
+ .ai-prompt-generate-dialog__preview-text--multiline {
4108
+ white-space: pre-wrap;
4109
+ }
4110
+
4111
+ .ai-prompt-generate-dialog__divider {
4112
+ height: 0;
4113
+ border-top: 1px dashed #dcdfe6;
4114
+ }
4115
+
4116
+ .ai-prompt-generate-dialog__footer {
4117
+ display: flex;
4118
+ align-items: center;
4119
+ justify-content: center;
4120
+ gap: 16px;
4121
+ width: 100%;
4122
+ }
4123
+
4124
+ .ai-prompt-generate-dialog__footer-btn {
4125
+ min-width: 140px;
4126
+ }
4127
+
4128
+ .ai-prompt-generate-dialog {
4129
+ --report-accent: #c53355;
4130
+ --report-accent-soft: #fdf0f2;
4131
+ }
4132
+
4133
+ .ai-prompt-generate-dialog .template-center-toolbar-btn {
4134
+ display: inline-flex;
4135
+ align-items: center;
4136
+ justify-content: center;
4137
+ gap: 6px;
4138
+ box-sizing: border-box;
4139
+ height: 32px;
4140
+ min-width: 120px;
4141
+ padding: 0 14px;
4142
+ border-radius: 4px;
4143
+ line-height: 1;
4144
+ white-space: nowrap;
4145
+ cursor: pointer;
4146
+ font-size: 15px;
4147
+ font-family: var(--gen-ai-font-family, 'Noto Sans', 'Noto Sans TC', sans-serif);
4148
+ font-weight: 400;
4149
+ transition: all 0.2s ease;
4150
+ }
4151
+
4152
+ .ai-prompt-generate-dialog .template-center-toolbar-btn--primary {
4153
+ border: 1px solid var(--report-accent);
4154
+ background: var(--report-accent);
4155
+ color: #fff;
4156
+ }
4157
+
4158
+ .ai-prompt-generate-dialog .template-center-toolbar-btn--primary:hover:not(:disabled) {
4159
+ background: #fff;
4160
+ border-color: var(--report-accent);
4161
+ color: var(--report-accent);
4162
+ }
4163
+
4164
+ .ai-prompt-generate-dialog .template-center-toolbar-btn--outline {
4165
+ border: 1px solid var(--report-accent);
4166
+ color: var(--report-accent);
4167
+ background: #fff;
4168
+ }
4169
+
4170
+ .ai-prompt-generate-dialog .template-center-toolbar-btn--outline:hover:not(:disabled) {
4171
+ border-color: var(--report-accent);
4172
+ color: var(--report-accent);
4173
+ background: var(--report-accent-soft);
4174
+ }
4175
+
4176
+ .ai-prompt-generate-dialog.el-dialog {
4177
+ --el-dialog-padding-primary: 0;
4178
+ border-radius: 4px;
4179
+ overflow: hidden;
4180
+ }
4181
+
4182
+ .ai-prompt-generate-dialog .el-dialog__header,
4183
+ .ai-prompt-generate-dialog .report-workflow-dialog__header {
4184
+ border-bottom: none !important;
4185
+ }
4186
+
4187
+ .ai-prompt-generate-dialog .el-dialog__body {
4188
+ padding: 24px 20px;
4189
+ background: #fff;
4190
+ }
4191
+
4192
+ .ai-prompt-generate-dialog .el-dialog__footer {
4193
+ padding: 12px 16px 30px;
4194
+ background: #fff;
4195
+ display: flex;
4196
+ justify-content: center;
4197
+ }
4198
+
4199
+ .ai-prompt-generate-dialog__input .el-input__wrapper,
4200
+ .ai-prompt-generate-dialog__textarea .el-textarea__inner {
4201
+ border-radius: 2px;
4202
+ }
4203
+
4204
+ /* // new prompt sections — 章節下拉:空態空白 / >1024 最小 500px / 長標題換行(集中放置) */
4205
+ .report-chapter-workspace__chapter-select-empty {
4206
+ min-height: 48px;
4207
+ background: #fff;
4208
+ }
4209
+
4210
+ .report-chapter-workspace__chapter-select-popper .el-select-dropdown__empty {
4211
+ padding: 0 !important;
4212
+ color: transparent !important;
4213
+ }
4214
+
4215
+ @media (min-width: 1025px) {
4216
+ .report-chapter-workspace__chapter-select-popper.el-popper {
4217
+ width: max-content !important;
4218
+ min-width: 500px !important;
4219
+ max-width: min(720px, calc(100vw - 48px)) !important;
4220
+ }
4221
+
4222
+ .report-chapter-workspace__chapter-select-popper .el-select-dropdown {
4223
+ width: 100% !important;
4224
+ min-width: 500px !important;
4225
+ max-width: 100% !important;
4226
+ }
4227
+ }
4228
+
4229
+ .report-chapter-workspace__chapter-select-popper .el-select-dropdown__item {
4230
+ height: auto !important;
4231
+ min-height: 40px !important;
4232
+ max-height: none !important;
4233
+ align-items: flex-start !important;
4234
+ padding-top: 8px !important;
4235
+ padding-bottom: 8px !important;
4236
+ white-space: normal !important;
4237
+ }
4238
+
4239
+ .report-chapter-workspace__chapter-select-popper .report-chapter-workspace__chapter-option {
4240
+ align-items: flex-start;
4241
+ }
4242
+
4243
+ .report-chapter-workspace__chapter-select-popper .report-chapter-workspace__chapter-option-title {
4244
+ overflow: visible;
4245
+ text-overflow: unset;
4246
+ white-space: normal;
4247
+ word-break: break-word;
4248
+ overflow-wrap: anywhere;
4249
+ line-height: 1.4;
4250
+ }
4251
+
4252
+ .report-chapter-workspace__chapter-select-popper .report-chapter-workspace__sort-handle,
4253
+ .report-chapter-workspace__chapter-select-popper .report-chapter-workspace__chapter-option-status {
4254
+ margin-top: 2px;
4255
+ flex-shrink: 0;
4256
+ }
4257
+ /* // new prompt sections end */
4258
+ </style>