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.
- package/gen-ai/build-report/chartStreamRefs.ts +227 -0
- package/gen-ai/build-report/coherence-check/index.vue +559 -0
- package/gen-ai/build-report/components/ReportChapterWorkspace.vue +4571 -0
- package/gen-ai/build-report/components/ReportConfigPanel.vue +727 -0
- package/gen-ai/build-report/components/ReportGenerateSettingsPanel.vue +2361 -0
- package/gen-ai/build-report/components/ReportGeneratingPanel.vue +1765 -0
- package/gen-ai/build-report/components/ReportPromptPanel.vue +625 -0
- package/gen-ai/build-report/components/ReportSourceFileSummary.vue +665 -0
- package/gen-ai/build-report/components/ReportSourcesPanel.vue +1327 -0
- package/gen-ai/build-report/components/ReportTemplateDetail.vue +191 -0
- package/gen-ai/build-report/components/ReportTemplateEditorFormPanel.vue +667 -0
- package/gen-ai/build-report/components/ReportTemplatePicker.vue +302 -0
- package/gen-ai/build-report/components/ReportWorkflowSidebar.vue +148 -0
- package/gen-ai/build-report/components/paramsSetting.vue +886 -0
- package/gen-ai/build-report/components/reportEdit.vue +2283 -0
- package/gen-ai/build-report/index.vue +856 -0
- package/gen-ai/build-report/report-domain.ts +1133 -0
- package/gen-ai/build-report/report-template-data.ts +295 -0
- package/gen-ai/build-report/useReportWorkflow.ts +1386 -0
- package/gen-ai/common/assets/NotoSans-SC.woff2 +0 -0
- package/gen-ai/common/assets/NotoSans-TC.woff2 +0 -0
- package/gen-ai/common/assets/NotoSans.woff2 +0 -0
- package/gen-ai/common/assets/fonts.css +278 -0
- package/gen-ai/common/assets/pagination.css +163 -0
- package/gen-ai/common/directives/btnLoading.ts +455 -0
- package/gen-ai/common/directives/debounce.ts +98 -0
- package/gen-ai/common/directives/index.ts +30 -0
- package/gen-ai/common/en.md +417 -0
- package/gen-ai/common/index.ts +36 -0
- package/gen-ai/common/types/component-expose.ts +9 -0
- package/gen-ai/common/utils/api-res-status.ts +7 -0
- package/gen-ai/common/utils/navigation.ts +59 -0
- package/gen-ai/common/utils/remote-request.ts +45 -0
- package/gen-ai/common/utils/scroll.ts +577 -0
- package/gen-ai/components/AIExport/docx-export.ts +950 -0
- package/gen-ai/components/AIExport/docxExport.ts +2566 -0
- package/gen-ai/components/AIExport/index.ts +52 -0
- package/gen-ai/components/AIExport/parseMarkdownHtml.ts +653 -0
- package/gen-ai/components/CapsuleScrollbar.vue +149 -0
- package/gen-ai/components/ChapterTitleScroll.vue +468 -0
- package/gen-ai/components/EntryHeroNavCards.vue +379 -0
- package/gen-ai/components/GenAiPagination.vue +52 -0
- package/gen-ai/components/ModelCard.vue +1114 -0
- package/gen-ai/components/Preview/FilePreviewPage.vue +949 -0
- package/gen-ai/components/Preview/PreviewCapsuleScroll.vue +205 -0
- package/gen-ai/components/Preview/PreviewOutlineTree.vue +148 -0
- package/gen-ai/components/Preview/index.ts +11 -0
- package/gen-ai/components/Preview/preview.ts +724 -0
- package/gen-ai/components/PromptField.vue +505 -0
- package/gen-ai/components/RecentReportsTable.vue +974 -0
- package/gen-ai/components/ReportCollapsibleSection.vue +172 -0
- package/gen-ai/components/ReportHistoryList.vue +637 -0
- package/gen-ai/components/ReportSectionConfirmPopover.vue +251 -0
- package/gen-ai/components/editor/ChapterTiptapEditor.vue +3718 -0
- package/gen-ai/components/editor/StructureBlockView.vue +441 -0
- package/gen-ai/components/editor/TiptapToolbar.vue +1036 -0
- package/gen-ai/components/editor/TypewriterDocEditor.vue +422 -0
- package/gen-ai/components/editor/_StructureOrgNode.vue +326 -0
- package/gen-ai/components/editor/index.ts +28 -0
- package/gen-ai/components/editor/parseMarkdownHtml.ts +754 -0
- package/gen-ai/components/editor/reviewCitationTip.ts +163 -0
- package/gen-ai/gen-entry/api/index.ts +32 -0
- package/gen-ai/gen-entry/index.vue +287 -0
- package/gen-ai/gen-entry/types/index.ts +4 -0
- package/gen-ai/i.mark +2 -0
- package/gen-ai/management-center/index.vue +1815 -0
- package/gen-ai/management-center/mockData.ts +248 -0
- package/gen-ai/management-center/types/index.ts +37 -0
- package/gen-ai/model-manager/TemplateEditorFormPanel.vue +661 -0
- package/gen-ai/model-manager/api/index.ts +32 -0
- package/gen-ai/model-manager/api/mockApi.ts +33 -0
- package/gen-ai/model-manager/index.vue +1583 -0
- package/gen-ai/model-manager/mockData.ts +177 -0
- package/gen-ai/model-manager/template-detail-shared.ts +154 -0
- package/gen-ai/model-manager/template-editor.vue +345 -0
- package/gen-ai/model-manager/types/index.ts +40 -0
- package/gen-ai/model-manager/utils.ts +25 -0
- package/package.json +12 -0
|
@@ -0,0 +1,1765 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div ref="pageRef" class="report-generating-page">
|
|
3
|
+
<div class="report-generating-page__layout">
|
|
4
|
+
<div class="report-generating-page__main">
|
|
5
|
+
<header class="report-generating-page__hero">
|
|
6
|
+
<h1 class="report-generating-page__hero-title">{{ heroTitle }}</h1>
|
|
7
|
+
</header>
|
|
8
|
+
|
|
9
|
+
<div class="report-generating-page__body">
|
|
10
|
+
<el-scrollbar class="report-generating-page__scrollbar">
|
|
11
|
+
<div class="report-generating-page__content">
|
|
12
|
+
<ul v-if="visibleChapters.length" class="report-generating-chapters">
|
|
13
|
+
<li
|
|
14
|
+
v-for="chapter in visibleChapters"
|
|
15
|
+
:key="chapter.id"
|
|
16
|
+
:ref="(el) => bindChapterRowEl(chapter.id, el)"
|
|
17
|
+
class="report-generating-chapters__row"
|
|
18
|
+
:class="{ 'is-creating-active': chapter.status === 'creating' }"
|
|
19
|
+
:data-chapter-id="chapter.id"
|
|
20
|
+
>
|
|
21
|
+
<span class="report-generating-chapters__state" :class="`is-${chapter.status}`">
|
|
22
|
+
<template v-if="chapter.status === 'success'">
|
|
23
|
+
<el-icon
|
|
24
|
+
class="report-generating-chapters__icon is-success"
|
|
25
|
+
aria-hidden="true"
|
|
26
|
+
>
|
|
27
|
+
<Check />
|
|
28
|
+
</el-icon>
|
|
29
|
+
</template>
|
|
30
|
+
<template v-else-if="chapter.status === 'failure'">
|
|
31
|
+
<el-icon
|
|
32
|
+
class="report-generating-chapters__icon is-failed"
|
|
33
|
+
aria-hidden="true"
|
|
34
|
+
>
|
|
35
|
+
<Close />
|
|
36
|
+
</el-icon>
|
|
37
|
+
</template>
|
|
38
|
+
<template v-else-if="chapter.status === 'creating'">
|
|
39
|
+
<el-icon
|
|
40
|
+
class="report-generating-chapters__icon is-loading"
|
|
41
|
+
aria-hidden="true"
|
|
42
|
+
>
|
|
43
|
+
<RefreshRight />
|
|
44
|
+
</el-icon>
|
|
45
|
+
</template>
|
|
46
|
+
<template v-else>
|
|
47
|
+
<span
|
|
48
|
+
class="report-generating-chapters__icon is-pending report-generating-hourglass"
|
|
49
|
+
aria-hidden="true"
|
|
50
|
+
>
|
|
51
|
+
<svg
|
|
52
|
+
class="report-generating-hourglass__svg"
|
|
53
|
+
viewBox="0 0 24 28"
|
|
54
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
55
|
+
>
|
|
56
|
+
<path
|
|
57
|
+
class="report-generating-hourglass__frame"
|
|
58
|
+
d="M6 2h12l-5.2 9.2a1.2 1.2 0 0 0 0 1.2L18 22H6l5.2-9.6a1.2 1.2 0 0 0 0-1.2L6 2z"
|
|
59
|
+
fill="none"
|
|
60
|
+
stroke="currentColor"
|
|
61
|
+
stroke-width="1.4"
|
|
62
|
+
stroke-linejoin="round"
|
|
63
|
+
/>
|
|
64
|
+
<path
|
|
65
|
+
class="report-generating-hourglass__sand-top"
|
|
66
|
+
d="M8.2 4.2h7.6L12 10.8 8.2 4.2z"
|
|
67
|
+
fill="currentColor"
|
|
68
|
+
/>
|
|
69
|
+
<path
|
|
70
|
+
class="report-generating-hourglass__sand-bottom"
|
|
71
|
+
d="M12 17.2 15.8 23.8H8.2L12 17.2z"
|
|
72
|
+
fill="currentColor"
|
|
73
|
+
/>
|
|
74
|
+
<circle
|
|
75
|
+
class="report-generating-hourglass__drip report-generating-hourglass__drip--1"
|
|
76
|
+
cx="12"
|
|
77
|
+
cy="12.8"
|
|
78
|
+
r="0.75"
|
|
79
|
+
fill="currentColor"
|
|
80
|
+
/>
|
|
81
|
+
<circle
|
|
82
|
+
class="report-generating-hourglass__drip report-generating-hourglass__drip--2"
|
|
83
|
+
cx="12"
|
|
84
|
+
cy="12.8"
|
|
85
|
+
r="0.6"
|
|
86
|
+
fill="currentColor"
|
|
87
|
+
/>
|
|
88
|
+
<circle
|
|
89
|
+
class="report-generating-hourglass__drip report-generating-hourglass__drip--3"
|
|
90
|
+
cx="12"
|
|
91
|
+
cy="12.8"
|
|
92
|
+
r="0.55"
|
|
93
|
+
fill="currentColor"
|
|
94
|
+
/>
|
|
95
|
+
</svg>
|
|
96
|
+
</span>
|
|
97
|
+
</template>
|
|
98
|
+
</span>
|
|
99
|
+
<div class="report-generating-chapters__name">
|
|
100
|
+
<span class="report-generating-chapters__index">{{ chapter.orderSeq }}.</span>
|
|
101
|
+
<ChapterTitleScroll
|
|
102
|
+
root-tag="span"
|
|
103
|
+
:title="chapter.sectionName"
|
|
104
|
+
:max-chars="19"
|
|
105
|
+
/>
|
|
106
|
+
</div>
|
|
107
|
+
</li>
|
|
108
|
+
</ul>
|
|
109
|
+
</div>
|
|
110
|
+
</el-scrollbar>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
<aside class="report-generating-page__video" aria-label="視頻播放區">
|
|
115
|
+
<div class="report-generating-page__video-slot">
|
|
116
|
+
<!-- 視頻已刪:guide.mp4 相關暫時注釋
|
|
117
|
+
<video
|
|
118
|
+
ref="videoEl"
|
|
119
|
+
class="report-generating-page__player"
|
|
120
|
+
:src="GENERATING_VIDEO_SRC"
|
|
121
|
+
muted
|
|
122
|
+
autoplay
|
|
123
|
+
loop
|
|
124
|
+
playsinline
|
|
125
|
+
webkit-playsinline
|
|
126
|
+
preload="auto"
|
|
127
|
+
/>
|
|
128
|
+
-->
|
|
129
|
+
</div>
|
|
130
|
+
</aside>
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
<footer v-if="showActionFooter" class="report-generating-page__footer">
|
|
134
|
+
<p class="report-generating-page__summary">{{ completionSummary }}</p>
|
|
135
|
+
<div class="report-generating-page__footer-actions">
|
|
136
|
+
<button
|
|
137
|
+
type="button"
|
|
138
|
+
class="app-btn app-btn--wide app-btn--outline"
|
|
139
|
+
:disabled="isGenerating || retrying || isSectionRequesting || !failedCount"
|
|
140
|
+
@click="retryFailedChapters"
|
|
141
|
+
>
|
|
142
|
+
重試失敗的章節
|
|
143
|
+
</button>
|
|
144
|
+
<!-- <button
|
|
145
|
+
type="button"
|
|
146
|
+
class="app-btn app-btn--wide"
|
|
147
|
+
:disabled="isGenerating || retrying || isSectionRequesting || !canEnterEditing"
|
|
148
|
+
@click="enterEditing"
|
|
149
|
+
>
|
|
150
|
+
進入編輯
|
|
151
|
+
</button> -->
|
|
152
|
+
<button type="button" class="app-btn app-btn--wide" @click="enterEditing">進入編輯</button>
|
|
153
|
+
</div>
|
|
154
|
+
</footer>
|
|
155
|
+
</div>
|
|
156
|
+
</template>
|
|
157
|
+
|
|
158
|
+
<script setup lang="ts">
|
|
159
|
+
import { Check, Close, RefreshRight } from '@element-plus/icons-vue'
|
|
160
|
+
import { ElIcon, ElMessage, ElScrollbar } from 'element-plus'
|
|
161
|
+
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
|
|
162
|
+
import { useRoute, useRouter } from 'vue-router'
|
|
163
|
+
import { ChapterTitleScroll } from '../../common'
|
|
164
|
+
// 視頻已刪:guide.mp4 相關暫時注釋
|
|
165
|
+
// import generatingVideoSrc from '../../common/assets/guide.mp4?url'
|
|
166
|
+
import { postGenAiRequest } from '../../common/utils/remote-request'
|
|
167
|
+
import { getViewportHeight, lockDocumentScroll, navigateToReportEdit } from '../report-domain'
|
|
168
|
+
import {
|
|
169
|
+
clearGeneratingSession,
|
|
170
|
+
loadGeneratingSession,
|
|
171
|
+
saveGeneratingSession,
|
|
172
|
+
useReportWorkflow,
|
|
173
|
+
type GeneratingSessionChapter,
|
|
174
|
+
} from '../useReportWorkflow'
|
|
175
|
+
const emit = defineEmits<{
|
|
176
|
+
'enter-editing': []
|
|
177
|
+
}>()
|
|
178
|
+
|
|
179
|
+
/** 生成中頁視頻源(視頻已刪,暫時注釋) */
|
|
180
|
+
// const GENERATING_VIDEO_SRC = generatingVideoSrc
|
|
181
|
+
|
|
182
|
+
/** 輪詢間隔 2s;未收到 completed:true 則持續輪詢(無次數上限) */
|
|
183
|
+
const SECTION_POLL_INTERVAL_MS = 2000 // 轮询间隔2s
|
|
184
|
+
/** CREATING 計時閾值(僅追蹤開始時間;failure 叉號只認接口 createdFlag=FAILURE,本地超時不寫成 failure) */
|
|
185
|
+
const SECTION_CREATING_TIMEOUT_MS = 1_800_000 // 30分钟超时
|
|
186
|
+
|
|
187
|
+
type GenAiSectionCreatedFlag = 'PENDING' | 'CREATING' | 'SUCCESS' | 'FAILURE'
|
|
188
|
+
|
|
189
|
+
interface genAiSearchReportParams {
|
|
190
|
+
categoryId: number
|
|
191
|
+
instanceId: string
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
interface GenAiGeneratingSectionItem {
|
|
195
|
+
sectionId: string
|
|
196
|
+
sectionName: string
|
|
197
|
+
orderSeq: number
|
|
198
|
+
createdFlag: GenAiSectionCreatedFlag
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
type ChapterGenerationStatus = 'pending' | 'creating' | 'success' | 'failure'
|
|
202
|
+
|
|
203
|
+
interface ChapterGenerationItem {
|
|
204
|
+
id: string
|
|
205
|
+
sectionId: string
|
|
206
|
+
sectionName: string
|
|
207
|
+
orderSeq: number
|
|
208
|
+
status: ChapterGenerationStatus
|
|
209
|
+
body?: string
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const CREATED_FLAG_STATUS_MAP: Record<GenAiSectionCreatedFlag, ChapterGenerationStatus> = {
|
|
213
|
+
PENDING: 'pending',
|
|
214
|
+
CREATING: 'creating',
|
|
215
|
+
SUCCESS: 'success',
|
|
216
|
+
FAILURE: 'failure',
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const route = useRoute()
|
|
220
|
+
const router = useRouter()
|
|
221
|
+
const instanceId = ref('')
|
|
222
|
+
|
|
223
|
+
const {
|
|
224
|
+
markGenerationSuccess,
|
|
225
|
+
selectedTemplate,
|
|
226
|
+
setGeneratedChaptersFromApi,
|
|
227
|
+
loadTemplateFromSession,
|
|
228
|
+
} = useReportWorkflow()
|
|
229
|
+
|
|
230
|
+
const templateId = computed(() => {
|
|
231
|
+
const fromRoute = route.query.templateId
|
|
232
|
+
if (typeof fromRoute === 'string' && fromRoute) return fromRoute
|
|
233
|
+
return selectedTemplate.value?.id ?? '1'
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
const pageRef = ref<HTMLElement | null>(null)
|
|
237
|
+
// 視頻已刪:guide.mp4 相關暫時注釋
|
|
238
|
+
// const videoEl = ref<HTMLVideoElement | null>(null)
|
|
239
|
+
let measureRafId = 0
|
|
240
|
+
let cleanupLock: (() => void) | null = null
|
|
241
|
+
let cleanupBodyHeight: (() => void) | null = null
|
|
242
|
+
let generationAbortController: AbortController | null = null
|
|
243
|
+
/** 章節進入 CREATING 的時間戳(僅觀測;不因超時改 status) */
|
|
244
|
+
const creatingStartedAt = new Map<string, number>()
|
|
245
|
+
let creatingTimeoutTimer = 0
|
|
246
|
+
|
|
247
|
+
// 視頻已刪:guide.mp4 相關暫時注釋
|
|
248
|
+
// function ensureVideoPlaying() {
|
|
249
|
+
// const el = videoEl.value
|
|
250
|
+
// if (!el) return
|
|
251
|
+
// el.muted = true
|
|
252
|
+
// el.loop = true
|
|
253
|
+
// const playPromise = el.play()
|
|
254
|
+
// if (playPromise && typeof playPromise.then === 'function') {
|
|
255
|
+
// void playPromise.catch(() => {
|
|
256
|
+
// window.setTimeout(() => {
|
|
257
|
+
// void videoEl.value?.play()?.catch(() => undefined)
|
|
258
|
+
// }, 120)
|
|
259
|
+
// })
|
|
260
|
+
// }
|
|
261
|
+
// }
|
|
262
|
+
|
|
263
|
+
// function initGeneratingVideo() {
|
|
264
|
+
// const el = videoEl.value
|
|
265
|
+
// if (!el) return
|
|
266
|
+
//
|
|
267
|
+
// el.muted = true
|
|
268
|
+
// el.defaultMuted = true
|
|
269
|
+
// el.autoplay = true
|
|
270
|
+
// el.loop = true
|
|
271
|
+
// el.playsInline = true
|
|
272
|
+
// el.setAttribute('muted', '')
|
|
273
|
+
// el.setAttribute('playsinline', '')
|
|
274
|
+
// el.setAttribute('webkit-playsinline', '')
|
|
275
|
+
//
|
|
276
|
+
// el.addEventListener('loadeddata', ensureVideoPlaying)
|
|
277
|
+
// el.addEventListener('canplay', ensureVideoPlaying)
|
|
278
|
+
// el.addEventListener('ended', onVideoEnded)
|
|
279
|
+
// ensureVideoPlaying()
|
|
280
|
+
// }
|
|
281
|
+
|
|
282
|
+
// function onVideoEnded() {
|
|
283
|
+
// const el = videoEl.value
|
|
284
|
+
// if (!el) return
|
|
285
|
+
// el.currentTime = 0
|
|
286
|
+
// ensureVideoPlaying()
|
|
287
|
+
// }
|
|
288
|
+
|
|
289
|
+
// function disposeGeneratingVideo() {
|
|
290
|
+
// const el = videoEl.value
|
|
291
|
+
// if (!el) return
|
|
292
|
+
// el.removeEventListener('loadeddata', ensureVideoPlaying)
|
|
293
|
+
// el.removeEventListener('canplay', ensureVideoPlaying)
|
|
294
|
+
// el.removeEventListener('ended', onVideoEnded)
|
|
295
|
+
// el.pause()
|
|
296
|
+
// }
|
|
297
|
+
|
|
298
|
+
function delay(ms: number) {
|
|
299
|
+
return new Promise<void>((resolve) => {
|
|
300
|
+
window.setTimeout(resolve, ms)
|
|
301
|
+
})
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function throwIfAborted(signal: AbortSignal) {
|
|
305
|
+
if (signal.aborted) {
|
|
306
|
+
throw new DOMException('章節生成已取消', 'AbortError')
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function mapCreatedFlag(flag: GenAiSectionCreatedFlag | string): ChapterGenerationStatus {
|
|
311
|
+
// 僅認接口明確返回值;缺省 / 未知一律 pending(叉號必須 createdFlag=FAILURE)
|
|
312
|
+
const key = String(flag ?? '')
|
|
313
|
+
.trim()
|
|
314
|
+
.toUpperCase() as GenAiSectionCreatedFlag
|
|
315
|
+
return CREATED_FLAG_STATUS_MAP[key] ?? 'pending'
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function resolveOrderSeq(raw: unknown, fallback: number): number {
|
|
319
|
+
const n = typeof raw === 'number' ? raw : Number.parseInt(String(raw ?? ''), 10)
|
|
320
|
+
return Number.isFinite(n) && n > 0 ? n : fallback
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* 按 orderSeq 排序;同 sectionId 去重;保證 orderSeq 唯一不亂序
|
|
325
|
+
*/
|
|
326
|
+
function normalizeSectionList(list: GenAiGeneratingSectionItem[]): GenAiGeneratingSectionItem[] {
|
|
327
|
+
const byId = new Map<string, GenAiGeneratingSectionItem>()
|
|
328
|
+
|
|
329
|
+
list.forEach((raw, index) => {
|
|
330
|
+
const sectionId = String(raw?.sectionId ?? '').trim()
|
|
331
|
+
if (!sectionId || byId.has(sectionId)) return
|
|
332
|
+
byId.set(sectionId, {
|
|
333
|
+
sectionId,
|
|
334
|
+
sectionName: String(raw?.sectionName ?? ''),
|
|
335
|
+
orderSeq: resolveOrderSeq(raw?.orderSeq, index + 1),
|
|
336
|
+
createdFlag: raw?.createdFlag,
|
|
337
|
+
})
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
const sorted = [...byId.values()].sort((a, b) => {
|
|
341
|
+
if (a.orderSeq !== b.orderSeq) return a.orderSeq - b.orderSeq
|
|
342
|
+
return a.sectionId.localeCompare(b.sectionId, 'en', { numeric: true })
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
const used = new Set<number>()
|
|
346
|
+
return sorted.map((item, index) => {
|
|
347
|
+
let orderSeq = item.orderSeq
|
|
348
|
+
if (used.has(orderSeq)) {
|
|
349
|
+
orderSeq = index + 1
|
|
350
|
+
while (used.has(orderSeq)) orderSeq += 1
|
|
351
|
+
}
|
|
352
|
+
used.add(orderSeq)
|
|
353
|
+
return { ...item, orderSeq }
|
|
354
|
+
})
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function sortSectionList(list: GenAiGeneratingSectionItem[]) {
|
|
358
|
+
return normalizeSectionList(list)
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function toChapterItems(list: GenAiGeneratingSectionItem[]): ChapterGenerationItem[] {
|
|
362
|
+
return normalizeSectionList(list).map((item) => ({
|
|
363
|
+
id: item.sectionId,
|
|
364
|
+
sectionId: item.sectionId,
|
|
365
|
+
sectionName: item.sectionName,
|
|
366
|
+
orderSeq: item.orderSeq,
|
|
367
|
+
status: mapCreatedFlag(item.createdFlag),
|
|
368
|
+
}))
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function resolveGeneratingParams(): genAiSearchReportParams {
|
|
372
|
+
const categoryRaw = route.query.categoryId
|
|
373
|
+
const instanceRaw = route.query.instanceId
|
|
374
|
+
const categoryId =
|
|
375
|
+
typeof categoryRaw === 'string' && categoryRaw
|
|
376
|
+
? Number.parseInt(categoryRaw, 10)
|
|
377
|
+
: Number.parseInt(templateId.value, 10)
|
|
378
|
+
|
|
379
|
+
if (typeof instanceRaw === 'string' && instanceRaw) {
|
|
380
|
+
instanceId.value = instanceRaw
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return {
|
|
384
|
+
categoryId: Number.isFinite(categoryId) && categoryId > 0 ? categoryId : 1,
|
|
385
|
+
instanceId: instanceId.value,
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* 發送請求封裝:僅 path + 業務參數(超時由網關處理,不傳 timeoutMs)
|
|
391
|
+
*/
|
|
392
|
+
const genAiCreateReportApi = {
|
|
393
|
+
getGeneratingSection(params: { categoryId: number; instanceId: string }) {
|
|
394
|
+
return postGenAiRequest('/generatingSection', params)
|
|
395
|
+
},
|
|
396
|
+
getGeneratingSectionStatus(params: {
|
|
397
|
+
categoryId: number
|
|
398
|
+
instanceId: string
|
|
399
|
+
sectionIdList: string[]
|
|
400
|
+
}) {
|
|
401
|
+
return postGenAiRequest('/generatingSectionStatus', params)
|
|
402
|
+
},
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/** success === false 或非 200 視為失敗,呼叫端應停止後續連續請求 */
|
|
406
|
+
function isGenAiResponseOk(res: any): boolean {
|
|
407
|
+
if (res == null) return false
|
|
408
|
+
if (res.success === false) return false
|
|
409
|
+
if (res.success === true) return true
|
|
410
|
+
return String(res.code) === '200'
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* —— 發送方式 / 參數 / res 結構不可改 ——
|
|
415
|
+
* success === false 或非 200 時返回 ok:false,呼叫端停止後續連續請求
|
|
416
|
+
*/
|
|
417
|
+
const generatingSection = async (params: genAiSearchReportParams) => {
|
|
418
|
+
const res: any = await genAiCreateReportApi
|
|
419
|
+
.getGeneratingSection({
|
|
420
|
+
categoryId: params.categoryId,
|
|
421
|
+
instanceId: params.instanceId,
|
|
422
|
+
})
|
|
423
|
+
.catch((err) => {
|
|
424
|
+
console.error('接口異常', err)
|
|
425
|
+
return null
|
|
426
|
+
})
|
|
427
|
+
|
|
428
|
+
try {
|
|
429
|
+
if (!isGenAiResponseOk(res)) {
|
|
430
|
+
ElMessage.warning(res?.message || '接口異常')
|
|
431
|
+
return {
|
|
432
|
+
ok: false as const,
|
|
433
|
+
sectionList: [] as GenAiGeneratingSectionItem[],
|
|
434
|
+
completed: false,
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const sectionList = normalizeSectionList(
|
|
439
|
+
(res.data?.sectionList ?? []) as GenAiGeneratingSectionItem[]
|
|
440
|
+
)
|
|
441
|
+
const allFinish = !!res.data?.completed
|
|
442
|
+
if (res.data?.instanceId) {
|
|
443
|
+
instanceId.value = String(res.data.instanceId)
|
|
444
|
+
void syncInstanceIdToRoute(instanceId.value)
|
|
445
|
+
}
|
|
446
|
+
console.log('分段列表', sectionList, '是否全部完成', allFinish)
|
|
447
|
+
return { ok: true as const, sectionList, completed: allFinish }
|
|
448
|
+
} catch (err) {
|
|
449
|
+
console.error('接口異常', err)
|
|
450
|
+
ElMessage.warning('接口異常')
|
|
451
|
+
return { ok: false as const, sectionList: [] as GenAiGeneratingSectionItem[], completed: false }
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const generatingSectionStatus = async (params: {
|
|
456
|
+
categoryId: number
|
|
457
|
+
instanceId: string
|
|
458
|
+
sectionIdList: string[]
|
|
459
|
+
}) => {
|
|
460
|
+
// 傳入 id 順序保證按當前章節 orderSeq 由小到大
|
|
461
|
+
const orderedSectionIdList = (() => {
|
|
462
|
+
const idSet = new Set(params.sectionIdList.map(String))
|
|
463
|
+
const fromChapters = [...chapters.value]
|
|
464
|
+
.filter((item) => idSet.has(item.id))
|
|
465
|
+
.sort((a, b) => {
|
|
466
|
+
if (a.orderSeq !== b.orderSeq) return a.orderSeq - b.orderSeq
|
|
467
|
+
return a.sectionId.localeCompare(b.sectionId, 'en', { numeric: true })
|
|
468
|
+
})
|
|
469
|
+
.map((item) => item.id)
|
|
470
|
+
if (fromChapters.length === idSet.size) return fromChapters
|
|
471
|
+
// 章節尚未寫入時,保持傳入順序(呼叫方 buildSectionIdList 已按 orderSeq 排好)
|
|
472
|
+
return params.sectionIdList.map(String)
|
|
473
|
+
})()
|
|
474
|
+
|
|
475
|
+
const res: any = await genAiCreateReportApi
|
|
476
|
+
.getGeneratingSectionStatus({
|
|
477
|
+
categoryId: params.categoryId,
|
|
478
|
+
instanceId: params.instanceId,
|
|
479
|
+
sectionIdList: orderedSectionIdList,
|
|
480
|
+
})
|
|
481
|
+
.catch((err) => {
|
|
482
|
+
console.error('接口異常', err)
|
|
483
|
+
return null
|
|
484
|
+
})
|
|
485
|
+
|
|
486
|
+
try {
|
|
487
|
+
if (!isGenAiResponseOk(res)) {
|
|
488
|
+
ElMessage.warning(res?.message || '接口異常')
|
|
489
|
+
return { ok: false as const, list: [] as GenAiGeneratingSectionItem[] }
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
return {
|
|
493
|
+
ok: true as const,
|
|
494
|
+
list: normalizeSectionList(
|
|
495
|
+
(Array.isArray(res.data) ? res.data : []) as GenAiGeneratingSectionItem[]
|
|
496
|
+
),
|
|
497
|
+
}
|
|
498
|
+
} catch (err) {
|
|
499
|
+
console.error('接口異常', err)
|
|
500
|
+
ElMessage.warning('接口異常')
|
|
501
|
+
return { ok: false as const, list: [] as GenAiGeneratingSectionItem[] }
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const chapters = ref<ChapterGenerationItem[]>([])
|
|
506
|
+
const isGenerating = ref(false)
|
|
507
|
+
const isComplete = ref(false)
|
|
508
|
+
const retrying = ref(false)
|
|
509
|
+
/** 正在請求 / 輪詢 getGeneratingSection 時為 true,期間不展示 footer */
|
|
510
|
+
const isSectionRequesting = ref(false)
|
|
511
|
+
|
|
512
|
+
const chapterRowEls = new Map<string, HTMLElement>()
|
|
513
|
+
|
|
514
|
+
function bindChapterRowEl(id: string, el: unknown) {
|
|
515
|
+
if (el instanceof HTMLElement) chapterRowEls.set(id, el)
|
|
516
|
+
else chapterRowEls.delete(id)
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const visibleChapters = computed(() =>
|
|
520
|
+
[...chapters.value].sort((a, b) => {
|
|
521
|
+
if (a.orderSeq !== b.orderSeq) return a.orderSeq - b.orderSeq
|
|
522
|
+
return a.sectionId.localeCompare(b.sectionId, 'en', { numeric: true })
|
|
523
|
+
})
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
/** 當前正在生成的章節(creating) */
|
|
527
|
+
const activeCreatingChapterId = computed(
|
|
528
|
+
() => visibleChapters.value.find((item) => item.status === 'creating')?.id ?? null
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
/** 滾動條滾到當前生成中的章節行 */
|
|
532
|
+
function scrollToActiveCreatingChapter() {
|
|
533
|
+
const id = activeCreatingChapterId.value
|
|
534
|
+
if (!id) return
|
|
535
|
+
|
|
536
|
+
void nextTick(() => {
|
|
537
|
+
const row =
|
|
538
|
+
chapterRowEls.get(id) ??
|
|
539
|
+
(document.querySelector(
|
|
540
|
+
`.report-generating-chapters__row[data-chapter-id="${CSS.escape(id)}"]`
|
|
541
|
+
) as HTMLElement | null)
|
|
542
|
+
if (!row) return
|
|
543
|
+
|
|
544
|
+
row.scrollIntoView({ block: 'center', behavior: 'smooth', inline: 'nearest' })
|
|
545
|
+
})
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
watch(activeCreatingChapterId, (id) => {
|
|
549
|
+
if (id) scrollToActiveCreatingChapter()
|
|
550
|
+
})
|
|
551
|
+
|
|
552
|
+
/** 生成頁 height / min-height 相對視口固定扣減;章節溢出時由 __body 內部滾動 */
|
|
553
|
+
const GENERATING_VIEWPORT_OFFSET_PX = 110
|
|
554
|
+
|
|
555
|
+
const doneCount = computed(() => chapters.value.filter((item) => item.status === 'success').length)
|
|
556
|
+
const failedCount = computed(
|
|
557
|
+
() => chapters.value.filter((item) => item.status === 'failure').length
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
/** 至少有一章成功才可進入編輯;全部失敗時置灰不可點 */
|
|
561
|
+
const canEnterEditing = computed(() => doneCount.value > 0)
|
|
562
|
+
|
|
563
|
+
const completionSummary = computed(
|
|
564
|
+
() => `報告生成完畢,${doneCount.value}個章節已完成,${failedCount.value}個章節失敗`
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
const heroTitle = computed(() =>
|
|
568
|
+
isComplete.value && !isGenerating.value && !isSectionRequesting.value
|
|
569
|
+
? '報告生成完畢'
|
|
570
|
+
: '報告生成中...'
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
/** 僅在 generatingSection 請求全部結束後顯示底部操作區 */
|
|
574
|
+
const showActionFooter = computed(
|
|
575
|
+
() => isComplete.value && !isGenerating.value && !isSectionRequesting.value && !retrying.value
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
function getGeneratingHostEl(page: HTMLElement) {
|
|
579
|
+
return page.closest('.report-edit-page__generating') as HTMLElement | null
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function clearGeneratingHeightStyles(page: HTMLElement) {
|
|
583
|
+
page.style.height = ''
|
|
584
|
+
page.style.maxHeight = ''
|
|
585
|
+
page.style.minHeight = ''
|
|
586
|
+
const host = getGeneratingHostEl(page)
|
|
587
|
+
if (host) {
|
|
588
|
+
host.style.height = ''
|
|
589
|
+
host.style.maxHeight = ''
|
|
590
|
+
host.style.minHeight = ''
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/** 視口高度減去固定 110px 後寫入 page + report-edit-page__generating(始終貼合視口) */
|
|
595
|
+
function applyGeneratingViewportHeights(page: HTMLElement) {
|
|
596
|
+
const heightPx = Math.max(0, getViewportHeight() - GENERATING_VIEWPORT_OFFSET_PX)
|
|
597
|
+
const height = `${heightPx}px`
|
|
598
|
+
const host = getGeneratingHostEl(page)
|
|
599
|
+
|
|
600
|
+
page.style.height = height
|
|
601
|
+
page.style.maxHeight = height
|
|
602
|
+
page.style.minHeight = height
|
|
603
|
+
if (host) {
|
|
604
|
+
host.style.height = height
|
|
605
|
+
host.style.maxHeight = height
|
|
606
|
+
host.style.minHeight = height
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
async function syncViewportHeightMode() {
|
|
611
|
+
await nextTick()
|
|
612
|
+
await new Promise<void>((resolve) => {
|
|
613
|
+
cancelAnimationFrame(measureRafId)
|
|
614
|
+
measureRafId = requestAnimationFrame(() => {
|
|
615
|
+
measureRafId = requestAnimationFrame(() => resolve())
|
|
616
|
+
})
|
|
617
|
+
})
|
|
618
|
+
|
|
619
|
+
const page = pageRef.value
|
|
620
|
+
if (!page) return
|
|
621
|
+
|
|
622
|
+
cleanupBodyHeight?.()
|
|
623
|
+
cleanupBodyHeight = null
|
|
624
|
+
|
|
625
|
+
applyGeneratingViewportHeights(page)
|
|
626
|
+
const update = () => applyGeneratingViewportHeights(page)
|
|
627
|
+
window.addEventListener('resize', update)
|
|
628
|
+
cleanupBodyHeight = () => {
|
|
629
|
+
window.removeEventListener('resize', update)
|
|
630
|
+
clearGeneratingHeightStyles(page)
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function scheduleSyncViewportHeightMode() {
|
|
635
|
+
cancelAnimationFrame(measureRafId)
|
|
636
|
+
measureRafId = requestAnimationFrame(() => {
|
|
637
|
+
void syncViewportHeightMode()
|
|
638
|
+
})
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function clearCreatingTimeoutState(ids?: string[]) {
|
|
642
|
+
if (!ids?.length) {
|
|
643
|
+
creatingStartedAt.clear()
|
|
644
|
+
return
|
|
645
|
+
}
|
|
646
|
+
for (const id of ids) {
|
|
647
|
+
creatingStartedAt.delete(id)
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function stopCreatingTimeoutWatcher() {
|
|
652
|
+
if (creatingTimeoutTimer) {
|
|
653
|
+
clearInterval(creatingTimeoutTimer)
|
|
654
|
+
creatingTimeoutTimer = 0
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function startCreatingTimeoutWatcher() {
|
|
659
|
+
stopCreatingTimeoutWatcher()
|
|
660
|
+
creatingTimeoutTimer = window.setInterval(() => {
|
|
661
|
+
applyCreatingTimeouts()
|
|
662
|
+
}, 1000)
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/** 同步 CREATING 計時觀測;不把本地超時寫成 failure(叉號只認接口 createdFlag=FAILURE) */
|
|
666
|
+
function applyCreatingTimeouts() {
|
|
667
|
+
const now = Date.now()
|
|
668
|
+
void SECTION_CREATING_TIMEOUT_MS
|
|
669
|
+
|
|
670
|
+
for (const chapter of chapters.value) {
|
|
671
|
+
if (chapter.status === 'creating') {
|
|
672
|
+
if (!creatingStartedAt.has(chapter.id)) {
|
|
673
|
+
creatingStartedAt.set(chapter.id, now)
|
|
674
|
+
}
|
|
675
|
+
continue
|
|
676
|
+
}
|
|
677
|
+
creatingStartedAt.delete(chapter.id)
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function applySectionList(sectionList: GenAiGeneratingSectionItem[]) {
|
|
682
|
+
const prevCount = chapters.value.length
|
|
683
|
+
const nextChapters = toChapterItems(sectionList)
|
|
684
|
+
const prevMap = new Map(chapters.value.map((row) => [row.id, row] as const))
|
|
685
|
+
|
|
686
|
+
chapters.value = nextChapters.map((item) => {
|
|
687
|
+
const prev = prevMap.get(item.id)
|
|
688
|
+
// 前一次已是 success:後續輪詢不再改動該章節(含 orderSeq)
|
|
689
|
+
if (prev?.status === 'success') {
|
|
690
|
+
return prev
|
|
691
|
+
}
|
|
692
|
+
return {
|
|
693
|
+
...item,
|
|
694
|
+
// 已有 orderSeq 則保持,避免輪詢改序錯亂
|
|
695
|
+
orderSeq: prev?.orderSeq && prev.orderSeq > 0 ? prev.orderSeq : item.orderSeq,
|
|
696
|
+
body: item.status === 'success' ? prev?.body : undefined,
|
|
697
|
+
}
|
|
698
|
+
})
|
|
699
|
+
// 最終再按 orderSeq 排序,防止合併後錯亂
|
|
700
|
+
chapters.value = [...chapters.value].sort((a, b) => {
|
|
701
|
+
if (a.orderSeq !== b.orderSeq) return a.orderSeq - b.orderSeq
|
|
702
|
+
return a.sectionId.localeCompare(b.sectionId, 'en', { numeric: true })
|
|
703
|
+
})
|
|
704
|
+
applyCreatingTimeouts()
|
|
705
|
+
// 章節數量變化才重測;純狀態輪詢不觸發,避免視頻跳動
|
|
706
|
+
if (chapters.value.length !== prevCount) {
|
|
707
|
+
scheduleSyncViewportHeightMode()
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function applySectionStatus(statusList: GenAiGeneratingSectionItem[]) {
|
|
712
|
+
if (!statusList.length) return
|
|
713
|
+
|
|
714
|
+
const statusMap = new Map(
|
|
715
|
+
sortSectionList(statusList).map((item) => [String(item.sectionId), item] as const)
|
|
716
|
+
)
|
|
717
|
+
|
|
718
|
+
chapters.value = chapters.value.map((chapter) => {
|
|
719
|
+
// 前一次已是 success:後續輪詢不再改動該章節
|
|
720
|
+
if (chapter.status === 'success') return chapter
|
|
721
|
+
|
|
722
|
+
const next = statusMap.get(chapter.id)
|
|
723
|
+
if (!next) return chapter
|
|
724
|
+
const status = mapCreatedFlag(next.createdFlag)
|
|
725
|
+
|
|
726
|
+
return {
|
|
727
|
+
...chapter,
|
|
728
|
+
sectionName: next.sectionName || chapter.sectionName,
|
|
729
|
+
// status 輪詢不改 orderSeq,避免序號重複/錯亂
|
|
730
|
+
status,
|
|
731
|
+
body: status === 'success' ? chapter.body : undefined,
|
|
732
|
+
}
|
|
733
|
+
})
|
|
734
|
+
|
|
735
|
+
applyCreatingTimeouts()
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/** 接口 sectionList 是否全部為 FAILURE(此情況不再繼續請求 generatingSection) */
|
|
739
|
+
function isSectionListAllFailure(sectionList: GenAiGeneratingSectionItem[]) {
|
|
740
|
+
const list = normalizeSectionList(sectionList)
|
|
741
|
+
if (!list.length) return false
|
|
742
|
+
return list.every((item) => mapCreatedFlag(item.createdFlag) === 'failure')
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
function buildSectionIdList(sectionList: GenAiGeneratingSectionItem[]) {
|
|
746
|
+
// generatingSectionStatus 的 sectionIdList 必須按 orderSeq 由小到大
|
|
747
|
+
return normalizeSectionList(sectionList).map((item) => String(item.sectionId))
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function stopGeneratingFlow() {
|
|
751
|
+
isSectionRequesting.value = false
|
|
752
|
+
isComplete.value = true
|
|
753
|
+
isGenerating.value = false
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* 流程:
|
|
758
|
+
* 1. 第一次請求 generatingSection,拿到 sectionList
|
|
759
|
+
* 2. 用 sectionList 拼裝 sectionIdList,generatingSectionStatus 只請求一次
|
|
760
|
+
* 3. 之後每 2s 只輪詢 generatingSection;僅 completed / 全部 FAILURE / 報錯 / 取消時停止
|
|
761
|
+
*/
|
|
762
|
+
async function runGeneratingFlow(signal: AbortSignal) {
|
|
763
|
+
isSectionRequesting.value = true
|
|
764
|
+
// —— 第一次:generatingSection → 拼裝 sectionIdList → status 僅一次 ——
|
|
765
|
+
throwIfAborted(signal)
|
|
766
|
+
let params = resolveGeneratingParams()
|
|
767
|
+
const firstSection = await generatingSection(params)
|
|
768
|
+
throwIfAborted(signal)
|
|
769
|
+
|
|
770
|
+
if (!firstSection.ok) {
|
|
771
|
+
stopGeneratingFlow()
|
|
772
|
+
return
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
applySectionList(firstSection.sectionList)
|
|
776
|
+
|
|
777
|
+
// 全部 FAILURE:不再打 status,也不再輪詢 generatingSection
|
|
778
|
+
if (isSectionListAllFailure(firstSection.sectionList)) {
|
|
779
|
+
stopGeneratingFlow()
|
|
780
|
+
return
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
const sectionIdList = buildSectionIdList(firstSection.sectionList)
|
|
784
|
+
|
|
785
|
+
if (sectionIdList.length) {
|
|
786
|
+
const firstStatus = await generatingSectionStatus({
|
|
787
|
+
categoryId: params.categoryId,
|
|
788
|
+
instanceId: instanceId.value,
|
|
789
|
+
sectionIdList,
|
|
790
|
+
})
|
|
791
|
+
throwIfAborted(signal)
|
|
792
|
+
if (!firstStatus.ok) {
|
|
793
|
+
stopGeneratingFlow()
|
|
794
|
+
return
|
|
795
|
+
}
|
|
796
|
+
applySectionStatus(firstStatus.list)
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// 僅認接口 completed;未完成則進入持續輪詢
|
|
800
|
+
if (firstSection.completed) {
|
|
801
|
+
stopGeneratingFlow()
|
|
802
|
+
return
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// —— 後續只輪詢 generatingSection(不再打 status);無次數上限 ——
|
|
806
|
+
while (true) {
|
|
807
|
+
throwIfAborted(signal)
|
|
808
|
+
await delay(SECTION_POLL_INTERVAL_MS)
|
|
809
|
+
throwIfAborted(signal)
|
|
810
|
+
|
|
811
|
+
params = resolveGeneratingParams()
|
|
812
|
+
const sectionRes = await generatingSection(params)
|
|
813
|
+
throwIfAborted(signal)
|
|
814
|
+
if (!sectionRes.ok) {
|
|
815
|
+
stopGeneratingFlow()
|
|
816
|
+
return
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
applySectionList(sectionRes.sectionList)
|
|
820
|
+
|
|
821
|
+
if (sectionRes.completed || isSectionListAllFailure(sectionRes.sectionList)) {
|
|
822
|
+
stopGeneratingFlow()
|
|
823
|
+
return
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
/**
|
|
829
|
+
* 重試失敗章節(每次點擊完整走一遍):
|
|
830
|
+
* 1. 先調用 generatingSection
|
|
831
|
+
* 2. 無論 completed true / false(含首包仍全 FAILURE)→ 都先調用一次 generatingSectionStatus
|
|
832
|
+
* 3. 再每 2s 循環輪詢 generatingSection
|
|
833
|
+
* 進頁 runGeneratingFlow 不變;已成功章節不改。
|
|
834
|
+
*/
|
|
835
|
+
async function pollFailedChaptersBySection(signal: AbortSignal, failedIds: string[]) {
|
|
836
|
+
if (!failedIds.length) return
|
|
837
|
+
|
|
838
|
+
isSectionRequesting.value = true
|
|
839
|
+
|
|
840
|
+
// —— 第一次:generatingSection ——
|
|
841
|
+
throwIfAborted(signal)
|
|
842
|
+
let params = resolveGeneratingParams()
|
|
843
|
+
const firstSection = await generatingSection(params)
|
|
844
|
+
throwIfAborted(signal)
|
|
845
|
+
|
|
846
|
+
if (!firstSection.ok) {
|
|
847
|
+
stopGeneratingFlow()
|
|
848
|
+
return
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
applySectionList(firstSection.sectionList)
|
|
852
|
+
|
|
853
|
+
// 無論 completed / 是否全 FAILURE:都先打一次 status(修復重試首包仍 FAILURE 時被提前 return 跳過 status)
|
|
854
|
+
const sectionIdList = buildSectionIdList(firstSection.sectionList)
|
|
855
|
+
const idsForStatus = sectionIdList.length ? sectionIdList : failedIds.map(String)
|
|
856
|
+
if (idsForStatus.length) {
|
|
857
|
+
const firstStatus = await generatingSectionStatus({
|
|
858
|
+
categoryId: params.categoryId,
|
|
859
|
+
instanceId: instanceId.value,
|
|
860
|
+
sectionIdList: idsForStatus,
|
|
861
|
+
})
|
|
862
|
+
throwIfAborted(signal)
|
|
863
|
+
if (!firstStatus.ok) {
|
|
864
|
+
stopGeneratingFlow()
|
|
865
|
+
return
|
|
866
|
+
}
|
|
867
|
+
applySectionStatus(firstStatus.list)
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// —— 後續只輪詢 generatingSection(不再打 status);無次數上限 ——
|
|
871
|
+
while (true) {
|
|
872
|
+
throwIfAborted(signal)
|
|
873
|
+
await delay(SECTION_POLL_INTERVAL_MS)
|
|
874
|
+
throwIfAborted(signal)
|
|
875
|
+
|
|
876
|
+
params = resolveGeneratingParams()
|
|
877
|
+
const sectionRes = await generatingSection(params)
|
|
878
|
+
throwIfAborted(signal)
|
|
879
|
+
if (!sectionRes.ok) {
|
|
880
|
+
stopGeneratingFlow()
|
|
881
|
+
return
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
applySectionList(sectionRes.sectionList)
|
|
885
|
+
|
|
886
|
+
if (sectionRes.completed || isSectionListAllFailure(sectionRes.sectionList)) {
|
|
887
|
+
stopGeneratingFlow()
|
|
888
|
+
return
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
async function startGeneration() {
|
|
894
|
+
generationAbortController?.abort()
|
|
895
|
+
const controller = new AbortController()
|
|
896
|
+
generationAbortController = controller
|
|
897
|
+
|
|
898
|
+
clearCreatingTimeoutState()
|
|
899
|
+
chapters.value = []
|
|
900
|
+
isComplete.value = false
|
|
901
|
+
isGenerating.value = true
|
|
902
|
+
isSectionRequesting.value = true
|
|
903
|
+
startCreatingTimeoutWatcher()
|
|
904
|
+
|
|
905
|
+
try {
|
|
906
|
+
await runGeneratingFlow(controller.signal)
|
|
907
|
+
} catch (error) {
|
|
908
|
+
if (error instanceof DOMException && error.name === 'AbortError') {
|
|
909
|
+
isSectionRequesting.value = false
|
|
910
|
+
return
|
|
911
|
+
}
|
|
912
|
+
isSectionRequesting.value = false
|
|
913
|
+
throw error
|
|
914
|
+
} finally {
|
|
915
|
+
stopCreatingTimeoutWatcher()
|
|
916
|
+
applyCreatingTimeouts()
|
|
917
|
+
if (!controller.signal.aborted) {
|
|
918
|
+
isGenerating.value = false
|
|
919
|
+
isSectionRequesting.value = false
|
|
920
|
+
// 流程正常結束時 stopGeneratingFlow 已設 isComplete;異常中斷也需收斂請求態
|
|
921
|
+
if (!isComplete.value && chapters.value.length) {
|
|
922
|
+
isComplete.value = true
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
scheduleSyncViewportHeightMode()
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
function buildChapterBody(chapter: ChapterGenerationItem) {
|
|
930
|
+
// 僅用接口/會話寫入的 body;無正文則空(不回落本地假文案)
|
|
931
|
+
return chapter.body || ''
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
function persistGeneratingSession() {
|
|
935
|
+
const tid = String(templateId.value || selectedTemplate.value?.id || '')
|
|
936
|
+
if (!tid || !chapters.value.length) return
|
|
937
|
+
saveGeneratingSession({
|
|
938
|
+
templateId: tid,
|
|
939
|
+
instanceId: instanceId.value,
|
|
940
|
+
chapters: chapters.value.map(
|
|
941
|
+
(chapter): GeneratingSessionChapter => ({
|
|
942
|
+
id: chapter.id,
|
|
943
|
+
sectionId: chapter.sectionId,
|
|
944
|
+
sectionName: chapter.sectionName,
|
|
945
|
+
orderSeq: chapter.orderSeq,
|
|
946
|
+
status: chapter.status,
|
|
947
|
+
body: chapter.body,
|
|
948
|
+
})
|
|
949
|
+
),
|
|
950
|
+
isComplete: isComplete.value,
|
|
951
|
+
})
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
async function syncInstanceIdToRoute(id: string) {
|
|
955
|
+
if (!id || route.query.instanceId === id) return
|
|
956
|
+
// 強制保持 generating,避免遷移後 query 拼接把 step 沖掉
|
|
957
|
+
await navigateToReportEdit(
|
|
958
|
+
router,
|
|
959
|
+
'generating',
|
|
960
|
+
typeof route.query.templateId === 'string' ? route.query.templateId : undefined,
|
|
961
|
+
true,
|
|
962
|
+
undefined,
|
|
963
|
+
id
|
|
964
|
+
)
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
function enterEditing() {
|
|
968
|
+
// 僅在仍在生成 / 輪詢時攔截;按鈕已常駐可點,遷移/接口失敗時也要能進編輯頁
|
|
969
|
+
if (isGenerating.value || retrying.value || isSectionRequesting.value) {
|
|
970
|
+
ElMessage.warning('報告仍在生成中,請稍候再進入編輯')
|
|
971
|
+
return
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
if (!selectedTemplate.value) {
|
|
975
|
+
ElMessage.warning('缺少報告模板,無法進入編輯')
|
|
976
|
+
return
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
const orderedChapters = [...chapters.value].sort((a, b) => {
|
|
980
|
+
if (a.orderSeq !== b.orderSeq) return a.orderSeq - b.orderSeq
|
|
981
|
+
return a.sectionId.localeCompare(b.sectionId, 'en', { numeric: true })
|
|
982
|
+
})
|
|
983
|
+
|
|
984
|
+
if (orderedChapters.length) {
|
|
985
|
+
setGeneratedChaptersFromApi(
|
|
986
|
+
orderedChapters.map((chapter) => {
|
|
987
|
+
const label =
|
|
988
|
+
String(chapter.sectionName || '')
|
|
989
|
+
.replace(/^\d+\.\s*/, '')
|
|
990
|
+
.trim() || chapter.sectionName
|
|
991
|
+
return {
|
|
992
|
+
title: `${chapter.orderSeq}. ${label}`,
|
|
993
|
+
body: buildChapterBody(chapter),
|
|
994
|
+
}
|
|
995
|
+
})
|
|
996
|
+
)
|
|
997
|
+
} else {
|
|
998
|
+
// 接口無章節:空列表進入編輯(不回落本地假標題)
|
|
999
|
+
setGeneratedChaptersFromApi([])
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
markGenerationSuccess()
|
|
1003
|
+
clearGeneratingSession()
|
|
1004
|
+
emit('enter-editing')
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
async function retryFailedChapters() {
|
|
1008
|
+
if (isGenerating.value || retrying.value || !failedCount.value) return
|
|
1009
|
+
|
|
1010
|
+
const failedIds = [...chapters.value]
|
|
1011
|
+
.filter((item) => item.status === 'failure')
|
|
1012
|
+
.sort((a, b) => {
|
|
1013
|
+
if (a.orderSeq !== b.orderSeq) return a.orderSeq - b.orderSeq
|
|
1014
|
+
return a.sectionId.localeCompare(b.sectionId, 'en', { numeric: true })
|
|
1015
|
+
})
|
|
1016
|
+
.map((item) => item.id)
|
|
1017
|
+
|
|
1018
|
+
if (!failedIds.length) return
|
|
1019
|
+
|
|
1020
|
+
generationAbortController?.abort()
|
|
1021
|
+
const controller = new AbortController()
|
|
1022
|
+
generationAbortController = controller
|
|
1023
|
+
|
|
1024
|
+
retrying.value = true
|
|
1025
|
+
isGenerating.value = true
|
|
1026
|
+
isComplete.value = false
|
|
1027
|
+
isSectionRequesting.value = true
|
|
1028
|
+
clearCreatingTimeoutState(failedIds)
|
|
1029
|
+
|
|
1030
|
+
chapters.value = chapters.value.map((item) =>
|
|
1031
|
+
item.status === 'failure' ? { ...item, status: 'pending', body: undefined } : item
|
|
1032
|
+
)
|
|
1033
|
+
startCreatingTimeoutWatcher()
|
|
1034
|
+
scheduleSyncViewportHeightMode()
|
|
1035
|
+
|
|
1036
|
+
try {
|
|
1037
|
+
await pollFailedChaptersBySection(controller.signal, failedIds)
|
|
1038
|
+
} catch (error) {
|
|
1039
|
+
if (error instanceof DOMException && error.name === 'AbortError') {
|
|
1040
|
+
isSectionRequesting.value = false
|
|
1041
|
+
return
|
|
1042
|
+
}
|
|
1043
|
+
isSectionRequesting.value = false
|
|
1044
|
+
throw error
|
|
1045
|
+
} finally {
|
|
1046
|
+
stopCreatingTimeoutWatcher()
|
|
1047
|
+
applyCreatingTimeouts()
|
|
1048
|
+
retrying.value = false
|
|
1049
|
+
if (!controller.signal.aborted) {
|
|
1050
|
+
isGenerating.value = false
|
|
1051
|
+
isSectionRequesting.value = false
|
|
1052
|
+
if (!isComplete.value) {
|
|
1053
|
+
isComplete.value = true
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
scheduleSyncViewportHeightMode()
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
onMounted(() => {
|
|
1061
|
+
loadTemplateFromSession()
|
|
1062
|
+
cleanupLock = lockDocumentScroll()
|
|
1063
|
+
window.addEventListener('resize', scheduleSyncViewportHeightMode)
|
|
1064
|
+
|
|
1065
|
+
const snapshot = loadGeneratingSession()
|
|
1066
|
+
const tid = String(templateId.value || '')
|
|
1067
|
+
const routeInstanceId = typeof route.query.instanceId === 'string' ? route.query.instanceId : ''
|
|
1068
|
+
|
|
1069
|
+
if (snapshot && snapshot.templateId === tid && snapshot.chapters?.length) {
|
|
1070
|
+
chapters.value = snapshot.chapters.map((chapter) => ({ ...chapter }))
|
|
1071
|
+
instanceId.value = snapshot.instanceId || routeInstanceId
|
|
1072
|
+
isComplete.value = snapshot.isComplete
|
|
1073
|
+
isGenerating.value = false
|
|
1074
|
+
isSectionRequesting.value = false
|
|
1075
|
+
if (instanceId.value) void syncInstanceIdToRoute(instanceId.value)
|
|
1076
|
+
// 已完成:直接恢復列表;未完成:重新跑生成以繼續進度
|
|
1077
|
+
if (!snapshot.isComplete) {
|
|
1078
|
+
void startGeneration()
|
|
1079
|
+
}
|
|
1080
|
+
} else {
|
|
1081
|
+
void startGeneration()
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// 視頻已刪:guide.mp4 相關暫時注釋
|
|
1085
|
+
// void nextTick(() => initGeneratingVideo())
|
|
1086
|
+
scheduleSyncViewportHeightMode()
|
|
1087
|
+
})
|
|
1088
|
+
|
|
1089
|
+
onUnmounted(() => {
|
|
1090
|
+
persistGeneratingSession()
|
|
1091
|
+
generationAbortController?.abort()
|
|
1092
|
+
stopCreatingTimeoutWatcher()
|
|
1093
|
+
clearCreatingTimeoutState()
|
|
1094
|
+
cancelAnimationFrame(measureRafId)
|
|
1095
|
+
window.removeEventListener('resize', scheduleSyncViewportHeightMode)
|
|
1096
|
+
cleanupBodyHeight?.()
|
|
1097
|
+
cleanupBodyHeight = null
|
|
1098
|
+
cleanupLock?.()
|
|
1099
|
+
// 視頻已刪:guide.mp4 相關暫時注釋
|
|
1100
|
+
// disposeGeneratingVideo()
|
|
1101
|
+
})
|
|
1102
|
+
|
|
1103
|
+
watch(
|
|
1104
|
+
[chapters, isComplete, instanceId],
|
|
1105
|
+
() => {
|
|
1106
|
+
persistGeneratingSession()
|
|
1107
|
+
},
|
|
1108
|
+
{ deep: true }
|
|
1109
|
+
)
|
|
1110
|
+
|
|
1111
|
+
/** footer 顯隱時同步測高,避免按鈕區撐破視口 */
|
|
1112
|
+
watch(showActionFooter, () => {
|
|
1113
|
+
scheduleSyncViewportHeightMode()
|
|
1114
|
+
})
|
|
1115
|
+
</script>
|
|
1116
|
+
|
|
1117
|
+
<style scoped>
|
|
1118
|
+
.report-generating-page {
|
|
1119
|
+
--gen-ai-fs-hero: clamp(26px, calc(32 * 100vw / 1920), 32px);
|
|
1120
|
+
--gen-ai-fw-bold: 700;
|
|
1121
|
+
--report-generating-video-slot-width: 646px;
|
|
1122
|
+
--report-generating-video-slot-height: 363px;
|
|
1123
|
+
--report-generating-video-aspect: 646 / 363;
|
|
1124
|
+
--report-generating-video-sticky-top: 24px;
|
|
1125
|
+
--report-generating-gap: 50px;
|
|
1126
|
+
/* 左右等寬(以 video 為準)+ 間距 */
|
|
1127
|
+
--report-generating-layout-max-width: calc(
|
|
1128
|
+
2 * var(--report-generating-video-slot-width) + var(--report-generating-gap)
|
|
1129
|
+
);
|
|
1130
|
+
display: flex;
|
|
1131
|
+
flex-direction: column;
|
|
1132
|
+
box-sizing: border-box;
|
|
1133
|
+
min-height: 0;
|
|
1134
|
+
height: 100%;
|
|
1135
|
+
margin-bottom: 0;
|
|
1136
|
+
padding: 0;
|
|
1137
|
+
background: #fff;
|
|
1138
|
+
overflow: hidden;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
.report-generating-page__layout {
|
|
1142
|
+
display: grid;
|
|
1143
|
+
grid-template-columns: minmax(0, var(--report-generating-video-slot-width)) minmax(
|
|
1144
|
+
0,
|
|
1145
|
+
var(--report-generating-video-slot-width)
|
|
1146
|
+
);
|
|
1147
|
+
grid-template-rows: minmax(0, 1fr);
|
|
1148
|
+
column-gap: var(--report-generating-gap);
|
|
1149
|
+
align-items: stretch;
|
|
1150
|
+
justify-content: center;
|
|
1151
|
+
flex: 1 1 auto;
|
|
1152
|
+
width: min(100%, var(--report-generating-layout-max-width));
|
|
1153
|
+
min-height: 0;
|
|
1154
|
+
margin: 0 auto;
|
|
1155
|
+
padding: clamp(32px, 12vh, 96px) 24px 0;
|
|
1156
|
+
box-sizing: border-box;
|
|
1157
|
+
overflow: hidden;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
.report-generating-page__main {
|
|
1161
|
+
display: flex;
|
|
1162
|
+
flex-direction: column;
|
|
1163
|
+
align-items: stretch;
|
|
1164
|
+
grid-row: 1;
|
|
1165
|
+
width: 100%;
|
|
1166
|
+
max-width: var(--report-generating-video-slot-width);
|
|
1167
|
+
min-width: 0;
|
|
1168
|
+
min-height: 0;
|
|
1169
|
+
height: 100%;
|
|
1170
|
+
overflow: hidden;
|
|
1171
|
+
justify-self: stretch;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
.report-generating-page__video {
|
|
1175
|
+
position: relative;
|
|
1176
|
+
grid-row: 1;
|
|
1177
|
+
align-self: start;
|
|
1178
|
+
justify-self: stretch;
|
|
1179
|
+
margin-top: 0;
|
|
1180
|
+
width: 100%;
|
|
1181
|
+
max-width: var(--report-generating-video-slot-width);
|
|
1182
|
+
height: fit-content;
|
|
1183
|
+
max-height: none;
|
|
1184
|
+
display: block;
|
|
1185
|
+
z-index: 2;
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
.report-generating-page__video-slot {
|
|
1189
|
+
position: relative;
|
|
1190
|
+
display: block;
|
|
1191
|
+
width: 100%;
|
|
1192
|
+
max-width: var(--report-generating-video-slot-width);
|
|
1193
|
+
height: auto;
|
|
1194
|
+
aspect-ratio: var(--report-generating-video-aspect);
|
|
1195
|
+
margin-left: auto;
|
|
1196
|
+
margin-right: auto;
|
|
1197
|
+
border-radius: 12px;
|
|
1198
|
+
overflow: hidden;
|
|
1199
|
+
line-height: 0;
|
|
1200
|
+
font-size: 0;
|
|
1201
|
+
flex-shrink: 0;
|
|
1202
|
+
/* 實心底,避免滾動條出現後亞像素縫露出 video 默認黑底 */
|
|
1203
|
+
background: #f3f0ff;
|
|
1204
|
+
isolation: isolate;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
/* 遮住底邊 1px 黑縫(滾動條導致寬高重算時常見) */
|
|
1208
|
+
.report-generating-page__video-slot::after {
|
|
1209
|
+
content: '';
|
|
1210
|
+
position: absolute;
|
|
1211
|
+
left: 0;
|
|
1212
|
+
right: 0;
|
|
1213
|
+
bottom: -1px;
|
|
1214
|
+
height: 3px;
|
|
1215
|
+
background: #f3f0ff;
|
|
1216
|
+
pointer-events: none;
|
|
1217
|
+
z-index: 2;
|
|
1218
|
+
border-radius: 0 0 12px 12px;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
.report-generating-page__video-slot .report-generating-page__player {
|
|
1222
|
+
position: absolute;
|
|
1223
|
+
/* 略放大並多蓋住底邊,消除 sticky + 滾動條後的黑線 */
|
|
1224
|
+
inset: -3px -2px -5px -2px;
|
|
1225
|
+
width: calc(100% + 4px);
|
|
1226
|
+
height: calc(100% + 8px);
|
|
1227
|
+
max-width: none;
|
|
1228
|
+
object-fit: cover;
|
|
1229
|
+
object-position: center bottom;
|
|
1230
|
+
background: #f3f0ff;
|
|
1231
|
+
border: 0;
|
|
1232
|
+
outline: none;
|
|
1233
|
+
display: block;
|
|
1234
|
+
vertical-align: top;
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
.report-generating-page__hero {
|
|
1238
|
+
flex-shrink: 0;
|
|
1239
|
+
text-align: left;
|
|
1240
|
+
padding: 0 0 20px;
|
|
1241
|
+
box-sizing: border-box;
|
|
1242
|
+
background: transparent;
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
.report-generating-page__hero-title {
|
|
1246
|
+
margin: 0;
|
|
1247
|
+
font-size: var(--gen-ai-fs-hero);
|
|
1248
|
+
font-weight: var(--gen-ai-fw-bold);
|
|
1249
|
+
color: #1a1a1a;
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
.report-generating-page__body {
|
|
1253
|
+
position: relative;
|
|
1254
|
+
flex: 1 1 0;
|
|
1255
|
+
min-height: 0;
|
|
1256
|
+
overflow: hidden;
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
/* 與章節 el-select 下拉滾動條同款(el-scrollbar) */
|
|
1260
|
+
.report-generating-page__scrollbar {
|
|
1261
|
+
position: absolute;
|
|
1262
|
+
inset: 0;
|
|
1263
|
+
height: 100%;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
.report-generating-page__scrollbar :deep(.el-scrollbar) {
|
|
1267
|
+
height: 100%;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
.report-generating-page__scrollbar :deep(.el-scrollbar__wrap) {
|
|
1271
|
+
overflow-x: hidden !important;
|
|
1272
|
+
overflow-y: auto !important;
|
|
1273
|
+
max-height: 100% !important;
|
|
1274
|
+
scrollbar-width: none !important;
|
|
1275
|
+
-ms-overflow-style: none !important;
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
.report-generating-page__scrollbar :deep(.el-scrollbar__wrap::-webkit-scrollbar) {
|
|
1279
|
+
width: 0 !important;
|
|
1280
|
+
height: 0 !important;
|
|
1281
|
+
display: none !important;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
.report-generating-page__scrollbar :deep(.el-scrollbar__bar.is-horizontal) {
|
|
1285
|
+
display: none !important;
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
.report-generating-page__scrollbar :deep(.el-scrollbar__bar.is-vertical) {
|
|
1289
|
+
width: 8px !important;
|
|
1290
|
+
right: 4px !important;
|
|
1291
|
+
top: 2px !important;
|
|
1292
|
+
bottom: 2px !important;
|
|
1293
|
+
opacity: 1 !important;
|
|
1294
|
+
z-index: 10;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
.report-generating-page__scrollbar :deep(.el-scrollbar__bar.is-vertical .el-scrollbar__thumb) {
|
|
1298
|
+
display: block !important;
|
|
1299
|
+
width: 8px !important;
|
|
1300
|
+
min-height: 50px !important;
|
|
1301
|
+
border-radius: 4px !important;
|
|
1302
|
+
background-color: #999 !important;
|
|
1303
|
+
opacity: 1 !important;
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
.report-generating-page__content {
|
|
1307
|
+
width: 100%;
|
|
1308
|
+
max-width: 100%;
|
|
1309
|
+
margin: 0;
|
|
1310
|
+
padding: 0 12px 12px 0;
|
|
1311
|
+
box-sizing: border-box;
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
.report-generating-chapters {
|
|
1315
|
+
margin: 0;
|
|
1316
|
+
padding: 0;
|
|
1317
|
+
list-style: none;
|
|
1318
|
+
width: 100%;
|
|
1319
|
+
max-width: 100%;
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
.report-generating-chapters__row {
|
|
1323
|
+
display: flex;
|
|
1324
|
+
align-items: center;
|
|
1325
|
+
justify-content: flex-start;
|
|
1326
|
+
gap: 10px;
|
|
1327
|
+
min-height: 40px;
|
|
1328
|
+
width: 100%;
|
|
1329
|
+
max-width: 100%;
|
|
1330
|
+
font-size: 15px;
|
|
1331
|
+
font-weight: 500;
|
|
1332
|
+
color: #303133;
|
|
1333
|
+
overflow: hidden;
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
.report-generating-chapters__state {
|
|
1337
|
+
display: inline-flex;
|
|
1338
|
+
align-items: center;
|
|
1339
|
+
justify-content: center;
|
|
1340
|
+
flex-shrink: 0;
|
|
1341
|
+
width: 15px;
|
|
1342
|
+
min-height: 40px;
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
.report-generating-chapters__state .report-generating-chapters__icon {
|
|
1346
|
+
font-weight: 700;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
.report-generating-chapters__state .report-generating-chapters__icon :deep(svg) {
|
|
1350
|
+
stroke: currentColor;
|
|
1351
|
+
stroke-width: 48;
|
|
1352
|
+
paint-order: stroke fill;
|
|
1353
|
+
overflow: visible;
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
.report-generating-chapters__state .report-generating-hourglass__svg {
|
|
1357
|
+
stroke-width: 1.8;
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
.report-generating-chapters__state .report-generating-hourglass__frame {
|
|
1361
|
+
stroke-width: 1.8;
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
.report-generating-chapters__state .report-generating-hourglass__sand-top,
|
|
1365
|
+
.report-generating-chapters__state .report-generating-hourglass__sand-bottom,
|
|
1366
|
+
.report-generating-chapters__state .report-generating-hourglass__drip {
|
|
1367
|
+
stroke: currentColor;
|
|
1368
|
+
stroke-width: 0.6;
|
|
1369
|
+
paint-order: stroke fill;
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
.report-generating-chapters__name {
|
|
1373
|
+
flex: 1;
|
|
1374
|
+
min-width: 0;
|
|
1375
|
+
overflow: hidden;
|
|
1376
|
+
display: flex;
|
|
1377
|
+
align-items: center;
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
.report-generating-chapters__index {
|
|
1381
|
+
flex-shrink: 0;
|
|
1382
|
+
height: 40px;
|
|
1383
|
+
line-height: 40px;
|
|
1384
|
+
margin-right: 6px;
|
|
1385
|
+
font-family: var(--gen-ai-font-family, 'Noto Sans', 'Noto Sans TC', sans-serif);
|
|
1386
|
+
font-size: 15px;
|
|
1387
|
+
font-weight: 500;
|
|
1388
|
+
color: #303133;
|
|
1389
|
+
white-space: nowrap;
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
.report-generating-chapters__name :deep(.chapter-title-scroll) {
|
|
1393
|
+
display: flex;
|
|
1394
|
+
align-items: center;
|
|
1395
|
+
flex: 1;
|
|
1396
|
+
min-width: 0;
|
|
1397
|
+
width: auto;
|
|
1398
|
+
max-width: 100%;
|
|
1399
|
+
height: 40px;
|
|
1400
|
+
line-height: 40px;
|
|
1401
|
+
font-family: var(--gen-ai-font-family, 'Noto Sans', 'Noto Sans TC', sans-serif);
|
|
1402
|
+
font-size: 15px;
|
|
1403
|
+
font-weight: 500;
|
|
1404
|
+
color: #303133;
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
.report-generating-chapters__name :deep(.chapter-title-scroll.is-char-truncated) {
|
|
1408
|
+
flex: 0 1 auto;
|
|
1409
|
+
width: max-content;
|
|
1410
|
+
max-width: 100%;
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
.report-generating-chapters__name :deep(.chapter-title-scroll__viewport) {
|
|
1414
|
+
display: block;
|
|
1415
|
+
flex: 1 1 auto;
|
|
1416
|
+
min-width: 0;
|
|
1417
|
+
height: 40px;
|
|
1418
|
+
line-height: 40px;
|
|
1419
|
+
overflow: hidden;
|
|
1420
|
+
font-family: var(--gen-ai-font-family, 'Noto Sans', 'Noto Sans TC', sans-serif);
|
|
1421
|
+
font-size: 15px;
|
|
1422
|
+
font-weight: 500;
|
|
1423
|
+
color: #303133;
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
/* 19 字前綴完整可見,不被 overflow / ... 裁切遮擋 */
|
|
1427
|
+
.report-generating-chapters__name
|
|
1428
|
+
:deep(.chapter-title-scroll.is-char-truncated .chapter-title-scroll__viewport) {
|
|
1429
|
+
flex: 0 0 auto;
|
|
1430
|
+
min-width: auto;
|
|
1431
|
+
width: max-content;
|
|
1432
|
+
max-width: none;
|
|
1433
|
+
overflow: visible;
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
.report-generating-chapters__name :deep(.chapter-title-scroll__text) {
|
|
1437
|
+
font-family: inherit;
|
|
1438
|
+
font-size: inherit;
|
|
1439
|
+
font-weight: 500;
|
|
1440
|
+
line-height: inherit;
|
|
1441
|
+
color: #303133;
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
.report-generating-chapters__name :deep(.chapter-title-scroll.is-truncatable) {
|
|
1445
|
+
cursor: pointer;
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
.report-generating-chapters__name :deep(.chapter-title-scroll.is-truncatable.is-dragging) {
|
|
1449
|
+
cursor: grabbing;
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
.report-generating-chapters__name :deep(.chapter-title-scroll__ellipsis) {
|
|
1453
|
+
position: static;
|
|
1454
|
+
flex: 0 0 auto;
|
|
1455
|
+
flex-shrink: 0;
|
|
1456
|
+
display: inline-flex;
|
|
1457
|
+
align-items: center;
|
|
1458
|
+
height: 1em;
|
|
1459
|
+
margin-left: 2px;
|
|
1460
|
+
padding: 0;
|
|
1461
|
+
background: none;
|
|
1462
|
+
color: #c53355;
|
|
1463
|
+
font-size: 15px;
|
|
1464
|
+
font-weight: 700;
|
|
1465
|
+
line-height: 1;
|
|
1466
|
+
white-space: nowrap;
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
.report-generating-chapters__icon {
|
|
1470
|
+
--report-generating-icon-color: rgb(31, 199, 255);
|
|
1471
|
+
font-size: 15px;
|
|
1472
|
+
width: 15px;
|
|
1473
|
+
height: 15px;
|
|
1474
|
+
color: var(--report-generating-icon-color);
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
.report-generating-chapters__icon.is-success {
|
|
1478
|
+
color: var(--report-generating-icon-color);
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
.report-generating-chapters__icon.is-failed {
|
|
1482
|
+
color: #c53355;
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
.report-generating-chapters__icon.is-loading {
|
|
1486
|
+
color: var(--report-generating-icon-color);
|
|
1487
|
+
animation: report-generating-spin 1s linear infinite;
|
|
1488
|
+
-webkit-mask-image: conic-gradient(from 0deg, transparent 8%, rgba(0, 0, 0, 0.25) 35%, #000 78%);
|
|
1489
|
+
mask-image: conic-gradient(from 0deg, transparent 8%, rgba(0, 0, 0, 0.25) 35%, #000 78%);
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
.report-generating-chapters__icon.is-pending,
|
|
1493
|
+
.report-generating-hourglass {
|
|
1494
|
+
display: inline-flex;
|
|
1495
|
+
align-items: center;
|
|
1496
|
+
justify-content: center;
|
|
1497
|
+
width: 15px;
|
|
1498
|
+
height: 15px;
|
|
1499
|
+
color: var(--report-generating-icon-color, rgb(31, 199, 255));
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
.report-generating-hourglass__svg {
|
|
1503
|
+
width: 100%;
|
|
1504
|
+
height: 100%;
|
|
1505
|
+
overflow: visible;
|
|
1506
|
+
animation: report-generating-hourglass-flip 2.8s ease-in-out infinite;
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
.report-generating-hourglass__sand-top {
|
|
1510
|
+
transform-box: fill-box;
|
|
1511
|
+
transform-origin: center top;
|
|
1512
|
+
animation: report-generating-sand-top 2.8s ease-in-out infinite;
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
.report-generating-hourglass__sand-bottom {
|
|
1516
|
+
transform-box: fill-box;
|
|
1517
|
+
transform-origin: center bottom;
|
|
1518
|
+
animation: report-generating-sand-bottom 2.8s ease-in-out infinite;
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
.report-generating-hourglass__drip {
|
|
1522
|
+
opacity: 0;
|
|
1523
|
+
animation: report-generating-sand-drip 1.1s linear infinite;
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
.report-generating-hourglass__drip--2 {
|
|
1527
|
+
animation-delay: 0.35s;
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
.report-generating-hourglass__drip--3 {
|
|
1531
|
+
animation-delay: 0.7s;
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
@keyframes report-generating-hourglass-flip {
|
|
1535
|
+
0%,
|
|
1536
|
+
42% {
|
|
1537
|
+
transform: rotate(0deg);
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
50%,
|
|
1541
|
+
92% {
|
|
1542
|
+
transform: rotate(180deg);
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
100% {
|
|
1546
|
+
transform: rotate(360deg);
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
@keyframes report-generating-sand-top {
|
|
1551
|
+
0%,
|
|
1552
|
+
8% {
|
|
1553
|
+
transform: scaleY(1);
|
|
1554
|
+
opacity: 0.9;
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
40%,
|
|
1558
|
+
50% {
|
|
1559
|
+
transform: scaleY(0.12);
|
|
1560
|
+
opacity: 0.35;
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
58%,
|
|
1564
|
+
100% {
|
|
1565
|
+
transform: scaleY(1);
|
|
1566
|
+
opacity: 0.9;
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
@keyframes report-generating-sand-bottom {
|
|
1571
|
+
0%,
|
|
1572
|
+
8% {
|
|
1573
|
+
transform: scaleY(0.12);
|
|
1574
|
+
opacity: 0.35;
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
40%,
|
|
1578
|
+
50% {
|
|
1579
|
+
transform: scaleY(1);
|
|
1580
|
+
opacity: 0.9;
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
58%,
|
|
1584
|
+
100% {
|
|
1585
|
+
transform: scaleY(0.12);
|
|
1586
|
+
opacity: 0.35;
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
@keyframes report-generating-sand-drip {
|
|
1591
|
+
0% {
|
|
1592
|
+
transform: translateY(0);
|
|
1593
|
+
opacity: 0;
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
12% {
|
|
1597
|
+
opacity: 0.95;
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
88% {
|
|
1601
|
+
opacity: 0.85;
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
100% {
|
|
1605
|
+
transform: translateY(6px);
|
|
1606
|
+
opacity: 0;
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
.report-generating-chapters__state.is-creating .report-generating-chapters__icon.is-loading {
|
|
1611
|
+
color: var(--report-generating-icon-color, rgb(31, 199, 255));
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
.report-generating-page__footer {
|
|
1615
|
+
flex-shrink: 0;
|
|
1616
|
+
display: flex;
|
|
1617
|
+
flex-direction: column;
|
|
1618
|
+
align-items: center;
|
|
1619
|
+
justify-content: center;
|
|
1620
|
+
gap: 20px;
|
|
1621
|
+
width: 100%;
|
|
1622
|
+
margin-top: auto;
|
|
1623
|
+
padding: 32px 0 40px;
|
|
1624
|
+
background: transparent;
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
.report-generating-page__footer-actions {
|
|
1628
|
+
display: flex;
|
|
1629
|
+
align-items: center;
|
|
1630
|
+
justify-content: center;
|
|
1631
|
+
gap: 16px;
|
|
1632
|
+
flex-wrap: wrap;
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
.report-generating-page__summary {
|
|
1636
|
+
flex-shrink: 0;
|
|
1637
|
+
margin: 0;
|
|
1638
|
+
font-size: 16px;
|
|
1639
|
+
font-weight: 600;
|
|
1640
|
+
color: #303133;
|
|
1641
|
+
text-align: center;
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
@keyframes report-generating-spin {
|
|
1645
|
+
from {
|
|
1646
|
+
transform: rotate(0deg);
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
to {
|
|
1650
|
+
transform: rotate(360deg);
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
.app-btn {
|
|
1655
|
+
display: inline-flex;
|
|
1656
|
+
align-items: center;
|
|
1657
|
+
justify-content: center;
|
|
1658
|
+
box-sizing: border-box;
|
|
1659
|
+
width: auto;
|
|
1660
|
+
min-width: 160px;
|
|
1661
|
+
height: 39px;
|
|
1662
|
+
padding: 0 20px;
|
|
1663
|
+
border: 1px solid #c53355;
|
|
1664
|
+
border-radius: 4px;
|
|
1665
|
+
background: #c53355;
|
|
1666
|
+
color: #fff;
|
|
1667
|
+
font-size: 16px;
|
|
1668
|
+
font-weight: 600;
|
|
1669
|
+
line-height: 1;
|
|
1670
|
+
white-space: nowrap;
|
|
1671
|
+
cursor: pointer;
|
|
1672
|
+
transition: all 0.2s ease;
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
.app-btn:hover:not(:disabled) {
|
|
1676
|
+
background: #fff;
|
|
1677
|
+
color: #c53355;
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
.app-btn:disabled {
|
|
1681
|
+
opacity: 0.45;
|
|
1682
|
+
cursor: not-allowed;
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
.app-btn--wide {
|
|
1686
|
+
width: auto;
|
|
1687
|
+
min-width: 160px;
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
.app-btn--outline {
|
|
1691
|
+
background: #fff;
|
|
1692
|
+
color: #c53355;
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
.app-btn--outline:hover:not(:disabled) {
|
|
1696
|
+
background: #c53355;
|
|
1697
|
+
color: #fff;
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
@media (max-width: 1023px) {
|
|
1701
|
+
.report-generating-page__layout {
|
|
1702
|
+
display: flex;
|
|
1703
|
+
flex-direction: column;
|
|
1704
|
+
align-items: center;
|
|
1705
|
+
justify-content: flex-start;
|
|
1706
|
+
gap: clamp(16px, 3vw, 24px);
|
|
1707
|
+
width: 100%;
|
|
1708
|
+
max-width: min(100%, var(--report-generating-video-slot-width));
|
|
1709
|
+
margin-left: auto;
|
|
1710
|
+
margin-right: auto;
|
|
1711
|
+
padding: 0 clamp(16px, 4vw, 24px);
|
|
1712
|
+
overflow: hidden;
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
/* 視頻在上、章節在下 */
|
|
1716
|
+
.report-generating-page__video {
|
|
1717
|
+
position: relative;
|
|
1718
|
+
top: auto;
|
|
1719
|
+
order: -1;
|
|
1720
|
+
width: 100%;
|
|
1721
|
+
max-width: min(100%, var(--report-generating-video-slot-width));
|
|
1722
|
+
margin-top: 0;
|
|
1723
|
+
align-self: center;
|
|
1724
|
+
justify-self: center;
|
|
1725
|
+
z-index: 1;
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
.report-generating-page__video-slot {
|
|
1729
|
+
width: 100%;
|
|
1730
|
+
max-width: 100%;
|
|
1731
|
+
height: auto;
|
|
1732
|
+
aspect-ratio: var(--report-generating-video-aspect);
|
|
1733
|
+
margin-left: auto;
|
|
1734
|
+
margin-right: auto;
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
.report-generating-page__main {
|
|
1738
|
+
order: 1;
|
|
1739
|
+
width: 100%;
|
|
1740
|
+
max-width: min(100%, var(--report-generating-video-slot-width));
|
|
1741
|
+
margin-top: 0;
|
|
1742
|
+
align-self: center;
|
|
1743
|
+
flex: 1 1 auto;
|
|
1744
|
+
min-height: 0;
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
.report-generating-page__hero-title {
|
|
1748
|
+
font-size: clamp(22px, 4.2vw, var(--gen-ai-fs-hero));
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
.report-generating-chapters__row {
|
|
1752
|
+
font-size: clamp(13px, 2.2vw, 15px);
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
.report-generating-page__footer {
|
|
1756
|
+
justify-content: center;
|
|
1757
|
+
align-items: center;
|
|
1758
|
+
padding-bottom: 24px;
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
.report-generating-page__footer-actions {
|
|
1762
|
+
justify-content: center;
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
</style>
|