Przeglądaj źródła

1、新加课程类型:AI生成课类型
2、调整原有通识课、实操课的所有逻辑,统一调取字典数据管理
3、调整AI生成课数据调通,json格式校验、弹框脚本全屏关闭等逻辑

liyanbo 4 miesięcy temu
rodzic
commit
8130dab000

+ 11 - 0
src/components/Dialog/src/Dialog.vue

@@ -28,8 +28,11 @@ const getBindValue = computed(() => {
 
 
 const isFullscreen = ref(false)
 const isFullscreen = ref(false)
 
 
+const emit = defineEmits(['update:modelValue', 'update:fullscreen'])
+
 const toggleFull = () => {
 const toggleFull = () => {
   isFullscreen.value = !unref(isFullscreen)
   isFullscreen.value = !unref(isFullscreen)
+  emit('update:fullscreen', isFullscreen.value)
 }
 }
 
 
 const dialogHeight = ref(isNumber(props.maxHeight) ? `${props.maxHeight}px` : props.maxHeight)
 const dialogHeight = ref(isNumber(props.maxHeight) ? `${props.maxHeight}px` : props.maxHeight)
@@ -50,6 +53,14 @@ watch(
   }
   }
 )
 )
 
 
+watch(
+  () => props.fullscreen,
+  (val: boolean) => {
+    isFullscreen.value = val
+    emit('update:fullscreen', val)
+  }
+)
+
 const dialogStyle = computed(() => {
 const dialogStyle = computed(() => {
   return {
   return {
     height: unref(dialogHeight)
     height: unref(dialogHeight)

+ 7 - 0
src/types/auto-components.d.ts

@@ -45,13 +45,16 @@ declare module 'vue' {
     Draggable: typeof import('./../components/Draggable/index.vue')['default']
     Draggable: typeof import('./../components/Draggable/index.vue')['default']
     Echart: typeof import('./../components/Echart/src/Echart.vue')['default']
     Echart: typeof import('./../components/Echart/src/Echart.vue')['default']
     Editor: typeof import('./../components/Editor/src/Editor.vue')['default']
     Editor: typeof import('./../components/Editor/src/Editor.vue')['default']
+    ElAside: typeof import('element-plus/es')['ElAside']
     ElAvatar: typeof import('element-plus/es')['ElAvatar']
     ElAvatar: typeof import('element-plus/es')['ElAvatar']
     ElBadge: typeof import('element-plus/es')['ElBadge']
     ElBadge: typeof import('element-plus/es')['ElBadge']
     ElButton: typeof import('element-plus/es')['ElButton']
     ElButton: typeof import('element-plus/es')['ElButton']
     ElCard: typeof import('element-plus/es')['ElCard']
     ElCard: typeof import('element-plus/es')['ElCard']
     ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
     ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
+    ElCheckboxButton: typeof import('element-plus/es')['ElCheckboxButton']
     ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
     ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
     ElCol: typeof import('element-plus/es')['ElCol']
     ElCol: typeof import('element-plus/es')['ElCol']
+    ElContainer: typeof import('element-plus/es')['ElContainer']
     ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
     ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
     ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
     ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
     ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
     ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
@@ -69,14 +72,18 @@ declare module 'vue' {
     ElementOtherConfig: typeof import('./../components/bpmnProcessDesigner/package/penal/other/ElementOtherConfig.vue')['default']
     ElementOtherConfig: typeof import('./../components/bpmnProcessDesigner/package/penal/other/ElementOtherConfig.vue')['default']
     ElementProperties: typeof import('./../components/bpmnProcessDesigner/package/penal/properties/ElementProperties.vue')['default']
     ElementProperties: typeof import('./../components/bpmnProcessDesigner/package/penal/properties/ElementProperties.vue')['default']
     ElementTask: typeof import('./../components/bpmnProcessDesigner/package/penal/task/ElementTask.vue')['default']
     ElementTask: typeof import('./../components/bpmnProcessDesigner/package/penal/task/ElementTask.vue')['default']
+    ElEmpty: typeof import('element-plus/es')['ElEmpty']
+    ElFooter: typeof import('element-plus/es')['ElFooter']
     ElForm: typeof import('element-plus/es')['ElForm']
     ElForm: typeof import('element-plus/es')['ElForm']
     ElFormItem: typeof import('element-plus/es')['ElFormItem']
     ElFormItem: typeof import('element-plus/es')['ElFormItem']
+    ElHeader: typeof import('element-plus/es')['ElHeader']
     ElIcon: typeof import('element-plus/es')['ElIcon']
     ElIcon: typeof import('element-plus/es')['ElIcon']
     ElImage: typeof import('element-plus/es')['ElImage']
     ElImage: typeof import('element-plus/es')['ElImage']
     ElImageViewer: typeof import('element-plus/es')['ElImageViewer']
     ElImageViewer: typeof import('element-plus/es')['ElImageViewer']
     ElInput: typeof import('element-plus/es')['ElInput']
     ElInput: typeof import('element-plus/es')['ElInput']
     ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
     ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
     ElLink: typeof import('element-plus/es')['ElLink']
     ElLink: typeof import('element-plus/es')['ElLink']
+    ElMain: typeof import('element-plus/es')['ElMain']
     ElOption: typeof import('element-plus/es')['ElOption']
     ElOption: typeof import('element-plus/es')['ElOption']
     ElPagination: typeof import('element-plus/es')['ElPagination']
     ElPagination: typeof import('element-plus/es')['ElPagination']
     ElPopover: typeof import('element-plus/es')['ElPopover']
     ElPopover: typeof import('element-plus/es')['ElPopover']

+ 1 - 0
src/utils/dict.ts

@@ -168,6 +168,7 @@ export enum DICT_TYPE {
   COURSE_LABEL = 'bjdx_course_label', // 课程标签
   COURSE_LABEL = 'bjdx_course_label', // 课程标签
   COURSE_COUTNET_TYPE = 'bjdx_course_content_type', // 课程内容类型
   COURSE_COUTNET_TYPE = 'bjdx_course_content_type', // 课程内容类型
   COURSE_QUEST_SHOW_TYPE = 'bjdx_course_quest_show_type', // 课程试题呈现类型
   COURSE_QUEST_SHOW_TYPE = 'bjdx_course_quest_show_type', // 课程试题呈现类型
+  COURSE_TYPE = 'bjdx_course_type', // 课程类型
 
 
   // ========== QUESTIONNAIRE - 评估模块  ==========
   // ========== QUESTIONNAIRE - 评估模块  ==========
   BJDX_QUESTIONNAIRE_STATUS = 'bjdx_questionnaire_status', // 问卷状态
   BJDX_QUESTIONNAIRE_STATUS = 'bjdx_questionnaire_status', // 问卷状态

+ 45 - 14
src/views/bjdx/course/CourseForm.vue

@@ -13,8 +13,10 @@
           placeholder="请选择课程类型"
           placeholder="请选择课程类型"
           @change="handleCtTypeNodeChange"
           @change="handleCtTypeNodeChange"
         >
         >
-          <el-option label="ai通识课" value="1" />
-          <el-option label="ai实操课" value="2" />
+          <el-option v-for="dict in getStrDictOptions(DICT_TYPE.COURSE_TYPE)"
+                     :key="dict.value"
+                     :label="dict.label"
+                     :value="dict.value" />
         </el-select>
         </el-select>
       </el-form-item>
       </el-form-item>
       <el-form-item label="大纲课程" prop="courseType">
       <el-form-item label="大纲课程" prop="courseType">
@@ -85,25 +87,20 @@
       </el-form-item>
       </el-form-item>
 
 
       <!-- 上传进度条 -->
       <!-- 上传进度条 -->
-
       <el-form-item>
       <el-form-item>
         <div v-if="isUploading" class="uploadProgress">
         <div v-if="isUploading" class="uploadProgress">
           <el-progress :percentage="uploadProgress" />
           <el-progress :percentage="uploadProgress" />
           <div class="text-xs text-gray-500 text-right mt-1">{{ uploadProgress }}% 已上传</div>
           <div class="text-xs text-gray-500 text-right mt-1">{{ uploadProgress }}% 已上传</div>
         </div>
         </div>
       </el-form-item>
       </el-form-item>
-      <!--            <el-form-item label="课程大小" prop="courseSize">-->
-      <!--              <el-input-number v-model="formData.courseSize" :min="1" :step="1" step-strictly>-->
-      <!--                <template #suffix><span>MB</span></template>-->
-      <!--              </el-input-number>-->
-      <!--            </el-form-item>-->
-
 
 
-      <template v-if="formData.courseContentType === 'ailab'">
-        <el-form-item >
-          <br/><h2>AI实验室</h2><br/>
-        </el-form-item>
-      </template>
+      <el-form-item v-if="formData.courseContentType === 'ailab'" label="AI生成课" prop="courseContent">
+        <div class="flex items-center">
+          <el-button type="primary" @click="handleAiGenerateClick">{{ isScriptValid ? '编辑脚本' : 'AI创作' }}</el-button>
+          <el-icon v-if="isScriptValid" class="ml-2 text-green-500"><i-ep-check /></el-icon>
+          <span v-if="isScriptValid" class="ml-1 text-green-500 text-sm">脚本校验通过!</span>
+        </div>
+      </el-form-item>
 
 
       <template v-else-if="formData.courseContentType === 'quest'">
       <template v-else-if="formData.courseContentType === 'quest'">
         <el-form-item >
         <el-form-item >
@@ -281,12 +278,18 @@
       <el-button @click="dialogVisible = false">取 消</el-button>
       <el-button @click="dialogVisible = false">取 消</el-button>
     </template>
     </template>
   </Dialog>
   </Dialog>
+
+  <!-- AI 生成课程页面 -->
+  <AiGengrate v-model:visible="showAiGengrate" :initial-step="aiGengrateStep" :initial-script-data="formData.courseContent" @save="handleAiSave" />
 </template>
 </template>
+
 <script setup lang="ts">
 <script setup lang="ts">
+import { computed, ref } from 'vue'
 import { getStrDictOptions, DICT_TYPE } from '@/utils/dict'
 import { getStrDictOptions, DICT_TYPE } from '@/utils/dict'
 import { CourseApi, CourseVO } from '@/api/bjdx/course'
 import { CourseApi, CourseVO } from '@/api/bjdx/course'
 import { defaultProps, handleTree } from '@/utils/tree'
 import { defaultProps, handleTree } from '@/utils/tree'
 import { CourseTypeApi } from '@/api/bjdx/coursetype'
 import { CourseTypeApi } from '@/api/bjdx/coursetype'
+import AiGengrate from './aiGenerate/aiGengrate.vue'
 
 
 /** 课程 表单 */
 /** 课程 表单 */
 defineOptions({ name: 'CourseForm' })
 defineOptions({ name: 'CourseForm' })
@@ -298,6 +301,20 @@ const dialogVisible = ref(false) // 弹窗的是否展示
 const dialogTitle = ref('') // 弹窗的标题
 const dialogTitle = ref('') // 弹窗的标题
 const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
 const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
 const formType = ref('') // 表单的类型:create - 新增;update - 修改
 const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const showAiGengrate = ref(false) // 控制 AI 生成页面的显示
+const aiGengrateStep = ref(1) // 控制 AI 生成页面的初始步骤
+
+// 计算属性:检查 courseContent 的 JSON 脚本是否合规
+const isScriptValid = computed(() => {
+  if (!formData.value.courseContent) return false
+  try {
+    const scriptData = JSON.parse(formData.value.courseContent)
+    // 这里可以根据实际需求添加更详细的验证逻辑
+    return typeof scriptData === 'object' && scriptData !== null
+  } catch (error) {
+    return false
+  }
+})
 const formData = ref({
 const formData = ref({
   id: undefined,
   id: undefined,
   ctTypeNode: undefined,
   ctTypeNode: undefined,
@@ -419,6 +436,20 @@ const handleUploadComplete = () => {
   uploadProgress.value = 100
   uploadProgress.value = 100
 }
 }
 
 
+// 处理 AI 生成课程保存
+const handleAiSave = (scriptData) => {
+  formData.value.courseContent = JSON.stringify(scriptData)
+  formData.value.courseContentType = 'ailab'
+  showAiGengrate.value = false
+}
+
+// 处理 AI 生成按钮点击
+const handleAiGenerateClick = () => {
+  // 根据脚本是否有效设置初始步骤
+  aiGengrateStep.value = isScriptValid.value ? 2 : 1
+  showAiGengrate.value = true
+}
+
 /** 处理课程类型节点变更 */
 /** 处理课程类型节点变更 */
 const handleCtTypeNodeChange = (ctTypeNode) => {
 const handleCtTypeNodeChange = (ctTypeNode) => {
   formData.value.courseType = undefined
   formData.value.courseType = undefined

+ 210 - 79
src/views/bjdx/course/aiGenerate/aiGengrate.vue

@@ -1,13 +1,14 @@
 <template>
 <template>
-  <div class="course-script-editor">
-    <!-- 全屏遮挡层 -->
-    <div v-if="isGenerating" class="loading-overlay">
-      <div class="loading-content">
-        <div class="loading-spinner"></div>
-        <div class="loading-text">生成中...</div>
+  <Dialog :title="'AI生成课程'" v-model="dialogVisible" width="90%" :fullscreen="true" @update:fullscreen="(value) => fullscreen = value">
+    <div class="course-script-editor">
+      <!-- 全屏遮挡层 -->
+      <div v-if="isGenerating" class="loading-overlay">
+        <div class="loading-content">
+          <div class="loading-spinner"></div>
+          <div class="loading-text">生成中...</div>
+        </div>
       </div>
       </div>
-    </div>
-    <div class="main-container">
+      <div class="main-container">
       <!-- 左侧步骤指示器 -->
       <!-- 左侧步骤指示器 -->
       <div class="steps-sidebar">
       <div class="steps-sidebar">
         <div class="steps-container">
         <div class="steps-container">
@@ -63,13 +64,22 @@
               </div>
               </div>
             </div>
             </div>
 
 
-            <button
-              class="generate-btn primary"
-              :disabled="!scriptPrompt || !selectedMainTeacher || isGenerating"
-              @click="generateScript"
-            >
-              {{ isGenerating ? '生成中...' : '生成课程脚本' }}
-            </button>
+            <div class="button-group">
+              <button
+                class="generate-btn primary"
+                :disabled="!scriptPrompt || !selectedMainTeacher || isGenerating"
+                @click="generateScript"
+              >
+                {{ isGenerating ? '生成中...' : '生成课程脚本' }}
+              </button>
+              <button
+                v-if="hasDraftCache"
+                class="generate-btn secondary"
+                @click="currentStep = 2"
+              >
+                查看草稿
+              </button>
+            </div>
           </div>
           </div>
         </div>
         </div>
 
 
@@ -77,15 +87,15 @@
         <div v-else-if="currentStep === 2" class="step-content">
         <div v-else-if="currentStep === 2" class="step-content">
           <!-- 固定顶部的一键操作栏 -->
           <!-- 固定顶部的一键操作栏 -->
           <div class="editor-toolbar sticky-top">
           <div class="editor-toolbar sticky-top">
-            <button 
-              class="toolbar-btn" 
+            <button
+              class="toolbar-btn"
               @click="generateAllImages"
               @click="generateAllImages"
               :disabled="isGeneratingImages || isAnyImageGenerating"
               :disabled="isGeneratingImages || isAnyImageGenerating"
             >
             >
               {{ isGeneratingImages ? '生成中...' : '一键生成所有背景图' }}
               {{ isGeneratingImages ? '生成中...' : '一键生成所有背景图' }}
             </button>
             </button>
-            <button 
-              class="toolbar-btn" 
+            <button
+              class="toolbar-btn"
               @click="generateAllVoiceovers"
               @click="generateAllVoiceovers"
               :disabled="isGeneratingVoiceovers || isAnyVoiceoverGenerating"
               :disabled="isGeneratingVoiceovers || isAnyVoiceoverGenerating"
             >
             >
@@ -141,7 +151,7 @@
                 <div class="media-item">
                 <div class="media-item">
                   <div class="media-input-group">
                   <div class="media-input-group">
                     <span class="media-label">背景音</span>
                     <span class="media-label">背景音</span>
-                    <el-select v-model="section.backgroundAudio.type" placeholder="选择背景音" style="width: 240px" clearable size="large" @change="(value) => handleBackgroundAudioChange(value, section)">
+                    <el-select v-model="section.backgroundAudio.type" placeholder="选择背景音" :style="{ width: fullscreen ? '240px' : '130px' }" clearable size="large" @change="(value) => handleBackgroundAudioChange(value, section)">
                       <el-option
                       <el-option
                         v-for="musicType in backgroundMusicTypes"
                         v-for="musicType in backgroundMusicTypes"
                         :key="musicType.id"
                         :key="musicType.id"
@@ -378,27 +388,30 @@
         </div>
         </div>
       </div>
       </div>
     </div>
     </div>
-  </div>
-
-  <!-- 视频预览模态框 -->
-  <VideoPreview
-    :visible="showVideoPreviewModal"
-    :script-data="scriptData"
-    :script-roles="digitalHumans"
-    @close="closeVideoPreview"
-  />
+
+      <!-- 视频预览模态框 -->
+      <VideoPreview
+      :visible="showVideoPreviewModal"
+      :script-data="scriptData"
+      :script-roles="digitalHumans"
+      @close="closeVideoPreview"
+    />
+    </div>
+  </Dialog>
 </template>
 </template>
 
 
 <script setup>
 <script setup>
 import { ref, reactive, computed, onMounted, onUnmounted, watch } from 'vue'
 import { ref, reactive, computed, onMounted, onUnmounted, watch } from 'vue'
+import { Dialog } from '@/components/Dialog'
 import { ChatMessageApi } from '@/api/ai/chat/message'
 import { ChatMessageApi } from '@/api/ai/chat/message'
 import { ChatConversationApi } from '@/api/ai/chat/conversation'
 import { ChatConversationApi } from '@/api/ai/chat/conversation'
 import { ChatRoleApi } from '@/api/ai/model/chatRole'
 import { ChatRoleApi } from '@/api/ai/model/chatRole'
 import { ImageApi } from '@/api/ai/image'
 import { ImageApi } from '@/api/ai/image'
 import { TtsApi } from '@/api/ai/tts'
 import { TtsApi } from '@/api/ai/tts'
 import VideoPreview from '@/views/bjdx/course/aiGenerate/VideoPreview.vue'
 import VideoPreview from '@/views/bjdx/course/aiGenerate/VideoPreview.vue'
-import { AiImageStatusEnum } from '@/views/ai/utils/constants'
+import { AiImageStatusEnum, AiPlatformEnum } from '@/views/ai/utils/constants'
 import { marked } from 'marked'
 import { marked } from 'marked'
+import { useUserStore } from '@/store/modules/user'
 
 
 // 步骤配置
 // 步骤配置
 const steps = [
 const steps = [
@@ -408,14 +421,51 @@ const steps = [
 ]
 ]
 
 
 // 当前步骤
 // 当前步骤
-const currentStep = ref(2)
+const currentStep = ref(1)
 
 
 // 步骤1数据
 // 步骤1数据
-const scriptPrompt = ref('请给我一个讲解万有引力的课程')
-const selectedMainTeacher = ref('牛顿')
+const scriptPrompt = ref('')
+const selectedMainTeacher = ref('')
 const selectedAssistants = ref([])
 const selectedAssistants = ref([])
 const isGenerating = ref(false)
 const isGenerating = ref(false)
 
 
+// 对话框可见性
+const props = defineProps({
+  visible: {
+    type: Boolean,
+    default: false
+  },
+  initialStep: {
+    type: Number,
+    default: 1
+  },
+  initialScriptData: {
+    type: String,
+    default: ''
+  }
+})
+
+const dialogVisible = computed({
+  get: () => props.visible,
+  set: (value) => emit('update:visible', value)
+})
+
+// 获取用户信息
+const userStore = useUserStore()
+const userId = computed(() => userStore.getUser.id)
+
+// 检查是否存在草稿缓存
+const hasDraftCache = computed(() => {
+  try {
+    const key = `courseScriptData_${userId.value}`
+    const cachedData = localStorage.getItem(key)
+    return !!cachedData
+  } catch (error) {
+    console.error('检查草稿缓存失败:', error)
+    return false
+  }
+})
+
 // 步骤2生成状态
 // 步骤2生成状态
 const isGeneratingImages = ref(false)
 const isGeneratingImages = ref(false)
 const isGeneratingVoiceovers = ref(false)
 const isGeneratingVoiceovers = ref(false)
@@ -430,54 +480,60 @@ const backgroundMusicTypes = ref([
 
 
 // 从localStorage加载脚本数据
 // 从localStorage加载脚本数据
 const loadScriptDataFromCache = () => {
 const loadScriptDataFromCache = () => {
-  const cachedData = localStorage.getItem('courseScriptData')
-  if (cachedData) {
-    try {
-      return JSON.parse(cachedData)
-    } catch (error) {
-      console.error('解析缓存数据失败:', error)
-    }
-  }
-  return {
-    title: '',
-    sections: [
-      {
-        name: '环节一',
-        backgroundImage: {
-          prompt: '',
-          url: '',
-          generating: false
-        },
-        backgroundAudio: {
-          type: '',
-          url: ''
-        },
-        dialogues: [
-          {
-            type: 'digital',
-            roleName: '',
-            content: '',
-            voiceoverUrl: '',
-            generatingVoiceover: false
-          },
-          {
-            type: 'user',
-            roleName: '',
-            content: '',
-          }
-        ]
+  try {
+    const key = `courseScriptData_${userId.value}`
+    const cachedData = localStorage.getItem(key)
+    if (cachedData) {
+      try {
+        return JSON.parse(cachedData)
+      } catch (error) {
+        console.error('解析缓存数据失败:', error)
       }
       }
-    ]
+    }
+  } catch (error) {
+    console.error('获取用户信息失败:', error)
   }
   }
+  return null
 }
 }
 
 
 // 脚本数据结构
 // 脚本数据结构
-const scriptData = reactive(loadScriptDataFromCache())
+const scriptData = reactive(loadScriptDataFromCache() || {
+  title: '',
+  sections: [
+    {
+      name: '环节一',
+      backgroundImage: {
+        prompt: '',
+        url: '',
+        generating: false
+      },
+      backgroundAudio: {
+        type: '',
+        url: ''
+      },
+      dialogues: [
+        {
+          type: 'digital',
+          roleName: '',
+          content: '',
+          voiceoverUrl: '',
+          generatingVoiceover: false
+        },
+        {
+          type: 'user',
+          roleName: '',
+          content: '',
+        }
+      ]
+    }
+  ]
+})
 
 
 // 保存脚本数据到localStorage
 // 保存脚本数据到localStorage
 const saveScriptDataToCache = () => {
 const saveScriptDataToCache = () => {
   try {
   try {
-    localStorage.setItem('courseScriptData', JSON.stringify(scriptData))
+    const key = `courseScriptData_${userId.value}`
+    localStorage.setItem(key, JSON.stringify(scriptData))
   } catch (error) {
   } catch (error) {
     console.error('保存缓存数据失败:', error)
     console.error('保存缓存数据失败:', error)
   }
   }
@@ -485,7 +541,8 @@ const saveScriptDataToCache = () => {
 
 
 // 清空脚本数据缓存
 // 清空脚本数据缓存
 const clearScriptDataCache = () => {
 const clearScriptDataCache = () => {
-  localStorage.removeItem('courseScriptData')
+  const key = `courseScriptData_${userId.value}`
+  localStorage.removeItem(key)
 }
 }
 
 
 // 监听脚本数据变化,自动保存到缓存
 // 监听脚本数据变化,自动保存到缓存
@@ -493,6 +550,14 @@ watch(() => scriptData, () => {
   saveScriptDataToCache()
   saveScriptDataToCache()
 }, { deep: true })
 }, { deep: true })
 
 
+// 监听对话框可见性变化,重新设置初始步骤
+watch(() => props.visible, (newVisible) => {
+  if (newVisible) {
+    // 对话框打开时,设置初始步骤
+    currentStep.value = props.initialStep
+  }
+})
+
 // 音频播放状态
 // 音频播放状态
 const audioState = reactive({
 const audioState = reactive({
   currentAudio: null,
   currentAudio: null,
@@ -538,6 +603,9 @@ const validationMessage = ref('')
 // 视频预览状态
 // 视频预览状态
 const showVideoPreviewModal = ref(false)
 const showVideoPreviewModal = ref(false)
 
 
+// 全屏状态
+const fullscreen = ref(false)
+
 const activeConversationId = ref(null) // 选中的对话编号
 const activeConversationId = ref(null) // 选中的对话编号
 const conversationInAbortController = ref() // 对话进行中 abort 控制器(控制 stream 对话)
 const conversationInAbortController = ref() // 对话进行中 abort 控制器(控制 stream 对话)
 const conversationInProgress = ref(false) // 对话是否正在进行中。目前只有【发送】消息时,会更新为 true,避免切换对话、删除对话等操作
 const conversationInProgress = ref(false) // 对话是否正在进行中。目前只有【发送】消息时,会更新为 true,避免切换对话、删除对话等操作
@@ -619,6 +687,7 @@ const doSendMessageStream = async (conversationId, content) => {
 
 
           receiveMessageFullText.value += data.receive.content
           receiveMessageFullText.value += data.receive.content
 
 
+          console.log("数据:", receiveMessageFullText.value)
           try {
           try {
             const parsedData = JSON.parse(receiveMessageFullText.value)
             const parsedData = JSON.parse(receiveMessageFullText.value)
             scriptDataTemp.value = parsedData
             scriptDataTemp.value = parsedData
@@ -635,7 +704,10 @@ const doSendMessageStream = async (conversationId, content) => {
       () => {
       () => {
         try {
         try {
           if (receiveMessageFullText.value) {
           if (receiveMessageFullText.value) {
+            console.log("最终数据:", receiveMessageFullText.value)
             const parsedData = JSON.parse(receiveMessageFullText.value)
             const parsedData = JSON.parse(receiveMessageFullText.value)
+            console.log("最终数据--:", parsedData)
+
             scriptDataTemp.value = parsedData
             scriptDataTemp.value = parsedData
             Object.assign(scriptData, parsedData)
             Object.assign(scriptData, parsedData)
           }
           }
@@ -709,7 +781,7 @@ const generateMedia = async (sectionIndex) => {
   media.generating = true
   media.generating = true
   try {
   try {
     const form = {
     const form = {
-      platform: 'DOU_BAO',
+      platform: AiPlatformEnum.DOU_BAO,
       modelId: 56,
       modelId: 56,
       prompt: media.prompt,
       prompt: media.prompt,
       width: 1024,
       width: 1024,
@@ -937,6 +1009,8 @@ const saveScript = async () => {
     return
     return
   }
   }
   console.log('scriptData===', JSON.stringify(scriptData))
   console.log('scriptData===', JSON.stringify(scriptData))
+  emit('save', scriptData)
+  emit('update:visible', false)
 }
 }
 
 
 // 下一步操作
 // 下一步操作
@@ -961,6 +1035,30 @@ const closeVideoPreview = () => {
 
 
 // 组件挂载时
 // 组件挂载时
 onMounted(async () => {
 onMounted(async () => {
+  // 处理脚本数据
+  if (props.initialScriptData) {
+    try {
+      const parsedData = JSON.parse(props.initialScriptData)
+      Object.assign(scriptData, parsedData)
+    } catch (error) {
+      console.error('解析脚本数据失败:', error)
+      // 解析失败,尝试加载草稿
+      const cachedData = loadScriptDataFromCache()
+      if (cachedData) {
+        Object.assign(scriptData, cachedData)
+      }
+    }
+  } else {
+    // 没有脚本数据,尝试加载草稿
+    const cachedData = loadScriptDataFromCache()
+    if (cachedData) {
+      Object.assign(scriptData, cachedData)
+    }
+  }
+  
+  // 设置初始步骤
+  currentStep.value = props.initialStep
+  
   try {
   try {
     const params = {
     const params = {
       pageOn: 1,
       pageOn: 1,
@@ -971,8 +1069,8 @@ onMounted(async () => {
     const { list } = await ChatRoleApi.getMyPage(params)
     const { list } = await ChatRoleApi.getMyPage(params)
     params.category += "AI"
     params.category += "AI"
     const { list:listAi } = await ChatRoleApi.getMyPage(params)
     const { list:listAi } = await ChatRoleApi.getMyPage(params)
-    list.push(...listAi)
-    digitalHumans.value = list
+    listAi.push(...list)
+    digitalHumans.value = listAi
   } catch (error) {
   } catch (error) {
     console.error('获取角色数据失败:', error)
     console.error('获取角色数据失败:', error)
   }
   }
@@ -982,6 +1080,12 @@ onMounted(async () => {
   }, 3000)
   }, 3000)
 })
 })
 
 
+// 关闭事件
+const emit = defineEmits(['update:visible', 'save'])
+const handleClose = () => {
+  emit('update:visible', false)
+}
+
 // 组件卸载时
 // 组件卸载时
 onUnmounted(() => {
 onUnmounted(() => {
   if (inProgressTimer.value) {
   if (inProgressTimer.value) {
@@ -993,7 +1097,7 @@ onUnmounted(() => {
 <style scoped>
 <style scoped>
 .course-script-editor {
 .course-script-editor {
   width: 100%;
   width: 100%;
-  height: 100vh;
+  height: 90vh;
   font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
   font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
   display: flex;
   display: flex;
   flex-direction: column;
   flex-direction: column;
@@ -1161,6 +1265,25 @@ onUnmounted(() => {
   box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
   box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
 }
 }
 
 
+/* 按钮组样式 */
+.button-group {
+  display: flex;
+  gap: 10px;
+  margin-top: 20px;
+  justify-content: center;
+}
+
+/* 次要按钮样式 */
+.generate-btn.secondary {
+  background-color: #606266;
+  color: white;
+}
+
+.generate-btn.secondary:hover {
+  background-color: #ffaa72;
+  color: white;
+}
+
 /* 对话类型标签 */
 /* 对话类型标签 */
 .dialogue-type-tag {
 .dialogue-type-tag {
   position: absolute;
   position: absolute;
@@ -1346,9 +1469,17 @@ onUnmounted(() => {
   margin-bottom: 30px;
   margin-bottom: 30px;
 }
 }
 
 
-.ai-input-container button.generate-btn.primary {
+.ai-input-container .button-group {
+  display: flex;
+  gap: 10px;
   margin-top: 30px;
   margin-top: 30px;
-  align-self: center;
+  justify-content: center;
+}
+
+.ai-input-container .button-group button {
+  flex: 1;
+  min-width: 200px;
+  text-align: center;
 }
 }
 
 
 .ai-input-wrapper {
 .ai-input-wrapper {

+ 15 - 3
src/views/bjdx/course/index.vue

@@ -12,8 +12,11 @@
               class="!w-full"
               class="!w-full"
               @change="handleCtTypeNodeChange"
               @change="handleCtTypeNodeChange"
             >
             >
-              <el-option label="ai通识课" value="1" />
-              <el-option label="ai实操课" value="2" />
+              <el-option v-for="courseType in courseTypeList"
+                :key="courseType.id"
+                :label="courseType.name"
+                :value="courseType.id"
+              />
             </el-select>
             </el-select>
           </el-form-item>
           </el-form-item>
         </div>
         </div>
@@ -277,6 +280,14 @@ const previewRef = ref()
 const courseTypeTree = ref() // 树形结构
 const courseTypeTree = ref() // 树形结构
 const treeSearchValue = ref('') // 树形搜索值
 const treeSearchValue = ref('') // 树形搜索值
 
 
+// 从字典获取课程类型列表
+const courseTypeList = computed(() => {
+  return getStrDictOptions(DICT_TYPE.COURSE_TYPE).map(dict => ({
+    id: dict.value,
+    name: dict.label
+  }))
+})
+
 /** 打开预览弹窗 */
 /** 打开预览弹窗 */
 const openPreview = (row) => {
 const openPreview = (row) => {
   // 使用$nextTick确保组件挂载后调用
   // 使用$nextTick确保组件挂载后调用
@@ -418,7 +429,8 @@ const getCourseTypeTree = async (filterCtTypeNode = "1") => {
   if (filterCtTypeNode !== undefined) {
   if (filterCtTypeNode !== undefined) {
     filteredData = data.filter(item => item.ctTypeNode === '0' || item.ctTypeNode === filterCtTypeNode)
     filteredData = data.filter(item => item.ctTypeNode === '0' || item.ctTypeNode === filterCtTypeNode)
   }
   }
-  const root = { id: 0, ctType: '课程类型' + (filterCtTypeNode === "1" ? '(ai通识课)' : '(ai实操课)'), children: [] }
+  const courseTypeItem = courseTypeList.value.find(item => item.id === filterCtTypeNode)
+  const root = { id: 0, ctType: '课程类型(' + courseTypeItem.name + ')', children: [] }
   root.children = handleTree(filteredData, 'id', 'ctParentId')
   root.children = handleTree(filteredData, 'id', 'ctParentId')
   courseTypeTree.value = [root]
   courseTypeTree.value = [root]
 }
 }

+ 19 - 11
src/views/bjdx/coursetype/CourseTypeForm.vue

@@ -13,7 +13,7 @@
           v-model="formData.ctParentId"
           v-model="formData.ctParentId"
           :data="courseTypeTree"
           :data="courseTypeTree"
           :props="{...defaultProps,
           :props="{...defaultProps,
-            label: (node) => `${node.ctType}${node.ctTypeNode === '0' ? '(年级)' : node.ctTypeNode === '1' ? '(ai通识课)' :  node.ctTypeNode === '2' ?'(ai实操课)' : ''}`,
+            label: (node) => `${node.ctType}${node.ctTypeNode === '0' ? '(年级)' : `(${getDictObj(DICT_TYPE.COURSE_TYPE, node.ctTypeNode)?.label || '顶节点'})`}`,
             disabled: (node) => node.id !== 0 && node.ctTypeNode !== '0'
             disabled: (node) => node.id !== 0 && node.ctTypeNode !== '0'
             }"
             }"
           :default-expanded-keys="[0]"
           :default-expanded-keys="[0]"
@@ -43,7 +43,7 @@
         <el-segmented
         <el-segmented
           :disabled="formType === 'update'"
           :disabled="formType === 'update'"
           v-model="formData.ctTypeNode"
           v-model="formData.ctTypeNode"
-          :options="segmentedOptions"
+          :options="courseTypeOptions"
         />
         />
       </el-form-item>
       </el-form-item>
       <el-form-item label="课程类型名称" prop="ctType">
       <el-form-item label="课程类型名称" prop="ctType">
@@ -96,8 +96,10 @@
           class="!w-240px"
           class="!w-240px"
           @change="searchHistoryCourseTypes"
           @change="searchHistoryCourseTypes"
         >
         >
-          <el-option label="ai通识课" value="1" />
-          <el-option label="ai实操课" value="2" />
+          <el-option v-for="courseType in courseTypeOptions"
+                     :key="courseType.value"
+                     :label="courseType.label"
+                     :value="courseType.value" />
         </el-select>
         </el-select>
       </el-form-item>
       </el-form-item>
       <el-form-item>
       <el-form-item>
@@ -123,8 +125,7 @@
       <el-table-column label="节点类型" align="center" prop="ctTypeNode">
       <el-table-column label="节点类型" align="center" prop="ctTypeNode">
         <template #default="scope">
         <template #default="scope">
           <el-tag type="info" v-if="scope.row.ctTypeNode === '0'">年级</el-tag>
           <el-tag type="info" v-if="scope.row.ctTypeNode === '0'">年级</el-tag>
-          <el-tag type="warning" v-else-if="scope.row.ctTypeNode === '1'">ai通识课</el-tag>
-          <el-tag type="warning" v-else>ai实操课</el-tag>
+          <dict-tag :type="DICT_TYPE.COURSE_TYPE" :value="scope.row.ctTypeNode" />
         </template>
         </template>
       </el-table-column>
       </el-table-column>
       <el-table-column label="封面" align="center" prop="ctTypeImage">
       <el-table-column label="封面" align="center" prop="ctTypeImage">
@@ -152,6 +153,7 @@ import { CourseTypeApi, CourseTypeVO } from '@/api/bjdx/coursetype'
 import { defaultProps, handleTree } from '@/utils/tree'
 import { defaultProps, handleTree } from '@/utils/tree'
 import { Search, Refresh } from '@element-plus/icons-vue'
 import { Search, Refresh } from '@element-plus/icons-vue'
 import { ElMessage } from 'element-plus'
 import { ElMessage } from 'element-plus'
+import { getStrDictOptions, getDictObj, DICT_TYPE } from '@/utils/dict'
 
 
 /** 课程-类型 表单 */
 /** 课程-类型 表单 */
 defineOptions({ name: 'CourseTypeForm' })
 defineOptions({ name: 'CourseTypeForm' })
@@ -191,6 +193,11 @@ const historyCourseFormData = reactive({
 const selectedHistoryCourseType = ref<CourseTypeVO>() // 选中的历史课程类型
 const selectedHistoryCourseType = ref<CourseTypeVO>() // 选中的历史课程类型
 const importedCourseTypeInfo = ref('') // 引入的课程类型信息
 const importedCourseTypeInfo = ref('') // 引入的课程类型信息
 
 
+// 课程类型选项(从字典获取)
+const courseTypeOptions = computed(() => {
+  return getStrDictOptions(DICT_TYPE.COURSE_TYPE)
+})
+
 // 开关状态变量
 // 开关状态变量
 const switchValue = ref(false)
 const switchValue = ref(false)
 // 计算属性:控制是否复制所有小节的开关状态
 // 计算属性:控制是否复制所有小节的开关状态
@@ -226,11 +233,12 @@ const isRootParent = computed(() => {
 
 
 // 分段控件选项(根据父级ID动态配置禁用状态)
 // 分段控件选项(根据父级ID动态配置禁用状态)
 const segmentedOptions = computed(() => {
 const segmentedOptions = computed(() => {
-  return [
-    { label: '年级', value: '0', disabled: !isRootParent.value },  // 非根节点时禁用年级
-    { label: '通识课', value: '1', disabled: isRootParent.value },  // 根节点时禁用通识课
-    { label: '实操课', value: '2', disabled: isRootParent.value },  // 根节点时禁用实操课
-  ]
+  const dictOptions = getStrDictOptions(DICT_TYPE.COURSE_TYPE)
+  return dictOptions.map(dict => ({
+    label: dict.label,
+    value: dict.value,
+    disabled: dict.value === '0' ? !isRootParent.value : isRootParent.value
+  }))
 })
 })
 
 
 // 处理父级类型变更
 // 处理父级类型变更

+ 2 - 5
src/views/bjdx/coursetype/index.vue

@@ -25,9 +25,7 @@
           class="!w-240px"
           class="!w-240px"
           @change="handleQuery"
           @change="handleQuery"
         >
         >
-          <el-option label="年级" value="0" />
-          <el-option label="ai通识课" value="1" />
-          <el-option label="ai实操课" value="2" />
+          <el-option v-for="dict in getStrDictOptions(DICT_TYPE.COURSE_TYPE)" :key="dict.value" :label="dict.label" :value="dict.value" />
         </el-select>
         </el-select>
       </el-form-item>
       </el-form-item>
       <el-form-item label="课程类型描述" prop="ctTypeDescribe">
       <el-form-item label="课程类型描述" prop="ctTypeDescribe">
@@ -83,8 +81,7 @@
       <el-table-column label="节点类型" align="center" prop="ctTypeNode" >
       <el-table-column label="节点类型" align="center" prop="ctTypeNode" >
         <template #default="scope">
         <template #default="scope">
           <el-tag type="info" v-if="scope.row.ctTypeNode === '0'">年级</el-tag>
           <el-tag type="info" v-if="scope.row.ctTypeNode === '0'">年级</el-tag>
-          <el-tag type="warning" v-else-if="scope.row.ctTypeNode === '1'">ai通识课</el-tag>
-          <el-tag type="warning" v-else>ai实操课</el-tag>
+          <dict-tag :type="DICT_TYPE.COURSE_TYPE" :value="scope.row.ctTypeNode" />
         </template>
         </template>
       </el-table-column>
       </el-table-column>
       <el-table-column label="封面" align="center" prop="ctTypeImage" >
       <el-table-column label="封面" align="center" prop="ctTypeImage" >