|
@@ -378,11 +378,15 @@ const hasPendingTask = computed(() => {
|
|
|
}
|
|
}
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
|
|
+// 当前正在生成的小节索引(用于恢复时从正确位置继续)
|
|
|
|
|
+const currentLessonIndex = ref(0)
|
|
|
|
|
+
|
|
|
// 保存任务状态到缓存(使用统一的键并绑定用户ID)
|
|
// 保存任务状态到缓存(使用统一的键并绑定用户ID)
|
|
|
const saveTaskState = (currentStep) => {
|
|
const saveTaskState = (currentStep) => {
|
|
|
const taskState = {
|
|
const taskState = {
|
|
|
isRestoring: true,
|
|
isRestoring: true,
|
|
|
currentStep: currentStep,
|
|
currentStep: currentStep,
|
|
|
|
|
+ currentLessonIndex: currentLessonIndex.value,
|
|
|
progressSteps: progressSteps.value,
|
|
progressSteps: progressSteps.value,
|
|
|
progressCounters: progressCounters.value,
|
|
progressCounters: progressCounters.value,
|
|
|
prompt: prompt.value,
|
|
prompt: prompt.value,
|
|
@@ -448,6 +452,11 @@ const restoreTaskState = () => {
|
|
|
Object.assign(scriptData, parsedData)
|
|
Object.assign(scriptData, parsedData)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // 恢复当前小节索引
|
|
|
|
|
+ if (parsedState.currentLessonIndex !== undefined) {
|
|
|
|
|
+ currentLessonIndex.value = parsedState.currentLessonIndex
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
// 恢复用户输入
|
|
// 恢复用户输入
|
|
|
if (parsedState.prompt) {
|
|
if (parsedState.prompt) {
|
|
|
prompt.value = parsedState.prompt
|
|
prompt.value = parsedState.prompt
|
|
@@ -466,9 +475,35 @@ const restoreTaskState = () => {
|
|
|
selectedAssistants.value = [assistantDropdown.value.value]
|
|
selectedAssistants.value = [assistantDropdown.value.value]
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 恢复图片映射
|
|
|
|
|
|
|
+ // 恢复图片映射(只保留还未完成的图片)
|
|
|
if (parsedState.inProgressImageMap) {
|
|
if (parsedState.inProgressImageMap) {
|
|
|
- inProgressImageMap.value = 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()
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
showNewContainer.value = true
|
|
showNewContainer.value = true
|
|
@@ -524,6 +559,12 @@ const cancelGenerate = (showMessage = true) => {
|
|
|
}, 500)
|
|
}, 500)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// 当前步骤(响应式,用于保存和恢复)
|
|
|
|
|
+const currentStep = ref(0)
|
|
|
|
|
+
|
|
|
|
|
+// 组件是否已卸载(用于防止流式回调在组件卸载后继续执行)
|
|
|
|
|
+const isUnmounted = ref(false)
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* 生成脚本主函数
|
|
* 生成脚本主函数
|
|
|
* @param {boolean} isResume - 是否为恢复任务(从缓存中恢复)
|
|
* @param {boolean} isResume - 是否为恢复任务(从缓存中恢复)
|
|
@@ -534,7 +575,7 @@ const generateScript = async (isResume = false) => {
|
|
|
isGenerating.value = true
|
|
isGenerating.value = true
|
|
|
isCancelled.value = false
|
|
isCancelled.value = false
|
|
|
|
|
|
|
|
- let currentStep = 0
|
|
|
|
|
|
|
+ currentStep.value = 0
|
|
|
|
|
|
|
|
// 如果是恢复任务,从缓存中获取当前步骤
|
|
// 如果是恢复任务,从缓存中获取当前步骤
|
|
|
if (isResume) {
|
|
if (isResume) {
|
|
@@ -542,7 +583,7 @@ const generateScript = async (isResume = false) => {
|
|
|
if (taskState) {
|
|
if (taskState) {
|
|
|
try {
|
|
try {
|
|
|
const parsedState = JSON.parse(taskState)
|
|
const parsedState = JSON.parse(taskState)
|
|
|
- currentStep = parsedState.currentStep || 0
|
|
|
|
|
|
|
+ currentStep.value = parsedState.currentStep || 0
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
console.error('获取当前步骤失败:', error)
|
|
console.error('获取当前步骤失败:', error)
|
|
|
}
|
|
}
|
|
@@ -554,8 +595,8 @@ const generateScript = async (isResume = false) => {
|
|
|
// 检查是否已取消
|
|
// 检查是否已取消
|
|
|
if (isCancelled.value) return
|
|
if (isCancelled.value) return
|
|
|
|
|
|
|
|
- currentStep = 0
|
|
|
|
|
- saveTaskState(currentStep)
|
|
|
|
|
|
|
+ currentStep.value = 0
|
|
|
|
|
+ saveTaskState(currentStep.value)
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
// 创建会话
|
|
// 创建会话
|
|
@@ -592,8 +633,8 @@ const generateScript = async (isResume = false) => {
|
|
|
// 检查是否已取消
|
|
// 检查是否已取消
|
|
|
if (isCancelled.value) return
|
|
if (isCancelled.value) return
|
|
|
|
|
|
|
|
- currentStep = 1
|
|
|
|
|
- saveTaskState(currentStep)
|
|
|
|
|
|
|
+ currentStep.value = 1
|
|
|
|
|
+ saveTaskState(currentStep.value)
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
// 设置进度状态
|
|
// 设置进度状态
|
|
@@ -623,11 +664,15 @@ const generateScript = async (isResume = false) => {
|
|
|
// 构建基础提示词(不含课程列表)
|
|
// 构建基础提示词(不含课程列表)
|
|
|
const baseContent = `${prompt.value}(主讲人:${role.name},主讲人角色定位:${role.description};助讲有:${assistants})`
|
|
const baseContent = `${prompt.value}(主讲人:${role.name},主讲人角色定位:${role.description};助讲有:${assistants})`
|
|
|
|
|
|
|
|
- let courseLessons = [];
|
|
|
|
|
|
|
+ // 如果是恢复任务,从已保存的脚本数据中获取已完成的课程
|
|
|
|
|
+ let courseLessons = isResume && scriptData.lessons.length > 0 ? [...scriptData.lessons] : [];
|
|
|
scriptData.lessons = []
|
|
scriptData.lessons = []
|
|
|
|
|
|
|
|
- // 循环生成每节课程内容
|
|
|
|
|
- for (let i = 0; i < lessonList.value.length; i++) {
|
|
|
|
|
|
|
+ // 循环生成每节课程内容(从保存的索引位置开始)
|
|
|
|
|
+ for (let i = currentLessonIndex.value; i < lessonList.value.length; i++) {
|
|
|
|
|
+ // 更新当前小节索引
|
|
|
|
|
+ currentLessonIndex.value = i
|
|
|
|
|
+
|
|
|
// 检查是否已取消
|
|
// 检查是否已取消
|
|
|
if (isCancelled.value) return
|
|
if (isCancelled.value) return
|
|
|
|
|
|
|
@@ -649,10 +694,12 @@ const generateScript = async (isResume = false) => {
|
|
|
console.log(`生成第${i+1}节课程【${lesson.name}】成功:`, lessonInfo)
|
|
console.log(`生成第${i+1}节课程【${lesson.name}】成功:`, lessonInfo)
|
|
|
courseLessons.push(lessonInfo)
|
|
courseLessons.push(lessonInfo)
|
|
|
|
|
|
|
|
- // 保存当前进度
|
|
|
|
|
- saveTaskState(currentStep)
|
|
|
|
|
|
|
+ // 保存当前进度(包含小节索引)
|
|
|
|
|
+ saveTaskState(currentStep.value)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // 重置小节索引为0,准备下一次生成
|
|
|
|
|
+ currentLessonIndex.value = 0
|
|
|
scriptData.lessons = courseLessons;
|
|
scriptData.lessons = courseLessons;
|
|
|
progressSteps.value[1].completed = true;
|
|
progressSteps.value[1].completed = true;
|
|
|
progressSteps.value[1].active = false;
|
|
progressSteps.value[1].active = false;
|
|
@@ -667,13 +714,18 @@ const generateScript = async (isResume = false) => {
|
|
|
// 检查是否已取消
|
|
// 检查是否已取消
|
|
|
if (isCancelled.value) return
|
|
if (isCancelled.value) return
|
|
|
|
|
|
|
|
- currentStep = 2
|
|
|
|
|
- saveTaskState(currentStep)
|
|
|
|
|
|
|
+ currentStep.value = 2
|
|
|
|
|
+
|
|
|
|
|
+ // 重置图片进度为0%,避免恢复后显示旧进度
|
|
|
|
|
+ progressSteps.value[2].progress = 0
|
|
|
|
|
+ progressCounters.value.images.current = 0
|
|
|
|
|
+
|
|
|
|
|
+ saveTaskState(currentStep.value)
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
progressSteps.value[2].visible = true;
|
|
progressSteps.value[2].visible = true;
|
|
|
progressSteps.value[2].active = true;
|
|
progressSteps.value[2].active = true;
|
|
|
- await generateAllImages()
|
|
|
|
|
|
|
+ await generateAllImages(isResume)
|
|
|
|
|
|
|
|
// 验证图片生成结果
|
|
// 验证图片生成结果
|
|
|
if (scriptData.coverImage && scriptData.coverImage.prompt && !scriptData.coverImage.url) {
|
|
if (scriptData.coverImage && scriptData.coverImage.prompt && !scriptData.coverImage.url) {
|
|
@@ -693,8 +745,8 @@ const generateScript = async (isResume = false) => {
|
|
|
// 检查是否已取消
|
|
// 检查是否已取消
|
|
|
if (isCancelled.value) return
|
|
if (isCancelled.value) return
|
|
|
|
|
|
|
|
- currentStep = 3
|
|
|
|
|
- saveTaskState(currentStep)
|
|
|
|
|
|
|
+ currentStep.value = 3
|
|
|
|
|
+ saveTaskState(currentStep.value)
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
progressSteps.value[3].visible = true;
|
|
progressSteps.value[3].visible = true;
|
|
@@ -839,13 +891,13 @@ const doSendMessageStream = async (conversationId, content, outputContent) => {
|
|
|
conversationInAbortController.value,
|
|
conversationInAbortController.value,
|
|
|
enableContext.value,
|
|
enableContext.value,
|
|
|
async (res) => {
|
|
async (res) => {
|
|
|
- if (isCancelled.value) return
|
|
|
|
|
|
|
+ // 检查组件是否已卸载或已取消
|
|
|
|
|
+ if (isUnmounted.value || isCancelled.value) return
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
const { code, data, msg } = JSON.parse(res.data)
|
|
const { code, data, msg } = JSON.parse(res.data)
|
|
|
if (code !== 0) {
|
|
if (code !== 0) {
|
|
|
console.error(`对话异常! ${msg}`)
|
|
console.error(`对话异常! ${msg}`)
|
|
|
- // API返回错误码,抛出异常终止生成
|
|
|
|
|
throw new Error(`对话异常:${msg}`)
|
|
throw new Error(`对话异常:${msg}`)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -864,21 +916,23 @@ const doSendMessageStream = async (conversationId, content, outputContent) => {
|
|
|
const parsedData = JSON.parse(receiveMessageFullText.value)
|
|
const parsedData = JSON.parse(receiveMessageFullText.value)
|
|
|
Object.assign(outputContent, parsedData)
|
|
Object.assign(outputContent, parsedData)
|
|
|
} catch (e) {
|
|
} catch (e) {
|
|
|
- // 流式接收时解析失败可以忽略(可能数据不完整),但记录日志
|
|
|
|
|
- // console.log('流式数据暂不完整,继续接收...')
|
|
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
} catch (e) {
|
|
} catch (e) {
|
|
|
console.error('消息处理异常:', e)
|
|
console.error('消息处理异常:', e)
|
|
|
- // 消息处理失败,抛出异常终止生成
|
|
|
|
|
throw new Error('消息处理失败:' + e.message)
|
|
throw new Error('消息处理失败:' + e.message)
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
(error) => {
|
|
(error) => {
|
|
|
- console.error(`对话异常! ${error}`)
|
|
|
|
|
- throw error
|
|
|
|
|
|
|
+ if (!isUnmounted.value) {
|
|
|
|
|
+ console.error(`对话异常! ${error}`)
|
|
|
|
|
+ throw error
|
|
|
|
|
+ }
|
|
|
},
|
|
},
|
|
|
() => {
|
|
() => {
|
|
|
|
|
+ // 检查组件是否已卸载
|
|
|
|
|
+ if (isUnmounted.value) return
|
|
|
|
|
+
|
|
|
try {
|
|
try {
|
|
|
if (receiveMessageFullText.value) {
|
|
if (receiveMessageFullText.value) {
|
|
|
console.log('最终数据:', receiveMessageFullText.value)
|
|
console.log('最终数据:', receiveMessageFullText.value)
|
|
@@ -916,18 +970,27 @@ const doSendMessageStream = async (conversationId, content, outputContent) => {
|
|
|
/**
|
|
/**
|
|
|
* 生成所有背景图
|
|
* 生成所有背景图
|
|
|
* 包括封面图和所有环节的背景图
|
|
* 包括封面图和所有环节的背景图
|
|
|
|
|
+ * @param {boolean} isResume - 是否为恢复任务
|
|
|
* @returns {Promise}
|
|
* @returns {Promise}
|
|
|
*/
|
|
*/
|
|
|
-const generateAllImages = async () => {
|
|
|
|
|
|
|
+const generateAllImages = async (isResume = false) => {
|
|
|
return new Promise(async (resolve, reject) => {
|
|
return new Promise(async (resolve, reject) => {
|
|
|
isGeneratingImages.value = true
|
|
isGeneratingImages.value = true
|
|
|
try {
|
|
try {
|
|
|
// ========== 计算总图片数 ==========
|
|
// ========== 计算总图片数 ==========
|
|
|
|
|
+ // 如果是恢复任务,保留已有的轮询列表;否则清空重新开始
|
|
|
|
|
+ if (!isResume) {
|
|
|
|
|
+ inProgressImageMap.value = {}
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 统计需要生成的图片总数(有prompt且无url的图片)
|
|
|
progressCounters.value.images.total = 0
|
|
progressCounters.value.images.total = 0
|
|
|
|
|
+
|
|
|
// 统计封面图
|
|
// 统计封面图
|
|
|
if (scriptData.coverImage && scriptData.coverImage.prompt && !scriptData.coverImage.url) {
|
|
if (scriptData.coverImage && scriptData.coverImage.prompt && !scriptData.coverImage.url) {
|
|
|
progressCounters.value.images.total++
|
|
progressCounters.value.images.total++
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
// 统计所有环节的背景图
|
|
// 统计所有环节的背景图
|
|
|
if (scriptData.lessons && scriptData.lessons.length > 0) {
|
|
if (scriptData.lessons && scriptData.lessons.length > 0) {
|
|
|
for (let lessonIndex = 0; lessonIndex < scriptData.lessons.length; lessonIndex++) {
|
|
for (let lessonIndex = 0; lessonIndex < scriptData.lessons.length; lessonIndex++) {
|
|
@@ -942,7 +1005,17 @@ const generateAllImages = async () => {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
- progressCounters.value.images.current = 0
|
|
|
|
|
|
|
+
|
|
|
|
|
+ // 计算已完成的图片数(总数 - 正在生成的数量)
|
|
|
|
|
+ progressCounters.value.images.current = progressCounters.value.images.total - Object.keys(inProgressImageMap.value).length
|
|
|
|
|
+
|
|
|
|
|
+ // 更新进度显示
|
|
|
|
|
+ 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)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ progressSteps.value[2].progress = 100
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
// 初始化封面图对象
|
|
// 初始化封面图对象
|
|
|
if (!scriptData.coverImage) {
|
|
if (!scriptData.coverImage) {
|
|
@@ -951,7 +1024,15 @@ const generateAllImages = async () => {
|
|
|
|
|
|
|
|
// ========== 生成封面图(如果尚未生成) ==========
|
|
// ========== 生成封面图(如果尚未生成) ==========
|
|
|
if (scriptData.coverImage.prompt && !scriptData.coverImage.url) {
|
|
if (scriptData.coverImage.prompt && !scriptData.coverImage.url) {
|
|
|
- await generateCoverImage()
|
|
|
|
|
|
|
+ // 在恢复模式下,检查封面图是否已经在生成中
|
|
|
|
|
+ const coverAlreadyGenerating = Object.keys(inProgressImageMap.value).some(
|
|
|
|
|
+ key => inProgressImageMap.value[key].type === 'cover'
|
|
|
|
|
+ )
|
|
|
|
|
+ if (!isResume || !coverAlreadyGenerating) {
|
|
|
|
|
+ await generateCoverImage()
|
|
|
|
|
+ } else {
|
|
|
|
|
+ console.log('封面图已在生成中,跳过重复生成')
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// ========== 生成所有环节的背景图 ==========
|
|
// ========== 生成所有环节的背景图 ==========
|
|
@@ -974,7 +1055,17 @@ const generateAllImages = async () => {
|
|
|
}
|
|
}
|
|
|
// 如果有prompt但没有url,则生成图片
|
|
// 如果有prompt但没有url,则生成图片
|
|
|
if (section.backgroundImage.prompt && !section.backgroundImage.url) {
|
|
if (section.backgroundImage.prompt && !section.backgroundImage.url) {
|
|
|
- await generateImage(lessonIndex, sectionIndex)
|
|
|
|
|
|
|
+ // 在恢复模式下,检查该图片是否已经在生成中
|
|
|
|
|
+ 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}]已在生成中,跳过重复生成`)
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -1222,13 +1313,14 @@ const refreshWatchImages = async () => {
|
|
|
})
|
|
})
|
|
|
inProgressImageMap.value = newWatchImages
|
|
inProgressImageMap.value = newWatchImages
|
|
|
|
|
|
|
|
- // 累计完成数 = 之前完成的 + 本次新完成的
|
|
|
|
|
- progressCounters.value.images.current += completedCount
|
|
|
|
|
|
|
+ // 计算当前已完成的图片数量(重新统计,避免刷新后累加导致超过100%)
|
|
|
|
|
+ const completedImages = progressCounters.value.images.total - Object.keys(inProgressImageMap.value).length
|
|
|
|
|
+ progressCounters.value.images.current = completedImages
|
|
|
|
|
|
|
|
- // 确保进度百分比只增不减,避免进度条重置
|
|
|
|
|
|
|
+ // 更新进度百分比
|
|
|
if (progressCounters.value.images.total > 0) {
|
|
if (progressCounters.value.images.total > 0) {
|
|
|
- const newProgress = Math.round((progressCounters.value.images.current / progressCounters.value.images.total) * 100)
|
|
|
|
|
- progressSteps.value[2].progress = Math.max(progressSteps.value[2].progress, newProgress)
|
|
|
|
|
|
|
+ const newProgress = Math.round((completedImages / progressCounters.value.images.total) * 100)
|
|
|
|
|
+ progressSteps.value[2].progress = Math.min(newProgress, 100)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
saveTaskState(2)
|
|
saveTaskState(2)
|
|
@@ -1647,6 +1739,9 @@ onMounted(async () => {
|
|
|
* 清理所有资源,避免内存泄漏
|
|
* 清理所有资源,避免内存泄漏
|
|
|
*/
|
|
*/
|
|
|
onUnmounted(() => {
|
|
onUnmounted(() => {
|
|
|
|
|
+ // 标记组件已卸载
|
|
|
|
|
+ isUnmounted.value = true
|
|
|
|
|
+
|
|
|
// 停止图片轮询定时器
|
|
// 停止图片轮询定时器
|
|
|
stopImagePolling()
|
|
stopImagePolling()
|
|
|
|
|
|
|
@@ -1657,7 +1752,7 @@ onUnmounted(() => {
|
|
|
|
|
|
|
|
// 如果正在生成中,保存当前状态以便下次恢复
|
|
// 如果正在生成中,保存当前状态以便下次恢复
|
|
|
if (isGenerating.value) {
|
|
if (isGenerating.value) {
|
|
|
- saveTaskState(0)
|
|
|
|
|
|
|
+ saveTaskState(currentStep.value)
|
|
|
}
|
|
}
|
|
|
});
|
|
});
|
|
|
</script>
|
|
</script>
|