|
|
@@ -169,73 +169,119 @@
|
|
|
<button class="remove-section-btn" @click="removeSection(sectionIndex)">×</button>
|
|
|
</div>
|
|
|
<div class="media-controls">
|
|
|
- <div class="media-item">
|
|
|
- <div class="media-input-group">
|
|
|
- <span class="media-label">背景图</span>
|
|
|
- <el-input
|
|
|
- v-model="section.backgroundImage.prompt"
|
|
|
- type="textarea"
|
|
|
- :autosize="{ minRows: 2, maxRows: 4 }"
|
|
|
- placeholder="描述词"
|
|
|
- class="media-prompt"
|
|
|
- />
|
|
|
- <el-button
|
|
|
- type="primary"
|
|
|
- size="small"
|
|
|
- :loading="section.backgroundImage.generating"
|
|
|
- :disabled="
|
|
|
- !section.backgroundImage.prompt || section.backgroundImage.generating
|
|
|
- "
|
|
|
- @click="generateMedia(sectionIndex)"
|
|
|
- class="generate-btn"
|
|
|
- >
|
|
|
- {{
|
|
|
- section.backgroundImage.generating
|
|
|
- ? '生成中...'
|
|
|
- : section.backgroundImage.url
|
|
|
- ? '重新生成'
|
|
|
- : '生成'
|
|
|
- }}
|
|
|
- </el-button>
|
|
|
- </div>
|
|
|
- <div v-if="section.backgroundImage.url" class="media-preview">
|
|
|
- <img :src="section.backgroundImage.url" alt="背景图预览" />
|
|
|
+ <!-- 背景类型切换 -->
|
|
|
+ <el-radio-group v-model="section.backgroundType" class="background-type-switch">
|
|
|
+ <el-radio-button label="imageAudio">图音背景</el-radio-button>
|
|
|
+ <el-radio-button label="video">视频背景</el-radio-button>
|
|
|
+ </el-radio-group>
|
|
|
+
|
|
|
+ <!-- 图音背景 -->
|
|
|
+ <template v-if="section.backgroundType === 'imageAudio'">
|
|
|
+ <div class="media-item">
|
|
|
+ <div class="media-input-group">
|
|
|
+ <span class="media-label">背景图</span>
|
|
|
+ <el-input
|
|
|
+ v-model="section.backgroundImage.prompt"
|
|
|
+ type="textarea"
|
|
|
+ :autosize="{ minRows: 2, maxRows: 4 }"
|
|
|
+ placeholder="描述词"
|
|
|
+ class="media-prompt"
|
|
|
+ />
|
|
|
+ <el-button
|
|
|
+ type="primary"
|
|
|
+ size="small"
|
|
|
+ :loading="section.backgroundImage.generating"
|
|
|
+ :disabled="
|
|
|
+ !section.backgroundImage.prompt || section.backgroundImage.generating
|
|
|
+ "
|
|
|
+ @click="generateMedia(sectionIndex)"
|
|
|
+ class="generate-btn"
|
|
|
+ >
|
|
|
+ {{
|
|
|
+ section.backgroundImage.generating
|
|
|
+ ? '生成中...'
|
|
|
+ : section.backgroundImage.url
|
|
|
+ ? '重新生成'
|
|
|
+ : '生成'
|
|
|
+ }}
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ <div v-if="section.backgroundImage.url" class="media-preview">
|
|
|
+ <img :src="section.backgroundImage.url" alt="背景图预览" />
|
|
|
+ </div>
|
|
|
</div>
|
|
|
- </div>
|
|
|
|
|
|
- <div class="media-item">
|
|
|
- <div class="media-input-group">
|
|
|
- <span class="media-label">背景音</span>
|
|
|
- <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"
|
|
|
- :label="musicType.name"
|
|
|
- :value="musicType.id"
|
|
|
+ <div class="media-item">
|
|
|
+ <div class="media-input-group">
|
|
|
+ <span class="media-label">背景音</span>
|
|
|
+ <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"
|
|
|
+ :label="musicType.name"
|
|
|
+ :value="musicType.id"
|
|
|
+ />
|
|
|
+ </el-select>
|
|
|
+ <button
|
|
|
+ v-if="section.backgroundAudio.type"
|
|
|
+ class="play-btn small"
|
|
|
+ @click="playBackgroundAudio(section.backgroundAudio.type)"
|
|
|
+ >
|
|
|
+ <span class="play-icon">{{
|
|
|
+ audioState.isPlaying &&
|
|
|
+ audioState.currentType === 'background' &&
|
|
|
+ audioState.currentUrl === section.backgroundAudio.url
|
|
|
+ ? '⏸'
|
|
|
+ : '▶'
|
|
|
+ }}</span>
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <!-- 视频背景 -->
|
|
|
+ <template v-else-if="section.backgroundType === 'video'">
|
|
|
+ <div class="media-item">
|
|
|
+ <div class="media-input-group">
|
|
|
+ <span class="media-label">视频提示词</span>
|
|
|
+ <el-input
|
|
|
+ v-model="section.backgroundVideo.prompt"
|
|
|
+ type="textarea"
|
|
|
+ :autosize="{ minRows: 2, maxRows: 4 }"
|
|
|
+ placeholder="描述词"
|
|
|
+ class="media-prompt-video"
|
|
|
/>
|
|
|
- </el-select>
|
|
|
- <button
|
|
|
- v-if="section.backgroundAudio.type"
|
|
|
- class="play-btn small"
|
|
|
- @click="playBackgroundAudio(section.backgroundAudio.type)"
|
|
|
- >
|
|
|
- <span class="play-icon">{{
|
|
|
- audioState.isPlaying &&
|
|
|
- audioState.currentType === 'background' &&
|
|
|
- audioState.currentUrl === section.backgroundAudio.url
|
|
|
- ? '⏸'
|
|
|
- : '▶'
|
|
|
- }}</span>
|
|
|
- </button>
|
|
|
+ <el-button
|
|
|
+ type="primary"
|
|
|
+ size="small"
|
|
|
+ :loading="section.backgroundVideo.generating"
|
|
|
+ :disabled="
|
|
|
+ !section.backgroundVideo.prompt || section.backgroundVideo.generating
|
|
|
+ "
|
|
|
+ @click="generateVideo(sectionIndex)"
|
|
|
+ class="generate-btn"
|
|
|
+ >
|
|
|
+ {{
|
|
|
+ section.backgroundVideo.generating
|
|
|
+ ? '生成中...'
|
|
|
+ : section.backgroundVideo.url
|
|
|
+ ? '重新生成'
|
|
|
+ : '生成'
|
|
|
+ }}
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ <div v-if="section.backgroundVideo.url" class="media-preview">
|
|
|
+ <video :src="section.backgroundVideo.url" alt="视频预览" controls ></video>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
- </div>
|
|
|
+ </template>
|
|
|
</div>
|
|
|
|
|
|
<div class="dialogues-container">
|
|
|
@@ -247,14 +293,16 @@
|
|
|
>
|
|
|
<div class="dialogue-header">
|
|
|
<div class="dialogue-type-tag" :class="dialogue.type">
|
|
|
- {{
|
|
|
+ {{
|
|
|
dialogue.type === 'digital'
|
|
|
? '数字人'
|
|
|
: dialogue.type === 'user'
|
|
|
? '用户'
|
|
|
: dialogue.type === 'quest'
|
|
|
? '提问'
|
|
|
- : '诗词'
|
|
|
+ : dialogue.type === 'poem'
|
|
|
+ ? '诗词'
|
|
|
+ : '视频'
|
|
|
}}
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -535,6 +583,46 @@
|
|
|
</div>
|
|
|
</div>
|
|
|
</template>
|
|
|
+
|
|
|
+ <!-- 视频 -->
|
|
|
+ <template v-else-if="dialogue.type === 'video'">
|
|
|
+ <div class="media-input-group">
|
|
|
+ <span class="media-label">视频提示词</span>
|
|
|
+ <el-input
|
|
|
+ v-model="dialogue.videoPrompt"
|
|
|
+ type="textarea"
|
|
|
+ :autosize="{ minRows: 2, maxRows: 4 }"
|
|
|
+ placeholder="描述词"
|
|
|
+ class="media-prompt-video"
|
|
|
+ />
|
|
|
+ <div v-if="dialogue.videoUrl" class="media-preview">
|
|
|
+ <video :src="dialogue.videoUrl" alt="视频预览" controls></video>
|
|
|
+ </div>
|
|
|
+ <el-button
|
|
|
+ type="primary"
|
|
|
+ size="small"
|
|
|
+ :loading="dialogue.generatingVideo"
|
|
|
+ :disabled="
|
|
|
+ !dialogue.videoPrompt
|
|
|
+ "
|
|
|
+ @click="generateDialogueVideo(sectionIndex, dialogueIndex)"
|
|
|
+ class="generate-btn"
|
|
|
+ >
|
|
|
+ {{
|
|
|
+ dialogue.generatingVideo
|
|
|
+ ? '生成中...'
|
|
|
+ : dialogue.videoUrl
|
|
|
+ ? '重新生成'
|
|
|
+ : '生成'
|
|
|
+ }}
|
|
|
+ </el-button>
|
|
|
+ <button
|
|
|
+ class="remove-btn"
|
|
|
+ @click="removeDialogue(sectionIndex, dialogueIndex)"
|
|
|
+ >×</button
|
|
|
+ >
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
@@ -542,14 +630,21 @@
|
|
|
<button class="add-dialogue-btn digital" @click="addDialogue(sectionIndex)"
|
|
|
>+ 添加对话</button
|
|
|
>
|
|
|
+
|
|
|
<button
|
|
|
class="add-dialogue-btn quest-user"
|
|
|
@click="addQuestWithUserReply(sectionIndex)"
|
|
|
>+ 添加提问与回复</button
|
|
|
>
|
|
|
+
|
|
|
<button class="add-dialogue-btn poem" @click="addPoemDialogue(sectionIndex)"
|
|
|
>+ 添加诗词</button
|
|
|
>
|
|
|
+
|
|
|
+ <button class="add-dialogue-btn video" @click="addVideoDialogue(sectionIndex)"
|
|
|
+ >+ 添加视频</button
|
|
|
+ >
|
|
|
+
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -593,7 +688,7 @@
|
|
|
:key="sectionIndex"
|
|
|
class="preview-section"
|
|
|
:style="{
|
|
|
- backgroundImage: section.backgroundImage.url
|
|
|
+ backgroundImage: section.backgroundType === 'imageAudio' && section.backgroundImage.url
|
|
|
? `url(${section.backgroundImage.url})`
|
|
|
: 'none',
|
|
|
backgroundSize: 'cover',
|
|
|
@@ -601,6 +696,9 @@
|
|
|
backgroundRepeat: 'no-repeat'
|
|
|
}"
|
|
|
>
|
|
|
+ <div v-if="section.backgroundType === 'video' && section.backgroundVideo.url" class="preview-video">
|
|
|
+ <video :src="section.backgroundVideo.url" alt="视频背景" autoplay loop muted ></video>
|
|
|
+ </div>
|
|
|
<div class="preview-section-content">
|
|
|
<div class="preview-media">
|
|
|
<div class="preview-media-left">
|
|
|
@@ -608,7 +706,7 @@
|
|
|
<strong>{{ section.name }}</strong>
|
|
|
</div>
|
|
|
<div class="preview-media-right">
|
|
|
- <span v-if="section.backgroundAudio.type" class="preview-audio">
|
|
|
+ <span v-if="section.backgroundType === 'imageAudio' && section.backgroundAudio.type" class="preview-audio">
|
|
|
背景音:{{ section.backgroundAudio.type }}
|
|
|
<button
|
|
|
class="play-btn small"
|
|
|
@@ -638,21 +736,23 @@
|
|
|
<div class="dialogue-header">
|
|
|
<div class="dialogue-header-left">
|
|
|
<div class="dialogue-type-tag" :class="dialogue.type">
|
|
|
- {{
|
|
|
- dialogue.type === 'digital'
|
|
|
- ? '数字人'
|
|
|
- : dialogue.type === 'user'
|
|
|
- ? '用户'
|
|
|
- : dialogue.type === 'quest'
|
|
|
- ? '提问'
|
|
|
- : '诗词'
|
|
|
- }}
|
|
|
+ {{
|
|
|
+ dialogue.type === 'digital'
|
|
|
+ ? '数字人'
|
|
|
+ : dialogue.type === 'user'
|
|
|
+ ? '用户'
|
|
|
+ : dialogue.type === 'quest'
|
|
|
+ ? '提问'
|
|
|
+ : dialogue.type === 'poem'
|
|
|
+ ? '诗词'
|
|
|
+ : '视频'
|
|
|
+ }}
|
|
|
</div>
|
|
|
<div class="dialogue-role">
|
|
|
- {{
|
|
|
- dialogue.type !== 'user'
|
|
|
+ {{
|
|
|
+ dialogue.type !== 'user' && dialogue.type !== 'video'
|
|
|
? getRoleName(dialogue.roleName)
|
|
|
- : '用户'
|
|
|
+ : dialogue.type === 'video' ? '视频' : '用户'
|
|
|
}}:
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -670,7 +770,15 @@
|
|
|
}}</span>
|
|
|
</button>
|
|
|
</div>
|
|
|
- <div class="dialogue-text" v-html="parseMarkdown(dialogue.content)"></div>
|
|
|
+ <template v-if="dialogue.type === 'video'">
|
|
|
+ <div class="dialogue-text">{{ dialogue.videoPrompt }}</div>
|
|
|
+ <div v-if="dialogue.videoUrl" class="media-preview">
|
|
|
+ <video :src="dialogue.videoUrl" alt="视频预览" controls></video>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <template v-else>
|
|
|
+ <div class="dialogue-text" v-html="parseMarkdown(dialogue.content)"></div>
|
|
|
+ </template>
|
|
|
<div
|
|
|
v-if="dialogue.type === 'user' && dialogue.roleName"
|
|
|
class="reply-info"
|
|
|
@@ -783,7 +891,7 @@ const backgroundMusicTypes = ref([
|
|
|
{
|
|
|
id: '轻松欢快',
|
|
|
name: '轻松欢快',
|
|
|
- url: 'https://learn-aliyun-oss.oss-cn-beijing.aliyuncs.com/20260310/AI_1773106630966.MP3'
|
|
|
+ url: 'https://learn-ai.com.cn/admin-api/infra/file/29/get/20260310/AI_1773106630966.MP3'
|
|
|
}
|
|
|
])
|
|
|
|
|
|
@@ -815,6 +923,7 @@ const scriptData = reactive(
|
|
|
sections: [
|
|
|
{
|
|
|
name: '环节一',
|
|
|
+ backgroundType: 'imageAudio', // imageAudio: 图音背景, video: 视频背景
|
|
|
backgroundImage: {
|
|
|
prompt: '',
|
|
|
url: '',
|
|
|
@@ -824,6 +933,11 @@ const scriptData = reactive(
|
|
|
type: '',
|
|
|
url: ''
|
|
|
},
|
|
|
+ backgroundVideo: {
|
|
|
+ prompt: '',
|
|
|
+ url: '',
|
|
|
+ generating: false
|
|
|
+ },
|
|
|
dialogues: [
|
|
|
{
|
|
|
type: 'digital',
|
|
|
@@ -843,6 +957,22 @@ const scriptData = reactive(
|
|
|
}
|
|
|
)
|
|
|
|
|
|
+// 兼容旧数据,为旧数据添加背景类型字段
|
|
|
+if (scriptData.sections) {
|
|
|
+ scriptData.sections.forEach(section => {
|
|
|
+ if (!section.backgroundType) {
|
|
|
+ section.backgroundType = 'imageAudio';
|
|
|
+ }
|
|
|
+ if (!section.backgroundVideo) {
|
|
|
+ section.backgroundVideo = {
|
|
|
+ prompt: '',
|
|
|
+ url: '',
|
|
|
+ generating: false
|
|
|
+ };
|
|
|
+ }
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
// 保存脚本数据到localStorage
|
|
|
const saveScriptDataToCache = () => {
|
|
|
try {
|
|
|
@@ -868,6 +998,21 @@ const loadDraftAndGotoStep2 = () => {
|
|
|
const cachedData = loadScriptDataFromCache()
|
|
|
if (cachedData) {
|
|
|
Object.assign(scriptData, cachedData)
|
|
|
+ // 为加载的草稿数据添加背景类型和视频背景字段
|
|
|
+ if (scriptData.sections) {
|
|
|
+ scriptData.sections.forEach(section => {
|
|
|
+ if (!section.backgroundType) {
|
|
|
+ section.backgroundType = 'imageAudio';
|
|
|
+ }
|
|
|
+ if (!section.backgroundVideo) {
|
|
|
+ section.backgroundVideo = {
|
|
|
+ prompt: '',
|
|
|
+ url: '',
|
|
|
+ generating: false
|
|
|
+ };
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
currentStep.value = 2
|
|
|
}
|
|
|
}
|
|
|
@@ -897,12 +1042,42 @@ watch(
|
|
|
try {
|
|
|
const parsedData = JSON.parse(props.initialScriptData)
|
|
|
Object.assign(scriptData, parsedData)
|
|
|
+ // 为加载的脚本数据添加背景类型和视频背景字段
|
|
|
+ if (scriptData.sections) {
|
|
|
+ scriptData.sections.forEach(section => {
|
|
|
+ if (!section.backgroundType) {
|
|
|
+ section.backgroundType = 'imageAudio';
|
|
|
+ }
|
|
|
+ if (!section.backgroundVideo) {
|
|
|
+ section.backgroundVideo = {
|
|
|
+ prompt: '',
|
|
|
+ url: '',
|
|
|
+ generating: false
|
|
|
+ };
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
} catch (error) {
|
|
|
console.error('解析脚本数据失败:', error)
|
|
|
// 解析失败,尝试加载草稿
|
|
|
const cachedData = loadScriptDataFromCache()
|
|
|
if (cachedData) {
|
|
|
Object.assign(scriptData, cachedData)
|
|
|
+ // 为加载的草稿数据添加背景类型和视频背景字段
|
|
|
+ if (scriptData.sections) {
|
|
|
+ scriptData.sections.forEach(section => {
|
|
|
+ if (!section.backgroundType) {
|
|
|
+ section.backgroundType = 'imageAudio';
|
|
|
+ }
|
|
|
+ if (!section.backgroundVideo) {
|
|
|
+ section.backgroundVideo = {
|
|
|
+ prompt: '',
|
|
|
+ url: '',
|
|
|
+ generating: false
|
|
|
+ };
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
} else {
|
|
|
@@ -912,6 +1087,21 @@ watch(
|
|
|
const cachedData = loadScriptDataFromCache()
|
|
|
if (cachedData) {
|
|
|
Object.assign(scriptData, cachedData)
|
|
|
+ // 为加载的草稿数据添加背景类型和视频背景字段
|
|
|
+ if (scriptData.sections) {
|
|
|
+ scriptData.sections.forEach(section => {
|
|
|
+ if (!section.backgroundType) {
|
|
|
+ section.backgroundType = 'imageAudio';
|
|
|
+ }
|
|
|
+ if (!section.backgroundVideo) {
|
|
|
+ section.backgroundVideo = {
|
|
|
+ prompt: '',
|
|
|
+ url: '',
|
|
|
+ generating: false
|
|
|
+ };
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
} else if (oldVisible) {
|
|
|
@@ -1077,6 +1267,21 @@ const doSendMessageStream = async (conversationId, content) => {
|
|
|
const parsedData = JSON.parse(receiveMessageFullText.value)
|
|
|
scriptDataTemp.value = parsedData
|
|
|
Object.assign(scriptData, parsedData)
|
|
|
+ // 为新生成的脚本数据添加背景类型和视频背景字段
|
|
|
+ if (scriptData.sections) {
|
|
|
+ scriptData.sections.forEach(section => {
|
|
|
+ if (!section.backgroundType) {
|
|
|
+ section.backgroundType = 'imageAudio';
|
|
|
+ }
|
|
|
+ if (!section.backgroundVideo) {
|
|
|
+ section.backgroundVideo = {
|
|
|
+ prompt: '',
|
|
|
+ url: '',
|
|
|
+ generating: false
|
|
|
+ };
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
} catch (e) {
|
|
|
// 解析失败,说明数据还不完整,继续等待
|
|
|
}
|
|
|
@@ -1095,6 +1300,21 @@ const doSendMessageStream = async (conversationId, content) => {
|
|
|
|
|
|
scriptDataTemp.value = parsedData
|
|
|
Object.assign(scriptData, parsedData)
|
|
|
+ // 为新生成的脚本数据添加背景类型和视频背景字段
|
|
|
+ if (scriptData.sections) {
|
|
|
+ scriptData.sections.forEach(section => {
|
|
|
+ if (!section.backgroundType) {
|
|
|
+ section.backgroundType = 'imageAudio';
|
|
|
+ }
|
|
|
+ if (!section.backgroundVideo) {
|
|
|
+ section.backgroundVideo = {
|
|
|
+ prompt: '',
|
|
|
+ url: '',
|
|
|
+ generating: false
|
|
|
+ };
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
}
|
|
|
} catch (e) {
|
|
|
console.error('最终数据解析失败:', e)
|
|
|
@@ -1112,6 +1332,21 @@ const doSendMessageStream = async (conversationId, content) => {
|
|
|
console.log('最终清洗后数据json:', parsedData)
|
|
|
scriptDataTemp.value = parsedData
|
|
|
Object.assign(scriptData, parsedData)
|
|
|
+ // 为新生成的脚本数据添加背景类型和视频背景字段
|
|
|
+ if (scriptData.sections) {
|
|
|
+ scriptData.sections.forEach(section => {
|
|
|
+ if (!section.backgroundType) {
|
|
|
+ section.backgroundType = 'imageAudio';
|
|
|
+ }
|
|
|
+ if (!section.backgroundVideo) {
|
|
|
+ section.backgroundVideo = {
|
|
|
+ prompt: '',
|
|
|
+ url: '',
|
|
|
+ generating: false
|
|
|
+ };
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
} catch (cleanError) {
|
|
|
console.error('清洗后数据解析失败:', cleanError)
|
|
|
}
|
|
|
@@ -1129,8 +1364,10 @@ const doSendMessageStream = async (conversationId, content) => {
|
|
|
const addSection = () => {
|
|
|
scriptData.sections.push({
|
|
|
name: `环节${scriptData.sections.length + 1}`,
|
|
|
+ backgroundType: 'imageAudio', // imageAudio: 图音背景, video: 视频背景
|
|
|
backgroundImage: { prompt: '', url: '', generating: false },
|
|
|
backgroundAudio: { type: '', url: '' },
|
|
|
+ backgroundVideo: { prompt: '', url: '', generating: false },
|
|
|
dialogues: []
|
|
|
})
|
|
|
}
|
|
|
@@ -1146,6 +1383,16 @@ const addDialogue = (sectionIndex) => {
|
|
|
})
|
|
|
}
|
|
|
|
|
|
+// 步骤2:添加视频对话
|
|
|
+const addVideoDialogue = (sectionIndex) => {
|
|
|
+ scriptData.sections[sectionIndex].dialogues.push({
|
|
|
+ type: 'video',
|
|
|
+ videoUrl: '',
|
|
|
+ videoPrompt: '',
|
|
|
+ generatingVideo: false
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
// 步骤2:添加用户回复
|
|
|
const addUserReply = (sectionIndex) => {
|
|
|
scriptData.sections[sectionIndex].dialogues.push({
|
|
|
@@ -1264,6 +1511,65 @@ const generateMedia = async (sectionIndex) => {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+// 步骤2:生成视频背景
|
|
|
+const generateVideo = async (sectionIndex) => {
|
|
|
+ const section = scriptData.sections[sectionIndex]
|
|
|
+ const media = section.backgroundVideo
|
|
|
+
|
|
|
+ // 记录旧的URL
|
|
|
+ let oldUrl = null
|
|
|
+ if (media.url) {
|
|
|
+ oldUrl = media.url
|
|
|
+ replacedUrls.value.add(oldUrl)
|
|
|
+ }
|
|
|
+
|
|
|
+ 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)
|
|
|
+ } catch (error) {
|
|
|
+ console.error(`生成视频失败:`, error)
|
|
|
+ media.generating = false
|
|
|
+ // 生成失败,从replacedUrls中移除旧URL
|
|
|
+ if (oldUrl) {
|
|
|
+ replacedUrls.value.delete(oldUrl)
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 步骤2:生成对话视频
|
|
|
+const generateDialogueVideo = async (sectionIndex, dialogueIndex) => {
|
|
|
+ const dialogue = scriptData.sections[sectionIndex].dialogues[dialogueIndex]
|
|
|
+
|
|
|
+ // 记录旧的URL
|
|
|
+ let oldUrl = null
|
|
|
+ if (dialogue.videoUrl) {
|
|
|
+ oldUrl = dialogue.videoUrl
|
|
|
+ replacedUrls.value.add(oldUrl)
|
|
|
+ }
|
|
|
+
|
|
|
+ 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)
|
|
|
+ } catch (error) {
|
|
|
+ console.error(`生成对话视频失败:`, error)
|
|
|
+ dialogue.generatingVideo = false
|
|
|
+ // 生成失败,从replacedUrls中移除旧URL
|
|
|
+ if (oldUrl) {
|
|
|
+ replacedUrls.value.delete(oldUrl)
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
// 步骤2:一键生成所有背景图
|
|
|
const generateAllImages = async () => {
|
|
|
isGeneratingImages.value = true
|
|
|
@@ -1468,16 +1774,31 @@ const validateScript = () => {
|
|
|
let isValid = true
|
|
|
errorMessages.value = []
|
|
|
|
|
|
- // 检查环节名称、背景图
|
|
|
+ // 检查环节名称、背景图或视频
|
|
|
scriptData.sections.forEach((section, sectionIndex) => {
|
|
|
- if (!section.name.trim() || !section.backgroundImage.url) {
|
|
|
- errorMessages.value.push(`环节${sectionIndex + 1}:名称或背景图未配置!`)
|
|
|
+ if (!section.name.trim()) {
|
|
|
+ errorMessages.value.push(`环节${sectionIndex + 1}:名称未配置!`)
|
|
|
isValid = false
|
|
|
}
|
|
|
|
|
|
+ // 根据背景类型检查对应的媒体URL
|
|
|
+ if (section.backgroundType === 'video') {
|
|
|
+ // 视频背景需要检查视频URL
|
|
|
+ if (!section.backgroundVideo.url) {
|
|
|
+ errorMessages.value.push(`环节${sectionIndex + 1}:视频背景URL未配置!`)
|
|
|
+ isValid = false
|
|
|
+ }
|
|
|
+ } else if (section.backgroundType === 'imageAudio') {
|
|
|
+ // 图音背景需要检查背景图URL
|
|
|
+ if (!section.backgroundImage.url) {
|
|
|
+ errorMessages.value.push(`环节${sectionIndex + 1}:背景图URL未配置!`)
|
|
|
+ isValid = false
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
// 检查对话
|
|
|
section.dialogues.forEach((dialogue, dialogueIndex) => {
|
|
|
- // 只检查数字人、提问和诗词类型的对话
|
|
|
+ // 检查数字人、提问和诗词类型的对话
|
|
|
if (dialogue.type === 'digital' || dialogue.type === 'quest' || dialogue.type === 'poem') {
|
|
|
if (
|
|
|
!dialogue.roleName ||
|
|
|
@@ -1489,6 +1810,17 @@ const validateScript = () => {
|
|
|
)
|
|
|
isValid = false
|
|
|
}
|
|
|
+ } else if (dialogue.type === 'video') {
|
|
|
+ // 检查视频类型的对话
|
|
|
+ if (
|
|
|
+ !dialogue.videoPrompt.trim() ||
|
|
|
+ !dialogue.videoUrl
|
|
|
+ ) {
|
|
|
+ errorMessages.value.push(
|
|
|
+ `环节${sectionIndex + 1}:对话${dialogueIndex + 1}:视频内容未配置完整!`
|
|
|
+ )
|
|
|
+ isValid = false
|
|
|
+ }
|
|
|
}
|
|
|
})
|
|
|
})
|
|
|
@@ -1670,6 +2002,64 @@ onUnmounted(() => {
|
|
|
border-radius: 3px;
|
|
|
}
|
|
|
|
|
|
+/* 背景类型切换样式 */
|
|
|
+.background-type-switch {
|
|
|
+ position: absolute;
|
|
|
+ top: -15px;
|
|
|
+ left: 10px;
|
|
|
+ z-index: 10;
|
|
|
+}
|
|
|
+
|
|
|
+/* 媒体控制区域样式调整 */
|
|
|
+.media-controls {
|
|
|
+ position: relative;
|
|
|
+ margin-top: 30px;
|
|
|
+ padding-top: 40px; /* 为背景类型切换留出空间 */
|
|
|
+ height: 165px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 视频预览样式 */
|
|
|
+.media-preview video {
|
|
|
+ width: 100%;
|
|
|
+ max-height: 300px;
|
|
|
+ object-fit: cover;
|
|
|
+ border-radius: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 预览视频背景样式 */
|
|
|
+.preview-section {
|
|
|
+ position: relative;
|
|
|
+ min-height: 400px;
|
|
|
+ margin-bottom: 20px;
|
|
|
+ border-radius: 8px;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+
|
|
|
+.preview-video {
|
|
|
+ position: absolute;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ z-index: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.preview-video video {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ object-fit: cover;
|
|
|
+}
|
|
|
+
|
|
|
+.preview-section-content {
|
|
|
+ position: relative;
|
|
|
+ z-index: 1;
|
|
|
+ padding: 20px;
|
|
|
+ background: rgba(255, 255, 255, 0.8);
|
|
|
+ height: 100%;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+}
|
|
|
+
|
|
|
.script-editor::-webkit-scrollbar-thumb:hover {
|
|
|
background: #a1a1a1;
|
|
|
}
|
|
|
@@ -1804,6 +2194,10 @@ onUnmounted(() => {
|
|
|
background-color: #909399;
|
|
|
}
|
|
|
|
|
|
+.dialogue-type-tag.video {
|
|
|
+ background-color: #8b4513;
|
|
|
+}
|
|
|
+
|
|
|
/* 对话项样式 */
|
|
|
.dialogue-item {
|
|
|
position: relative;
|
|
|
@@ -1831,6 +2225,10 @@ onUnmounted(() => {
|
|
|
border-left: 4px solid #e6a23c;
|
|
|
}
|
|
|
|
|
|
+.dialogue-item.video {
|
|
|
+ border-left: 4px solid #8b4513;
|
|
|
+}
|
|
|
+
|
|
|
/* 对话头部 */
|
|
|
.dialogue-header {
|
|
|
margin-bottom: 10px;
|
|
|
@@ -1937,6 +2335,17 @@ onUnmounted(() => {
|
|
|
color: white;
|
|
|
}
|
|
|
|
|
|
+.add-dialogue-btn.video {
|
|
|
+ background-color: #8b4513;
|
|
|
+ color: white;
|
|
|
+}
|
|
|
+
|
|
|
+.add-dialogue-btn.video:hover {
|
|
|
+ background-color: #a0522d;
|
|
|
+ border-color: #a0522d;
|
|
|
+ color: white;
|
|
|
+}
|
|
|
+
|
|
|
/* 预览对话样式 */
|
|
|
.preview-dialogue {
|
|
|
position: relative;
|
|
|
@@ -1962,6 +2371,10 @@ onUnmounted(() => {
|
|
|
border-left: 4px solid #909399;
|
|
|
}
|
|
|
|
|
|
+.preview-dialogue.video {
|
|
|
+ border-left: 4px solid #8b4513;
|
|
|
+}
|
|
|
+
|
|
|
.preview-dialogue .dialogue-header {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
@@ -2421,7 +2834,7 @@ onUnmounted(() => {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: 10px;
|
|
|
- margin-right: 50px;
|
|
|
+ min-height: 50px;
|
|
|
}
|
|
|
|
|
|
.media-input-group {
|
|
|
@@ -2442,6 +2855,73 @@ onUnmounted(() => {
|
|
|
transition: all 0.3s ease;
|
|
|
width: 300px;
|
|
|
}
|
|
|
+.media-prompt-video{
|
|
|
+ font-size: 14px;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+ width: 500px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 视频对话样式 */
|
|
|
+.dialogue-item.video .dialogue-row {
|
|
|
+ justify-content: center;
|
|
|
+}
|
|
|
+
|
|
|
+.dialogue-item.video .media-item {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ width: 100%;
|
|
|
+ max-width: 900px;
|
|
|
+}
|
|
|
+
|
|
|
+.dialogue-item.video .media-input-group {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ width: 100%;
|
|
|
+ gap: 15px;
|
|
|
+}
|
|
|
+
|
|
|
+.dialogue-item.video .media-label {
|
|
|
+ white-space: nowrap;
|
|
|
+ margin-bottom: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.dialogue-item.video .media-prompt-video {
|
|
|
+ flex: 1;
|
|
|
+ min-width: 300px;
|
|
|
+ max-width: 600px;
|
|
|
+}
|
|
|
+
|
|
|
+.dialogue-item.video .media-prompt-video .el-textarea {
|
|
|
+ min-height: 40px;
|
|
|
+}
|
|
|
+
|
|
|
+.dialogue-item.video .media-prompt-video .el-textarea__inner {
|
|
|
+ min-height: 40px;
|
|
|
+ padding: 6px 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.dialogue-item.video .media-preview {
|
|
|
+ flex: 0 0 200px;
|
|
|
+ margin: 5px 0;
|
|
|
+}
|
|
|
+
|
|
|
+.dialogue-item.video .media-preview video {
|
|
|
+ width: 100%;
|
|
|
+ max-height: 120px;
|
|
|
+ object-fit: cover;
|
|
|
+ border-radius: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.dialogue-item.video .generate-btn {
|
|
|
+ min-width: 80px;
|
|
|
+ white-space: nowrap;
|
|
|
+}
|
|
|
+
|
|
|
+.dialogue-item.video .remove-btn {
|
|
|
+ margin-left: 10px;
|
|
|
+}
|
|
|
|
|
|
.media-prompt .el-textarea {
|
|
|
width: 100%;
|
|
|
@@ -2465,7 +2945,6 @@ onUnmounted(() => {
|
|
|
}
|
|
|
|
|
|
.media-preview {
|
|
|
- margin-top: 12px;
|
|
|
margin-left: 20px;
|
|
|
}
|
|
|
|