|
|
@@ -29,8 +29,7 @@
|
|
|
<el-icon><component :is="courseDropdown.icon" /></el-icon>
|
|
|
</template>
|
|
|
<el-option
|
|
|
- v-for="item in courseDropdown.options"
|
|
|
- v-show="item.show"
|
|
|
+ v-for="item in availableThemeOptions"
|
|
|
:key="item.value"
|
|
|
:label="item.label"
|
|
|
:value="item.value"
|
|
|
@@ -129,7 +128,7 @@
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="cancel-box">
|
|
|
- <button class="cancel-button" @click="cancelGenerate">
|
|
|
+ <button class="cancel-button" @click="handleCancel">
|
|
|
<span>取消</span>
|
|
|
</button>
|
|
|
</div>
|
|
|
@@ -147,6 +146,7 @@ import {ref, onMounted, onUnmounted, computed, reactive, defineEmits, watch} fro
|
|
|
// 定义事件
|
|
|
const emit = defineEmits(['refreshData']);
|
|
|
import { Top, Search, User, Memo } from '@element-plus/icons-vue'
|
|
|
+import { ElMessageBox } from 'element-plus'
|
|
|
import {
|
|
|
aIGenerateCourse,
|
|
|
AiImageStatusEnum,
|
|
|
@@ -186,13 +186,7 @@ const inProgressImageMap = ref({}) // 监听的图片映射
|
|
|
const inProgressTimer = ref(null) // 生成中的图片定时器
|
|
|
const checkImagesCompleteTimer = ref(null) // 图片完成检查定时器
|
|
|
|
|
|
-// 任务恢复相关 - 使用统一的缓存键并绑定用户ID
|
|
|
-const getTaskStateKey = () => {
|
|
|
- const userId = localStorage.getItem('userId') || 'anonymous'
|
|
|
- return `ai_course_task_state_${userId}`
|
|
|
-}
|
|
|
-
|
|
|
-// 单个脚本数据的缓存键(保持原有逻辑,用于单独存储脚本数据)
|
|
|
+// 单个脚本数据的缓存键(用于存储最终生成的脚本数据)
|
|
|
const SCRIPT_DATA_KEY = 'scriptData'
|
|
|
|
|
|
// 小节列表(包含详细讲解要求,适用于所有课程类型)
|
|
|
@@ -379,138 +373,25 @@ const hasPendingTask = computed(() => {
|
|
|
}
|
|
|
})
|
|
|
|
|
|
-// 当前正在生成的小节索引(用于恢复时从正确位置继续)
|
|
|
-const currentLessonIndex = ref(0)
|
|
|
-
|
|
|
-// 保存任务状态到缓存(使用统一的键并绑定用户ID)
|
|
|
-const saveTaskState = (currentStep) => {
|
|
|
- const taskState = {
|
|
|
- isRestoring: true,
|
|
|
- currentStep: currentStep,
|
|
|
- currentLessonIndex: currentLessonIndex.value,
|
|
|
- progressSteps: progressSteps.value,
|
|
|
- progressCounters: progressCounters.value,
|
|
|
- prompt: prompt.value,
|
|
|
- courseDropdown: courseDropdown.value,
|
|
|
- teacherDropdown: teacherDropdown.value,
|
|
|
- assistantDropdown: assistantDropdown.value,
|
|
|
- inProgressImageMap: inProgressImageMap.value
|
|
|
- }
|
|
|
-
|
|
|
- localStorage.setItem(getTaskStateKey(), JSON.stringify(taskState))
|
|
|
- // 脚本数据单独存储(保持原有逻辑)
|
|
|
- localStorage.setItem(SCRIPT_DATA_KEY, JSON.stringify(scriptData))
|
|
|
-}
|
|
|
-
|
|
|
-// 清除任务状态缓存
|
|
|
-const clearTaskState = () => {
|
|
|
- localStorage.removeItem(getTaskStateKey())
|
|
|
- localStorage.removeItem(SCRIPT_DATA_KEY)
|
|
|
-}
|
|
|
-
|
|
|
-// 标记任务完成
|
|
|
-const markTaskCompleted = () => {
|
|
|
- const taskState = localStorage.getItem(getTaskStateKey())
|
|
|
- if (taskState) {
|
|
|
- try {
|
|
|
- const parsedState = JSON.parse(taskState)
|
|
|
- parsedState.completionTime = String(Date.now())
|
|
|
- parsedState.isRestoring = false
|
|
|
- localStorage.setItem(getTaskStateKey(), JSON.stringify(parsedState))
|
|
|
- } catch (error) {
|
|
|
- console.error('标记任务完成失败:', error)
|
|
|
- }
|
|
|
- }
|
|
|
- clearTaskState()
|
|
|
-}
|
|
|
-
|
|
|
-// 从缓存恢复任务状态
|
|
|
-const restoreTaskState = () => {
|
|
|
- try {
|
|
|
- const taskState = localStorage.getItem(getTaskStateKey())
|
|
|
- const savedScriptData = localStorage.getItem(SCRIPT_DATA_KEY)
|
|
|
-
|
|
|
- if (!taskState) {
|
|
|
- console.warn('未找到任务状态缓存')
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- const parsedState = JSON.parse(taskState)
|
|
|
-
|
|
|
- // 恢复进度步骤
|
|
|
- if (parsedState.progressSteps) {
|
|
|
- progressSteps.value = parsedState.progressSteps
|
|
|
- }
|
|
|
-
|
|
|
- // 恢复进度计数器
|
|
|
- if (parsedState.progressCounters) {
|
|
|
- progressCounters.value = parsedState.progressCounters
|
|
|
- }
|
|
|
-
|
|
|
- // 恢复脚本数据
|
|
|
- if (savedScriptData) {
|
|
|
- const parsedData = JSON.parse(savedScriptData)
|
|
|
- Object.assign(scriptData, parsedData)
|
|
|
- }
|
|
|
-
|
|
|
- // 恢复当前小节索引
|
|
|
- if (parsedState.currentLessonIndex !== undefined) {
|
|
|
- currentLessonIndex.value = parsedState.currentLessonIndex
|
|
|
- }
|
|
|
-
|
|
|
- // 恢复用户输入
|
|
|
- if (parsedState.prompt) {
|
|
|
- prompt.value = parsedState.prompt
|
|
|
- }
|
|
|
-
|
|
|
- // 恢复下拉框选择
|
|
|
- if (parsedState.courseDropdown) {
|
|
|
- Object.assign(courseDropdown.value, parsedState.courseDropdown)
|
|
|
- }
|
|
|
- if (parsedState.teacherDropdown) {
|
|
|
- Object.assign(teacherDropdown.value, parsedState.teacherDropdown)
|
|
|
- selectedMainTeacher.value = teacherDropdown.value.value
|
|
|
- }
|
|
|
- if (parsedState.assistantDropdown) {
|
|
|
- Object.assign(assistantDropdown.value, parsedState.assistantDropdown)
|
|
|
- selectedAssistants.value = [assistantDropdown.value.value]
|
|
|
- }
|
|
|
-
|
|
|
- // 恢复图片映射(只保留还未完成的图片)
|
|
|
- if (parsedState.inProgressImageMap) {
|
|
|
- const savedImageMap = parsedState.inProgressImageMap
|
|
|
- const validImageMap = {}
|
|
|
-
|
|
|
- // 过滤掉已经完成的图片(有url的图片不需要再轮询)
|
|
|
- Object.keys(savedImageMap).forEach(key => {
|
|
|
- const info = savedImageMap[key]
|
|
|
- if (info.type === 'cover') {
|
|
|
- if (!scriptData.coverImage || !scriptData.coverImage.url) {
|
|
|
- validImageMap[key] = info
|
|
|
- }
|
|
|
- } else {
|
|
|
- const lesson = scriptData.lessons[info.lessonIndex]
|
|
|
- if (lesson && lesson.sections && lesson.sections[info.sectionIndex]) {
|
|
|
- const section = lesson.sections[info.sectionIndex]
|
|
|
- if (!section.backgroundImage || !section.backgroundImage.url) {
|
|
|
- validImageMap[key] = info
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- })
|
|
|
-
|
|
|
- inProgressImageMap.value = validImageMap
|
|
|
-
|
|
|
- // 恢复后立即检查一次图片状态,确保进度准确
|
|
|
- if (Object.keys(inProgressImageMap.value).length > 0) {
|
|
|
- refreshWatchImages()
|
|
|
- }
|
|
|
+/**
|
|
|
+ * 取消生成确认(带确认提示)
|
|
|
+ */
|
|
|
+const handleCancel = async () => {
|
|
|
+ const confirmResult = await ElMessageBox.confirm(
|
|
|
+ '确定要取消当前的课程生成吗?取消后所有已生成的内容将丢失,需要重新开始。',
|
|
|
+ '取消确认',
|
|
|
+ {
|
|
|
+ confirmButtonText: '确定取消',
|
|
|
+ cancelButtonText: '继续生成',
|
|
|
+ type: 'warning',
|
|
|
+ showClose: false
|
|
|
}
|
|
|
+ ).catch(() => {
|
|
|
+ return 'cancel'
|
|
|
+ })
|
|
|
|
|
|
- showNewContainer.value = true
|
|
|
- } catch (error) {
|
|
|
- console.error('恢复任务状态失败:', error)
|
|
|
- clearTaskState()
|
|
|
+ if (confirmResult === 'confirm') {
|
|
|
+ cancelGenerate(true)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -535,8 +416,8 @@ const cancelGenerate = (showMessage = true) => {
|
|
|
isGeneratingImages.value = false
|
|
|
isGeneratingVoiceovers.value = false
|
|
|
|
|
|
- // 清除任务状态缓存
|
|
|
- clearTaskState()
|
|
|
+ // 清除脚本数据缓存
|
|
|
+ localStorage.removeItem(SCRIPT_DATA_KEY)
|
|
|
|
|
|
// 重置进度步骤
|
|
|
progressSteps.value.forEach(step => {
|
|
|
@@ -568,9 +449,8 @@ const isUnmounted = ref(false)
|
|
|
|
|
|
/**
|
|
|
* 生成脚本主函数
|
|
|
- * @param {boolean} isResume - 是否为恢复任务(从缓存中恢复)
|
|
|
*/
|
|
|
-const generateScript = async (isResume = false) => {
|
|
|
+const generateScript = async () => {
|
|
|
try {
|
|
|
// 设置生成状态为进行中
|
|
|
isGenerating.value = true
|
|
|
@@ -578,30 +458,18 @@ const generateScript = async (isResume = false) => {
|
|
|
|
|
|
currentStep.value = 0
|
|
|
|
|
|
- // 如果是恢复任务,从缓存中获取当前步骤
|
|
|
- if (isResume) {
|
|
|
- const taskState = localStorage.getItem(getTaskStateKey())
|
|
|
- if (taskState) {
|
|
|
- try {
|
|
|
- const parsedState = JSON.parse(taskState)
|
|
|
- currentStep.value = parsedState.currentStep || 0
|
|
|
- } catch (error) {
|
|
|
- console.error('获取当前步骤失败:', error)
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
// ========== 步骤1:生成课程基本信息 ==========
|
|
|
if (!progressSteps.value[0].completed) {
|
|
|
// 检查是否已取消
|
|
|
if (isCancelled.value) return
|
|
|
|
|
|
currentStep.value = 0
|
|
|
- saveTaskState(currentStep.value)
|
|
|
|
|
|
try {
|
|
|
- // 创建会话
|
|
|
- await createAiRoleIdConversation(255)
|
|
|
+ // 创建会话 - 使用第一个隐藏的主题选项
|
|
|
+ const hiddenTheme = themeOptions.value.find(item => !item.show)
|
|
|
+ const themeId = hiddenTheme ? hiddenTheme.value : 255
|
|
|
+ await createAiRoleIdConversation(themeId)
|
|
|
|
|
|
// 检查会话是否创建成功
|
|
|
if (!activeConversationId.value || activeConversationId.value === null) {
|
|
|
@@ -635,7 +503,6 @@ const generateScript = async (isResume = false) => {
|
|
|
if (isCancelled.value) return
|
|
|
|
|
|
currentStep.value = 1
|
|
|
- saveTaskState(currentStep.value)
|
|
|
|
|
|
try {
|
|
|
// 设置进度状态
|
|
|
@@ -665,17 +532,16 @@ const generateScript = async (isResume = false) => {
|
|
|
// 构建基础提示词(不含课程列表)
|
|
|
const baseContent = `${prompt.value}(主讲人:${role.name},主讲人角色定位:${role.description};助讲有:${assistants})`
|
|
|
|
|
|
- // 如果是恢复任务,从已保存的脚本数据中获取已完成的课程
|
|
|
- let courseLessons = isResume && scriptData.lessons.length > 0 ? [...scriptData.lessons] : [];
|
|
|
+ // 初始化课程列表(不再支持恢复,每次都重新生成)
|
|
|
+ const courseLessons = []
|
|
|
scriptData.lessons = []
|
|
|
|
|
|
- // 循环生成每节课程内容(从保存的索引位置开始)
|
|
|
- for (let i = currentLessonIndex.value; i < lessonList.value.length; i++) {
|
|
|
- // 更新当前小节索引
|
|
|
- currentLessonIndex.value = i
|
|
|
-
|
|
|
- // 检查是否已取消
|
|
|
- if (isCancelled.value) return
|
|
|
+ // 循环生成每节课程内容(从第0节开始)
|
|
|
+ for (let i = 0; i < lessonList.value.length; i++) {
|
|
|
+ // 检查是否已取消或组件已卸载
|
|
|
+ if (isCancelled.value || isUnmounted.value) {
|
|
|
+ throw new Error('生成已取消')
|
|
|
+ }
|
|
|
|
|
|
let lesson = lessonList.value[i]
|
|
|
progressSteps.value[1].text = "生成课程小节【" + lesson.name + "】..."
|
|
|
@@ -685,22 +551,31 @@ const generateScript = async (isResume = false) => {
|
|
|
|
|
|
// 创建独立的输出对象,避免数据污染
|
|
|
let lessonInfo = {courseName: lesson.name, courseLabel: lesson.id};
|
|
|
- await doSendMessageStream(activeConversationId.value, content, lessonInfo)
|
|
|
+
|
|
|
+ try {
|
|
|
+ await doSendMessageStream(activeConversationId.value, content, lessonInfo)
|
|
|
+ } catch (error) {
|
|
|
+ // 如果是取消或卸载导致的错误,直接抛出,不保存数据
|
|
|
+ if (error.message.includes('已取消') || error.message.includes('已卸载')) {
|
|
|
+ throw error
|
|
|
+ }
|
|
|
+ throw new Error(`生成第${i+1}节课程【${lesson.name}】失败:${error.message}`)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查是否已取消或卸载(流式请求可能在后台完成)
|
|
|
+ if (isCancelled.value || isUnmounted.value) {
|
|
|
+ throw new Error('生成已取消')
|
|
|
+ }
|
|
|
|
|
|
// 检查生成结果是否有效
|
|
|
- if (!lessonInfo || !lessonInfo.courseName) {
|
|
|
- throw new Error(`生成第${i+1}节课程【${lesson.name}】失败`)
|
|
|
+ if (!lessonInfo || !lessonInfo.courseName || !lessonInfo.sections) {
|
|
|
+ throw new Error(`生成第${i+1}节课程【${lesson.name}】失败:返回数据不完整`)
|
|
|
}
|
|
|
|
|
|
console.log(`生成第${i+1}节课程【${lesson.name}】成功:`, lessonInfo)
|
|
|
courseLessons.push(lessonInfo)
|
|
|
-
|
|
|
- // 保存当前进度(包含小节索引)
|
|
|
- saveTaskState(currentStep.value)
|
|
|
}
|
|
|
|
|
|
- // 重置小节索引为0,准备下一次生成
|
|
|
- currentLessonIndex.value = 0
|
|
|
scriptData.lessons = courseLessons;
|
|
|
progressSteps.value[1].completed = true;
|
|
|
progressSteps.value[1].active = false;
|
|
|
@@ -717,16 +592,14 @@ const generateScript = async (isResume = false) => {
|
|
|
|
|
|
currentStep.value = 2
|
|
|
|
|
|
- // 重置图片进度为0%,避免恢复后显示旧进度
|
|
|
+ // 重置图片进度为0%
|
|
|
progressSteps.value[2].progress = 0
|
|
|
progressCounters.value.images.current = 0
|
|
|
-
|
|
|
- saveTaskState(currentStep.value)
|
|
|
|
|
|
try {
|
|
|
progressSteps.value[2].visible = true;
|
|
|
progressSteps.value[2].active = true;
|
|
|
- await generateAllImages(isResume)
|
|
|
+ await generateAllImages()
|
|
|
|
|
|
// 验证图片生成结果
|
|
|
if (scriptData.coverImage && scriptData.coverImage.prompt && !scriptData.coverImage.url) {
|
|
|
@@ -747,7 +620,6 @@ const generateScript = async (isResume = false) => {
|
|
|
if (isCancelled.value) return
|
|
|
|
|
|
currentStep.value = 3
|
|
|
- saveTaskState(currentStep.value)
|
|
|
|
|
|
try {
|
|
|
progressSteps.value[3].visible = true;
|
|
|
@@ -782,10 +654,6 @@ const generateScript = async (isResume = false) => {
|
|
|
} finally {
|
|
|
// 确保生成状态被重置
|
|
|
isGenerating.value = false
|
|
|
- // 如果不是用户主动取消,则标记任务完成
|
|
|
- if (!isCancelled.value) {
|
|
|
- markTaskCompleted()
|
|
|
- }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -878,6 +746,11 @@ const doSendMessageStream = async (conversationId, content, outputContent) => {
|
|
|
throw new Error('输出对象无效')
|
|
|
}
|
|
|
|
|
|
+ // 检查是否已取消或卸载
|
|
|
+ if (isCancelled.value || isUnmounted.value) {
|
|
|
+ throw new Error('生成已取消或组件已卸载')
|
|
|
+ }
|
|
|
+
|
|
|
// 创建AbortController用于中止请求
|
|
|
conversationInAbortController.value = new AbortController()
|
|
|
conversationInProgress.value = true
|
|
|
@@ -971,18 +844,15 @@ const doSendMessageStream = async (conversationId, content, outputContent) => {
|
|
|
/**
|
|
|
* 生成所有背景图
|
|
|
* 包括封面图和所有环节的背景图
|
|
|
- * @param {boolean} isResume - 是否为恢复任务
|
|
|
* @returns {Promise}
|
|
|
*/
|
|
|
-const generateAllImages = async (isResume = false) => {
|
|
|
+const generateAllImages = async () => {
|
|
|
return new Promise(async (resolve, reject) => {
|
|
|
isGeneratingImages.value = true
|
|
|
try {
|
|
|
// ========== 计算总图片数 ==========
|
|
|
- // 如果是恢复任务,保留已有的轮询列表;否则清空重新开始
|
|
|
- if (!isResume) {
|
|
|
- inProgressImageMap.value = {}
|
|
|
- }
|
|
|
+ // 清空轮询列表,每次都重新生成
|
|
|
+ inProgressImageMap.value = {}
|
|
|
|
|
|
// 统计需要生成的图片总数(有prompt且无url的图片)
|
|
|
progressCounters.value.images.total = 0
|
|
|
@@ -1007,13 +877,12 @@ const generateAllImages = async (isResume = false) => {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // 计算已完成的图片数(总数 - 正在生成的数量)
|
|
|
- progressCounters.value.images.current = progressCounters.value.images.total - Object.keys(inProgressImageMap.value).length
|
|
|
+ // 初始化已完成数为0
|
|
|
+ progressCounters.value.images.current = 0
|
|
|
|
|
|
// 更新进度显示
|
|
|
if (progressCounters.value.images.total > 0) {
|
|
|
- const progress = Math.round((progressCounters.value.images.current / progressCounters.value.images.total) * 100)
|
|
|
- progressSteps.value[2].progress = Math.min(progress, 100)
|
|
|
+ progressSteps.value[2].progress = 0
|
|
|
} else {
|
|
|
progressSteps.value[2].progress = 100
|
|
|
}
|
|
|
@@ -1025,15 +894,7 @@ const generateAllImages = async (isResume = false) => {
|
|
|
|
|
|
// ========== 生成封面图(如果尚未生成) ==========
|
|
|
if (scriptData.coverImage.prompt && !scriptData.coverImage.url) {
|
|
|
- // 在恢复模式下,检查封面图是否已经在生成中
|
|
|
- const coverAlreadyGenerating = Object.keys(inProgressImageMap.value).some(
|
|
|
- key => inProgressImageMap.value[key].type === 'cover'
|
|
|
- )
|
|
|
- if (!isResume || !coverAlreadyGenerating) {
|
|
|
- await generateCoverImage()
|
|
|
- } else {
|
|
|
- console.log('封面图已在生成中,跳过重复生成')
|
|
|
- }
|
|
|
+ await generateCoverImage()
|
|
|
}
|
|
|
|
|
|
// ========== 生成所有环节的背景图 ==========
|
|
|
@@ -1056,17 +917,7 @@ const generateAllImages = async (isResume = false) => {
|
|
|
}
|
|
|
// 如果有prompt但没有url,则生成图片
|
|
|
if (section.backgroundImage.prompt && !section.backgroundImage.url) {
|
|
|
- // 在恢复模式下,检查该图片是否已经在生成中
|
|
|
- const imageAlreadyGenerating = Object.keys(inProgressImageMap.value).some(
|
|
|
- key => inProgressImageMap.value[key].type === 'image' &&
|
|
|
- inProgressImageMap.value[key].lessonIndex === lessonIndex &&
|
|
|
- inProgressImageMap.value[key].sectionIndex === sectionIndex
|
|
|
- )
|
|
|
- if (!isResume || !imageAlreadyGenerating) {
|
|
|
- await generateImage(lessonIndex, sectionIndex)
|
|
|
- } else {
|
|
|
- console.log(`背景图[${lessonIndex}-${sectionIndex}]已在生成中,跳过重复生成`)
|
|
|
- }
|
|
|
+ await generateImage(lessonIndex, sectionIndex)
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
@@ -1081,9 +932,11 @@ const generateAllImages = async (isResume = false) => {
|
|
|
if (Object.keys(inProgressImageMap.value).length === 0) {
|
|
|
progressSteps.value[2].progress = 100
|
|
|
isGeneratingImages.value = false
|
|
|
- saveTaskState(2)
|
|
|
resolve()
|
|
|
} else {
|
|
|
+ // 先停止之前的定时器,避免多个定时器同时运行
|
|
|
+ stopImagePolling()
|
|
|
+
|
|
|
// 启动轮询检查图片生成状态
|
|
|
startImagePolling()
|
|
|
console.log('启动图片完成检查定时器')
|
|
|
@@ -1107,7 +960,6 @@ const generateAllImages = async (isResume = false) => {
|
|
|
clearInterval(checkImagesCompleteTimer.value)
|
|
|
checkImagesCompleteTimer.value = null
|
|
|
stopImagePolling()
|
|
|
- saveTaskState(2)
|
|
|
resolve()
|
|
|
}
|
|
|
}, 1000)
|
|
|
@@ -1149,10 +1001,12 @@ const generateCoverImage = async () => {
|
|
|
} else {
|
|
|
console.error('无效的图片ID:', response.data)
|
|
|
scriptData.coverImage.generating = false
|
|
|
+ throw new Error('生成封面图失败:无效的图片ID')
|
|
|
}
|
|
|
} catch (error) {
|
|
|
console.error('生成封面图失败:', error)
|
|
|
scriptData.coverImage.generating = false
|
|
|
+ throw error
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -1197,14 +1051,12 @@ const generateImage = async (lessonIndex, sectionIndex) => {
|
|
|
} else {
|
|
|
console.error('无效的图片ID:', res.data)
|
|
|
media.generating = false
|
|
|
+ throw new Error('生成背景图失败:无效的图片ID')
|
|
|
}
|
|
|
} catch (error) {
|
|
|
console.error('生成图片失败:', error);
|
|
|
- // 检查是否是速率限制错误
|
|
|
- if (error.response && error.response.status === 429) {
|
|
|
- console.error('图片生成速率限制,请稍后重试')
|
|
|
- }
|
|
|
media.generating = false
|
|
|
+ throw error
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -1323,8 +1175,6 @@ const refreshWatchImages = async () => {
|
|
|
const newProgress = Math.round((completedImages / progressCounters.value.images.total) * 100)
|
|
|
progressSteps.value[2].progress = Math.min(newProgress, 100)
|
|
|
}
|
|
|
-
|
|
|
- saveTaskState(2)
|
|
|
} catch (error) {
|
|
|
console.error('轮询图片状态失败:', error)
|
|
|
}
|
|
|
@@ -1364,7 +1214,9 @@ const generateAllVoiceovers = async () => {
|
|
|
if (scriptData.lessons && scriptData.lessons.length > 0) {
|
|
|
for (let lessonIndex = 0; lessonIndex < scriptData.lessons.length; lessonIndex++) {
|
|
|
// 检查是否已取消
|
|
|
- if (isCancelled.value) return
|
|
|
+ if (isCancelled.value) {
|
|
|
+ throw new Error('生成配音已取消')
|
|
|
+ }
|
|
|
|
|
|
const lesson = scriptData.lessons[lessonIndex]
|
|
|
if (lesson && lesson.sections) {
|
|
|
@@ -1381,8 +1233,6 @@ const generateAllVoiceovers = async () => {
|
|
|
if (progressCounters.value.voiceovers.total > 0) {
|
|
|
progressSteps.value[3].progress = Math.round((progressCounters.value.voiceovers.current / progressCounters.value.voiceovers.total) * 100)
|
|
|
}
|
|
|
- // 保存当前进度
|
|
|
- saveTaskState(3)
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
@@ -1393,6 +1243,7 @@ const generateAllVoiceovers = async () => {
|
|
|
progressSteps.value[3].progress = 100
|
|
|
} catch (error) {
|
|
|
console.error('生成所有语音失败:', error)
|
|
|
+ throw error
|
|
|
} finally {
|
|
|
isGeneratingVoiceovers.value = false
|
|
|
}
|
|
|
@@ -1425,7 +1276,7 @@ const generateVoiceover = async (lessonIndex, sectionIndex, dialogueIndex) => {
|
|
|
if (!role) {
|
|
|
console.error('未找到角色:', dialogue.roleName)
|
|
|
dialogue.generatingVoiceover = false
|
|
|
- return
|
|
|
+ throw new Error('生成配音失败:未找到角色 ' + dialogue.roleName)
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
@@ -1441,13 +1292,21 @@ const generateVoiceover = async (lessonIndex, sectionIndex, dialogueIndex) => {
|
|
|
command: command
|
|
|
})
|
|
|
|
|
|
+ if (!response.data) {
|
|
|
+ throw new Error('生成配音失败:返回无效数据')
|
|
|
+ }
|
|
|
+
|
|
|
dialogue.voiceoverUrl = response.data
|
|
|
// 保存脚本数据
|
|
|
localStorage.setItem(SCRIPT_DATA_KEY, JSON.stringify(scriptData))
|
|
|
} catch (error) {
|
|
|
console.error('生成语音失败:', error)
|
|
|
- } finally {
|
|
|
dialogue.generatingVoiceover = false
|
|
|
+ throw error
|
|
|
+ } finally {
|
|
|
+ if (!dialogue.generatingVoiceover) {
|
|
|
+ dialogue.generatingVoiceover = false
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -1492,31 +1351,22 @@ const assistantOptions = computed(() => {
|
|
|
return options;
|
|
|
});
|
|
|
|
|
|
-// 计算生成主题下拉框选项
|
|
|
-const themeOptions = computed(() => {
|
|
|
- const themes = [
|
|
|
- {
|
|
|
- name: '课程封面',
|
|
|
- value: 255,
|
|
|
- sections: [],
|
|
|
- show: false
|
|
|
- },
|
|
|
- {
|
|
|
- name: '诗词课',
|
|
|
- value: 256,
|
|
|
- sections: [1, 2, 3],
|
|
|
- show: true
|
|
|
- },
|
|
|
- {
|
|
|
- name: '通用课程',
|
|
|
- value: 13,
|
|
|
- sections: [1, 2, 3] ,
|
|
|
- show: true
|
|
|
+// 生成主题选项(使用 ref 保持引用稳定)
|
|
|
+const themeOptions = ref([
|
|
|
+ {
|
|
|
+ name: '课程封面',
|
|
|
+ value: 255,
|
|
|
+ sections: [],
|
|
|
+ show: false,
|
|
|
+ get label() {
|
|
|
+ return '课程封面'
|
|
|
}
|
|
|
- ]
|
|
|
-
|
|
|
- return themes.map(theme => ({
|
|
|
- ...theme,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: '诗词课',
|
|
|
+ value: 256,
|
|
|
+ sections: [1, 2, 3],
|
|
|
+ show: true,
|
|
|
get label() {
|
|
|
const lessonNames = this.sections
|
|
|
.map(sectionId => {
|
|
|
@@ -1524,9 +1374,29 @@ const themeOptions = computed(() => {
|
|
|
return lesson ? lesson.name : ''
|
|
|
})
|
|
|
.filter(Boolean)
|
|
|
- return `${this.name}(${lessonNames.join(', ')})`
|
|
|
+ return `诗词课(${lessonNames.join(', ')})`
|
|
|
}
|
|
|
- }))
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: '通用课程',
|
|
|
+ value: 13,
|
|
|
+ sections: [1, 2, 3] ,
|
|
|
+ show: true,
|
|
|
+ get label() {
|
|
|
+ const lessonNames = this.sections
|
|
|
+ .map(sectionId => {
|
|
|
+ const lesson = lessonList.value.find(l => l.id === sectionId.toString())
|
|
|
+ return lesson ? lesson.name : ''
|
|
|
+ })
|
|
|
+ .filter(Boolean)
|
|
|
+ return `通用课程(${lessonNames.join(', ')})`
|
|
|
+ }
|
|
|
+ }
|
|
|
+])
|
|
|
+
|
|
|
+// 获取可用的主题选项(过滤掉不显示的选项)
|
|
|
+const availableThemeOptions = computed(() => {
|
|
|
+ return themeOptions.value.filter(item => item.show)
|
|
|
})
|
|
|
|
|
|
/**
|
|
|
@@ -1560,10 +1430,6 @@ const courseDropdown = ref({
|
|
|
options: []
|
|
|
});
|
|
|
|
|
|
-watch(themeOptions, (newOptions) => {
|
|
|
- courseDropdown.value.options = newOptions;
|
|
|
-}, { immediate: true });
|
|
|
-
|
|
|
const teacherDropdown = ref({
|
|
|
key: 'teacher',
|
|
|
value: '主讲',
|
|
|
@@ -1641,12 +1507,30 @@ const doSendMessage = async (content) => {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
+ // 弹出确认框
|
|
|
+ const confirmResult = await ElMessageBox.confirm(
|
|
|
+ '本次生成会一次性生成完整课程,预计需要3-5分钟时间。生成过程中请保持页面打开,不要刷新或退出,否则可能导致课程生成不完整。建议您在网络稳定的环境下进行此操作,确保获得最佳体验。',
|
|
|
+ '生成确认',
|
|
|
+ {
|
|
|
+ confirmButtonText: '确认生成',
|
|
|
+ cancelButtonText: '取消',
|
|
|
+ type: 'warning',
|
|
|
+ showClose: false
|
|
|
+ }
|
|
|
+ ).catch(() => {
|
|
|
+ return 'cancel'
|
|
|
+ })
|
|
|
+
|
|
|
// 检查是否正在生成中
|
|
|
if (isGenerating.value) {
|
|
|
Message().info('正在生成中,请稍候')
|
|
|
return
|
|
|
}
|
|
|
|
|
|
+ if (confirmResult !== 'confirm') {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
// 清除之前的防抖定时器
|
|
|
if (sendDebounceTimer) {
|
|
|
clearTimeout(sendDebounceTimer)
|
|
|
@@ -1727,34 +1611,39 @@ onMounted(async () => {
|
|
|
teacherDropdown.value.options = teacherOptions.value;
|
|
|
assistantDropdown.value.options = assistantOptions.value;
|
|
|
|
|
|
- // 检查是否有未完成的任务需要恢复
|
|
|
- if (hasPendingTask.value) {
|
|
|
- Message().info('检测到未完成的任务,正在恢复...')
|
|
|
- restoreTaskState()
|
|
|
- await generateScript(true)
|
|
|
- }
|
|
|
+ // 清除可能存在的旧脚本数据缓存
|
|
|
+ localStorage.removeItem(SCRIPT_DATA_KEY)
|
|
|
});
|
|
|
|
|
|
/**
|
|
|
- * 组件卸载时执行
|
|
|
* 清理所有资源,避免内存泄漏
|
|
|
*/
|
|
|
onUnmounted(() => {
|
|
|
// 标记组件已卸载
|
|
|
isUnmounted.value = true
|
|
|
|
|
|
- // 停止图片轮询定时器
|
|
|
+ // 标记为已取消,阻止所有后续操作
|
|
|
+ isCancelled.value = true
|
|
|
+
|
|
|
+ // 停止图片轮询定时器(包含所有相关定时器)
|
|
|
stopImagePolling()
|
|
|
|
|
|
// 中止正在进行的对话请求
|
|
|
if (conversationInAbortController.value) {
|
|
|
conversationInAbortController.value.abort()
|
|
|
+ conversationInAbortController.value = null
|
|
|
}
|
|
|
|
|
|
- // 如果正在生成中,保存当前状态以便下次恢复
|
|
|
- if (isGenerating.value) {
|
|
|
- saveTaskState(currentStep.value)
|
|
|
- }
|
|
|
+ // 重置所有生成状态
|
|
|
+ isGenerating.value = false
|
|
|
+ isGeneratingImages.value = false
|
|
|
+ isGeneratingVoiceovers.value = false
|
|
|
+ conversationInProgress.value = false
|
|
|
+
|
|
|
+ // 清除脚本数据缓存
|
|
|
+ localStorage.removeItem(SCRIPT_DATA_KEY)
|
|
|
+
|
|
|
+ console.log('组件已卸载,所有资源已清理')
|
|
|
});
|
|
|
</script>
|
|
|
|