Просмотр исходного кода

AI生成课加入生成视频demo

liyanbo 2 недель назад
Родитель
Сommit
12e831e34a
1 измененных файлов с 566 добавлено и 87 удалено
  1. 566 87
      src/views/bjdx/course/aiGenerate/aiGengrate.vue

+ 566 - 87
src/views/bjdx/course/aiGenerate/aiGengrate.vue

@@ -169,73 +169,119 @@
                   <button class="remove-section-btn" @click="removeSection(sectionIndex)">×</button>
                   <button class="remove-section-btn" @click="removeSection(sectionIndex)">×</button>
                 </div>
                 </div>
                 <div class="media-controls">
                 <div class="media-controls">
-                  <div class="media-item">
-                    <div class="media-input-group">
-                      <span class="media-label">背景图</span>
-                      <el-input
-                        v-model="section.backgroundImage.prompt"
-                        type="textarea"
-                        :autosize="{ minRows: 2, maxRows: 4 }"
-                        placeholder="描述词"
-                        class="media-prompt"
-                      />
-                      <el-button
-                        type="primary"
-                        size="small"
-                        :loading="section.backgroundImage.generating"
-                        :disabled="
-                          !section.backgroundImage.prompt || section.backgroundImage.generating
-                        "
-                        @click="generateMedia(sectionIndex)"
-                        class="generate-btn"
-                      >
-                        {{
-                          section.backgroundImage.generating
-                            ? '生成中...'
-                            : section.backgroundImage.url
-                              ? '重新生成'
-                              : '生成'
-                        }}
-                      </el-button>
-                    </div>
-                    <div v-if="section.backgroundImage.url" class="media-preview">
-                      <img :src="section.backgroundImage.url" alt="背景图预览" />
+                  <!-- 背景类型切换 -->
+                  <el-radio-group v-model="section.backgroundType" class="background-type-switch">
+                    <el-radio-button label="imageAudio">图音背景</el-radio-button>
+                    <el-radio-button label="video">视频背景</el-radio-button>
+                  </el-radio-group>
+
+                  <!-- 图音背景 -->
+                  <template v-if="section.backgroundType === 'imageAudio'">
+                    <div class="media-item">
+                      <div class="media-input-group">
+                        <span class="media-label">背景图</span>
+                        <el-input
+                          v-model="section.backgroundImage.prompt"
+                          type="textarea"
+                          :autosize="{ minRows: 2, maxRows: 4 }"
+                          placeholder="描述词"
+                          class="media-prompt"
+                        />
+                        <el-button
+                          type="primary"
+                          size="small"
+                          :loading="section.backgroundImage.generating"
+                          :disabled="
+                            !section.backgroundImage.prompt || section.backgroundImage.generating
+                          "
+                          @click="generateMedia(sectionIndex)"
+                          class="generate-btn"
+                        >
+                          {{ 
+                            section.backgroundImage.generating
+                              ? '生成中...'
+                              : section.backgroundImage.url
+                                ? '重新生成'
+                                : '生成'
+                          }}
+                        </el-button>
+                      </div>
+                      <div v-if="section.backgroundImage.url" class="media-preview">
+                        <img :src="section.backgroundImage.url" alt="背景图预览" />
+                      </div>
                     </div>
                     </div>
