|
|
@@ -1,13 +1,14 @@
|
|
|
<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 class="main-container">
|
|
|
+ <div class="main-container">
|
|
|
<!-- 左侧步骤指示器 -->
|
|
|
<div class="steps-sidebar">
|
|
|
<div class="steps-container">
|
|
|
@@ -63,13 +64,22 @@
|
|
|
</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>
|
|
|
|
|
|
@@ -77,15 +87,15 @@
|
|
|
<div v-else-if="currentStep === 2" class="step-content">
|
|
|
<!-- 固定顶部的一键操作栏 -->
|
|
|
<div class="editor-toolbar sticky-top">
|
|
|
- <button
|
|
|
- class="toolbar-btn"
|
|
|
+ <button
|
|
|
+ class="toolbar-btn"
|
|
|
@click="generateAllImages"
|
|
|
:disabled="isGeneratingImages || isAnyImageGenerating"
|
|
|
>
|
|
|
{{ isGeneratingImages ? '生成中...' : '一键生成所有背景图' }}
|
|
|
</button>
|
|
|
- <button
|
|
|
- class="toolbar-btn"
|
|
|
+ <button
|
|
|
+ class="toolbar-btn"
|
|
|
@click="generateAllVoiceovers"
|
|
|
:disabled="isGeneratingVoiceovers || isAnyVoiceoverGenerating"
|
|
|
>
|
|
|
@@ -141,7 +151,7 @@
|
|
|
<div class="media-item">
|
|
|
<div class="media-input-group">
|
|
|
<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
|
|
|
v-for="musicType in backgroundMusicTypes"
|
|
|
:key="musicType.id"
|
|
|
@@ -378,27 +388,30 @@
|
|
|
</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>
|
|
|
|
|
|
<script setup>
|
|
|
import { ref, reactive, computed, onMounted, onUnmounted, watch } from 'vue'
|
|
|
+import { Dialog } from '@/components/Dialog'
|
|
|
import { ChatMessageApi } from '@/api/ai/chat/message'
|
|
|
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 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 { useUserStore } from '@/store/modules/user'
|
|
|
|
|
|
// 步骤配置
|
|
|
const steps = [
|
|
|
@@ -408,14 +421,51 @@ const steps = [
|
|
|
]
|
|
|
|
|
|
// 当前步骤
|
|
|
-const currentStep = ref(2)
|
|
|
+const currentStep = ref(1)
|
|
|
|
|
|
// 步骤1数据
|
|
|
-const scriptPrompt = ref('请给我一个讲解万有引力的课程')
|
|
|
-const selectedMainTeacher = ref('牛顿')
|
|
|
+const scriptPrompt = ref('')
|
|
|
+const selectedMainTeacher = ref('')
|
|
|
const selectedAssistants = ref([])
|
|
|
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生成状态
|
|
|
const isGeneratingImages = ref(false)
|
|
|
const isGeneratingVoiceovers = ref(false)
|
|
|
@@ -430,54 +480,60 @@ const backgroundMusicTypes = ref([
|
|
|
|
|
|
// 从localStorage加载脚本数据
|
|
|
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
|
|
|
const saveScriptDataToCache = () => {
|
|
|
try {
|
|
|
- localStorage.setItem('courseScriptData', JSON.stringify(scriptData))
|
|
|
+ const key = `courseScriptData_${userId.value}`
|
|
|
+ localStorage.setItem(key, JSON.stringify(scriptData))
|
|
|
} catch (error) {
|
|
|
console.error('保存缓存数据失败:', error)
|
|
|
}
|
|
|
@@ -485,7 +541,8 @@ const saveScriptDataToCache = () => {
|
|
|
|
|
|
// 清空脚本数据缓存
|
|
|
const clearScriptDataCache = () => {
|
|
|
- localStorage.removeItem('courseScriptData')
|
|
|
+ const key = `courseScriptData_${userId.value}`
|
|
|
+ localStorage.removeItem(key)
|
|
|
}
|
|
|
|
|
|
// 监听脚本数据变化,自动保存到缓存
|
|
|
@@ -493,6 +550,14 @@ watch(() => scriptData, () => {
|
|
|
saveScriptDataToCache()
|
|
|
}, { deep: true })
|
|
|
|
|
|
+// 监听对话框可见性变化,重新设置初始步骤
|
|
|
+watch(() => props.visible, (newVisible) => {
|
|
|
+ if (newVisible) {
|
|
|
+ // 对话框打开时,设置初始步骤
|
|
|
+ currentStep.value = props.initialStep
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
// 音频播放状态
|
|
|
const audioState = reactive({
|
|
|
currentAudio: null,
|
|
|
@@ -538,6 +603,9 @@ const validationMessage = ref('')
|
|
|
// 视频预览状态
|
|
|
const showVideoPreviewModal = ref(false)
|
|
|
|
|
|
+// 全屏状态
|
|
|
+const fullscreen = ref(false)
|
|
|
+
|
|
|
const activeConversationId = ref(null) // 选中的对话编号
|
|
|
const conversationInAbortController = ref() // 对话进行中 abort 控制器(控制 stream 对话)
|
|
|
const conversationInProgress = ref(false) // 对话是否正在进行中。目前只有【发送】消息时,会更新为 true,避免切换对话、删除对话等操作
|
|
|
@@ -619,6 +687,7 @@ const doSendMessageStream = async (conversationId, content) => {
|
|
|
|
|
|
receiveMessageFullText.value += data.receive.content
|
|
|
|
|
|
+ console.log("数据:", receiveMessageFullText.value)
|
|
|
try {
|
|
|
const parsedData = JSON.parse(receiveMessageFullText.value)
|
|
|
scriptDataTemp.value = parsedData
|
|
|
@@ -635,7 +704,10 @@ const doSendMessageStream = async (conversationId, content) => {
|
|
|
() => {
|
|
|
try {
|
|
|
if (receiveMessageFullText.value) {
|
|
|
+ console.log("最终数据:", receiveMessageFullText.value)
|
|
|
const parsedData = JSON.parse(receiveMessageFullText.value)
|
|
|
+ console.log("最终数据--:", parsedData)
|
|
|
+
|
|
|
scriptDataTemp.value = parsedData
|
|
|
Object.assign(scriptData, parsedData)
|
|
|
}
|
|
|
@@ -709,7 +781,7 @@ const generateMedia = async (sectionIndex) => {
|
|
|
media.generating = true
|
|
|
try {
|
|
|
const form = {
|
|
|
- platform: 'DOU_BAO',
|
|
|
+ platform: AiPlatformEnum.DOU_BAO,
|
|
|
modelId: 56,
|
|
|
prompt: media.prompt,
|
|
|
width: 1024,
|
|
|
@@ -937,6 +1009,8 @@ const saveScript = async () => {
|
|
|
return
|
|
|
}
|
|
|
console.log('scriptData===', JSON.stringify(scriptData))
|
|
|
+ emit('save', scriptData)
|
|
|
+ emit('update:visible', false)
|
|
|
}
|
|
|
|
|
|
// 下一步操作
|
|
|
@@ -961,6 +1035,30 @@ const closeVideoPreview = () => {
|
|
|
|
|
|
// 组件挂载时
|
|
|
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 {
|
|
|
const params = {
|
|
|
pageOn: 1,
|
|
|
@@ -971,8 +1069,8 @@ onMounted(async () => {
|
|
|
const { list } = await ChatRoleApi.getMyPage(params)
|
|
|
params.category += "AI"
|
|
|
const { list:listAi } = await ChatRoleApi.getMyPage(params)
|
|
|
- list.push(...listAi)
|
|
|
- digitalHumans.value = list
|
|
|
+ listAi.push(...list)
|
|
|
+ digitalHumans.value = listAi
|
|
|
} catch (error) {
|
|
|
console.error('获取角色数据失败:', error)
|
|
|
}
|
|
|
@@ -982,6 +1080,12 @@ onMounted(async () => {
|
|
|
}, 3000)
|
|
|
})
|
|
|
|
|
|
+// 关闭事件
|
|
|
+const emit = defineEmits(['update:visible', 'save'])
|
|
|
+const handleClose = () => {
|
|
|
+ emit('update:visible', false)
|
|
|
+}
|
|
|
+
|
|
|
// 组件卸载时
|
|
|
onUnmounted(() => {
|
|
|
if (inProgressTimer.value) {
|
|
|
@@ -993,7 +1097,7 @@ onUnmounted(() => {
|
|
|
<style scoped>
|
|
|
.course-script-editor {
|
|
|
width: 100%;
|
|
|
- height: 100vh;
|
|
|
+ height: 90vh;
|
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
@@ -1161,6 +1265,25 @@ onUnmounted(() => {
|
|
|
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 {
|
|
|
position: absolute;
|
|
|
@@ -1346,9 +1469,17 @@ onUnmounted(() => {
|
|
|
margin-bottom: 30px;
|
|
|
}
|
|
|
|
|
|
-.ai-input-container button.generate-btn.primary {
|
|
|
+.ai-input-container .button-group {
|
|
|
+ display: flex;
|
|
|
+ gap: 10px;
|
|
|
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 {
|