ソースを参照

优化生成图片视频逻辑
增加失败状态处理和显示

liyanbo 3 週間 前
親
コミット
65ccefb6c0

+ 170 - 257
src/components/ai/image/ImageToImage.vue

@@ -7,7 +7,7 @@
         <!-- 对话消息列表 -->
         <div class="message-list">
           <div v-if="imageAllList.length > 0">
-              <div  v-for="(item, index) in imageAllList" :key="index">
+              <div  v-for="(item, index) in imageAllList" :key="item.id || index">
             <!-- 用户消息 -->
             <div class="user-message" v-if="item.type === 'user'">
               {{ item.content }}
@@ -24,7 +24,7 @@
                   >
                     <el-icon @click="prev"><Back /></el-icon>
                     <el-icon @click="next"><Right /></el-icon>
-                    <el-icon @click="setActiveItem(item.imageList.length - 1)">
+                    <el-icon @click="setActiveItem(0)">
                       <DArrowRight />
                     </el-icon>
                     <el-icon @click="actions('zoomOut')"><ZoomOut /></el-icon>
@@ -89,8 +89,8 @@
               style="flex: 1; margin-right: 8px;"
             />
           <!-- 风格选择下拉框 -->
-          <el-select 
-            v-model="selectedStyle" 
+          <el-select
+            v-model="selectedStyle"
             placeholder="选择风格"
             filterable
             allow-create
@@ -99,10 +99,10 @@
             size="small"
             class="custom-style-select"
           >
-            <el-option 
-              v-for="style in styleList" 
-              :key="style" 
-              :label="style" 
+            <el-option
+              v-for="style in styleList"
+              :key="style"
+              :label="style"
               :value="style"
             />
           </el-select>
@@ -138,26 +138,17 @@
 import {ref, onMounted, onUnmounted, defineEmits, computed} from 'vue'
 import {AiImageStatusEnum, CreatePainting, PaintingGetMys, getMyAiPointBalance} from '@/api/questions.js'
 import VoiceInput from '../voice/VoiceInput_Api.vue'
-
-// 导入全局状态
-import { globalState } from '@/utils/globalState.js'
-// 终止按钮
 import stopicon from "@/assets/icon/stopicon.png";
-// 消息组件
 import {Message} from "@/utils/message/Message.js";
-
-// 上传参考图
 import ImageUpload from '@/components/ImageUpload/index.vue';