-                  </div>
 
 
-                  <div class="media-item">
-                    <div class="media-input-group">
-                      <span class="media-label">背景音</span>
-                      <el-select
-                        v-model="section.backgroundAudio.type"
-                        placeholder="选择背景音"
-                        :style="{ width: fullscreen ? '240px' : '130px' }"
-                        clearable
-                        size="large"
-                        @change="(value) => handleBackgroundAudioChange(value, section)"
-                      >
-                        <el-option
-                          v-for="musicType in backgroundMusicTypes"
-                          :key="musicType.id"
-                          :label="musicType.name"
-                          :value="musicType.id"
+                    <div class="media-item">
+                      <div class="media-input-group">
+                        <span class="media-label">背景音</span>
+                        <el-select
+                          v-model="section.backgroundAudio.type"
+                          placeholder="选择背景音"
+                          :style="{ width: fullscreen ? '240px' : '130px' }"
+                          clearable
+                          size="large"
+                          @change="(value) => handleBackgroundAudioChange(value, section)"
+                        >
+                          <el-option
+                            v-for="musicType in backgroundMusicTypes"
+                            :key="musicType.id"
+                            :label="musicType.name"
+                            :value="musicType.id"
+                          />
+                        </el-select>
+                        <button
+                          v-if="section.backgroundAudio.type"
+                          class="play-btn small"
+                          @click="playBackgroundAudio(section.backgroundAudio.type)"
+                        >
+                          <span class="play-icon">{{
+                            audioState.isPlaying &&
+                            audioState.currentType === 'background' &&
+                            audioState.currentUrl === section.backgroundAudio.url
+                              ? '⏸'
+                              : '▶'
+                          }}</span>
+                        </button>
+                      </div>
+                    </div>
+                  </template>
+                  
+                  <!-- 视频背景 -->
+                  <template v-else-if="section.backgroundType === 'video'">
+                    <div class="media-item">
+                      <div class="media-input-group">
+                        <span class="media-label">视频提示词</span>
+                        <el-input
+                          v-model="section.backgroundVideo.prompt"
+                          type="textarea"
+                          :autosize="{ minRows: 2, maxRows: 4 }"
+                          placeholder="描述词"
+                          class="media-prompt-video"
                         />
                         />
-                      </el-select>
-                      <button
-                        v-if="section.backgroundAudio.type"
-                        class="play-btn small"
-                        @click="playBackgroundAudio(section.backgroundAudio.type)"
-                      >
-                        <span class="play-icon">{{
-                          audioState.isPlaying &&
-                          audioState.currentType === 'background' &&
-                          audioState.currentUrl === section.backgroundAudio.url
-                            ? '⏸'
-                            : '▶'
-                        }}</span>
-                      </button>
+                        <el-button
+                          type="primary"
+                          size="small"
+                          :loading="section.backgroundVideo.generating"
+                          :disabled="
+                            !section.backgroundVideo.prompt || section.backgroundVideo.generating
+                          "
+                          @click="generateVideo(sectionIndex)"
+                          class="generate-btn"
+                        >
+                          {{ 
+                            section.backgroundVideo.generating
+                              ? '生成中...'
+                              : section.backgroundVideo.url
+                                ? '重新生成'
+                                : '生成'
+                          }}
+                        </el-button>
+                      </div>
+                      <div v-if="section.backgroundVideo.url" class="media-preview">
+                        <video :src="section.backgroundVideo.url" alt="视频预览" controls ></video>
+                      </div>
                     </div>
                     </div>
-                  </div>
+                  </template>
                 </div>
                 </div>
 
 
                 <div class="dialogues-container">
                 <div class="dialogues-container">
@@ -247,14 +293,16 @@
                   >
                   >
                     <div class="dialogue-header">
                     <div class="dialogue-header">
                       <div class="dialogue-type-tag" :class="dialogue.type">
                       <div class="dialogue-type-tag" :class="dialogue.type">
-                        {{
+                        {{ 
                           dialogue.type === 'digital'
                           dialogue.type === 'digital'
                             ? '数字人'
                             ? '数字人'
                             : dialogue.type === 'user'
                             : dialogue.type === 'user'
                               ? '用户'
                               ? '用户'
                               : dialogue.type === 'quest'
                               : dialogue.type === 'quest'
                                 ? '提问'
                                 ? '提问'
-                                : '诗词'
+                                : dialogue.type === 'poem'
+                                  ? '诗词'
+                                  : '视频'
                         }}
                         }}
                       </div>
                       </div>
                     </div>
                     </div>
@@ -535,6 +583,46 @@
                           </div>
                           </div>
                         </div>
                         </div>
                       </template>
                       </template>
+                      
+                      <!-- 视频 -->
+                      <template v-else-if="dialogue.type === 'video'">
+                        <div class="media-input-group">
+                          <span class="media-label">视频提示词</span>
+                          <el-input
+                            v-model="dialogue.videoPrompt"
+                            type="textarea"
+                            :autosize="{ minRows: 2, maxRows: 4 }"
+                            placeholder="描述词"
+                            class="media-prompt-video"
+                          />
+                          <div v-if="dialogue.videoUrl" class="media-preview">
+                            <video :src="dialogue.videoUrl" alt="视频预览" controls></video>
+                          </div>
+                          <el-button
+                            type="primary"
+                            size="small"
+                            :loading="dialogue.generatingVideo"
+                            :disabled="
+                                  !dialogue.videoPrompt
+                                "
+                            @click="generateDialogueVideo(sectionIndex, dialogueIndex)"
+                            class="generate-btn"
+                          >
+                            {{
+                              dialogue.generatingVideo
+                                ? '生成中...'
+                                : dialogue.videoUrl
+                                  ? '重新生成'
+                                  : '生成'
+                            }}
+                          </el-button>
+                          <button
+                            class="remove-btn"
+                            @click="removeDialogue(sectionIndex, dialogueIndex)"
+                          >×</button
+                          >
+                        </div>
+                      </template>
                     </div>
                     </div>
                   </div>
                   </div>
 
 
