Ver Fonte

Merge remote-tracking branch 'origin/wanzi' into muzi

liyanbo há 1 mês atrás
pai
commit
8398593bad

+ 223 - 80
src/components/study/SelfDirectedLearning.vue

@@ -15,32 +15,28 @@
             @keyup.enter.exact="doSendMessage(prompt.trim())"
           ></textarea>
           <div class="dropdowns-container">
-            <el-dropdown
+            <el-select
               v-for="dropdown in dropdowns"
               :key="dropdown.key"
               v-model="dropdown.value"
-              @command="(command) => handleSelect(dropdown.key, command)"
-              @visible-change="(visible) => handleVisibleChange(dropdown.key, visible)"
+              @change="(value) => handleSelect(dropdown.key, value)"
+              filterable
+              class="custom-select"
+              :popper-append-to-body="false"
+              :teleported="false"
             >
-              <el-button type="primary">
-                <el-icon class="el-icon--left"><component :is="dropdown.icon" /></el-icon>
-                {{ dropdown.value }}
-                <el-icon class="el-icon--right" v-if="!dropdown.visible"><ArrowDownBold /></el-icon>
-                <el-icon class="el-icon--right" v-else><ArrowUpBold /></el-icon>
-              </el-button>
-              <template #dropdown>
-                <el-dropdown-menu class="dropdown-menu">
-                  <el-dropdown-item
-                    v-for="item in dropdown.options"
-                    :key="item.value"
-                    :command="item.value"
-                    >{{ item.label }}</el-dropdown-item
-                  >
-                </el-dropdown-menu>
+              <template #prefix>
+                <el-icon><component :is="dropdown.icon" /></el-icon>
               </template>
-            </el-dropdown>
+              <el-option
+                v-for="item in dropdown.options"
+                :key="item.value"
+                :label="item.label"
+                :value="item.value"
+              />
+            </el-select>
           </div>
-          <button class="send-button" :class="{ 'active': prompt.trim().length > 0 }" @click="doSendMessage(prompt.trim())" :disabled="!prompt.trim().length">
+          <button class="send-button" :class="{ 'active': prompt.trim().length > 0 && (!showNewContainer || allStepsCompleted) }" @click="doSendMessage(prompt.trim())" :disabled="!prompt.trim().length || (showNewContainer && !allStepsCompleted)">
             <Top />
           </button>
         </div>
@@ -66,12 +62,20 @@
               <div class="progress-title">正在生成课程</div>
               <div class="progress-steps">
                 <div class="progress-step" v-for="(step, index) in visibleProgressSteps" :key="index" :style="{ animationDelay: `${index * 0.2}s` }">
-                  <span class="step-text">{{ step.text }}</span>
-                  <span class="step-status" :class="{ 'completed': step.completed, 'active': step.active }">
-                    <span v-if="step.completed" class="checkmark">✓</span>
-                    <span v-else-if="step.active" class="loading-dot"></span>
-                    <span v-else class="step-placeholder"></span>
-                  </span>
+                  <div class="step-header">
+                    <span class="step-text">{{ step.text }}</span>
+                    <span class="step-status" :class="{ 'completed': step.completed, 'active': step.active }">
+                      <span v-if="step.completed" class="checkmark">✓</span>
+                      <span v-else-if="step.active" class="loading-dot"></span>
+                      <span v-else class="step-placeholder"></span>
+                    </span>
+                  </div>
+                  <div class="step-progress" v-if="(index === 2 || index === 3) && step.visible">
+                    <div class="progress-bar">
+                      <div class="progress-fill" :style="{ width: `${step.progress}%` }"></div>
+                    </div>
+                    <span class="progress-text">{{ step.progress }}%</span>
+                  </div>
                 </div>
               </div>
             </div>
