فهرست منبع

AI生成课加入一键生成所有视频
编程课类型封面加入默认封面

liyanbo 2 هفته پیش
والد
کامیت
fbb94d6eed

+ 13 - 13
src/views/ai/video/index/components/VideoList.vue

@@ -5,7 +5,7 @@
       <!-- TODO @fan:看看,怎么优化下这个样子哈。 -->
 <!--      <el-button @click="handleViewPublic">视频作品</el-button>-->
     </template>
-    <!-- 图片列表 -->
+    <!-- 视频列表 -->
     <div class="task-video-list" ref="videoListRef">
       <VideoCard
         v-for="video in videoList"
@@ -24,7 +24,7 @@
     </div>
   </el-card>
 
-  <!-- 图片详情 -->
+  <!-- 视频详情 -->
   <VideoDetail
     :show="isShowVideoDetail"
     :id="showVideoDetailId"
@@ -45,7 +45,7 @@ import download from '@/utils/download'
 const message = useMessage() // 消息弹窗
 const router = useRouter() // 路由
 
-// 图片分页相关的参数
+// 视频分页相关的参数
 const queryParams = reactive({
   pageNo: 1,
   pageSize: 10
@@ -54,12 +54,12 @@ const pageTotal = ref<number>(0) // page size
 const videoList = ref<VideoVO[]>([]) // video 列表
 const videoListLoadingInstance = ref<any>() // video 列表是否正在加载中
 const videoListRef = ref<any>() // ref
-// 图片轮询相关的参数(正在生成中的)
+// 视频轮询相关的参数(正在生成中的)
 const inProgressVideoMap = ref<{}>({}) // 监听的 video 映射,一般是生成中(需要轮询),key 为 video 编号,value 为 video
 const inProgressTimer = ref<any>() // 生成中的 video 定时器,轮询生成进展
-// 图片详情相关的参数
-const isShowVideoDetail = ref<boolean>(false) // 图片详情是否展示
-const showVideoDetailId = ref<number>(0) // 图片详情的图片编号
+// 视频详情相关的参数
+const isShowVideoDetail = ref<boolean>(false) // 视频详情是否展示
+const showVideoDetailId = ref<number>(0) // 视频详情的视频编号
 
 /** 处理查看视频作品 */
 const handleViewPublic = () => {
@@ -68,20 +68,20 @@ const handleViewPublic = () => {
   })
 }
 
-/** 查看图片的详情  */
+/** 查看视频的详情  */
 const handleDetailOpen = async () => {
   isShowVideoDetail.value = true
 }
 
-/** 关闭图片的详情  */
+/** 关闭视频的详情  */
 const handleDetailClose = async () => {
   isShowVideoDetail.value = false
 }
 
-/** 获得 video 图片列表 */
+/** 获得 video 视频列表 */
 const getVideoList = async () => {
   try {
-    // 1. 加载图片列表
+    // 1. 加载视频列表
     videoListLoadingInstance.value = ElLoading.service({
       target: videoListRef.value,
       text: '加载中...'
@@ -90,7 +90,7 @@ const getVideoList = async () => {
     videoList.value = list
     pageTotal.value = total
 
-    // 2. 计算需要轮询的图片
+    // 2. 计算需要轮询的视频
     const newWatVideos = {}
     videoList.value.forEach((item) => {
       if (item.status === AiVideoStatusEnum.IN_PROGRESS) {
@@ -129,7 +129,7 @@ const refreshWatchVideos = async () => {
   inProgressVideoMap.value = newWatchVideos
 }
 
-/** 图片的点击事件 */
+/** 视频的点击事件 */
 const handleVideoButtonClick = async (type: string, videoDetail: VideoVO) => {
   // 详情
   if (type === 'more') {

+ 146 - 13
src/views/bjdx/course/aiGenerate/aiGengrate.vue

@@ -141,6 +141,13 @@
               >
                 {{ isGeneratingImages ? '生成中...' : '一键生成所有背景图' }}
               </button>
+              <button
+                class="toolbar-btn"
+                @click="generateAllVideos"
+                :disabled="isGeneratingVideos || isAnyVideoGenerating"
+              >
+                {{ isGeneratingVideos ? '生成中...' : '一键生成所有视频' }}
+              </button>
               <button
                 class="toolbar-btn"
                 @click="generateAllVoiceovers"
@@ -829,8 +836,9 @@ import { ChatConversationApi } from '@/api/ai/chat/conversation'
 import { ChatRoleApi } from '@/api/ai/model/chatRole'
 import { ImageApi } from '@/api/ai/image'
 import { TtsApi } from '@/api/ai/tts'
+import { VideoApi} from '@/api/ai/video'
 import VideoPreview from '@/views/bjdx/course/aiGenerate/VideoPreview.vue'
-import { AiImageStatusEnum, AiPlatformEnum } from '@/views/ai/utils/constants'
+import { AiImageStatusEnum, AiPlatformEnum, AiVideoStatusEnum } from '@/views/ai/utils/constants'
 import { marked } from 'marked'
 import { useUserStore } from '@/store/modules/user'
 
@@ -882,6 +890,7 @@ const hasDraftCache = ref(false)
 // 步骤2生成状态
 const isGeneratingImages = ref(false)
 const isGeneratingVoiceovers = ref(false)
+const isGeneratingVideos = ref(false)
 
 // 数字人列表
 const digitalHumans = ref([])
@@ -1165,6 +1174,18 @@ const isAnyVoiceoverGenerating = computed(() => {
   )
 })
 
+// 计算属性:检查是否有任何视频正在生成
+const isAnyVideoGenerating = computed(() => {
+  return scriptData.sections.some((section) => {
+    // 检查背景视频是否正在生成
+    if (section.backgroundVideo.generating) {
+      return true
+    }
+    // 检查对话视频是否正在生成
+    return section.dialogues.some((dialogue) => dialogue.generatingVideo)
+  })
+})
+
 // 校验结果
 const isValidationPassed = ref(false)
 const validationMessage = ref('')
@@ -1476,6 +1497,10 @@ const removeSection = (sectionIndex) => {
 const inProgressImageMap = ref({}) // 监听的图片映射,key 为 image 编号,value 为 { sectionIndex, type: 'image'|'audio' }
 const inProgressTimer = ref(null) // 生成中的图片定时器,轮询生成进展
 
+// 视频生成相关状态
+const inProgressVideoMap = ref({}) // 监听的视频映射,key 为 video 编号,value 为 { sectionIndex, dialogueIndex, type: 'background'|'dialogue' }
+const inProgressVideoTimer = ref(null) // 生成中的视频定时器,轮询生成进展
+
 // 步骤2:生成单个媒体(仅背景图)
 const generateMedia = async (sectionIndex) => {
   const section = scriptData.sections[sectionIndex]
@@ -1525,12 +1550,21 @@ const generateVideo = async (sectionIndex) => {
 
   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)
+    const form = {
+      platform: AiPlatformEnum.DOU_BAO,
+      modelId: 60,
+      prompt: media.prompt,
+      width: 1024,
+      height: 768,
+      resolution: '1080P',
+      duration: 4,
+      options: {}
+    }
+    const response = await VideoApi.drawVideo(form)
+    inProgressVideoMap.value[response] = {
+      sectionIndex,
+      type: 'background'
+    }
   } catch (error) {
     console.error(`生成视频失败:`, error)
     media.generating = false
@@ -1554,12 +1588,22 @@ const generateDialogueVideo = async (sectionIndex, dialogueIndex) => {
 
   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)
+    const form = {
+      platform: AiPlatformEnum.DOU_BAO,
+      modelId: 60,
+      prompt: dialogue.videoPrompt,
+      width: 1024,
+      height: 768,
+      resolution: '1080P',
+      duration: 4,
+      options: {}
+    }
+    const response = await VideoApi.drawVideo(form)
+    inProgressVideoMap.value[response] = {
+      sectionIndex,
+      dialogueIndex,
+      type: 'dialogue'
+    }
   } catch (error) {
     console.error(`生成对话视频失败:`, error)
     dialogue.generatingVideo = false
@@ -1595,6 +1639,38 @@ const generateAllImages = async () => {
   }
 }
 
+// 步骤2:一键生成所有视频
+const generateAllVideos = async () => {
+  isGeneratingVideos.value = true
+  try {
+    for (let i = 0; i < scriptData.sections.length; i++) {
+      const section = scriptData.sections[i]
+      // 处理视频背景
+      if (section.backgroundType === 'video' && !section.backgroundVideo.url) {
+        await generateVideo(i)
+      }
+      // 处理对话视频
+      for (let j = 0; j < section.dialogues.length; j++) {
+        const dialogue = section.dialogues[j]
+        if (dialogue.type === 'video' && !dialogue.videoUrl) {
+          await generateDialogueVideo(i, j)
+        }
+      }
+    }
+
+    // 检查是否有视频正在生成
+    const checkVideosComplete = setInterval(() => {
+      if (Object.keys(inProgressVideoMap.value).length === 0) {
+        isGeneratingVideos.value = false
+        clearInterval(checkVideosComplete)
+      }
+    }, 1000)
+  } catch (error) {
+    console.error('生成所有视频失败:', error)
+    isGeneratingVideos.value = false
+  }
+}
+
 // 轮询生成中的图片列表
 const refreshWatchImages = async () => {
   const imageIds = Object.keys(inProgressImageMap.value).map(Number)
@@ -1630,6 +1706,56 @@ const refreshWatchImages = async () => {
   }
 }
 
+// 轮询生成中的视频列表
+const refreshWatchVideos = async () => {
+  const videoIds = Object.keys(inProgressVideoMap.value).map(Number)
+  if (videoIds.length === 0) {
+    return
+  }
+
+  try {
+    const list = await VideoApi.getVideoListMyByIds(videoIds)
+    const newWatchVideos = {}
+    list.forEach((video) => {
+      const info = inProgressVideoMap.value[video.id]
+      if (info) {
+        if (video.status === AiVideoStatusEnum.IN_PROGRESS) {
+          newWatchVideos[video.id] = info
+        } else if (video.status === AiVideoStatusEnum.SUCCESS && video.videoUrl) {
+          if (info.type === 'background') {
+            const section = scriptData.sections[info.sectionIndex]
+            section.backgroundVideo.url = video.videoUrl
+            section.backgroundVideo.generating = false
+          } else if (info.type === 'dialogue') {
+            const dialogue = scriptData.sections[info.sectionIndex].dialogues[info.dialogueIndex]
+            dialogue.videoUrl = video.videoUrl
+            dialogue.generatingVideo = false
+          }
+        } else if (video.status === AiVideoStatusEnum.FAIL) {
+          if (info.type === 'background') {
+            const section = scriptData.sections[info.sectionIndex]
+            section.backgroundVideo.generating = false
+            // 生成失败,从replacedUrls中移除旧URL
+            if (section.backgroundVideo.url) {
+              replacedUrls.value.delete(section.backgroundVideo.url)
+            }
+          } else if (info.type === 'dialogue') {
+            const dialogue = scriptData.sections[info.sectionIndex].dialogues[info.dialogueIndex]
+            dialogue.generatingVideo = false
+            // 生成失败,从replacedUrls中移除旧URL
+            if (dialogue.videoUrl) {
+              replacedUrls.value.delete(dialogue.videoUrl)
+            }
+          }
+        }
+      }
+    })
+    inProgressVideoMap.value = newWatchVideos
+  } catch (error) {
+    console.error('轮询视频状态失败:', error)
+  }
+}
+
 // 播放音频通用函数
 const playAudio = (url, type) => {
   if (audioState.currentAudio) {
@@ -1891,6 +2017,10 @@ onMounted(async () => {
   inProgressTimer.value = setInterval(async () => {
     await refreshWatchImages()
   }, 3000)
+  
+  inProgressVideoTimer.value = setInterval(async () => {
+    await refreshWatchVideos()
+  }, 3000)
 })
 
 // 关闭事件
@@ -1904,6 +2034,9 @@ onUnmounted(() => {
   if (inProgressTimer.value) {
     clearInterval(inProgressTimer.value)
   }
+  if (inProgressVideoTimer.value) {
+    clearInterval(inProgressVideoTimer.value)
+  }
 })
 </script>
 

+ 1 - 1
src/views/blockly/blocklyType/BlocklyTypeForm.vue

@@ -71,7 +71,7 @@ const formType = ref('') // 表单的类型:create - 新增;update - 修改
 const formData = ref({
   id: undefined,
   ctType: undefined,
-  ctTypeImage: undefined,
+  ctTypeImage: "https://learn-ai.com.cn/admin-api/infra/file/29/get/20251212/common_1765522078241.png",
   ctTypeNode: 0,
   ctTypeSort: undefined,
   ctParentId: 0,