@@ -542,14 +630,21 @@
                     <button class="add-dialogue-btn digital" @click="addDialogue(sectionIndex)"
                     <button class="add-dialogue-btn digital" @click="addDialogue(sectionIndex)"
                       >+ 添加对话</button
                       >+ 添加对话</button
                     >
                     >
+
                     <button
                     <button
                       class="add-dialogue-btn quest-user"
                       class="add-dialogue-btn quest-user"
                       @click="addQuestWithUserReply(sectionIndex)"
                       @click="addQuestWithUserReply(sectionIndex)"
                       >+ 添加提问与回复</button
                       >+ 添加提问与回复</button
                     >
                     >
+
                     <button class="add-dialogue-btn poem" @click="addPoemDialogue(sectionIndex)"
                     <button class="add-dialogue-btn poem" @click="addPoemDialogue(sectionIndex)"
                       >+ 添加诗词</button
                       >+ 添加诗词</button
                     >
                     >
+
+                    <button class="add-dialogue-btn video" @click="addVideoDialogue(sectionIndex)"
+                      >+ 添加视频</button
+                    >
+
                   </div>
                   </div>
                 </div>
                 </div>
               </div>
               </div>
@@ -593,7 +688,7 @@
                     :key="sectionIndex"
                     :key="sectionIndex"
                     class="preview-section"
                     class="preview-section"
                     :style="{
                     :style="{
-                      backgroundImage: section.backgroundImage.url
+                      backgroundImage: section.backgroundType === 'imageAudio' && section.backgroundImage.url
                         ? `url(${section.backgroundImage.url})`
                         ? `url(${section.backgroundImage.url})`
                         : 'none',
                         : 'none',
                       backgroundSize: 'cover',
                       backgroundSize: 'cover',
@@ -601,6 +696,9 @@
                       backgroundRepeat: 'no-repeat'
                       backgroundRepeat: 'no-repeat'
                     }"
                     }"
                   >
                   >
+                    <div v-if="section.backgroundType === 'video' && section.backgroundVideo.url" class="preview-video">
+                      <video :src="section.backgroundVideo.url" alt="视频背景" autoplay loop muted ></video>
+                    </div>
                     <div class="preview-section-content">
                     <div class="preview-section-content">
                       <div class="preview-media">
                       <div class="preview-media">
                         <div class="preview-media-left">
                         <div class="preview-media-left">
@@ -608,7 +706,7 @@
                           <strong>{{ section.name }}</strong>
                           <strong>{{ section.name }}</strong>
                         </div>
                         </div>
                         <div class="preview-media-right">
                         <div class="preview-media-right">
-                          <span v-if="section.backgroundAudio.type" class="preview-audio">
+                          <span v-if="section.backgroundType === 'imageAudio' && section.backgroundAudio.type" class="preview-audio">
                             背景音:{{ section.backgroundAudio.type }}
                             背景音:{{ section.backgroundAudio.type }}
                             <button
                             <button
                               class="play-btn small"
                               class="play-btn small"
@@ -638,21 +736,23 @@
                           <div class="dialogue-header">
                           <div class="dialogue-header">
                             <div class="dialogue-header-left">
                             <div class="dialogue-header-left">
                               <div class="dialogue-type-tag" :class="dialogue.type">
                               <div class="dialogue-type-tag" :class="dialogue.type">
-                                {{
-                                  dialogue.type === 'digital'
-                                    ? '数字人'
-                                    : dialogue.type === 'user'
-                                      ? '用户'
-                                      : dialogue.type === 'quest'
-                                        ? '提问'
-                                        : '诗词'
-                                }}
+                                {{ 
+                          dialogue.type === 'digital'
+                            ? '数字人'
+                            : dialogue.type === 'user'
+                              ? '用户'
+                              : dialogue.type === 'quest'
+                                ? '提问'
+                                : dialogue.type === 'poem'
+                                  ? '诗词'
+                                  : '视频'
+                        }}
                               </div>
                               </div>
                               <div class="dialogue-role">
                               <div class="dialogue-role">