@@ -462,6 +466,26 @@ const generateAllImages = async () => {
   return new Promise(async (resolve, reject) => {
     isGeneratingImages.value = true
     try {
+      // 计算总图片数
+      progressCounters.value.images.total = 0
+      if (scriptData.coverImage && scriptData.coverImage.prompt && !scriptData.coverImage.url) {
+        progressCounters.value.images.total++
+      }
+      if (scriptData.lessons && scriptData.lessons.length > 0) {
+        for (let lessonIndex = 0; lessonIndex < scriptData.lessons.length; lessonIndex++) {
+          const lesson = scriptData.lessons[lessonIndex]
+          if (lesson && lesson.sections) {
+            for (let sectionIndex = 0; sectionIndex < lesson.sections.length; sectionIndex++) {
+              const section = lesson.sections[sectionIndex]
+              if (section && section.backgroundImage && section.backgroundImage.prompt && !section.backgroundImage.url) {
+                progressCounters.value.images.total++
+              }
+            }
+          }
+        }
+      }
+      progressCounters.value.images.current = 0
+
       // 确保scriptData和coverImage存在
       if (!scriptData.coverImage) {
         scriptData.coverImage = { prompt: '', url: '', generating: false }
@@ -497,6 +521,7 @@ const generateAllImages = async () => {
       // 检查是否有图片正在生成
       if (Object.keys(inProgressImageMap.value).length === 0) {
         // 没有图片需要生成,直接resolve
+        progressSteps.value[2].progress = 100
         isGeneratingImages.value = false
         resolve()
       } else {
@@ -506,6 +531,7 @@ const generateAllImages = async () => {
         // 检查是否有图片正在生成
         const checkImagesComplete = setInterval(() => {
           if (Object.keys(inProgressImageMap.value).length === 0) {
+            progressSteps.value[2].progress = 100
             isGeneratingImages.value = false
             clearInterval(checkImagesComplete)
             stopImagePolling()
@@ -626,6 +652,7 @@ const refreshWatchImages = async () => {
   try {
     const list = await PaintingGetMys(imageIds)
     const newWatchImages = {}
+    let completedCount = 0
     // 遍历所有正在生成的图片
     Object.keys(inProgressImageMap.value).forEach((key) => {
       const imageId = Number(key)
@@ -638,6 +665,7 @@ const refreshWatchImages = async () => {
           newWatchImages[key] = info
         } else if (image.status === AiImageStatusEnum.SUCCESS && image.picUrl) {
           // 图片生成成功
+          completedCount++
           if (info.type === 'cover') {
             // 处理封面图
             scriptData.coverImage.url = image.picUrl
@@ -660,6 +688,7 @@ const refreshWatchImages = async () => {
           console.log("【最终数据,生成图片成功】", scriptData)
         } else if (image.status === AiImageStatusEnum.FAIL) {
           // 图片生成失败
+          completedCount++
           if (info.type === 'cover') {
             // 处理封面图失败
             scriptData.coverImage.generating = false
@@ -676,6 +705,7 @@ const refreshWatchImages = async () => {
         }
       } else {
         // 图片不存在于返回列表中,可能已经处理完成或超时,从map中移除
+        completedCount++
         if (info.type === 'cover') {
           scriptData.coverImage.generating = false
         } else {
@@ -690,6 +720,12 @@ const refreshWatchImages = async () => {
       }
     })
     inProgressImageMap.value = newWatchImages
+    
+    // 更新进度
+    progressCounters.value.images.current = completedCount
+    if (progressCounters.value.images.total > 0) {
+      progressSteps.value[2].progress = Math.round((completedCount / progressCounters.value.images.total) * 100)
+    }
   } catch (error) {
     console.error('轮询图片状态失败:', error)
   }
@@ -699,6 +735,28 @@ const refreshWatchImages = async () => {
 const generateAllVoiceovers = async () => {
   isGeneratingVoiceovers.value = true
   try {
+    // 计算总语音数
+    progressCounters.value.voiceovers.total = 0
+    if (scriptData.lessons && scriptData.lessons.length > 0) {
+      for (let lessonIndex = 0; lessonIndex < scriptData.lessons.length; lessonIndex++) {
+        const lesson = scriptData.lessons[lessonIndex]
+        if (lesson && lesson.sections) {
+          for (let sectionIndex = 0; sectionIndex < lesson.sections.length; sectionIndex++) {
+            const section = lesson.sections[sectionIndex]
+            if (section && section.dialogues) {
+              for (let dialogueIndex = 0; dialogueIndex < section.dialogues.length; dialogueIndex++) {
+                const dialogue = section.dialogues[dialogueIndex]
+                if (dialogue && dialogue.type !== 'user' && !dialogue.voiceoverUrl && dialogue.content) {
+                  progressCounters.value.voiceovers.total++
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+    progressCounters.value.voiceovers.current = 0
+
     // 确保scriptData和lessons存在
     if (scriptData.lessons && scriptData.lessons.length > 0) {
       for (let lessonIndex = 0; lessonIndex < scriptData.lessons.length; lessonIndex++) {
@@ -710,8 +768,13 @@ const generateAllVoiceovers = async () => {
               for (let dialogueIndex = 0; dialogueIndex < section.dialogues.length; dialogueIndex++) {
                 const dialogue = section.dialogues[dialogueIndex]
                 // 只处理没有语音URL且有内容的对话
-                if (dialogue && !dialogue.url && dialogue.content) {
+                if (dialogue && dialogue.type !== 'user' && !dialogue.voiceoverUrl && dialogue.content) {
                   await generateVoiceover(lessonIndex, sectionIndex, dialogueIndex)
+                  // 更新进度
+                  progressCounters.value.voiceovers.current++
+                  if (progressCounters.value.voiceovers.total > 0) {
+                    progressSteps.value[3].progress = Math.round((progressCounters.value.voiceovers.current / progressCounters.value.voiceovers.total) * 100)
+                  }
                 }
               }
             }
@@ -719,6 +782,8 @@ const generateAllVoiceovers = async () => {
         }
       }
     }
+    // 完成后设置为100%
+    progressSteps.value[3].progress = 100
   } catch (error) {
     console.error('生成所有语音失败:', error)
   } finally {
@@ -773,12 +838,18 @@ const showNewContainer = ref(false);
 
 // 生成进度步骤
 const progressSteps = ref([
-  { text: '生成课程脚本...', completed: false, active: false, visible: false },
-  { text: '生成课程小节...', completed: false, active: false, visible: false },
-  { text: '生成背景图...', completed: false, active: false, visible: false },
-  { text: '生成配音...', completed: false, active: false, visible: false }
+  { text: '生成课程脚本...', completed: false, active: false, visible: false, progress: 0 },
+  { text: '生成课程小节...', completed: false, active: false, visible: false, progress: 0 },
+  { text: '生成背景图...', completed: false, active: false, visible: false, progress: 0 },
+  { text: '生成配音...', completed: false, active: false, visible: false, progress: 0 }
 ]);
 
+// 进度计数器
+const progressCounters = ref({
+  images: { total: 0, current: 0 },
+  voiceovers: { total: 0, current: 0 }
+});
+
 // 计算主讲下拉框选项
 const teacherOptions = computed(() => {
   const roles = localScriptRoles.value.length > 0 ? localScriptRoles.value : localScriptRoles.value;
@@ -797,6 +868,18 @@ const assistantOptions = computed(() => {
   }));
 });
 
+// 计算生成主题下拉框选项,基于 lessonList 动态生成
+const courseOptions = computed(() => {
+  // 提取 lessonList 中的课程名称
+  const lessonNames = lessonList.value.map(lesson => lesson.name).join('、');
+  return [
+    {
+      label: `诗词课(${lessonNames})`,
+      value: '诗词课'
+    }
+  ];
+});
+
 /**
  * 获取角色列表
  * @returns {Promise<void>}
@@ -824,9 +907,7 @@ const dropdowns = ref([
     value: '生成主题',
     visible: false,
     icon: Search,
-    options: [
-      { label: '诗词课(课程引入、知识讲解、课程总结)', value: '诗词课' }
-    ]
+    options: courseOptions.value
   },
   {
     key: 'teacher',
@@ -955,6 +1036,8 @@ onMounted(async () => {
   
   await getRoleList();
 
+  // 初始化生成主题下拉框选项
+  dropdowns.value[0].options = courseOptions.value;
   // 初始化主讲下拉框选项
   dropdowns.value[1].options = teacherOptions.value;
   // 初始化助教下拉框选项
@@ -1067,83 +1150,110 @@ onMounted(async () => {
   align-self: flex-start;
 }
 
-.dropdowns-container .el-button {
-  width: rpx(60);
+.dropdowns-container .custom-select {
+  width: auto;
   height: rpx(18);
-  background-color: #f1f0ff;
-  border: rpx(1) solid #a7a4ed;
-  color: black;
-  border-radius: rpx(4);
   font-size: rpx(8);
-  display: flex;
-  align-items: center;
-  justify-content: center;
+  max-width: rpx(80);
 }
 
+.dropdowns-container .custom-select :deep(.el-select__wrapper) {
+  background-color: #f1f0ff !important;
+  border: rpx(1) solid #a7a4ed !important;
+  border-radius: rpx(4) !important;
+  height: rpx(18) !important;
+}
 
-.dropdown-menu {
+.dropdowns-container .custom-select .el-select__input {
+  font-size: rpx(8);
+  color: black;
+  height: rpx(18);
+  line-height: rpx(18);
+}
+
+.dropdowns-container .custom-select .el-select__prefix {
+  color: black;
+}
+
+.dropdowns-container .custom-select .el-select__caret {
+  color: black;
+}
+
+/* 下拉菜单样式 */
+.dropdowns-container .custom-select :deep(.el-select-dropdown) {
   min-width: rpx(50);
   width: auto;
   max-height: rpx(160);
   overflow-y: auto;
   border-radius: rpx(3);
   border: 1px white solid;
-  background-color: rgb(255, 255, 255, 0.5);
+  background-color: rgb(255, 255, 255, 0.8);
   backdrop-filter: blur(rpx(5));
-  box-shadow: 0 4px 8px rgba(202, 52, 52, 0.1);
+  // box-shadow: 0 4px 8px rgba(202, 52, 52, 0.1);
 }
 
-.dropdown-menu ::v-deep::-webkit-scrollbar {
+.dropdowns-container .custom-select :deep(.el-select-dropdown::-webkit-scrollbar) {
   width: rpx(2);
 }
 
-.dropdown-menu ::v-deep::-webkit-scrollbar-track {
+.dropdowns-container .custom-select :deep(.el-select-dropdown::-webkit-scrollbar-track) {
   background: #f1f1f1;
   border-radius: rpx(2);
 }
 
-.dropdown-menu ::v-deep::-webkit-scrollbar-thumb {
+.dropdowns-container .custom-select :deep(.el-select-dropdown::-webkit-scrollbar-thumb) {
   background: #c1c1c1;
   border-radius: rpx(2);
 }
 
-.dropdown-menu ::v-deep::-webkit-scrollbar-thumb:hover {
+.dropdowns-container .custom-select :deep(.el-select-dropdown::-webkit-scrollbar-thumb:hover) {
   background: #a8a8a8;
 }
 
-.dropdown-menu ::v-deep(.el-dropdown-menu__item) {
-  font-size: rpx(8);
-  color: black;
-  border-radius: rpx(5);
-  width: auto;
-  min-width: rpx(35);
-  height: rpx(20);
-  margin-bottom: rpx(8);
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  padding: 0 rpx(8);
+.dropdowns-container .custom-select :deep(.el-select-dropdown__item) {
+  font-size: rpx(8) !important;
+  color: black !important;
+  border-radius: rpx(5) !important;
+  width: auto !important;
+  min-width: rpx(35) !important;
+  height: rpx(20) !important;
+  margin-bottom: rpx(8) !important;
+  display: flex !important;
+  align-items: center !important;
+  justify-content: center !important;
+  padding: 0 rpx(8) !important;
 }
 
-.dropdown-menu ::v-deep(.el-dropdown-menu__item:hover),
-.dropdown-menu ::v-deep(.el-dropdown-menu__item:focus),
-.dropdown-menu ::v-deep(.el-dropdown-menu__item:active) {
+.dropdowns-container .custom-select :deep(.el-select-dropdown__item:hover),
+.dropdowns-container .custom-select :deep(.el-select-dropdown__item:focus),
+.dropdowns-container .custom-select :deep(.el-select-dropdown__item.is-active) {
   background: linear-gradient(
     to bottom,
     #fee78a,
     #ffce1b
-  );
-  border: none;
-  outline: none;
+  ) !important;
+  border: none !important;
+  outline: none !important;
+  color: black !important;
 }
 
-/* 确保下拉按钮点击时也没有边框 */
-.dropdowns-container .el-button:focus,
-.dropdowns-container .el-button:active {
+/* 确保下拉点击时也没有边框 */
+.dropdowns-container .custom-select:focus,
+.dropdowns-container .custom-select:active {
   outline: none;
   box-shadow: none;
 }
 
+/* 搜索输入框样式 */
+.dropdowns-container .custom-select :deep(.el-select__input.is-focus) {
+  color: black;
+}
+
+/* 下拉框清空按钮样式 */
+.dropdowns-container .custom-select .el-select__clear {
+  color: black;
+}
+
 /* 滚动条样式 */
 .user-input-textarea::-webkit-scrollbar {
   width: rpx(0);
@@ -1380,14 +1490,7 @@ onMounted(async () => {
 }
 
 .progress-step {
-  display: flex;
-  justify-content: flex-start;
-  align-items: center;
-  text-align: left;
-  font-size: rpx(8);
-  color: #666;
-  padding: rpx(1) 0;
-  transition: all 0.3s ease;
+  margin-bottom: rpx(5);
   opacity: 0;
   transform: translateY(rpx(5));
   animation: stepEnter 0.5s ease forwards;
@@ -1405,6 +1508,18 @@ onMounted(async () => {
   font-weight: bold;
 }
 
+.step-header {
+  display: flex;
+  justify-content: flex-start;
+  align-items: center;
+  text-align: left;
+  font-size: rpx(8);
+  color: #666;
+  padding: rpx(1) 0;
+  transition: all 0.3s ease;
+  margin-bottom: rpx(2);
+}
+
 .step-text {
   flex: 1;
   overflow: hidden;
@@ -1453,9 +1568,37 @@ onMounted(async () => {
   animation: pulse 1.5s ease-in-out infinite;
 }
 
+.step-progress {
+  display: flex;
+  align-items: center;
+  gap: rpx(5);
+  margin-left: rpx(0);
+}
+
+.progress-bar {
+  flex: 1;
+  height: rpx(2);
+  background-color: rgba(167, 164, 237, 0.2);
+  border-radius: rpx(1);
+  overflow: hidden;
+}
+
+.progress-fill {
+  height: 100%;
+  background-color: #a7a4ed;
+  transition: width 0.3s ease;
+}
+
+.progress-text {
+  font-size: rpx(6);
+  color: #666;
+  min-width: rpx(20);
+  text-align: right;
+}
+
 @keyframes pulse {
   0%, 100% { opacity: 1; transform: scale(1); }
   50% { opacity: 0.6; transform: scale(1.2); }
 }
 
-</style>
+</style>

+ 1 - 1
src/views/AIPage/AIGeneralCourse.vue

@@ -808,7 +808,7 @@ const goToAIExperience = outlineData => {
 
 .box-2 {
   width: 100%;
-  // flex: 1;
+  flex: 1;
   box-sizing: border-box;
   display: flex; // 确保子元素水平排列
   flex-wrap: wrap; // 允许子元素换行;