aegon-lv97 1.0.0

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