Ver Fonte

课程新增自动绑定大纲名称
一键配音可自主勾选配音配型

liyanbo há 4 dias atrás
pai
commit
dc619db277

+ 3 - 4
src/views/bjdx/course/CourseForm.vue

@@ -361,12 +361,11 @@ const formRef = ref() // 表单 Ref
 const courseTypeTree = ref() // 树形结构
 
 /** 打开弹窗 */
-const open = async (type: string, typeNode: string, id?: number) => {
+const open = async (type: string, typeNode: string, id?: number, courseType?: number) => {
   dialogVisible.value = true
   dialogTitle.value = t('action.' + type)
   formType.value = type
   resetForm()
-  // 修改时,设置数据
   if (id) {
     formLoading.value = true
     try {
@@ -374,14 +373,14 @@ const open = async (type: string, typeNode: string, id?: number) => {
       formData.value = {
         ...formData.value,
         ...courseData,
-        // courseLabel: courseData.courseLabel?.split(','),
         courseImagePath: courseData.courseImagePath?.split(',')
       }
-      // 确保 courseType 为正确的 id 类型
       formData.value.courseType = courseData.courseType ? Number(courseData.courseType) : undefined
     } finally {
       formLoading.value = false
     }
+  } else if (courseType !== undefined && courseType !== null) {
+    formData.value.courseType = Number(courseType)
   }
   formData.value.ctTypeNode = typeNode
   await getCourseTypeTree(typeNode)

+ 197 - 10
src/views/bjdx/course/aiGenerate/aiGengrate.vue

@@ -168,13 +168,62 @@
               >
                 {{ isGeneratingVideos ? '生成中...' : '一键生成所有视频' }}
               </button>
-              <button
-                class="toolbar-btn"
-                @click="generateAllVoiceovers"
-                :disabled="isGeneratingVoiceovers || isAnyVoiceoverGenerating"
-              >
-                {{ isGeneratingVoiceovers ? '生成中...' : '一键配音' }}
-              </button>
+              <div class="voiceover-split-button">
+                <button
+                  class="toolbar-btn voiceover-main-btn"
+                  @click="generateAllVoiceovers"
+                  :disabled="
+                    isGeneratingVoiceovers ||
+                    isAnyVoiceoverGenerating ||
+                    selectedVoiceoverTypes.length === 0
+                  "
+                  :aria-busy="isGeneratingVoiceovers"
+                >
+                  {{ isGeneratingVoiceovers ? '生成中...' : '一键配音' }}
+                </button>
+                <el-popover
+                  v-model:visible="voiceoverMenuVisible"
+                  trigger="click"
+                  placement="bottom-end"
+                  :width="220"
+                  :teleported="true"
+                  :disabled="isGeneratingVoiceovers || isAnyVoiceoverGenerating"
+                  popper-class="voiceover-type-popper"
+                >
+                  <template #reference>
+                    <button
+                      class="toolbar-btn voiceover-menu-btn"
+                      :class="{ active: voiceoverMenuVisible }"
+                      :disabled="isGeneratingVoiceovers || isAnyVoiceoverGenerating"
+                      aria-label="选择配音类型"
+                      aria-haspopup="dialog"
+                      :aria-expanded="voiceoverMenuVisible"
+                    >
+                      <span class="dropdown-triangle" aria-hidden="true"></span>
+                    </button>
+                  </template>
+                  <div class="voiceover-type-menu" role="group" aria-label="配音类型">
+                    <div class="voiceover-menu-title">
+                      <span>选择配音类型</span>
+                      <span
+                        >{{ selectedVoiceoverTypes.length }}/{{ voiceoverTypeOptions.length }}</span
+                      >
+                    </div>
+                    <el-checkbox-group v-model="selectedVoiceoverTypes">
+                      <el-checkbox
+                        v-for="option in voiceoverTypeOptions"
+                        :key="option.value"
+                        :value="option.value"
+                      >
+                        {{ option.label }}
+                      </el-checkbox>
+                    </el-checkbox-group>
+                    <div v-if="selectedVoiceoverTypes.length === 0" class="voiceover-menu-tip">
+                      请至少选择一种配音类型
+                    </div>
+                  </div>
+                </el-popover>
+              </div>
             </div>
 
             <!-- 可滚动的内容区域 -->
@@ -1101,6 +1150,17 @@ const isGeneratingImages = ref(false)
 const isGeneratingVoiceovers = ref(false)
 const isGeneratingVideos = ref(false)
 
+// 一键配音类型
+const voiceoverTypeOptions = [
+  { value: 'digital', label: '数字人对话', default: true },
+  { value: 'quest', label: '互动提问', default: true },
+  { value: 'poem', label: '诗词', default: true }
+]
+const selectedVoiceoverTypes = ref(
+  voiceoverTypeOptions.filter((option) => option.default).map((option) => option.value)
+)
+const voiceoverMenuVisible = ref(false)
+
 // 数字人列表
 const digitalHumans = ref([])
 
@@ -2294,13 +2354,17 @@ const generateVoiceover = async (sectionIndex, dialogueIndex) => {
 
 // 步骤3:一键生成所有语音
 const generateAllVoiceovers = async () => {
+  const selectedTypes = new Set(selectedVoiceoverTypes.value)
+  if (selectedTypes.size === 0) return
+
+  voiceoverMenuVisible.value = false
   isGeneratingVoiceovers.value = true
   try {
     for (let i = 0; i < scriptData.sections.length; i++) {
       for (let j = 0; j < scriptData.sections[i].dialogues.length; j++) {
         const dialogue = scriptData.sections[i].dialogues[j]
-        // 只处理没有语音URL的对话
-        if (!dialogue.voiceoverUrl) {
+        // 只处理已勾选类型且尚未生成语音的对话
+        if (selectedTypes.has(dialogue.type) && !dialogue.voiceoverUrl) {
           await generateVoiceover(i, j)
         }
       }
@@ -2361,7 +2425,7 @@ const validateScript = () => {
           (dialogue.type !== 'poem' && dialogue.type !== 'poem_reading' && dialogue.type !== 'write_reading' && dialogue.type !== 'recite_reading' && !dialogue.voiceoverUrl)
         ) {
           errorMessages.value.push(
-            `环节${sectionIndex + 1}:对话${dialogueIndex + 1}:存在完成内容!`
+            `环节${sectionIndex + 1}:对话${dialogueIndex + 1}:存在完成内容!`
           )
           isValid = false
         }
@@ -3549,6 +3613,129 @@ onUnmounted(() => {
   box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
 }
 
+.voiceover-split-button {
+  display: inline-flex;
+  margin: 0 12px;
+  vertical-align: middle;
+  border-radius: 10px;
+  transition:
+    box-shadow 0.3s ease,
+    transform 0.3s ease;
+}
+
+.voiceover-split-button:hover {
+  transform: translateY(-1px);
+  box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
+}
+
+.voiceover-split-button .toolbar-btn {
+  margin: 0;
+  transform: none;
+  box-shadow: none;
+}
+
+.voiceover-main-btn {
+  border-radius: 10px 0 0 10px;
+}
+
+.voiceover-menu-btn {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  width: 40px;
+  padding: 0;
+  margin-left: -2px !important;
+  border-radius: 0 10px 10px 0;
+}
+
+.voiceover-menu-btn::before {
+  position: absolute;
+  left: 0;
+  width: 1px;
+  height: 22px;
+  content: '';
+  background: #e8e8e8;
+}
+
+.voiceover-menu-btn {
+  position: relative;
+}
+
+.voiceover-menu-btn:hover::before,
+.voiceover-menu-btn.active::before {
+  background: rgba(255, 255, 255, 0.45);
+}
+
+.voiceover-menu-btn.active {
+  color: white;
+  background: #409eff;
+  border-color: #409eff;
+}
+
+.voiceover-split-button:focus-within {
+  outline: 2px solid rgba(64, 158, 255, 0.45);
+  outline-offset: 2px;
+}
+
+.voiceover-split-button:has(.toolbar-btn:disabled):hover {
+  transform: none;
+  box-shadow: none;
+}
+
+.dropdown-triangle {
+  width: 0;
+  height: 0;
+  border-top: 6px solid currentColor;
+  border-right: 5px solid transparent;
+  border-left: 5px solid transparent;
+  transition: transform 0.2s ease;
+}
+
+.voiceover-menu-btn.active .dropdown-triangle {
+  transform: rotate(180deg);
+}
+
+:global(.voiceover-type-popper.el-popover) {
+  padding: 12px 14px;
+  border: 1px solid #e4e7ed;
+  border-radius: 10px;
+  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.14);
+}
+
+.voiceover-menu-title {
+  display: flex;
+  justify-content: space-between;
+  padding-bottom: 8px;
+  margin-bottom: 4px;
+  font-size: 13px;
+  font-weight: 600;
+  color: #606266;
+  border-bottom: 1px solid #ebeef5;
+}
+
+.voiceover-type-menu :deep(.el-checkbox-group) {
+  display: flex;
+  flex-direction: column;
+  padding-top: 4px;
+}
+
+.voiceover-type-menu :deep(.el-checkbox) {
+  height: 36px;
+  margin-right: 0;
+}
+
+.voiceover-type-menu :deep(.el-checkbox__label) {
+  color: #303133;
+}
+
+.voiceover-menu-tip {
+  padding-top: 6px;
+  margin-top: 4px;
+  font-size: 12px;
+  color: #e6a23c;
+  border-top: 1px solid #ebeef5;
+}
+
 /* 脚本编辑器 */
 .script-section {
   margin-top: 40px;

+ 1 - 1
src/views/bjdx/course/index.vue

@@ -380,7 +380,7 @@ const resetQuery = () => {
 /** 添加/修改操作 */
 const formRef = ref()
 const openForm = (type: string, id?: number) => {
-  formRef.value.open(type, queryParams.ctTypeNode, id)
+  formRef.value.open(type, queryParams.ctTypeNode, id, queryParams.courseType)
 }
 
 /** 删除按钮操作 */