-                                {{
-                                  dialogue.type !== 'user'
+                                {{ 
+                                  dialogue.type !== 'user' && dialogue.type !== 'video'
                                     ? getRoleName(dialogue.roleName)
                                     ? getRoleName(dialogue.roleName)
-                                    : '用户'
+                                    : dialogue.type === 'video' ? '视频' : '用户'
                                 }}:
                                 }}:
                               </div>
                               </div>
                             </div>
                             </div>
@@ -670,7 +770,15 @@
                               }}</span>
                               }}</span>
                             </button>
                             </button>
                           </div>
                           </div>
-                          <div class="dialogue-text" v-html="parseMarkdown(dialogue.content)"></div>
+                          <template v-if="dialogue.type === 'video'">
+                            <div class="dialogue-text">{{ dialogue.videoPrompt }}</div>
+                            <div v-if="dialogue.videoUrl" class="media-preview">
+                              <video :src="dialogue.videoUrl" alt="视频预览" controls></video>
+                            </div>
+                          </template>
+                          <template v-else>
+                            <div class="dialogue-text" v-html="parseMarkdown(dialogue.content)"></div>
+                          </template>
                           <div
                           <div
                             v-if="dialogue.type === 'user' && dialogue.roleName"
                             v-if="dialogue.type === 'user' && dialogue.roleName"
                             class="reply-info"
                             class="reply-info"
@@ -783,7 +891,7 @@ const backgroundMusicTypes = ref([
   {
   {
     id: '轻松欢快',
     id: '轻松欢快',
     name: '轻松欢快',
     name: '轻松欢快',
-    url: 'https://learn-aliyun-oss.oss-cn-beijing.aliyuncs.com/20260310/AI_1773106630966.MP3'
+    url: 'https://learn-ai.com.cn/admin-api/infra/file/29/get/20260310/AI_1773106630966.MP3'
   }
   }
 ])
 ])
 
 