-
-// 导入接口
-import {getModelIdByType, ModelPlatformEnum} from '@/api/teachers.js'
-import { ModelTypeEnum } from '@/api/teachers.js'
+import {getModelIdByType, ModelPlatformEnum, ModelTypeEnum} from '@/api/teachers.js'
+import {
+  Back, DArrowRight, Download, Refresh, RefreshLeft, RefreshRight, Right, ZoomIn, ZoomOut,
+} from '@element-plus/icons-vue'
 
 // 定义props
 const props = defineProps({
-  //根据需求传输
   isCourse: { type: Boolean, default: false},
-  cacheDataKey: { type: String, default: localStorage.getItem('token') + "_ai_imageToImage" },
   cacheDataHistoryKey: { type: String, default: localStorage.getItem('token') + "_ai_imageToImage_history" },
   preDialogueList: { type: Array, default: () => []},
   replySupplement: { type: String, default: ''},
@@ -169,111 +160,68 @@ const emits = defineEmits(['saveProgress'])
 //历史记录缓存集合
 const cacheDataHistoryList = ref([]);
 
-// 存储上传的图片
 const uploadedImage = ref('');
-
 const imageUploadRef = ref(null);
-
-// 当前选中的风格
 const selectedStyle = ref('');
-
-
-
-// 风格列表
 const styleList = ['电影写真', '中国风', '卡通', '动漫', '素描', '像素风格'];
-// 对话状态变量
-const conversationInProgress = ref(false); // 对话是否正在进行中
-const conversationInAbortController = ref(); // 对话进行中 abort 控制器
-
-// tts 语音
-import { useAudioPlayer } from '@/api/tts/useAudioPlayer';
-
-// 添加抽屉显示状态
-const drawerVisible = ref(true)
-
-  // 年级ID相关
-const gradeId = ref('')
-// modelId响应式变量
+const conversationInProgress = ref(false);
 const modelId = ref(0)
-// 保存记录
+
 onMounted(async () => {
-    // 从全局状态初始化年级ID
-  gradeId.value = globalState.initGradeId()
-  try{
-    // 获取modelId
+  try {
     const modelRes = await getModelIdByType({ type: ModelTypeEnum.IMAGE_TO_IMAGE, platform: ModelPlatformEnum.DOUBAO })
     modelId.value = modelRes.data
-  }catch(error){
-    console.error('保存记录失败:', error);
+  } catch (error) {
+    console.error('获取模型ID失败:', error);
   }
 
-  // 取缓存历史记录
   let cacheDataHistoryStr = localStorage.getItem(props.cacheDataHistoryKey);
   if (cacheDataHistoryStr) {
     cacheDataHistoryList.value = JSON.parse(cacheDataHistoryStr) || [];
     imageAllList.value.push(...cacheDataHistoryList.value);
-  }else{
-    // 加入预置对话
+  } else {
     imageAllList.value.push(...props.preDialogueList);
     cacheDataHistoryList.value.push(...props.preDialogueList);
-    if (cacheDataHistoryList.value.length > 0) localStorage.setItem(props.cacheDataHistoryKey, JSON.stringify(cacheDataHistoryList.value));
+    if (cacheDataHistoryList.value.length > 0) {
+      localStorage.setItem(props.cacheDataHistoryKey, JSON.stringify(cacheDataHistoryList.value));
+    }
   }
 
-  // 自动刷新 image 列表
   refreshWatchImagesFun()
 });
 
-// 消息列表和输入内容的响应式变量
+onUnmounted(() => {
+  inProgressTimerFun()
+})
+
 const inputMessage = ref('')
-// 输入框引用
 const inputRef = ref(null)
 
+const isVoiceRecording = ref(false);
+const voiceRecognizedText = ref("");
 
-
-// 导入计算属性
-
-// 语音输入状态跟踪
-const isVoiceRecording = ref(false); // 当前是否正在录音
-const voiceRecognizedText = ref(""); // 实时语音识别结果
-
-// 用于控制输入框显示的内容
 const displayedPrompt = computed({
-  get() {
-    // 录音时,只显示inputMessage.value(已经在handleVoiceRecognized中实时更新)
-    if (isVoiceRecording.value) {
-      return inputMessage.value;
-    }
-    // 不录音时,只显示inputMessage.value
-    return inputMessage.value;
-  },
+  get() { return inputMessage.value },
   set(newValue) {
-    // 只在用户手动输入时更新inputMessage.value
-    if (!isVoiceRecording.value) {
-      inputMessage.value = newValue;
-    }
+    if (!isVoiceRecording.value) inputMessage.value = newValue
   }
 });
 
 // 语音输入识别结果处理
 const handleVoiceRecognized = (data) => {
   if (isVoiceRecording.value) {
-    // 在同一次录音过程中,实时更新文本框内容
     voiceRecognizedText.value = data.originalText;
     inputMessage.value = data.processedText;
   } else {
-    // 在录音结束时,将最终的语音内容插入到光标位置
     const input = document.querySelector('input[type="text"]');
     if (input) {
       inputMessage.value = data.processedText;
-      // 重新设置光标位置到插入文本的末尾
       setTimeout(() => {
         input.selectionStart = input.selectionEnd = data.cursorPos;
       }, 0);
     } else {
-      // 如果没有找到输入框,直接替换整个内容
       inputMessage.value = data.originalText;
     }
-    // 清空临时变量
     voiceRecognizedText.value = "";
   }
 };
@@ -283,24 +231,20 @@ const handleRecordingStatusChanged = (status) => {
   const wasRecording = isVoiceRecording.value;
   isVoiceRecording.value = status;
 
-  // 如果是从录音状态切换到非录音状态,清空临时变量
   if (wasRecording && !status) {
-    // 清空临时变量
     voiceRecognizedText.value = "";
   }
 };
 
 // 停止操作函数
-const stopStream = async () => {
-  // tip:如果 stream 进行中的 message,就需要调用 controller 结束
-  if (conversationInAbortController.value) {
-    conversationInAbortController.value.abort();
-  }
-  // 设置为 false
+const stopStream = () => {
   conversationInProgress.value = false;
+  inProgressTimerFun()
+  inProgressImageMap.value = {}
+  const idx = imageAllList.value.findIndex(m => m.loading === true)
+  if (idx !== -1) imageAllList.value.splice(idx, 1)
 };
 
-// 发送消息函数,图片数据
 const ensureAiPoint = async (cost) => {
   try {
     const res = await getMyAiPointBalance()
@@ -313,208 +257,191 @@ const ensureAiPoint = async (cost) => {
 }
 
 const sendMessage = async() => {
-  if (uploadedImage.value) {
-    if (!await ensureAiPoint(100)) return
-    // 如果正在录音,先停止录音
-    if (isVoiceRecording.value) {
-      const voiceInputComponent = document.querySelector('.voice-input-container .speech-btn');
-      if (voiceInputComponent) {
-        voiceInputComponent.click();
-      }
-      // 等待录音结束后再发送
-      setTimeout(() => {
-        sendMessage();
-      }, 100);
-      return;
-    }
-    // 创建 AbortController 实例,以便中止请求
-    conversationInAbortController.value = new AbortController();
-    // 标记对话进行中
-    conversationInProgress.value = true;
-    // 先保存内容 再置空输入框
-    let content = inputMessage.value;
-    inputMessage.value = '';
-    
-
-    // 创建用户消息对象,包含可能的图片
-    const userMessage = {
-      type: 'user',
-      content: content,
-    };
-    
-    // 如果有上传的图片,添加到用户消息中
-    if (uploadedImage.value) {
-      userMessage.imageUrl = uploadedImage.value;
-      // 清空上传的图片
-      // uploadedImage.value = '';
-    }
-    
-    imageAllList.value.push(userMessage);
-    imageAllList.value.push({
-      type: 'ai',
-      content: "正在为您生成图片,请稍等",
-      loading: true
-    })
+  if (!uploadedImage.value) {
+    Message().error('请先上传参考图!', true);
+    return
+  }
+  const content = inputMessage.value.trim();
+  if (!content) {
+    Message().error('请描述要生成的图片内容');
+    return
+  }
+  if (conversationInProgress.value) return
+
+  if (!await ensureAiPoint(100)) return
 
-    // 通过emit事件通知父组件保存进度
-    emits('saveProgress', "aiCount", 1)
-    if (props.isCourse){
-      emits('saveProgress', "course", 100)
+  // 如果正在录音,先停止录音
+  if (isVoiceRecording.value) {
+    const voiceInputComponent = document.querySelector('.voice-input-container .speech-btn');
+    if (voiceInputComponent) {
+      voiceInputComponent.click();
     }
+    setTimeout(() => sendMessage(), 100);
+    return;
+  }
 
+  conversationInProgress.value = true;
+  inputMessage.value = '';
 
-    // 同步历史记录缓存
-    cacheDataHistoryList.value.push(userMessage);
-    localStorage.setItem(props.cacheDataHistoryKey, JSON.stringify(cacheDataHistoryList.value));
+  const userMessage = {
+    id: genMsgId(),
+    type: 'user',
+    content,
+    imageUrl: uploadedImage.value,
+  };
 
-    try {
-      CreatePainting({
-        "modelId": modelId.value,
-        "prompt":content,
-        "width":1024,
-        "height":1024,
-        "promptImage":uploadedImage.value
-      }).then(res=>{
-        console.log("生成图片",res)
-        //目前写死调用已生成的图片,全部通了后再改
-        inProgressImageMap.value[res.data] = {id:res.data,status:AiImageStatusEnum.IN_PROGRESS}
-        // inProgressImageMap.value[260] = {id:260,status:AiImageStatusEnum.IN_PROGRESS}
-      }).finally(() => {
-        // 图片生成请求完成后更新状态
-        conversationInProgress.value = false;
-      });
-    } catch (error) {
-      console.error('生成图片失败:', error);
-      conversationInProgress.value = false;
-    }
-  } else {
-    // 如果没有上传图片,显示提示信息
-    Message().error('请先上传参考图!', true);
+  imageAllList.value.push(userMessage);
+
+  const loadingMsg = { id: genMsgId(), type: 'ai', content: '正在为您生成图片,请稍等', loading: true }
+  imageAllList.value.push(loadingMsg);
+
+  emits('saveProgress', 'aiCount', 1)
+  if (props.isCourse) emits('saveProgress', 'course', 100)
+
+  cacheDataHistoryList.value.push(userMessage);
+  localStorage.setItem(props.cacheDataHistoryKey, JSON.stringify(cacheDataHistoryList.value));
+
+  try {
+    const res = await CreatePainting({
+      modelId: modelId.value,
+      prompt: content,
+      width: 1024,
+      height: 1024,
+      promptImage: uploadedImage.value
+    })
+    inProgressImageMap.value[res.data] = { id: res.data, status: AiImageStatusEnum.IN_PROGRESS }
+    pollFailCount.value = 0
+    if (!inProgressTimer.value) refreshWatchImagesFun()
+  } catch (error) {
+    const loadingIdx = imageAllList.value.findIndex(m => m.loading === true)
+    if (loadingIdx !== -1) imageAllList.value.splice(loadingIdx, 1)
+    Message().error('创建图片生成任务失败,请稍后重试')
+  } finally {
+    conversationInProgress.value = false;
   }
-  // 调用子组件的方法清除预览图
+
   imageUploadRef.value?.clearPreview();
 };
 
 
-// 生成图片
-import { ElIcon } from 'element-plus'
-import {
-  Back,
-  DArrowRight,
-  Download,
-  Refresh,
-  RefreshLeft,
-  RefreshRight,
-  Right,
-  ZoomIn,
-  ZoomOut,
-} from '@element-plus/icons-vue'
+const imageAllList = ref([])
+const inProgressImageMap = ref({})
+const inProgressTimer = ref()
+const pollFailCount = ref(0)
+const MAX_POLL_FAIL = 5
 
+/** 生成唯一ID */
+const genMsgId = () => Date.now().toString(36) + Math.random().toString(36).slice(2, 6)
 
-const imageAllList = ref([]) // 对话的消息列表
-// 图片轮询相关的参数(正在生成中的)
-const inProgressImageMap = ref({}) // 监听的 image 映射,一般是生成中(需要轮询),key 为 image 编号,value 为 image
-const inProgressTimer = ref() // 生成中的 image 定时器,轮询生成进展
-
+/** 解析 ArkHttpException 错误信息 */
+const parseArkErrorMessage = (errorMessage) => {
+  if (!errorMessage) return '未知错误'
+  const match = errorMessage.match(/message='([^']*)'/)
+  if (match) return match[1]
+  return errorMessage
+}
 
-/** 轮询生成中的 image 列表 */
+/** 轮询生成中的任务 */
 const refreshWatchImages = async () => {
   const imageIds = Object.keys(inProgressImageMap.value).map(Number)
-  if (imageIds.length === 0) {
+  if (imageIds.length === 0) return
+
+  let list
+  try {
+    list = await PaintingGetMys(imageIds)
+    pollFailCount.value = 0
+  } catch (error) {
+    pollFailCount.value++
+    if (pollFailCount.value >= MAX_POLL_FAIL) {
+      inProgressTimerFun()
+      inProgressImageMap.value = {}
+      Message().error('查询图片生成状态失败,已停止轮询,请稍后重试')
+    }
     return
   }
-  const list = await PaintingGetMys(imageIds)
+
   const newWatchImages = {}
-  list.data.forEach((image) => {
-    
+
+  list?.data.forEach((image) => {
     if (image.status === AiImageStatusEnum.IN_PROGRESS) {
       newWatchImages[image.id] = image
-    } else {
-      imageAllList.value.pop();
-      console.log('AI生成的图片地址:', image.picUrl);
-      let imageMsg = {
-        type: 'ai',
-        content: "已为您生成图片:",
-        imageList: [image.picUrl],
-      }
-      imageAllList.value.push(imageMsg)
+      return
+    }
 
+    // 精确找到并移除对应的 loading 占位消息
+    const loadingIdx = imageAllList.value.findIndex(m => m.loading === true)
+    if (loadingIdx !== -1) imageAllList.value.splice(loadingIdx, 1)
+
+    if (image.status === AiImageStatusEnum.FAIL) {
+      const errorMsg = parseArkErrorMessage(image.errorMessage)
+      const failMsg = { id: genMsgId(), type: 'ai', content: `生成图片失败:${errorMsg}` }
+      imageAllList.value.push(failMsg)
+      cacheDataHistoryList.value.push(failMsg)
+      localStorage.setItem(props.cacheDataHistoryKey, JSON.stringify(cacheDataHistoryList.value))
+      Message().error(`图片生成失败:${errorMsg}`)
+      return
+    }
 
-      // 添加补充信息
-      if (!conversationInProgress.value && props.replySupplement) {
-        imageAllList.value.push({
-          type: "system",
-          content: props.replySupplement,
-        });
-      }
+    const imageMsg = { id: genMsgId(), type: 'ai', content: '已为您生成图片:', imageList: [image.picUrl] }
+    imageAllList.value.push(imageMsg)
 
-      // 同步缓存历史记录
-      cacheDataHistoryList.value.push(imageMsg);
-      localStorage.setItem(props.cacheDataHistoryKey, JSON.stringify(cacheDataHistoryList.value));
+    if (!conversationInProgress.value && props.replySupplement) {
+      imageAllList.value.push({ id: genMsgId(), type: 'system', content: props.replySupplement })
     }
+
+    cacheDataHistoryList.value.push(imageMsg);
+    localStorage.setItem(props.cacheDataHistoryKey, JSON.stringify(cacheDataHistoryList.value));
   })
+
   inProgressImageMap.value = newWatchImages
-  if (newWatchImages.size === 0) {
+  if (Object.keys(newWatchImages).length === 0) {
     inProgressTimerFun()
   }
 }
 
-/** 组件取消挂在的时候 */
-onUnmounted(async () => {
-  inProgressTimerFun()
-})
-
-// 自动刷新 image 列表
+/** 启动轮询(已运行则跳过) */
 const refreshWatchImagesFun = () => {
+  if (inProgressTimer.value) return
   inProgressTimer.value = setInterval(async () => {
     await refreshWatchImages()
-  }, 1000 * 3)
+  }, 3000)
 }
 
-// 停止刷新image列表
+/** 停止轮询 */
 const inProgressTimerFun = () => {
   if (inProgressTimer.value) {
     clearInterval(inProgressTimer.value)
+    inProgressTimer.value = null
   }
 }
 
 // 处理风格选择变化事件
 const handleStyleSelectChange = (style) => {
-  // 检查输入框中是否已经包含风格描述,如果有则替换,没有则添加
   const styleRegex = /生成图片风格为[^,]+/g;
   if (styleRegex.test(inputMessage.value)) {
-    // 替换现有的风格描述
     inputMessage.value = inputMessage.value.replace(styleRegex, `生成图片风格为${style}`);
   } else {
-    // 添加新的风格描述
     inputMessage.value += `生成图片风格为${style}`;
   }
-  // 选中风格后,将光标自动对焦到输入
   inputRef.value?.focus();
 }
 
 // 图片下载功能
 const download = (event, activeIndex) => {
-  // 获取当前点击的图片
   const target = event.target || {};
-  // 从父元素找到图片预览器,并获取当前图片URL
   const imgPreview = target.closest('.el-image-viewer__wrapper');
   if (!imgPreview) return;
-  // 获取所有预览图片
   const imgElements = imgPreview.querySelectorAll('.el-image-viewer__img');
   if (!imgElements || imgElements.length === 0) return;
-  // 确定要下载的图片URL,根据传入的 activeIndex 获取对应图片的URL
   let imgUrl;
   if (imgElements[activeIndex]) {
     imgUrl = imgElements[activeIndex].src;
   } else {
     imgUrl = imgElements[0].src;
   }
-  // 创建下载链接并触发下载
   const link = document.createElement('a');
   link.download = `image_${new Date().getTime()}.png`;
   link.href = imgUrl;
-  // 触发下载
   document.body.appendChild(link);
   link.click();
   document.body.removeChild(link);
@@ -524,11 +451,9 @@ const download = (event, activeIndex) => {
 
 <style scoped lang="scss">
 @use 'sass:math';
-// 定义rpx转换函数
 @function rpx($px) {
   @return math.div($px, 750) * 100vw;
 }
-// 用户图片列表样式
 .user-image-list {
   display: flex;
   flex-wrap: wrap;
@@ -543,15 +468,12 @@ const download = (event, activeIndex) => {
 
 .content-box {
   flex: 1;
-  // margin-top: rpx(10);
-  // margin-bottom: rpx(10);
   margin: rpx(7);
   border-radius: rpx(15);
   background: rgba($color: #ffffff, $alpha: 0.5);
   overflow-y: auto;
 }
 
-// 对话框
 .chat-dialog {
   display: flex;
   flex-direction: column;
@@ -562,48 +484,46 @@ const download = (event, activeIndex) => {
   overflow-y: auto;
   padding: rpx(15);
 }
-/* 自定义滚动条样式 */
 .message-list::-webkit-scrollbar {
-  width: rpx(2); /* 滚动条宽度 */
+  width: rpx(2);
 }
 .message-list::-webkit-scrollbar-track {
-  background: #f1effd; /* 滚动条轨道背景色 */
+  background: #f1effd;
   border-radius: rpx(4);
 }
 .message-list::-webkit-scrollbar-thumb {
-  background: #e2ddfc; /* 滚动条滑块颜色 */
+  background: #e2ddfc;
   border-radius: rpx(4);
 }
 .message-list::-webkit-scrollbar-thumb:hover {
-  background: #e2ddfc; /* 滚动条滑块 hover 状态颜色 */
+  background: #e2ddfc;
 }
 
 .message-list .user-message {
   background-color: #ffffff;
-  margin-left: auto; // 消息靠右显示
-  margin-right: 0; // 重置右边距
+  margin-left: auto;
+  margin-right: 0;
   max-width: rpx(400);
   font-size: rpx(8);
-  width: fit-content; // 宽度随文字内容变化
+  width: fit-content;
   border-radius: rpx(5);
   padding: rpx(5);
-  text-align: left; // 文字左对齐
+  text-align: left;
 }
 
 .message-list .ai-message {
   background-color: #ffdd55;
-  margin-left: 0; // 消息靠左显示
-  margin-right: auto; // 重置右边距
+  margin-left: 0;
+  margin-right: auto;
   margin-bottom: rpx(10);
   width: fit-content;
   max-width: rpx(400);
   padding: rpx(5);
   font-size: rpx(8);
   border-radius: rpx(5);
-  text-align: left; // 文字左对齐
+  text-align: left;
 }
 
-// 加载动画效果
 .loading-dots {
   display: inline-block;
   margin-left: rpx(5);
@@ -644,9 +564,7 @@ const download = (event, activeIndex) => {
   padding: 30px 10px;
 }
 
-// 风格下拉框
 .style-dropdown{
-  // min-width: auto;
   width: fit-content;
   padding: rpx(6);
 }
@@ -654,7 +572,7 @@ const download = (event, activeIndex) => {
   font-size: rpx(8);
   color: black;
   border-radius: rpx(5);
-  min-width: auto; 
+  min-width: auto;
   width: rpx(55);
   height: rpx(15);
   display: flex;
@@ -675,8 +593,7 @@ const download = (event, activeIndex) => {
       #ffce1b
     );
   }
-  
-  
+
 
 
 .input-section {
@@ -687,7 +604,7 @@ const download = (event, activeIndex) => {
   align-content: center;
   flex-wrap: nowrap;
   min-height: rpx(30);
-  
+
   > * {
     display: flex;
     align-items: center;
@@ -695,7 +612,7 @@ const download = (event, activeIndex) => {
     height: rpx(20);
     box-sizing: border-box;
   }
-  
+
   .speech-btn {
     padding: rpx(5) rpx(10);
     background: #fff;
@@ -721,8 +638,7 @@ const download = (event, activeIndex) => {
       transition: transform 0.3s ease;
     }
   }
-  
-  // 终止按钮样式
+
   .stop-btn {
     cursor: pointer;
     height: 100%;
@@ -731,15 +647,13 @@ const download = (event, activeIndex) => {
       height: rpx(20);
     }
   }
-  
-  // 确保图片上传组件垂直居中
+
   :deep(.el-upload) {
     display: flex;
     align-items: center;
     justify-content: center;
   }
-  
-  // 确保语音输入组件垂直居中
+
   :deep(.voice-input-container) {
     display: flex;
     align-items: center;
@@ -762,7 +676,7 @@ const download = (event, activeIndex) => {
     to bottom,
     #fee78a,
     #ffce1b
-  ); /* 设置悬停、聚焦、点击状态下的背景色 */
+  );
   color: black;
   border: none;
   font-size: rpx(7);
@@ -787,7 +701,6 @@ const download = (event, activeIndex) => {
   align-items: center;
 }
 
-// 风格下拉框
 .custom-style-select {
     width: rpx(55);
     color: black;

+ 150 - 193
src/components/ai/image/TextToImage.vue

@@ -7,7 +7,7 @@
         <!-- 对话消息列表 -->
         <div class="message-list">
           <div v-if="imageAllList.length > 0">
-            <div v-for="(item, index) in imageAllList" :key="index">
+            <div v-for="(item, index) in imageAllList" :key="item.id || index">
             <!-- 用户消息 -->
             <div class="user-message" v-if="item.type === 'user'">
               {{ item.content }}
@@ -145,21 +145,16 @@ import demo2 from '@/assets/images/ai-demo/ai-image-demo2.png'
 import demo3 from '@/assets/images/ai-demo/ai-image-demo3.png'
 import demo4 from '@/assets/images/ai-demo/ai-image-demo4.png'
 
-// 导入全局状态
-import { globalState } from '@/utils/globalState.js'
 import VoiceInput from '../voice/VoiceInput_Api.vue'
-// 终止按钮
 import stopicon from "@/assets/icon/stopicon.png";
-
-// 导入getModelIdByType接口
-import {getModelIdByType, ModelPlatformEnum} from '@/api/teachers.js'
-import { ModelTypeEnum } from '@/api/teachers.js'
+import {getModelIdByType, ModelPlatformEnum, ModelTypeEnum} from '@/api/teachers.js'
+import {
+  Back, DArrowRight, Download, Refresh, RefreshLeft, RefreshRight, Right, ZoomIn, ZoomOut,
+} from '@element-plus/icons-vue'
 
 // 定义props
 const props = defineProps({
-  //根据需求传输
   isCourse: { type: Boolean, default: false},
-  cacheDataKey: { type: String, default: localStorage.getItem('token') + "_ai_textToImage" },
   cacheDataHistoryKey: { type: String, default: localStorage.getItem('token') + "_ai_textToImage_history" },
   preDialogueList: { type: Array, default: () => []},
   replySupplement: { type: String, default: ''},
@@ -171,89 +166,66 @@ const emits = defineEmits(['saveProgress'])
 //历史记录缓存集合
 const cacheDataHistoryList = ref([]);
 
-// 对话状态变量
-const conversationInProgress = ref(false); // 对话是否正在进行中
-const conversationInAbortController = ref(); // 对话进行中 abort 控制器
-
+const conversationInProgress = ref(false);
 const demoImageList = [demo1, demo2, demo3, demo4]
-
-  // 年级ID相关
-const gradeId = ref('')
-// modelId响应式变量
 const modelId = ref(0)
 
-// 保存记录
 onMounted(async () => {
-    // 从全局状态初始化年级ID
-  gradeId.value = globalState.initGradeId()
-  try{
-    // 获取modelId
+  try {
     const modelRes = await getModelIdByType({ type: ModelTypeEnum.TEXT_TO_IMAGE, platform: ModelPlatformEnum.DOUBAO })
     modelId.value = modelRes.data
-    
-  }catch(error){
-    console.error('保存记录失败:', error);
+  } catch (error) {
+    console.error('获取模型ID失败:', error);
   }
 
-  // 取缓存历史记录
   let cacheDataHistoryStr = localStorage.getItem(props.cacheDataHistoryKey);
   if (cacheDataHistoryStr) {
     cacheDataHistoryList.value = JSON.parse(cacheDataHistoryStr) || [];
     imageAllList.value.push(...cacheDataHistoryList.value);
-  }else{
-    // 加入预置对话
+  } else {
     imageAllList.value.push(...props.preDialogueList);
     cacheDataHistoryList.value.push(...props.preDialogueList);
-    if (cacheDataHistoryList.value.length > 0) localStorage.setItem(props.cacheDataHistoryKey, JSON.stringify(cacheDataHistoryList.value));
+    if (cacheDataHistoryList.value.length > 0) {
+      localStorage.setItem(props.cacheDataHistoryKey, JSON.stringify(cacheDataHistoryList.value));
+    }
   }
+
+  refreshWatchImagesFun()
 });
 
-// 消息列表和输入内容的响应式变量
-const messages = ref([])
+onUnmounted(() => {
+  inProgressTimerFun()
+})
+
 const inputMessage = ref('')
 
 // 语音输入状态跟踪
-const isVoiceRecording = ref(false); // 当前是否正在录音
-const voiceRecognizedText = ref(""); // 实时语音识别结果
+const isVoiceRecording = ref(false);
+const voiceRecognizedText = ref("");
 
 // 用于控制输入框显示的内容
 const displayedPrompt = computed({
-  get() {
-    // 录音时,只显示inputMessage.value(已经在handleVoiceRecognized中实时更新)
-    if (isVoiceRecording.value) {
-      return inputMessage.value;
-    }
-    // 不录音时,只显示inputMessage.value
-    return inputMessage.value;
-  },
+  get() { return inputMessage.value },
   set(newValue) {
-    // 只在用户手动输入时更新inputMessage.value
-    if (!isVoiceRecording.value) {
-      inputMessage.value = newValue;
-    }
+    if (!isVoiceRecording.value) inputMessage.value = newValue
   }
 });
 
 // 语音输入识别结果处理
 const handleVoiceRecognized = (data) => {
   if (isVoiceRecording.value) {
-    // 在同一次录音过程中,实时更新文本框内容
     voiceRecognizedText.value = data.originalText;
     inputMessage.value = data.processedText;
   } else {
-    // 在录音结束时,将最终的语音内容插入到光标位置
     const input = document.querySelector('input[type="text"]');
     if (input) {
       inputMessage.value = data.processedText;
-      // 重新设置光标位置到插入文本的末尾
       setTimeout(() => {
         input.selectionStart = input.selectionEnd = data.cursorPos;
       }, 0);
     } else {
-      // 如果没有找到输入框,直接替换整个内容
       inputMessage.value = data.originalText;
     }
-    // 清空临时变量
     voiceRecognizedText.value = "";
   }
 };
@@ -263,21 +235,18 @@ const handleRecordingStatusChanged = (status) => {
   const wasRecording = isVoiceRecording.value;
   isVoiceRecording.value = status;
 
-  // 如果是从录音状态切换到非录音状态,清空临时变量
   if (wasRecording && !status) {
-    // 清空临时变量
     voiceRecognizedText.value = "";
   }
 };
 
-// 停止操作函数
-const stopStream = async () => {
-  // tip:如果 stream 进行中的 message,就需要调用 controller 结束
-  if (conversationInAbortController.value) {
-    conversationInAbortController.value.abort();
-  }
-  // 设置为 false
+// 停止操作函数:终止当前轮询
+const stopStream = () => {
   conversationInProgress.value = false;
+  inProgressTimerFun()
+  inProgressImageMap.value = {}
+  const idx = imageAllList.value.findIndex(m => m.loading === true)
+  if (idx !== -1) imageAllList.value.splice(idx, 1)
 };
 
 // 发送消息函数
@@ -293,168 +262,170 @@ const ensureAiPoint = async (cost) => {
 }
 
 const sendMessage = async() => {
-  if (inputMessage.value.trim()) {
-    if (!await ensureAiPoint(100)) return
-    // 创建 AbortController 实例,以便中止请求
-    conversationInAbortController.value = new AbortController();
-    // 标记对话进行中
-    conversationInProgress.value = true;
-    // messages.value.push(inputMessage.value.trim())
-    // 先保存内容 再置空输入框
-    let content = inputMessage.value;
-    inputMessage.value = ''
-    let userMsg = {
-      type: 'user',
-          content: content,
-    }
-    imageAllList.value.push(userMsg)
-    imageAllList.value.push({
-      type: 'ai',
-      content: "正在为您生成图片,请稍等",
-      loading: true
-    })
+  const content = inputMessage.value.trim();
+  if (!content) {
+    Message().error('请描述要生成的图片内容');
+    return
+  }
+  if (conversationInProgress.value) return
 
-    // 通过emit事件通知父组件保存进度
-    emits('saveProgress', "aiCount", 1)
-    if (props.isCourse){
-      emits('saveProgress', "course", 100)
+  if (!await ensureAiPoint(100)) return
+
+  // 如果正在录音,先停止录音
+  if (isVoiceRecording.value) {
+    const voiceInputComponent = document.querySelector('.voice-input-container .speech-btn');
+    if (voiceInputComponent) {
+      voiceInputComponent.click();
     }
+    setTimeout(() => sendMessage(), 100);
+    return;
+  }
 
+  conversationInProgress.value = true;
+  inputMessage.value = ''
 
-    // 同步历史记录缓存
-    cacheDataHistoryList.value.push(userMsg);
-    localStorage.setItem(props.cacheDataHistoryKey, JSON.stringify(cacheDataHistoryList.value));
+  const userMessage = { id: genMsgId(), type: 'user', content };
+  imageAllList.value.push(userMessage);
 
-    try {
-      CreatePainting({
-        "modelId": modelId.value,
-        "prompt":content,
-        "width":1024,
-        "height":1024
-      }).then(res=>{
-        console.log("生成图片",res)
-        inProgressImageMap.value[res.data] = {id:res.data,status:AiImageStatusEnum.IN_PROGRESS}
-      }).finally(() => {
-        // 图片生成请求完成后更新状态
-        conversationInProgress.value = false;
-      });
-    } catch (error) {
-      console.error('生成图片失败:', error);
-      conversationInProgress.value = false;
-    }
+  const loadingMsg = { id: genMsgId(), type: 'ai', content: '正在为您生成图片,请稍等', loading: true }
+  imageAllList.value.push(loadingMsg);
+
+  emits('saveProgress', 'aiCount', 1)
+  if (props.isCourse) emits('saveProgress', 'course', 100)
+
+  cacheDataHistoryList.value.push(userMessage);
+  localStorage.setItem(props.cacheDataHistoryKey, JSON.stringify(cacheDataHistoryList.value));
+
+  try {
+    const res = await CreatePainting({
+      modelId: modelId.value,
+      prompt: content,
+      width: 1024,
+      height: 1024
+    })
+    inProgressImageMap.value[res.data] = { id: res.data, status: AiImageStatusEnum.IN_PROGRESS }
+    pollFailCount.value = 0
+    if (!inProgressTimer.value) refreshWatchImagesFun()
+  } catch (error) {
+    const loadingIdx = imageAllList.value.findIndex(m => m.loading === true)
+    if (loadingIdx !== -1) imageAllList.value.splice(loadingIdx, 1)
+    Message().error('创建图片生成任务失败,请稍后重试')
+  } finally {
+    conversationInProgress.value = false;
   }
 };
-// 生成图片
-import { ElIcon } from 'element-plus'
-import {
-  Back,
-  DArrowRight,
-  Download,
-  Refresh,
-  RefreshLeft,
-  RefreshRight,
-  Right,
-  ZoomIn,
-  ZoomOut,
-} from '@element-plus/icons-vue'
 
 
-const imageAllList = ref([]) // 对话的消息列表
-const imageList = ref([]) // image 列表
-// 图片轮询相关的参数(正在生成中的)
-const inProgressImageMap = ref({}) // 监听的 image 映射,一般是生成中(需要轮询),key 为 image 编号,value 为 image
-const inProgressTimer = ref() // 生成中的 image 定时器,轮询生成进展
+const imageAllList = ref([])
+const inProgressImageMap = ref({})
+const inProgressTimer = ref()
+const pollFailCount = ref(0)
+const MAX_POLL_FAIL = 5
+
+/** 生成唯一ID */
+const genMsgId = () => Date.now().toString(36) + Math.random().toString(36).slice(2, 6)
 
+/** 解析 ArkHttpException 错误信息 */
+const parseArkErrorMessage = (errorMessage) => {
+  if (!errorMessage) return '未知错误'
+  const match = errorMessage.match(/message='([^']*)'/)
+  if (match) return match[1]
+  return errorMessage
+}
 
-/** 轮询生成中的 image 列表 */
+/** 轮询生成中的任务 */
 const refreshWatchImages = async () => {
   const imageIds = Object.keys(inProgressImageMap.value).map(Number)
-  if (imageIds.length === 0) {
+  if (imageIds.length === 0) return
+
+  let list
+  try {
+    list = await PaintingGetMys(imageIds)
+    pollFailCount.value = 0
+  } catch (error) {
+    pollFailCount.value++
+    if (pollFailCount.value >= MAX_POLL_FAIL) {
+      inProgressTimerFun()
+      inProgressImageMap.value = {}
+      Message().error('查询图片生成状态失败,已停止轮询,请稍后重试')
+    }
     return
   }
-  const list = await PaintingGetMys(imageIds)
+
   const newWatchImages = {}
-  list.data.forEach((image) => {
+
+  list?.data.forEach((image) => {
     if (image.status === AiImageStatusEnum.IN_PROGRESS) {
       newWatchImages[image.id] = image
-    } else {
-      imageAllList.value.pop();
-      let imageMsg = {
-        type: "system",
-        content: "已为您生成图片:",
-        imageList: [image.picUrl],
-      }
-      imageAllList.value.push(imageMsg)
+      return
+    }
 
+    // 精确找到并移除对应的 loading 占位消息
+    const loadingIdx = imageAllList.value.findIndex(m => m.loading === true)
+    if (loadingIdx !== -1) imageAllList.value.splice(loadingIdx, 1)
+
+    if (image.status === AiImageStatusEnum.FAIL) {
+      const errorMsg = parseArkErrorMessage(image.errorMessage)
+      const failMsg = { id: genMsgId(), type: 'ai', content: `生成图片失败:${errorMsg}` }
+      imageAllList.value.push(failMsg)
+      cacheDataHistoryList.value.push(failMsg)
+      localStorage.setItem(props.cacheDataHistoryKey, JSON.stringify(cacheDataHistoryList.value))
+      Message().error(`图片生成失败:${errorMsg}`)
+      return
+    }
 
-      // 添加补充信息
-      if (!conversationInProgress.value && props.replySupplement) {
-        imageAllList.value.push({
-          type: "system",
-          content: props.replySupplement,
-        });
-      }
+    const imageMsg = { id: genMsgId(), type: 'ai', content: '已为您生成图片:', imageList: [image.picUrl] }
+    imageAllList.value.push(imageMsg)
 
-      // 同步缓存历史记录
-      cacheDataHistoryList.value.push(imageMsg);
-      localStorage.setItem(props.cacheDataHistoryKey, JSON.stringify(cacheDataHistoryList.value));
+    if (!conversationInProgress.value && props.replySupplement) {
+      imageAllList.value.push({ id: genMsgId(), type: 'system', content: props.replySupplement })
     }
+
+    cacheDataHistoryList.value.push(imageMsg);
+    localStorage.setItem(props.cacheDataHistoryKey, JSON.stringify(cacheDataHistoryList.value));
   })
+
   inProgressImageMap.value = newWatchImages
   if (Object.keys(newWatchImages).length === 0) {
     inProgressTimerFun()
   }
 }
 
-
-/** 组件挂在的时候 */
-onMounted(async () => {
-  refreshWatchImagesFun()
-})
-
-/** 组件取消挂在的时候 */
-onUnmounted(async () => {
-  inProgressTimerFun() // 组件取消挂在的时候,清除定时器
-})
-
-// 自动刷新 image 列表
+/** 启动轮询(已运行则跳过) */
 const refreshWatchImagesFun = () => {
+  if (inProgressTimer.value) return
   inProgressTimer.value = setInterval(async () => {
     await refreshWatchImages()
-  }, 1000 * 3)
+  }, 3000)
 }
 
-// 停止刷新image列表
+/** 停止轮询 */
 const inProgressTimerFun = () => {
   if (inProgressTimer.value) {
     clearInterval(inProgressTimer.value)
+    inProgressTimer.value = null
   }
 }
 
 // 图片下载功能
 const download = (activeIndex) => {
-  // 从document中找到图片预览器
   const imgPreview = document.querySelector('.el-image-viewer__wrapper');
   if (!imgPreview) return;
-  
-  // 获取所有预览图片
+
   const imgElements = imgPreview.querySelectorAll('.el-image-viewer__img');
   if (!imgElements || imgElements.length === 0) return;
-  
-  // 确定要下载的图片URL,根据传入的 activeIndex 获取对应图片的URL
+
   let imgUrl;
   if (imgElements[activeIndex]) {
     imgUrl = imgElements[activeIndex].src;
   } else {
     imgUrl = imgElements[0].src;
   }
-  
-  // 创建下载链接并触发下载
+
   const link = document.createElement('a');
   link.download = `image_${new Date().getTime()}.png`;
   link.href = imgUrl;
-  
-  // 触发下载
+
   document.body.appendChild(link);
   link.click();
   document.body.removeChild(link);
@@ -463,11 +434,9 @@ const download = (activeIndex) => {
 
 <style scoped lang="scss">
 @use 'sass:math';
-// 定义rpx转换函数
 @function rpx($px) {
   @return math.div($px, 750) * 100vw;
 }
-//===========================================全局除了删除侧边栏内容唯一改动的地方
 .number-people {
   flex: 1;
   height: 100%;
@@ -477,21 +446,12 @@ const download = (activeIndex) => {
 
 .content-box {
   flex: 1;
-  // margin-top: rpx(10);
-  // margin-bottom: rpx(10);
   margin: rpx(7);
   border-radius: rpx(15);
   background: rgba($color: #ffffff, $alpha: 0.5);
   overflow-y: auto;
-
 }
 
-//左侧展览区图标
-.img-box {
-  margin-top: rpx(50);
-  color: #a39dce;
-}
-// 对话框
 .chat-dialog {
   display: flex;
   flex-direction: column;
@@ -502,48 +462,46 @@ const download = (activeIndex) => {
   overflow-y: auto;
   padding: rpx(15);
 }
-/* 自定义滚动条样式 */
 .message-list::-webkit-scrollbar {
-  width: rpx(2); /* 滚动条宽度 */
+  width: rpx(2);
 }
 .message-list::-webkit-scrollbar-track {
-  background: #f1effd; /* 滚动条轨道背景色 */
+  background: #f1effd;
   border-radius: rpx(4);
 }
 .message-list::-webkit-scrollbar-thumb {
-  background: #e2ddfc; /* 滚动条滑块颜色 */
+  background: #e2ddfc;
   border-radius: rpx(4);
 }
 .message-list::-webkit-scrollbar-thumb:hover {
-  background: #e2ddfc; /* 滚动条滑块 hover 状态颜色 */
+  background: #e2ddfc;
 }
 
 .message-list .user-message {
   background-color: #ffffff;
-  margin-left: auto; // 消息靠右显示
-  margin-right: 0; // 重置右边距
+  margin-left: auto;
+  margin-right: 0;
   max-width: rpx(400);
   font-size: rpx(8);
-  width: fit-content; // 宽度随文字内容变化
+  width: fit-content;
   border-radius: rpx(5);
   padding: rpx(5);
-  text-align: left; // 文字左对齐
+  text-align: left;
 }
 
 .message-list .ai-message {
   background-color: #ffdd55;
-  margin-left: 0; // 消息靠左显示
-  margin-right: auto; // 重置右边距
+  margin-left: 0;
+  margin-right: auto;
   margin-bottom: rpx(10);
   width: fit-content;
   max-width: rpx(400);
   padding: rpx(5);
   font-size: rpx(8);
   border-radius: rpx(5);
-  text-align: left; // 文字左对齐
+  text-align: left;
 }
 
-// 加载动画效果
 .loading-dots {
   display: inline-block;
   margin-left: rpx(5);
@@ -618,7 +576,6 @@ const download = (activeIndex) => {
       color: #666;
     }
   }
-  // 终止按钮样式
   .stop-btn {
     cursor: pointer;
     display: flex;
@@ -642,7 +599,7 @@ const download = (activeIndex) => {
     to bottom,
     #fee78a,
     #ffce1b
-  ); /* 设置悬停、聚焦、点击状态下的背景色 */
+  );
   color: black;
   border: none;
   font-size: rpx(7);

+ 146 - 191
src/components/ai/video/ImageToVideo.vue

@@ -7,7 +7,7 @@
           <!-- 对话消息列表 -->
           <div class="message-list">
             <div v-if="imageAllList.length > 0">
-                <div  v-for="(item, index) in imageAllList" :key="index">
+                <div  v-for="(item, index) in imageAllList" :key="item.id || index">
               <!-- 用户消息 -->
               <div class="user-message" v-if="item.type === 'user'">
                 {{ item.content }}
@@ -24,7 +24,7 @@
                     >
                       <el-icon @click="prev"><Back /></el-icon>
                       <el-icon @click="next"><Right /></el-icon>
-                      <el-icon @click="setActiveItem(item.imageList.length - 1)">
+                      <el-icon @click="setActiveItem(0)">
                         <DArrowRight />
                       </el-icon>
                       <el-icon @click="actions('zoomOut')"><ZoomOut /></el-icon>
@@ -32,7 +32,6 @@
                       <el-icon @click="actions('clockwise', { rotateDeg: 180, enableTransition: false })"><RefreshRight /></el-icon>
                       <el-icon @click="actions('anticlockwise')"><RefreshLeft /></el-icon>
                       <el-icon @click="reset"><Refresh /></el-icon>
-                      <el-icon @click="download(activeIndex)"><Download /></el-icon>
                     </template>
                   </el-image>
                 </div>
@@ -110,28 +109,25 @@
 <script setup>
 import {ref, onMounted, onUnmounted, defineEmits, computed} from 'vue'
 import {AiImageStatusEnum, CreateVideo, VideoGetMys, getMyAiPointBalance} from '@/api/questions.js'
-import { useRouter } from 'vue-router'
 
 import VoiceInput from '../voice/VoiceInput_Api.vue'
 
-// 导入全局状态
-import { globalState } from '@/utils/globalState.js'
 // 终止按钮
 import stopicon from "@/assets/icon/stopicon.png";
 // 消息组件
 import {Message} from "@/utils/message/Message.js";
-
 // 上传参考图
 import ImageUpload from '@/components/ImageUpload/index.vue';
-// 导入getModelIdByType接口
-import {getModelIdByType, ModelPlatformEnum} from '@/api/teachers.js'
-import { ModelTypeEnum } from '@/api/teachers.js'
+// 模型接口
+import {getModelIdByType, ModelPlatformEnum, ModelTypeEnum} from '@/api/teachers.js'
+// 图标
+import {
+  Back, DArrowRight, Refresh, RefreshLeft, RefreshRight, Right, ZoomIn, ZoomOut,
+} from '@element-plus/icons-vue'
 
 // 定义props
 const props = defineProps({
-  //根据需求传输
   isCourse: { type: Boolean, default: false},
-  cacheDataKey: { type: String, default: localStorage.getItem('token') + "_ai_imageToVideo" },
   cacheDataHistoryKey: { type: String, default: localStorage.getItem('token') + "_ai_imageToVideo_history" },
   preDialogueList: { type: Array, default: () => []},
   replySupplement: { type: String, default: ''},
@@ -148,24 +144,15 @@ const uploadedImage = ref('');
 const imageUploadRef = ref(null);
 
 // 对话状态变量
-const conversationInProgress = ref(false); // 对话是否正在进行中
-const conversationInAbortController = ref(); // 对话进行中 abort 控制器
-const router = useRouter()
-
-  // 年级ID相关
-const gradeId = ref('')
-// modelId响应式变量
+const conversationInProgress = ref(false);
 const modelId = ref(0)
-// 保存记录
+
 onMounted(async () => {
-    // 从全局状态初始化年级ID
-  gradeId.value = globalState.initGradeId()
-  try{
-    // 获取modelId
+  try {
     const modelRes = await getModelIdByType({ type: ModelTypeEnum.IMAGE_TO_VIDEO, platform: ModelPlatformEnum.DOUBAO })
     modelId.value = modelRes.data
-  }catch(error){
-    console.error('保存记录失败:', error);
+  } catch (error) {
+    console.error('获取模型ID失败:', error);
   }
 
   // 取缓存历史记录
@@ -173,60 +160,50 @@ onMounted(async () => {
   if (cacheDataHistoryStr) {
     cacheDataHistoryList.value = JSON.parse(cacheDataHistoryStr) || [];
     imageAllList.value.push(...cacheDataHistoryList.value);
-  }else{
-    // 加入预置对话
+  } else {
     imageAllList.value.push(...props.preDialogueList);
     cacheDataHistoryList.value.push(...props.preDialogueList);
-    if (cacheDataHistoryList.value.length > 0) localStorage.setItem(props.cacheDataHistoryKey, JSON.stringify(cacheDataHistoryList.value));
+    if (cacheDataHistoryList.value.length > 0) {
+      localStorage.setItem(props.cacheDataHistoryKey, JSON.stringify(cacheDataHistoryList.value));
+    }
   }
+
+  refreshWatchImagesFun()
 });
 
-// 消息列表和输入内容的响应式变量
-const messages = ref([])
+onUnmounted(() => {
+  inProgressTimerFun()
+})
+
 const inputMessage = ref('')
 
 // 语音输入状态跟踪
-const isVoiceRecording = ref(false); // 当前是否正在录音
-const voiceRecognizedText = ref(""); // 实时语音识别结果
+const isVoiceRecording = ref(false);
+const voiceRecognizedText = ref("");
 
 // 用于控制输入框显示的内容
 const displayedPrompt = computed({
-  get() {
-    // 录音时,只显示inputMessage.value(已经在handleVoiceRecognized中实时更新)
-    if (isVoiceRecording.value) {
-      return inputMessage.value;
-    }
-    // 不录音时,只显示inputMessage.value
-    return inputMessage.value;
-  },
+  get() { return inputMessage.value },
   set(newValue) {
-    // 只在用户手动输入时更新inputMessage.value
-    if (!isVoiceRecording.value) {
-      inputMessage.value = newValue;
-    }
+    if (!isVoiceRecording.value) inputMessage.value = newValue
   }
 });
 
 // 语音输入识别结果处理
 const handleVoiceRecognized = (data) => {
   if (isVoiceRecording.value) {
-    // 在同一次录音过程中,实时更新文本框内容
     voiceRecognizedText.value = data.originalText;
     inputMessage.value = data.processedText;
   } else {
-    // 在录音结束时,将最终的语音内容插入到光标位置
     const input = document.querySelector('input[type="text"]');
     if (input) {
       inputMessage.value = data.processedText;
-      // 重新设置光标位置到插入文本的末尾
       setTimeout(() => {
         input.selectionStart = input.selectionEnd = data.cursorPos;
       }, 0);
     } else {
-      // 如果没有找到输入框,直接替换整个内容
       inputMessage.value = data.originalText;
     }
-    // 清空临时变量
     voiceRecognizedText.value = "";
   }
 };
@@ -236,21 +213,18 @@ const handleRecordingStatusChanged = (status) => {
   const wasRecording = isVoiceRecording.value;
   isVoiceRecording.value = status;
 
-  // 如果是从录音状态切换到非录音状态,清空临时变量
   if (wasRecording && !status) {
-    // 清空临时变量
     voiceRecognizedText.value = "";
   }
 };
 
-// 停止操作函数
-const stopStream = async () => {
-  // tip:如果 stream 进行中的 message,就需要调用 controller 结束
-  if (conversationInAbortController.value) {
-    conversationInAbortController.value.abort();
-  }
-  // 设置为 false
+// 停止操作函数:终止当前轮询
+const stopStream = () => {
   conversationInProgress.value = false;
+  inProgressTimerFun()
+  inProgressImageMap.value = {}
+  const idx = imageAllList.value.findIndex(m => m.loading === true)
+  if (idx !== -1) imageAllList.value.splice(idx, 1)
 };
 
 // 发送消息函数
@@ -266,181 +240,162 @@ const ensureAiPoint = async (cost) => {
 }
 
 const sendMessage = async() => {
-  if (uploadedImage.value) {
-    if (!await ensureAiPoint(500)) return
-    // 如果正在录音,先停止录音
-    if (isVoiceRecording.value) {
-      const voiceInputComponent = document.querySelector('.voice-input-container .speech-btn');
-      if (voiceInputComponent) {
-        voiceInputComponent.click();
-      }
-      // 等待录音结束后再发送
-      setTimeout(() => {
-        sendMessage(inputMessage.value);
-      }, 100);
-      return;
-    }
-    // 创建 AbortController 实例,以便中止请求
-    conversationInAbortController.value = new AbortController();
-    // 标记对话进行中
-    conversationInProgress.value = true;
-    // messages.value.push(inputMessage.value.trim())
-    // 先保存内容 再置空输入框
-    let content = inputMessage.value;
-    inputMessage.value = ''
-    // 创建用户消息对象,包含可能的图片
-    const userMessage = {
-      type: 'user',
-      content: content,
-    };
-
-    // 如果有上传的图片,添加到用户消息中
-    if (uploadedImage.value) {
-      userMessage.imageUrl = uploadedImage.value;
-      // 清空上传的图片
-      // uploadedImage.value = '';
-    }
-    // 添加用户消息到消息列表
-    imageAllList.value.push(userMessage);
-    imageAllList.value.push({
-      type: 'ai',
-      content: "正在为您生成视频,请稍等",
-      loading: true
-    })
+  if (!uploadedImage.value) {
+    Message().error('请先上传参考图!', true);
+    return
+  }
+  const content = inputMessage.value.trim();
+  if (!content) {
+    Message().error('请描述要生成的视频内容');
+    return
+  }
+  if (conversationInProgress.value) return
 
-    // 通过emit事件通知父组件保存进度
-    emits('saveProgress', "aiCount", 1)
-    if (props.isCourse){
-      emits('saveProgress', "course", 100)
-    }
+  if (!await ensureAiPoint(500)) return
 
-    // 同步历史记录缓存
-    cacheDataHistoryList.value.push(userMessage);
-    localStorage.setItem(props.cacheDataHistoryKey, JSON.stringify(cacheDataHistoryList.value));
-
-    try {
-      CreateVideo({
-        "modelId": modelId.value,
-        "prompt":content,
-        "duration":4,
-        "resolution":"480P",
-        "promptImage":[uploadedImage.value]
-      }).then(res=>{
-        console.log("生成视频",res)
-        //目前写死调用已生成的图片,全部通了后再改
-        inProgressImageMap.value[res.data] = {id:res.data,status:AiImageStatusEnum.IN_PROGRESS}
-        // inProgressImageMap.value[260] = {id:260,status:AiImageStatusEnum.IN_PROGRESS}
-      }).finally(() => {
-        // 图片生成请求完成后更新状态
-        conversationInProgress.value = false;
-      });
-    } catch (error) {
-      console.error('生成视频失败:', error);
-      conversationInProgress.value = false;
+  // 如果正在录音,先停止录音
+  if (isVoiceRecording.value) {
+    const voiceInputComponent = document.querySelector('.voice-input-container .speech-btn');
+    if (voiceInputComponent) {
+      voiceInputComponent.click();
     }
-  } else {
-    // 如果没有上传图片,显示提示信息
-    Message().error('请先上传参考图!', true);
+    setTimeout(() => sendMessage(), 100);
+    return;
   }
-  // 调用子组件的方法清除预览图
-  imageUploadRef.value?.clearPreview();
-};
 
+  conversationInProgress.value = true;
+  inputMessage.value = ''
 
-// 生成图片
-import { ElIcon } from 'element-plus'
-import {
-  Back,
-  DArrowRight,
-  Download,
-  Refresh,
-  RefreshLeft,
-  RefreshRight,
-  Right,
-  ZoomIn,
-  ZoomOut,
-} from '@element-plus/icons-vue'
+  const userMessage = { id: genMsgId(), type: 'user', content, imageUrl: uploadedImage.value };
+  imageAllList.value.push(userMessage);
 
+  const loadingMsg = { id: genMsgId(), type: 'ai', content: '正在为您生成视频,请稍等', loading: true }
+  imageAllList.value.push(loadingMsg);
+
+  emits('saveProgress', 'aiCount', 1)
+  if (props.isCourse) emits('saveProgress', 'course', 100)
+
+  cacheDataHistoryList.value.push(userMessage);
+  localStorage.setItem(props.cacheDataHistoryKey, JSON.stringify(cacheDataHistoryList.value));
+
+  try {
+    const res = await CreateVideo({
+      modelId: modelId.value,
+      prompt: content,
+      duration: 4,
+      resolution: '480P',
+      promptImage: [uploadedImage.value]
+    })
+    inProgressImageMap.value[res.data] = { id: res.data, status: AiImageStatusEnum.IN_PROGRESS }
+    pollFailCount.value = 0
+    if (!inProgressTimer.value) refreshWatchImagesFun()
+  } catch (error) {
+    // 创建任务失败,移除 loading 消息
+    const loadingIdx = imageAllList.value.findIndex(m => m.loading === true)
+    if (loadingIdx !== -1) imageAllList.value.splice(loadingIdx, 1)
+    Message().error('创建视频生成任务失败,请稍后重试')
+  } finally {
+    conversationInProgress.value = false;
+  }
+
+  imageUploadRef.value?.clearPreview();
+};
 
-const imageAllList = ref([]) // 对话的消息列表
-const imageList = ref([]) // image 列表
-// 图片轮询相关的参数(正在生成中的)
-const inProgressImageMap = ref({}) // 监听的 image 映射,一般是生成中(需要轮询),key 为 image 编号,value 为 image
-const inProgressTimer = ref() // 生成中的 image 定时器,轮询生成进展
 
+const imageAllList = ref([]) // 对话的消息列表
+const inProgressImageMap = ref({}) // 监听的 image 映射,key 为 image 编号,value 为 image
+const inProgressTimer = ref() // 轮询定时器
+const pollFailCount = ref(0) // 轮询接口连续失败计数
+const MAX_POLL_FAIL = 5 // 轮询接口连续失败上限
+
+/** 生成唯一ID,给每条消息做 key */
+const genMsgId = () => Date.now().toString(36) + Math.random().toString(36).slice(2, 6)
+
+/** 解析 ArkHttpException 错误信息,提取关键 message */
+const parseArkErrorMessage = (errorMessage) => {
+  if (!errorMessage) return '未知错误'
+  const match = errorMessage.match(/message='([^']*)'/)
+  if (match) return match[1]
+  return errorMessage
+}
 
-/** 轮询生成中的 image 列表 */
+/** 轮询生成中的任务 */
 const refreshWatchImages = async () => {
   const imageIds = Object.keys(inProgressImageMap.value).map(Number)
-  if (imageIds.length === 0) {
+  if (imageIds.length === 0) return
+
+  let list
+  try {
+    list = await VideoGetMys(imageIds)
+    pollFailCount.value = 0
+  } catch (error) {
+    pollFailCount.value++
+    if (pollFailCount.value >= MAX_POLL_FAIL) {
+      inProgressTimerFun()
+      inProgressImageMap.value = {}
+      Message().error('查询视频生成状态失败,已停止轮询,请稍后重试')
+    }
     return
   }
-  const list = await VideoGetMys(imageIds)
 
-      console.log('AI生成的视频地址:', list,imageIds);
   const newWatchImages = {}
+
   list?.data.forEach((image) => {
     if (image.status === AiImageStatusEnum.IN_PROGRESS) {
       newWatchImages[image.id] = image
-    } else {
-      imageAllList.value.pop();
-      console.log('AI生成的视频地址:', image.videoUrl);
-      let videoMsg = {
-        type: 'ai',
-        content: "已为您生成视频:",
-        imageList: [image.videoUrl],
-      }
-      imageAllList.value.push(videoMsg)
+      return
+    }
 
+    // 精确找到并移除对应的 loading 占位消息
+    const loadingIdx = imageAllList.value.findIndex(m => m.loading === true)
+    if (loadingIdx !== -1) imageAllList.value.splice(loadingIdx, 1)
+
+    if (image.status === AiImageStatusEnum.FAIL) {
+      const errorMsg = parseArkErrorMessage(image.errorMessage)
+      const failMsg = { id: genMsgId(), type: 'ai', content: `生成视频失败:${errorMsg}` }
+      imageAllList.value.push(failMsg)
+      cacheDataHistoryList.value.push(failMsg)
+      localStorage.setItem(props.cacheDataHistoryKey, JSON.stringify(cacheDataHistoryList.value))
+      Message().error(`视频生成失败:${errorMsg}`)
+      return
+    }
 
-      // 添加补充信息
-      if (!conversationInProgress.value && props.replySupplement) {
-        imageAllList.value.push({
-          type: "system",
-          content: props.replySupplement,
-        });
-      }
+    const videoMsg = { id: genMsgId(), type: 'ai', content: '已为您生成视频:', imageList: [image.videoUrl] }
+    imageAllList.value.push(videoMsg)
 
-      // 同步缓存历史记录
-      cacheDataHistoryList.value.push(videoMsg);
-      localStorage.setItem(props.cacheDataHistoryKey, JSON.stringify(cacheDataHistoryList.value));
+    if (!conversationInProgress.value && props.replySupplement) {
+      imageAllList.value.push({ id: genMsgId(), type: 'system', content: props.replySupplement })
     }
+
+    cacheDataHistoryList.value.push(videoMsg)
+    localStorage.setItem(props.cacheDataHistoryKey, JSON.stringify(cacheDataHistoryList.value))
   })
+
   inProgressImageMap.value = newWatchImages
-  if (newWatchImages.size === 0) {
+  if (Object.keys(newWatchImages).length === 0) {
     inProgressTimerFun()
   }
 }
 
-
-/** 组件挂在的时候 */
-onMounted(async () => {
-  refreshWatchImagesFun()
-})
-
-/** 组件取消挂在的时候 */
-onUnmounted(async () => {
-  inProgressTimerFun()
-})
-
-// 自动刷新 image 列表
+/** 启动轮询(已运行则跳过) */
 const refreshWatchImagesFun = () => {
+  if (inProgressTimer.value) return
   inProgressTimer.value = setInterval(async () => {
     await refreshWatchImages()
-  }, 1000 * 3)
+  }, 3000)
 }
 
-// 停止刷新image列表
+/** 停止轮询 */
 const inProgressTimerFun = () => {
   if (inProgressTimer.value) {
     clearInterval(inProgressTimer.value)
+    inProgressTimer.value = null
   }
 }
 </script>
 
 <style scoped lang="scss">
 @use 'sass:math';
-// 定义rpx转换函数
 @function rpx($px) {
   @return math.div($px, 750) * 100vw;
 }