@@ -815,6 +923,7 @@ const scriptData = reactive(
     sections: [
     sections: [
       {
       {
         name: '环节一',
         name: '环节一',
+        backgroundType: 'imageAudio', // imageAudio: 图音背景, video: 视频背景
         backgroundImage: {
         backgroundImage: {
           prompt: '',
           prompt: '',
           url: '',
           url: '',
@@ -824,6 +933,11 @@ const scriptData = reactive(
           type: '',
           type: '',
           url: ''
           url: ''
         },
         },
+        backgroundVideo: {
+          prompt: '',
+          url: '',
+          generating: false
+        },
         dialogues: [
         dialogues: [
           {
           {
             type: 'digital',
             type: 'digital',
@@ -843,6 +957,22 @@ const scriptData = reactive(
   }
   }
 )
 )
 
 
+// 兼容旧数据,为旧数据添加背景类型字段
+if (scriptData.sections) {
+  scriptData.sections.forEach(section => {
+    if (!section.backgroundType) {
+      section.backgroundType = 'imageAudio';
+    }
+    if (!section.backgroundVideo) {
+      section.backgroundVideo = {
+        prompt: '',
+        url: '',
+        generating: false
+      };
+    }
+  });
+}
+
 // 保存脚本数据到localStorage
 // 保存脚本数据到localStorage
 const saveScriptDataToCache = () => {
 const saveScriptDataToCache = () => {
   try {
   try {
@@ -868,6 +998,21 @@ const loadDraftAndGotoStep2 = () => {
   const cachedData = loadScriptDataFromCache()
   const cachedData = loadScriptDataFromCache()
   if (cachedData) {
   if (cachedData) {
     Object.assign(scriptData, cachedData)
     Object.assign(scriptData, cachedData)
+    // 为加载的草稿数据添加背景类型和视频背景字段
+    if (scriptData.sections) {
+      scriptData.sections.forEach(section => {
+        if (!section.backgroundType) {
+          section.backgroundType = 'imageAudio';
+        }
+        if (!section.backgroundVideo) {
+          section.backgroundVideo = {
+            prompt: '',
+            url: '',
+            generating: false
+          };
+        }
+      });
+    }
     currentStep.value = 2
     currentStep.value = 2
   }
   }
 }
 }
@@ -897,12 +1042,42 @@ watch(
         try {
         try {
           const parsedData = JSON.parse(props.initialScriptData)
           const parsedData = JSON.parse(props.initialScriptData)
           Object.assign(scriptData, parsedData)
           Object.assign(scriptData, parsedData)
+          // 为加载的脚本数据添加背景类型和视频背景字段
+          if (scriptData.sections) {
+            scriptData.sections.forEach(section => {
+              if (!section.backgroundType) {
+                section.backgroundType = 'imageAudio';
+              }
+              if (!section.backgroundVideo) {
+                section.backgroundVideo = {
+                  prompt: '',
+                  url: '',
+                  generating: false
+                };
+              }
+            });
+          }
         } catch (error) {
         } catch (error) {
           console.error('解析脚本数据失败:', error)
           console.error('解析脚本数据失败:', error)
           // 解析失败,尝试加载草稿
           // 解析失败,尝试加载草稿
           const cachedData = loadScriptDataFromCache()
           const cachedData = loadScriptDataFromCache()
           if (cachedData) {
           if (cachedData) {
             Object.assign(scriptData, cachedData)
             Object.assign(scriptData, cachedData)
+            // 为加载的草稿数据添加背景类型和视频背景字段
+            if (scriptData.sections) {
+              scriptData.sections.forEach(section => {
+                if (!section.backgroundType) {
+                  section.backgroundType = 'imageAudio';
+                }
+                if (!section.backgroundVideo) {
+                  section.backgroundVideo = {
+                    prompt: '',
+                    url: '',
+                    generating: false
+                  };
+                }
+              });
+            }
           }
           }
         }
         }
       } else {
       } else {
@@ -912,6 +1087,21 @@ watch(
         const cachedData = loadScriptDataFromCache()
         const cachedData = loadScriptDataFromCache()
         if (cachedData) {
         if (cachedData) {
           Object.assign(scriptData, cachedData)
           Object.assign(scriptData, cachedData)
+          // 为加载的草稿数据添加背景类型和视频背景字段
+          if (scriptData.sections) {
+            scriptData.sections.forEach(section => {
+              if (!section.backgroundType) {
+                section.backgroundType = 'imageAudio';
+              }
+              if (!section.backgroundVideo) {
+                section.backgroundVideo = {
+                  prompt: '',
+                  url: '',
+                  generating: false
+                };
+              }
+            });
+          }
         }
         }
       }
       }
     } else if (oldVisible) {
     } else if (oldVisible) {
@@ -1077,6 +1267,21 @@ const doSendMessageStream = async (conversationId, content) => {
             const parsedData = JSON.parse(receiveMessageFullText.value)
             const parsedData = JSON.parse(receiveMessageFullText.value)
             scriptDataTemp.value = parsedData
             scriptDataTemp.value = parsedData
             Object.assign(scriptData, parsedData)
             Object.assign(scriptData, parsedData)
+            // 为新生成的脚本数据添加背景类型和视频背景字段
+            if (scriptData.sections) {
+              scriptData.sections.forEach(section => {
+                if (!section.backgroundType) {
+                  section.backgroundType = 'imageAudio';
+                }
+                if (!section.backgroundVideo) {
+                  section.backgroundVideo = {
+                    prompt: '',
+                    url: '',
+                    generating: false
+                  };
+                }
+              });
+            }
           } catch (e) {
           } catch (e) {
             // 解析失败,说明数据还不完整,继续等待
             // 解析失败,说明数据还不完整,继续等待
           }
           }
@@ -1095,6 +1300,21 @@ const doSendMessageStream = async (conversationId, content) => {
 
 
             scriptDataTemp.value = parsedData
             scriptDataTemp.value = parsedData
             Object.assign(scriptData, parsedData)
             Object.assign(scriptData, parsedData)
+            // 为新生成的脚本数据添加背景类型和视频背景字段
+            if (scriptData.sections) {
+              scriptData.sections.forEach(section => {
+                if (!section.backgroundType) {
+                  section.backgroundType = 'imageAudio';
+                }
+                if (!section.backgroundVideo) {
+                  section.backgroundVideo = {
+                    prompt: '',
+                    url: '',
+                    generating: false
+                  };
+                }
+              });
+            }
           }
           }
         } catch (e) {
         } catch (e) {
           console.error('最终数据解析失败:', e)
           console.error('最终数据解析失败:', e)
@@ -1112,6 +1332,21 @@ const doSendMessageStream = async (conversationId, content) => {
             console.log('最终清洗后数据json:', parsedData)
             console.log('最终清洗后数据json:', parsedData)
             scriptDataTemp.value = parsedData
             scriptDataTemp.value = parsedData
             Object.assign(scriptData, parsedData)
             Object.assign(scriptData, parsedData)
+            // 为新生成的脚本数据添加背景类型和视频背景字段
+            if (scriptData.sections) {
+              scriptData.sections.forEach(section => {
+                if (!section.backgroundType) {
+                  section.backgroundType = 'imageAudio';
+                }
+                if (!section.backgroundVideo) {
+                  section.backgroundVideo = {
+                    prompt: '',
+                    url: '',
+                    generating: false
+                  };
+                }
+              });
+            }
           } catch (cleanError) {
           } catch (cleanError) {
             console.error('清洗后数据解析失败:', cleanError)
             console.error('清洗后数据解析失败:', cleanError)
           }
           }
@@ -1129,8 +1364,10 @@ const doSendMessageStream = async (conversationId, content) => {
 const addSection = () => {
 const addSection = () => {
   scriptData.sections.push({
   scriptData.sections.push({
     name: `环节${scriptData.sections.length + 1}`,
     name: `环节${scriptData.sections.length + 1}`,
+    backgroundType: 'imageAudio', // imageAudio: 图音背景, video: 视频背景
     backgroundImage: { prompt: '', url: '', generating: false },
     backgroundImage: { prompt: '', url: '', generating: false },
     backgroundAudio: { type: '', url: '' },
     backgroundAudio: { type: '', url: '' },
+    backgroundVideo: { prompt: '', url: '', generating: false },
     dialogues: []
     dialogues: []
   })
   })
 }
 }
@@ -1146,6 +1383,16 @@ const addDialogue = (sectionIndex) => {
   })
   })
 }
 }
 
 
+// 步骤2:添加视频对话
+const addVideoDialogue = (sectionIndex) => {
+  scriptData.sections[sectionIndex].dialogues.push({
+    type: 'video',
+    videoUrl: '',
+    videoPrompt: '',
+    generatingVideo: false
+  })
+}
+
 // 步骤2:添加用户回复
 // 步骤2:添加用户回复
 const addUserReply = (sectionIndex) => {
 const addUserReply = (sectionIndex) => {
   scriptData.sections[sectionIndex].dialogues.push({
   scriptData.sections[sectionIndex].dialogues.push({
@@ -1264,6 +1511,65 @@ const generateMedia = async (sectionIndex) => {
   }
   }
 }
 }
 
 
+// 步骤2:生成视频背景
+const generateVideo = async (sectionIndex) => {
+  const section = scriptData.sections[sectionIndex]
+  const media = section.backgroundVideo
+
+  // 记录旧的URL
+  let oldUrl = null
+  if (media.url) {
+    oldUrl = media.url
+    replacedUrls.value.add(oldUrl)
+  }
+
+  media.generating = true
+  try {
+    // 这里需要实现视频生成的API调用
+    // 暂时使用模拟数据
+    setTimeout(() => {
+      media.url = 'http://learn-ai.com.cn/admin-api/infra/file/29/get/20260421/13-2_1776760082321.mp4'
+      media.generating = false
+    }, 2000)
+  } catch (error) {
+    console.error(`生成视频失败:`, error)
+    media.generating = false
+    // 生成失败,从replacedUrls中移除旧URL
+    if (oldUrl) {
+      replacedUrls.value.delete(oldUrl)
+    }
+  }
+}
+
+// 步骤2:生成对话视频
+const generateDialogueVideo = async (sectionIndex, dialogueIndex) => {
+  const dialogue = scriptData.sections[sectionIndex].dialogues[dialogueIndex]
+
+  // 记录旧的URL
+  let oldUrl = null
+  if (dialogue.videoUrl) {
+    oldUrl = dialogue.videoUrl
+    replacedUrls.value.add(oldUrl)
+  }
+
+  dialogue.generatingVideo = true
+  try {
+    // 这里需要实现视频生成的API调用
+    // 暂时使用模拟数据
+    setTimeout(() => {
+      dialogue.videoUrl = 'http://learn-ai.com.cn/admin-api/infra/file/29/get/20260421/13-2_1776760082321.mp4'
+      dialogue.generatingVideo = false
+    }, 2000)
+  } catch (error) {
+    console.error(`生成对话视频失败:`, error)
+    dialogue.generatingVideo = false
+    // 生成失败,从replacedUrls中移除旧URL
+    if (oldUrl) {
+      replacedUrls.value.delete(oldUrl)
+    }
+  }
+}
+
 // 步骤2:一键生成所有背景图
 // 步骤2:一键生成所有背景图
 const generateAllImages = async () => {
 const generateAllImages = async () => {
   isGeneratingImages.value = true
   isGeneratingImages.value = true
@@ -1468,16 +1774,31 @@ const validateScript = () => {
   let isValid = true
   let isValid = true
   errorMessages.value = []
   errorMessages.value = []
 
 
-  // 检查环节名称、背景图
+  // 检查环节名称、背景图或视频
   scriptData.sections.forEach((section, sectionIndex) => {
   scriptData.sections.forEach((section, sectionIndex) => {
-    if (!section.name.trim() || !section.backgroundImage.url) {
-      errorMessages.value.push(`环节${sectionIndex + 1}:名称或背景图未配置!`)
+    if (!section.name.trim()) {
+      errorMessages.value.push(`环节${sectionIndex + 1}:名称未配置!`)
       isValid = false
       isValid = false
     }
     }
 
 
+    // 根据背景类型检查对应的媒体URL
+    if (section.backgroundType === 'video') {
+      // 视频背景需要检查视频URL
+      if (!section.backgroundVideo.url) {
+        errorMessages.value.push(`环节${sectionIndex + 1}:视频背景URL未配置!`)
+        isValid = false
+      }
+    } else if (section.backgroundType === 'imageAudio') {
+      // 图音背景需要检查背景图URL
+      if (!section.backgroundImage.url) {
+        errorMessages.value.push(`环节${sectionIndex + 1}:背景图URL未配置!`)
+        isValid = false
+      }
+    }
+
     // 检查对话
     // 检查对话
     section.dialogues.forEach((dialogue, dialogueIndex) => {
     section.dialogues.forEach((dialogue, dialogueIndex) => {
-      // 只检查数字人、提问和诗词类型的对话
+      // 检查数字人、提问和诗词类型的对话
       if (dialogue.type === 'digital' || dialogue.type === 'quest' || dialogue.type === 'poem') {
       if (dialogue.type === 'digital' || dialogue.type === 'quest' || dialogue.type === 'poem') {
         if (
         if (
           !dialogue.roleName ||
           !dialogue.roleName ||
@@ -1489,6 +1810,17 @@ const validateScript = () => {
           )
           )
           isValid = false
           isValid = false
         }
         }
+      } else if (dialogue.type === 'video') {
+        // 检查视频类型的对话
+        if (
+          !dialogue.videoPrompt.trim() ||
+          !dialogue.videoUrl
+        ) {
+          errorMessages.value.push(
+            `环节${sectionIndex + 1}:对话${dialogueIndex + 1}:视频内容未配置完整!`
+          )
+          isValid = false
+        }
       }
       }
     })
     })
   })
   })
@@ -1670,6 +2002,64 @@ onUnmounted(() => {
   border-radius: 3px;
   border-radius: 3px;
 }
 }
 
 
+/* 背景类型切换样式 */
+.background-type-switch {
+  position: absolute;
+  top: -15px;
+  left: 10px;
+  z-index: 10;
+}
+
+/* 媒体控制区域样式调整 */
+.media-controls {
+  position: relative;
+  margin-top: 30px;
+  padding-top: 40px; /* 为背景类型切换留出空间 */
+  height: 165px;
+}
+
+/* 视频预览样式 */
+.media-preview video {
+  width: 100%;
+  max-height: 300px;
+  object-fit: cover;
+  border-radius: 4px;
+}
+
+/* 预览视频背景样式 */
+.preview-section {
+  position: relative;
+  min-height: 400px;
+  margin-bottom: 20px;
+  border-radius: 8px;
+  overflow: hidden;
+}
+
+.preview-video {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  z-index: 0;
+}
+
+.preview-video video {
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+}
+
+.preview-section-content {
+  position: relative;
+  z-index: 1;
+  padding: 20px;
+  background: rgba(255, 255, 255, 0.8);
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+}
+
 .script-editor::-webkit-scrollbar-thumb:hover {
 .script-editor::-webkit-scrollbar-thumb:hover {
   background: #a1a1a1;
   background: #a1a1a1;
 }
 }
@@ -1804,6 +2194,10 @@ onUnmounted(() => {
   background-color: #909399;
   background-color: #909399;
 }
 }
 
 
+.dialogue-type-tag.video {
+  background-color: #8b4513;
+}
+
 /* 对话项样式 */
 /* 对话项样式 */
 .dialogue-item {
 .dialogue-item {
   position: relative;
   position: relative;
@@ -1831,6 +2225,10 @@ onUnmounted(() => {
   border-left: 4px solid #e6a23c;
   border-left: 4px solid #e6a23c;
 }
 }
 
 
+.dialogue-item.video {
+  border-left: 4px solid #8b4513;
+}
+
 /* 对话头部 */
 /* 对话头部 */
 .dialogue-header {
 .dialogue-header {
   margin-bottom: 10px;
   margin-bottom: 10px;
@@ -1937,6 +2335,17 @@ onUnmounted(() => {
   color: white;
   color: white;
 }
 }
 
 
+.add-dialogue-btn.video {
+  background-color: #8b4513;
+  color: white;
+}
+
+.add-dialogue-btn.video:hover {
+  background-color: #a0522d;
+  border-color: #a0522d;
+  color: white;
+}
+
 /* 预览对话样式 */
 /* 预览对话样式 */
 .preview-dialogue {
 .preview-dialogue {
   position: relative;
   position: relative;
@@ -1962,6 +2371,10 @@ onUnmounted(() => {
   border-left: 4px solid #909399;
   border-left: 4px solid #909399;
 }
 }
 
 
+.preview-dialogue.video {
+  border-left: 4px solid #8b4513;
+}
+
 .preview-dialogue .dialogue-header {
 .preview-dialogue .dialogue-header {
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
@@ -2421,7 +2834,7 @@ onUnmounted(() => {
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
   gap: 10px;
   gap: 10px;
-  margin-right: 50px;
+  min-height: 50px;
 }
 }
 
 
 .media-input-group {
 .media-input-group {
@@ -2442,6 +2855,73 @@ onUnmounted(() => {
   transition: all 0.3s ease;
   transition: all 0.3s ease;
   width: 300px;
   width: 300px;
 }
 }
+.media-prompt-video{
+  font-size: 14px;
+  transition: all 0.3s ease;
+  width: 500px;
+}
+
+/* 视频对话样式 */
+.dialogue-item.video .dialogue-row {
+  justify-content: center;
+}
+
+.dialogue-item.video .media-item {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  width: 100%;
+  max-width: 900px;
+}
+
+.dialogue-item.video .media-input-group {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 100%;
+  gap: 15px;
+}
+
+.dialogue-item.video .media-label {
+  white-space: nowrap;
+  margin-bottom: 0;
+}
+
+.dialogue-item.video .media-prompt-video {
+  flex: 1;
+  min-width: 300px;
+  max-width: 600px;
+}
+
+.dialogue-item.video .media-prompt-video .el-textarea {
+  min-height: 40px;
+}
+
+.dialogue-item.video .media-prompt-video .el-textarea__inner {
+  min-height: 40px;
+  padding: 6px 12px;
+}
+
+.dialogue-item.video .media-preview {
+  flex: 0 0 200px;
+  margin: 5px 0;
+}
+
+.dialogue-item.video .media-preview video {
+  width: 100%;
+  max-height: 120px;
+  object-fit: cover;
+  border-radius: 4px;
+}
+
+.dialogue-item.video .generate-btn {
+  min-width: 80px;
+  white-space: nowrap;
+}
+
+.dialogue-item.video .remove-btn {
+  margin-left: 10px;
+}
 
 
 .media-prompt .el-textarea {
 .media-prompt .el-textarea {
   width: 100%;
   width: 100%;
@@ -2465,7 +2945,6 @@ onUnmounted(() => {
 }
 }
 
 
 .media-preview {
 .media-preview {
-  margin-top: 12px;
   margin-left: 20px;
   margin-left: 20px;
 }
 }