|
@@ -0,0 +1,2295 @@
|
|
|
|
|
+<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>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="main-container">
|
|
|
|
|
+ <!-- 左侧步骤指示器 -->
|
|
|
|
|
+ <div class="steps-sidebar">
|
|
|
|
|
+ <div class="steps-container">
|
|
|
|
|
+ <template v-for="(step, index) in steps" :key="step.id">
|
|
|
|
|
+ <div
|
|
|
|
|
+ :class="['step-item', { process: currentStep === step.id, finish: step.id < currentStep, wait: step.id > currentStep }]"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div class="step-circle">{{ step.id }}</div>
|
|
|
|
|
+ <div class="step-title">{{ step.label }}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div v-if="index < steps.length - 1" class="step-line" :class="{ active: currentStep > step.id }"></div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 右侧内容区域 -->
|
|
|
|
|
+ <div class="content-area">
|
|
|
|
|
+ <!-- 步骤1:AI一句话生成 -->
|
|
|
|
|
+ <div v-if="currentStep === 1" class="step-content">
|
|
|
|
|
+ <div class="ai-input-container">
|
|
|
|
|
+ <div class="ai-input-wrapper">
|
|
|
|
|
+ <div class="ai-icon">🤖</div>
|
|
|
|
|
+ <textarea
|
|
|
|
|
+ v-model="scriptPrompt"
|
|
|
|
|
+ placeholder="AI一句话生成课程脚本,例如:生成一节关于牛顿万有引力的小学科学课脚本..."
|
|
|
|
|
+ class="ai-textarea"
|
|
|
|
|
+ ></textarea>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="selection-group">
|
|
|
|
|
+ <div class="selection-item">
|
|
|
|
|
+ <label>主讲老师</label>
|
|
|
|
|
+ <el-select v-model="selectedMainTeacher" filterable size="large" placeholder="请选择主讲老师" style="width: 300px" clearable>
|
|
|
|
|
+ <el-option
|
|
|
|
|
+ v-for="teacher in digitalHumans"
|
|
|
|
|
+ :key="teacher.name"
|
|
|
|
|
+ :label="teacher.name"
|
|
|
|
|
+ :value="teacher.name"
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-select>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="selection-item">
|
|
|
|
|
+ <label>助讲老师</label>
|
|
|
|
|
+ <el-select v-model="selectedAssistants" filterable size="large" placeholder="请选择助讲老师" style="width: 300px" clearable multiple>
|
|
|
|
|
+ <el-option
|
|
|
|
|
+ v-for="teacher in digitalHumans"
|
|
|
|
|
+ :key="teacher.name"
|
|
|
|
|
+ :label="teacher.name"
|
|
|
|
|
+ :value="teacher.name"
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-select>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <button
|
|
|
|
|
+ class="generate-btn primary"
|
|
|
|
|
+ :disabled="!scriptPrompt || !selectedMainTeacher || isGenerating"
|
|
|
|
|
+ @click="generateScript"
|
|
|
|
|
+ >
|
|
|
|
|
+ {{ isGenerating ? '生成中...' : '生成课程脚本' }}
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 步骤2:脚本编辑(含背景图/音生成) -->
|
|
|
|
|
+ <div v-else-if="currentStep === 2" class="step-content">
|
|
|
|
|
+ <!-- 固定顶部的一键操作栏 -->
|
|
|
|
|
+ <div class="editor-toolbar sticky-top">
|
|
|
|
|
+ <button
|
|
|
|
|
+ class="toolbar-btn"
|
|
|
|
|
+ @click="generateAllImages"
|
|
|
|
|
+ :disabled="isGeneratingImages || isAnyImageGenerating"
|
|
|
|
|
+ >
|
|
|
|
|
+ {{ isGeneratingImages ? '生成中...' : '一键生成所有背景图' }}
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <button
|
|
|
|
|
+ class="toolbar-btn"
|
|
|
|
|
+ @click="generateAllVoiceovers"
|
|
|
|
|
+ :disabled="isGeneratingVoiceovers || isAnyVoiceoverGenerating"
|
|
|
|
|
+ >
|
|
|
|
|
+ {{ isGeneratingVoiceovers ? '生成中...' : '一键配音' }}
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 可滚动的内容区域 -->
|
|
|
|
|
+ <div class="script-editor">
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-for="(section, sectionIndex) in scriptData.sections"
|
|
|
|
|
+ :key="sectionIndex"
|
|
|
|
|
+ class="script-section"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div class="section-header">
|
|
|
|
|
+ <div class="section-name-container">
|
|
|
|
|
+ <label class="section-name-label">环节 {{sectionIndex+1}}</label>
|
|
|
|
|
+ <input
|
|
|
|
|
+ v-model="section.name"
|
|
|
|
|
+ class="section-title"
|
|
|
|
|
+ placeholder="如:环节一(引入)"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <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="背景图预览" />
|
|
|
|
|
+ </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: 240px" 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>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="dialogues-container">
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-for="(dialogue, dialogueIndex) in section.dialogues"
|
|
|
|
|
+ :key="dialogueIndex"
|
|
|
|
|
+ class="dialogue-item"
|
|
|
|
|
+ :class="dialogue.type"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div class="dialogue-header">
|
|
|
|
|
+ <div class="dialogue-type-tag" :class="dialogue.type">
|
|
|
|
|
+ {{ dialogue.type === 'digital' ? '数字人' : '用户' }}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="dialogue-row">
|
|
|
|
|
+ <!-- 数字人对话 -->
|
|
|
|
|
+ <template v-if="dialogue.type === 'digital'">
|
|
|
|
|
+ <div class="dialogue-role-select">
|
|
|
|
|
+ <el-select v-model="dialogue.roleName" placeholder="选择角色" style="width: 140px" clearable>
|
|
|
|
|
+ <el-option
|
|
|
|
|
+ v-for="role in digitalHumans"
|
|
|
|
|
+ :key="role.id"
|
|
|
|
|
+ :label="role.name"
|
|
|
|
|
+ :value="role.name"
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-select>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="dialogue-content-container">
|
|
|
|
|
+ <el-input
|
|
|
|
|
+ v-model="dialogue.content"
|
|
|
|
|
+ type="textarea"
|
|
|
|
|
+ class="dialogue-content"
|
|
|
|
|
+ placeholder="对话内容..."
|
|
|
|
|
+ :autosize="{ minRows: 2, maxRows: 4 }"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="action-buttons">
|
|
|
|
|
+ <div class="action-buttons-row">
|
|
|
|
|
+ <button
|
|
|
|
|
+ v-if="dialogue.voiceoverUrl"
|
|
|
|
|
+ class="play-btn"
|
|
|
|
|
+ @click="playVoiceover(dialogue.voiceoverUrl)"
|
|
|
|
|
+ >
|
|
|
|
|
+ <span class="play-icon">{{ audioState.isPlaying && audioState.currentType === 'voice' && audioState.currentUrl === dialogue.voiceoverUrl ? '⏸' : '▶' }}</span>
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <button class="remove-btn" @click="removeDialogue(sectionIndex, dialogueIndex)">×</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="action-buttons-row">
|
|
|
|
|
+ <button
|
|
|
|
|
+ v-if="!dialogue.voiceoverUrl"
|
|
|
|
|
+ class="generate-btn small"
|
|
|
|
|
+ :disabled="!dialogue.content || !dialogue.roleName || dialogue.generatingVoiceover"
|
|
|
|
|
+ @click="generateVoiceover(sectionIndex, dialogueIndex)"
|
|
|
|
|
+ >
|
|
|
|
|
+ <span class="voice-icon">{{ dialogue.generatingVoiceover ? '生成中...' : '生成语音' }}</span>
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <button
|
|
|
|
|
+ v-if="dialogue.voiceoverUrl"
|
|
|
|
|
+ class="generate-btn small"
|
|
|
|
|
+ :disabled="!dialogue.content || !dialogue.roleName || dialogue.generatingVoiceover"
|
|
|
|
|
+ @click="generateVoiceover(sectionIndex, dialogueIndex)"
|
|
|
|
|
+ >
|
|
|
|
|
+ <span class="voice-icon">{{ dialogue.generatingVoiceover ? '生成中...' : '重新生成' }}</span>
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 用户 -->
|
|
|
|
|
+ <template v-else-if="dialogue.type === 'user'">
|
|
|
|
|
+ <div class="dialogue-content-container full-width">
|
|
|
|
|
+ <el-input
|
|
|
|
|
+ v-model="dialogue.content"
|
|
|
|
|
+ type="textarea"
|
|
|
|
|
+ class="dialogue-content"
|
|
|
|
|
+ placeholder="用户回复内容..."
|
|
|
|
|
+ :autosize="{ minRows: 2, maxRows: 4 }"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="dialogue-reply-select">
|
|
|
|
|
+ <el-select v-model="dialogue.roleName" placeholder="选择数字人回复" style="width: 140px" clearable>
|
|
|
|
|
+ <el-option
|
|
|
|
|
+ v-for="role in digitalHumans"
|
|
|
|
|
+ :key="role.id"
|
|
|
|
|
+ :label="role.name"
|
|
|
|
|
+ :value="role.name"
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-select>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="action-buttons">
|
|
|
|
|
+ <div class="action-buttons-row">
|
|
|
|
|
+ <button class="remove-btn" @click="removeDialogue(sectionIndex, dialogueIndex)">×</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="add-dialogue-buttons">
|
|
|
|
|
+ <button class="add-dialogue-btn digital" @click="addDialogue(sectionIndex)">+ 添加对话</button>
|
|
|
|
|
+ <button class="add-dialogue-btn user" @click="addUserReply(sectionIndex)">+ 添加用户回复</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <button class="add-section-btn" @click="addSection">+ 添加环节</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ <div class="preview-actions">
|
|
|
|
|
+
|
|
|
|
|
+ <button
|
|
|
|
|
+ v-if="currentStep > 1"
|
|
|
|
|
+ class="secondary-btn"
|
|
|
|
|
+ @click="currentStep--"
|
|
|
|
|
+ >
|
|
|
|
|
+ 重新生成
|
|
|
|
|
+ </button>
|
|
|
|
|
+
|
|
|
|
|
+ <button
|
|
|
|
|
+ class="primary-btn"
|
|
|
|
|
+ :disabled="!canProceed"
|
|
|
|
|
+ @click="nextStep"
|
|
|
|
|
+ >
|
|
|
|
|
+ 预览完整脚本
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 步骤3:预览与保存 -->
|
|
|
|
|
+ <div v-else-if="currentStep === 3" class="step-content">
|
|
|
|
|
+ <div class="preview-container">
|
|
|
|
|
+ <h3>课程脚本预览</h3>
|
|
|
|
|
+ <br/>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="validation-result" :class="{ valid: isValidationPassed, invalid: !isValidationPassed }">
|
|
|
|
|
+ {{ validationMessage }}
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="preview-content">
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-for="(section, sectionIndex) in scriptData.sections"
|
|
|
|
|
+ :key="sectionIndex"
|
|
|
|
|
+ class="preview-section"
|
|
|
|
|
+ :style="{ backgroundImage: section.backgroundImage.url ? `url(${section.backgroundImage.url})` : 'none', backgroundSize: 'cover', backgroundPosition: 'center', backgroundRepeat: 'no-repeat' }"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div class="preview-section-content">
|
|
|
|
|
+ <div class="preview-media">
|
|
|
|
|
+ <div class="preview-media-left">
|
|
|
|
|
+ <label class="section-name-label">环节 {{sectionIndex+1}}</label>
|
|
|
|
|
+ <strong>{{ section.name }}</strong>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="preview-media-right">
|
|
|
|
|
+ <span v-if="section.backgroundAudio.type" class="preview-audio">
|
|
|
|
|
+ 背景音:{{ section.backgroundAudio.type }}
|
|
|
|
|
+ <button
|
|
|
|
|
+ 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>
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="preview-dialogues">
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-for="(dialogue, dialogueIndex) in section.dialogues"
|
|
|
|
|
+ :key="dialogueIndex"
|
|
|
|
|
+ class="preview-dialogue"
|
|
|
|
|
+ :class="dialogue.type"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div class="dialogue-header">
|
|
|
|
|
+ <div class="dialogue-header-left">
|
|
|
|
|
+ <div class="dialogue-type-tag" :class="dialogue.type">
|
|
|
|
|
+ {{ dialogue.type === 'digital' ? '数字人' : '用户' }}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="dialogue-role">
|
|
|
|
|
+ {{ dialogue.type === 'digital' ? getRoleName(dialogue.roleName) : '用户' }}:
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <button
|
|
|
|
|
+ v-if="dialogue.voiceoverUrl && dialogue.type === 'digital'"
|
|
|
|
|
+ class="play-btn small"
|
|
|
|
|
+ @click="playVoiceover(dialogue.voiceoverUrl)"
|
|
|
|
|
+ >
|
|
|
|
|
+ <span class="play-icon">{{ audioState.isPlaying && audioState.currentType === 'voice' && audioState.currentUrl === dialogue.voiceoverUrl ? '⏸' : '▶' }}</span>
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="dialogue-text" v-html="parseMarkdown(dialogue.content)"></div>
|
|
|
|
|
+ <div v-if="dialogue.type === 'user' && dialogue.roleName" class="reply-info">
|
|
|
|
|
+ 回复角色:{{ getRoleName(dialogue.roleName) }}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="preview-actions">
|
|
|
|
|
+ <button class="secondary-btn" @click="currentStep = 2">返回修改</button>
|
|
|
|
|
+ <button
|
|
|
|
|
+ class="primary-btn"
|
|
|
|
|
+ :disabled="!isValidationPassed"
|
|
|
|
|
+ @click="showVideoPreview"
|
|
|
|
|
+ >
|
|
|
|
|
+ 预览视频
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <button
|
|
|
|
|
+ class="primary-btn"
|
|
|
|
|
+ :disabled="!isValidationPassed"
|
|
|
|
|
+ @click="saveScript"
|
|
|
|
|
+ >
|
|
|
|
|
+ 保存课程脚本
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 视频预览模态框 -->
|
|
|
|
|
+ <VideoPreview
|
|
|
|
|
+ :visible="showVideoPreviewModal"
|
|
|
|
|
+ :script-data="scriptData"
|
|
|
|
|
+ :script-roles="digitalHumans"
|
|
|
|
|
+ @close="closeVideoPreview"
|
|
|
|
|
+ />
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script setup>
|
|
|
|
|
+import { ref, reactive, computed, onMounted, onUnmounted, watch } from 'vue'
|
|
|
|
|
+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 { marked } from 'marked'
|
|
|
|
|
+
|
|
|
|
|
+// 步骤配置
|
|
|
|
|
+const steps = [
|
|
|
|
|
+ { id: 1, label: 'AI生成脚本' },
|
|
|
|
|
+ { id: 2, label: '编辑脚本与媒体' },
|
|
|
|
|
+ { id: 3, label: '预览与保存' }
|
|
|
|
|
+]
|
|
|
|
|
+
|
|
|
|
|
+// 当前步骤
|
|
|
|
|
+const currentStep = ref(2)
|
|
|
|
|
+
|
|
|
|
|
+// 步骤1数据
|
|
|
|
|
+const scriptPrompt = ref('请给我一个讲解万有引力的课程')
|
|
|
|
|
+const selectedMainTeacher = ref('牛顿')
|
|
|
|
|
+const selectedAssistants = ref([])
|
|
|
|
|
+const isGenerating = ref(false)
|
|
|
|
|
+
|
|
|
|
|
+// 步骤2生成状态
|
|
|
|
|
+const isGeneratingImages = ref(false)
|
|
|
|
|
+const isGeneratingVoiceovers = ref(false)
|
|
|
|
|
+
|
|
|
|
|
+// 数字人列表
|
|
|
|
|
+const digitalHumans = ref([])
|
|
|
|
|
+
|
|
|
|
|
+// 背景音类型
|
|
|
|
|
+const backgroundMusicTypes = ref([
|
|
|
|
|
+ { id: '轻松欢快', name: '轻松欢快', url: 'http://192.168.110.9:8080/admin-api/infra/file/29/get/20260228/bgm_1772265321253.MP3' },
|
|
|
|
|
+])
|
|
|
|
|
+
|
|
|
|
|
+// 从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: '',
|
|
|
|
|
+ }
|
|
|
|
|
+ ]
|
|
|
|
|
+ }
|
|
|
|
|
+ ]
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 脚本数据结构
|
|
|
|
|
+const scriptData = reactive(loadScriptDataFromCache())
|
|
|
|
|
+
|
|
|
|
|
+// 保存脚本数据到localStorage
|
|
|
|
|
+const saveScriptDataToCache = () => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ localStorage.setItem('courseScriptData', JSON.stringify(scriptData))
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('保存缓存数据失败:', error)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 清空脚本数据缓存
|
|
|
|
|
+const clearScriptDataCache = () => {
|
|
|
|
|
+ localStorage.removeItem('courseScriptData')
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 监听脚本数据变化,自动保存到缓存
|
|
|
|
|
+watch(() => scriptData, () => {
|
|
|
|
|
+ saveScriptDataToCache()
|
|
|
|
|
+}, { deep: true })
|
|
|
|
|
+
|
|
|
|
|
+// 音频播放状态
|
|
|
|
|
+const audioState = reactive({
|
|
|
|
|
+ currentAudio: null,
|
|
|
|
|
+ isPlaying: false,
|
|
|
|
|
+ currentType: '',
|
|
|
|
|
+ currentUrl: ''
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+// 存储所有音频实例
|
|
|
|
|
+const audioInstances = new Map()
|
|
|
|
|
+
|
|
|
|
|
+// 计算属性:判断是否可以进入下一步
|
|
|
|
|
+const canProceed = computed(() => {
|
|
|
|
|
+ switch (currentStep.value) {
|
|
|
|
|
+ case 1:
|
|
|
|
|
+ return !!scriptPrompt.value && !!selectedMainTeacher.value
|
|
|
|
|
+ case 2:
|
|
|
|
|
+ case 3:
|
|
|
|
|
+ return scriptData.sections.every(section =>
|
|
|
|
|
+ section.backgroundImage.url && section.dialogues.every(dialogue => dialogue.type !== "digital" || dialogue.voiceoverUrl)
|
|
|
|
|
+ )
|
|
|
|
|
+ default:
|
|
|
|
|
+ return false
|
|
|
|
|
+ }
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+// 计算属性:检查是否有任何背景图正在生成
|
|
|
|
|
+const isAnyImageGenerating = computed(() => {
|
|
|
|
|
+ return scriptData.sections.some(section => section.backgroundImage.generating)
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+// 计算属性:检查是否有任何配音正在生成
|
|
|
|
|
+const isAnyVoiceoverGenerating = computed(() => {
|
|
|
|
|
+ return scriptData.sections.some(section =>
|
|
|
|
|
+ section.dialogues.some(dialogue => dialogue.generatingVoiceover)
|
|
|
|
|
+ )
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+// 校验结果
|
|
|
|
|
+const isValidationPassed = ref(false)
|
|
|
|
|
+const validationMessage = ref('')
|
|
|
|
|
+
|
|
|
|
|
+// 视频预览状态
|
|
|
|
|
+const showVideoPreviewModal = ref(false)
|
|
|
|
|
+
|
|
|
|
|
+const activeConversationId = ref(null) // 选中的对话编号
|
|
|
|
|
+const conversationInAbortController = ref() // 对话进行中 abort 控制器(控制 stream 对话)
|
|
|
|
|
+const conversationInProgress = ref(false) // 对话是否正在进行中。目前只有【发送】消息时,会更新为 true,避免切换对话、删除对话等操作
|
|
|
|
|
+const enableContext = ref(false) // 是否开启上下文
|
|
|
|
|
+
|
|
|
|
|
+// 接收 Stream 消息
|
|
|
|
|
+const receiveMessageFullText = ref('')
|
|
|
|
|
+const scriptDataTemp = ref(null)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+// 步骤1:生成脚本
|
|
|
|
|
+const generateScript = async () => {
|
|
|
|
|
+ isGenerating.value = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ clearScriptDataCache()
|
|
|
|
|
+ scriptData.title = ''
|
|
|
|
|
+ scriptData.sections = []
|
|
|
|
|
+ receiveMessageFullText.value = ''
|
|
|
|
|
+ scriptDataTemp.value = null
|
|
|
|
|
+
|
|
|
|
|
+ const role = digitalHumans.value.find(r => r.name === selectedMainTeacher.value)
|
|
|
|
|
+ let content = scriptPrompt.value + "(主讲人:" + role.name + "[" + role.description + "],助讲:"
|
|
|
|
|
+ let zhujiang = []
|
|
|
|
|
+
|
|
|
|
|
+ selectedAssistants.value.forEach((rName) => {
|
|
|
|
|
+ const role = digitalHumans.value.find(r => r.name === rName)
|
|
|
|
|
+ zhujiang.push( role.name + "[" + role.description + "]")
|
|
|
|
|
+ })
|
|
|
|
|
+ content+= zhujiang.join(',') + ")";
|
|
|
|
|
+
|
|
|
|
|
+ await createAiRoleIdConversation(13)
|
|
|
|
|
+ currentStep.value = 2
|
|
|
|
|
+ await doSendMessageStream(activeConversationId.value, content)
|
|
|
|
|
+
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('生成脚本失败:', error)
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ isGenerating.value = false
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/** 选择 card 角色:新建聊天对话 */
|
|
|
|
|
+const createAiRoleIdConversation = async (roleId) => {
|
|
|
|
|
+ activeConversationId.value = await ChatConversationApi.createChatConversationMy({
|
|
|
|
|
+ roleId: roleId
|
|
|
|
|
+ })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/** 真正执行【发送】消息操作 */
|
|
|
|
|
+const doSendMessageStream = async (conversationId, content) => {
|
|
|
|
|
+ conversationInAbortController.value = new AbortController()
|
|
|
|
|
+ conversationInProgress.value = true
|
|
|
|
|
+ scriptPrompt.value = ''
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ let isFirstChunk = true
|
|
|
|
|
+ await ChatMessageApi.sendChatMessageStream(
|
|
|
|
|
+ conversationId,
|
|
|
|
|
+ content,
|
|
|
|
|
+ conversationInAbortController.value,
|
|
|
|
|
+ enableContext.value,
|
|
|
|
|
+ async (res) => {
|
|
|
|
|
+ const { code, data, msg } = JSON.parse(res.data)
|
|
|
|
|
+ if (code !== 0) {
|
|
|
|
|
+ console.error(`对话异常! ${msg}`)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (data.eventType === 'TEXT') {
|
|
|
|
|
+ if (data.receive?.content === '') {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (isFirstChunk) {
|
|
|
|
|
+ isFirstChunk = false
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ receiveMessageFullText.value += data.receive.content
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const parsedData = JSON.parse(receiveMessageFullText.value)
|
|
|
|
|
+ scriptDataTemp.value = parsedData
|
|
|
|
|
+ Object.assign(scriptData, parsedData)
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ // 解析失败,说明数据还不完整,继续等待
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ (error) => {
|
|
|
|
|
+ console.error(`对话异常! ${error}`)
|
|
|
|
|
+ throw error
|
|
|
|
|
+ },
|
|
|
|
|
+ () => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (receiveMessageFullText.value) {
|
|
|
|
|
+ const parsedData = JSON.parse(receiveMessageFullText.value)
|
|
|
|
|
+ scriptDataTemp.value = parsedData
|
|
|
|
|
+ Object.assign(scriptData, parsedData)
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.error("最终数据解析失败:", e)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ )
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error("发送消息流失败:", error)
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ conversationInProgress.value = false
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 步骤2:添加环节
|
|
|
|
|
+const addSection = () => {
|
|
|
|
|
+ scriptData.sections.push({
|
|
|
|
|
+ name: `环节${scriptData.sections.length + 1}`,
|
|
|
|
|
+ backgroundImage: { prompt: '', url: '', generating: false },
|
|
|
|
|
+ backgroundAudio: { type: '', url: '' },
|
|
|
|
|
+ dialogues: []
|
|
|
|
|
+ })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 步骤2:添加对话(数字人)
|
|
|
|
|
+const addDialogue = (sectionIndex) => {
|
|
|
|
|
+ scriptData.sections[sectionIndex].dialogues.push({
|
|
|
|
|
+ type: 'digital',
|
|
|
|
|
+ roleName: '',
|
|
|
|
|
+ content: '',
|
|
|
|
|
+ voiceoverUrl: '',
|
|
|
|
|
+ generatingVoiceover: false
|
|
|
|
|
+ })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 步骤2:添加用户回复
|
|
|
|
|
+const addUserReply = (sectionIndex) => {
|
|
|
|
|
+ scriptData.sections[sectionIndex].dialogues.push({
|
|
|
|
|
+ type: 'user',
|
|
|
|
|
+ content: '',
|
|
|
|
|
+ roleName: '',
|
|
|
|
|
+ voiceoverUrl: '',
|
|
|
|
|
+ generatingVoiceover: false
|
|
|
|
|
+ })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 步骤2:删除对话
|
|
|
|
|
+const removeDialogue = (sectionIndex, dialogueIndex) => {
|
|
|
|
|
+ scriptData.sections[sectionIndex].dialogues.splice(dialogueIndex, 1)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 步骤2:删除环节
|
|
|
|
|
+const removeSection = (sectionIndex) => {
|
|
|
|
|
+ if (scriptData.sections.length > 1) {
|
|
|
|
|
+ scriptData.sections.splice(sectionIndex, 1)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ alert('至少需要保留一个环节')
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 图片生成相关状态
|
|
|
|
|
+const inProgressImageMap = ref({}) // 监听的图片映射,key 为 image 编号,value 为 { sectionIndex, type: 'image'|'audio' }
|
|
|
|
|
+const inProgressTimer = ref(null) // 生成中的图片定时器,轮询生成进展
|
|
|
|
|
+
|
|
|
|
|
+// 步骤2:生成单个媒体(仅背景图)
|
|
|
|
|
+const generateMedia = async (sectionIndex) => {
|
|
|
|
|
+ const section = scriptData.sections[sectionIndex]
|
|
|
|
|
+ const media = section.backgroundImage
|
|
|
|
|
+
|
|
|
|
|
+ media.generating = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ const form = {
|
|
|
|
|
+ platform: 'DOU_BAO',
|
|
|
|
|
+ modelId: 56,
|
|
|
|
|
+ prompt: media.prompt,
|
|
|
|
|
+ width: 1024,
|
|
|
|
|
+ height: 768
|
|
|
|
|
+ }
|
|
|
|
|
+ const response = await ImageApi.drawImage(form)
|
|
|
|
|
+ inProgressImageMap.value[response] = {
|
|
|
|
|
+ sectionIndex
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error(`生成图片失败:`, error)
|
|
|
|
|
+ media.generating = false
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 步骤2:一键生成所有背景图
|
|
|
|
|
+const generateAllImages = async () => {
|
|
|
|
|
+ isGeneratingImages.value = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ for (let i = 0; i < scriptData.sections.length; i++) {
|
|
|
|
|
+ const section = scriptData.sections[i]
|
|
|
|
|
+ // 只处理没有背景图URL的环节
|
|
|
|
|
+ if (!section.backgroundImage.url) {
|
|
|
|
|
+ await generateMedia(i)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 检查是否有图片正在生成
|
|
|
|
|
+ const checkImagesComplete = setInterval(() => {
|
|
|
|
|
+ if (Object.keys(inProgressImageMap.value).length === 0) {
|
|
|
|
|
+ isGeneratingImages.value = false
|
|
|
|
|
+ clearInterval(checkImagesComplete)
|
|
|
|
|
+ }
|
|
|
|
|
+ }, 1000)
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('生成所有背景图失败:', error)
|
|
|
|
|
+ isGeneratingImages.value = false
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+// 轮询生成中的图片列表
|
|
|
|
|
+const refreshWatchImages = async () => {
|
|
|
|
|
+ const imageIds = Object.keys(inProgressImageMap.value).map(Number)
|
|
|
|
|
+ if (imageIds.length === 0) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const list = await ImageApi.getImageListMyByIds(imageIds)
|
|
|
|
|
+ const newWatchImages = {}
|
|
|
|
|
+ list.forEach((image) => {
|
|
|
|
|
+ const info = inProgressImageMap.value[image.id]
|
|
|
|
|
+ if (info) {
|
|
|
|
|
+ if (image.status === AiImageStatusEnum.IN_PROGRESS) {
|
|
|
|
|
+ newWatchImages[image.id] = info
|
|
|
|
|
+ } else if (image.status === AiImageStatusEnum.SUCCESS && image.picUrl) {
|
|
|
|
|
+ const section = scriptData.sections[info.sectionIndex]
|
|
|
|
|
+ section.backgroundImage.url = image.picUrl
|
|
|
|
|
+ section.backgroundImage.generating = false
|
|
|
|
|
+ } else if (image.status === AiImageStatusEnum.FAIL) {
|
|
|
|
|
+ const section = scriptData.sections[info.sectionIndex]
|
|
|
|
|
+ section.backgroundImage.generating = false
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ inProgressImageMap.value = newWatchImages
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('轮询图片状态失败:', error)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 播放音频通用函数
|
|
|
|
|
+const playAudio = (url, type) => {
|
|
|
|
|
+ if (audioState.currentAudio) {
|
|
|
|
|
+ audioState.currentAudio.pause()
|
|
|
|
|
+ audioState.currentAudio.currentTime = 0
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (audioState.isPlaying && audioState.currentUrl === url && audioState.currentType === type) {
|
|
|
|
|
+ if (audioState.currentAudio) {
|
|
|
|
|
+ audioState.currentAudio.pause()
|
|
|
|
|
+ }
|
|
|
|
|
+ audioState.isPlaying = false
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ let audio = audioInstances.get(`${type}_${url}`)
|
|
|
|
|
+ if (!audio) {
|
|
|
|
|
+ audio = new Audio(url)
|
|
|
|
|
+ audioInstances.set(`${type}_${url}`, audio)
|
|
|
|
|
+
|
|
|
|
|
+ audio.onended = () => {
|
|
|
|
|
+ audioState.isPlaying = false
|
|
|
|
|
+ audioState.currentAudio = null
|
|
|
|
|
+ audioState.currentUrl = ''
|
|
|
|
|
+ audioState.currentType = ''
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ audio.play().catch(error => {
|
|
|
|
|
+ console.error('播放失败:', error)
|
|
|
|
|
+ audioState.isPlaying = false
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ audioState.currentAudio = audio
|
|
|
|
|
+ audioState.isPlaying = true
|
|
|
|
|
+ audioState.currentUrl = url
|
|
|
|
|
+ audioState.currentType = type
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 播放语音
|
|
|
|
|
+const playVoiceover = (voiceoverUrl) => {
|
|
|
|
|
+ playAudio(voiceoverUrl, 'voice')
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 处理背景音选择变化
|
|
|
|
|
+const handleBackgroundAudioChange = (value, section) => {
|
|
|
|
|
+ if (value) {
|
|
|
|
|
+ const selectedMusic = backgroundMusicTypes.value.find(music => music.id === value)
|
|
|
|
|
+ if (selectedMusic) {
|
|
|
|
|
+ section.backgroundAudio.url = selectedMusic.url
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ section.backgroundAudio.url = ''
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 播放背景音
|
|
|
|
|
+const playBackgroundAudio = (musicType) => {
|
|
|
|
|
+ const selectedMusic = backgroundMusicTypes.value.find(music => music.id === musicType)
|
|
|
|
|
+ const audioUrl = selectedMusic ? selectedMusic.url : `https://example.com/${musicType}.mp3`
|
|
|
|
|
+ playAudio(audioUrl, 'background')
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 生成单个语音
|
|
|
|
|
+const generateVoiceover = async (sectionIndex, dialogueIndex) => {
|
|
|
|
|
+ const dialogue = scriptData.sections[sectionIndex].dialogues[dialogueIndex]
|
|
|
|
|
+
|
|
|
|
|
+ // 只处理数字人类型的对话
|
|
|
|
|
+ if (dialogue.type !== 'digital') {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ dialogue.generatingVoiceover = true
|
|
|
|
|
+
|
|
|
|
|
+ let role = digitalHumans.value.find(r => r.name === dialogue.roleName)
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 调用后端API将文本转成语音
|
|
|
|
|
+ const speechUrl = await TtsApi.convert({
|
|
|
|
|
+ roleId: Number(role.id),
|
|
|
|
|
+ content: dialogue.content
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ // 将返回的URL赋值给对话
|
|
|
|
|
+ dialogue.voiceoverUrl = speechUrl
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('生成语音失败:', error)
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ dialogue.generatingVoiceover = false
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 步骤3:一键生成所有语音
|
|
|
|
|
+const generateAllVoiceovers = async () => {
|
|
|
|
|
+ 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) {
|
|
|
|
|
+ await generateVoiceover(i, j)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('生成所有语音失败:', error)
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ isGeneratingVoiceovers.value = false
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 获取角色名称
|
|
|
|
|
+const getRoleName = (roleName) => {
|
|
|
|
|
+ const role = digitalHumans.value.find(r => r.name === roleName)
|
|
|
|
|
+ return role ? role.name : '未知角色'
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 解析 Markdown 文本
|
|
|
|
|
+const parseMarkdown = (text) => {
|
|
|
|
|
+ if (!text) return ''
|
|
|
|
|
+ return marked(text)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 步骤4:校验脚本
|
|
|
|
|
+const validateScript = () => {
|
|
|
|
|
+ let isValid = true
|
|
|
|
|
+ let message = '校验通过'
|
|
|
|
|
+
|
|
|
|
|
+ if (!scriptData.sections.every(s => s.name.trim())) {
|
|
|
|
|
+ isValid = false
|
|
|
|
|
+ message = '存在空的环节名称'
|
|
|
|
|
+ } else if (!scriptData.sections.every(s => s.backgroundImage.url)) {
|
|
|
|
|
+ isValid = false
|
|
|
|
|
+ message = '存在未生成的背景图'
|
|
|
|
|
+ } else if (!scriptData.sections.every(s =>
|
|
|
|
|
+ s.dialogues.every(d => (d.type === 'digital' && d.roleName && d.content.trim() && d.voiceoverUrl) || (d.type === 'user' && d.roleName && d.content.trim()))
|
|
|
|
|
+ )) {
|
|
|
|
|
+ isValid = false
|
|
|
|
|
+ message = '存在未完成的对话或语音'
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ isValidationPassed.value = isValid
|
|
|
|
|
+ validationMessage.value = message
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 步骤4:保存脚本
|
|
|
|
|
+const saveScript = async () => {
|
|
|
|
|
+ validateScript()
|
|
|
|
|
+
|
|
|
|
|
+ if (!isValidationPassed.value) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ console.log('scriptData===', JSON.stringify(scriptData))
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 下一步操作
|
|
|
|
|
+const nextStep = () => {
|
|
|
|
|
+ if (canProceed.value) {
|
|
|
|
|
+ if (currentStep.value === 2) {
|
|
|
|
|
+ validateScript()
|
|
|
|
|
+ }
|
|
|
|
|
+ currentStep.value++
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 显示视频预览
|
|
|
|
|
+const showVideoPreview = () => {
|
|
|
|
|
+ showVideoPreviewModal.value = true
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 关闭视频预览
|
|
|
|
|
+const closeVideoPreview = () => {
|
|
|
|
|
+ showVideoPreviewModal.value = false
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 组件挂载时
|
|
|
|
|
+onMounted(async () => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const params = {
|
|
|
|
|
+ pageOn: 1,
|
|
|
|
|
+ pageSize: 100,
|
|
|
|
|
+ category: "小学低年级",
|
|
|
|
|
+ publicStatus: true
|
|
|
|
|
+ }
|
|
|
|
|
+ const { list } = await ChatRoleApi.getMyPage(params)
|
|
|
|
|
+ params.category += "AI"
|
|
|
|
|
+ const { list:listAi } = await ChatRoleApi.getMyPage(params)
|
|
|
|
|
+ list.push(...listAi)
|
|
|
|
|
+ digitalHumans.value = list
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('获取角色数据失败:', error)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ inProgressTimer.value = setInterval(async () => {
|
|
|
|
|
+ await refreshWatchImages()
|
|
|
|
|
+ }, 3000)
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+// 组件卸载时
|
|
|
|
|
+onUnmounted(() => {
|
|
|
|
|
+ if (inProgressTimer.value) {
|
|
|
|
|
+ clearInterval(inProgressTimer.value)
|
|
|
|
|
+ }
|
|
|
|
|
+})
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<style scoped>
|
|
|
|
|
+.course-script-editor {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 100vh;
|
|
|
|
|
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ background-color: #f8f9fa;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 主容器样式 */
|
|
|
|
|
+.main-container {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 左侧步骤指示器样式 */
|
|
|
|
|
+.steps-sidebar {
|
|
|
|
|
+ width: 200px;
|
|
|
|
|
+ background-color: #f5f7fa;
|
|
|
|
|
+ border-right: 1px solid #ebeef5;
|
|
|
|
|
+ padding: 30px 0;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 右侧内容区域样式 */
|
|
|
|
|
+.content-area {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 步骤内容样式 */
|
|
|
|
|
+.step-content {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ padding: 20px;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ background-color: white;
|
|
|
|
|
+ border-radius: 0;
|
|
|
|
|
+ box-shadow: none;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 步骤1垂直居中 */
|
|
|
|
|
+.step-content:has(.ai-input-container) {
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 编辑器工具栏样式 - 固定顶部 */
|
|
|
|
|
+.editor-toolbar.sticky-top {
|
|
|
|
|
+ position: sticky;
|
|
|
|
|
+ top: 0;
|
|
|
|
|
+ z-index: 100;
|
|
|
|
|
+ margin-top: -20px;
|
|
|
|
|
+ margin-left: -20px;
|
|
|
|
|
+ margin-right: -20px;
|
|
|
|
|
+ border-radius: 0;
|
|
|
|
|
+ border-bottom: 1px solid #ebeef5;
|
|
|
|
|
+ background: rgba(250, 250, 250, 0.95);
|
|
|
|
|
+ backdrop-filter: blur(10px);
|
|
|
|
|
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 脚本编辑器样式 - 可滚动 */
|
|
|
|
|
+.script-editor {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ overflow-y: auto;
|
|
|
|
|
+ margin-top: 20px;
|
|
|
|
|
+ padding-right: 10px;
|
|
|
|
|
+ background-color: white;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ padding: 20px;
|
|
|
|
|
+ box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.script-editor::-webkit-scrollbar {
|
|
|
|
|
+ width: 6px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.script-editor::-webkit-scrollbar-track {
|
|
|
|
|
+ background: #f1f1f1;
|
|
|
|
|
+ border-radius: 3px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.script-editor::-webkit-scrollbar-thumb {
|
|
|
|
|
+ background: #c1c1c1;
|
|
|
|
|
+ border-radius: 3px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.script-editor::-webkit-scrollbar-thumb:hover {
|
|
|
|
|
+ background: #a1a1a1;
|
|
|
|
|
+}
|
|
|
|
|
+/* 步骤导航 */
|
|
|
|
|
+.steps-container {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 20px;
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ .step-item {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ .step-circle {
|
|
|
|
|
+ width: 32px;
|
|
|
|
|
+ height: 32px;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ font-weight: bold;
|
|
|
|
|
+ margin-bottom: 8px;
|
|
|
|
|
+ }
|
|
|
|
|
+ .step-title {
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ padding: 0 20px;
|
|
|
|
|
+ }
|
|
|
|
|
+ &.process {
|
|
|
|
|
+ .step-circle {
|
|
|
|
|
+ background-color: #409EFF;
|
|
|
|
|
+ color: white;
|
|
|
|
|
+ }
|
|
|
|
|
+ .step-title {
|
|
|
|
|
+ color: #409EFF;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ &.wait {
|
|
|
|
|
+ .step-circle {
|
|
|
|
|
+ background-color: #E4E7ED;
|
|
|
|
|
+ color: #C0C4CC;
|
|
|
|
|
+ }
|
|
|
|
|
+ .step-title {
|
|
|
|
|
+ color: #C0C4CC;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ &.finish {
|
|
|
|
|
+ .step-circle {
|
|
|
|
|
+ background-color: #67C23A;
|
|
|
|
|
+ color: white;
|
|
|
|
|
+ }
|
|
|
|
|
+ .step-title {
|
|
|
|
|
+ color: #67C23A;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ .step-line {
|
|
|
|
|
+ width: 2px;
|
|
|
|
|
+ height: 40px;
|
|
|
|
|
+ background-color: #E4E7ED;
|
|
|
|
|
+ &.active {
|
|
|
|
|
+ background-color: #409EFF;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 步骤内容 */
|
|
|
|
|
+.step-content {
|
|
|
|
|
+ background-color: white;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ padding: 20px;
|
|
|
|
|
+ box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 对话类型标签 */
|
|
|
|
|
+.dialogue-type-tag {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ top: -10px;
|
|
|
|
|
+ left: 10px;
|
|
|
|
|
+ padding: 2px 8px;
|
|
|
|
|
+ border-radius: 10px;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ color: white;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.dialogue-type-tag.digital {
|
|
|
|
|
+ background-color: #409EFF;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.dialogue-type-tag.user {
|
|
|
|
|
+ background-color: #67C23A;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 对话项样式 */
|
|
|
|
|
+.dialogue-item {
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ background-color: #f9f9f9;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ padding: 20px;
|
|
|
|
|
+ margin-bottom: 15px;
|
|
|
|
|
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
|
|
|
+ transition: all 0.3s ease;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.dialogue-item:hover {
|
|
|
|
|
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.dialogue-item.digital {
|
|
|
|
|
+ border-left: 4px solid #409EFF;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.dialogue-item.user {
|
|
|
|
|
+ border-left: 4px solid #67C23A;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 对话头部 */
|
|
|
|
|
+.dialogue-header {
|
|
|
|
|
+ margin-bottom: 10px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 对话行 */
|
|
|
|
|
+.dialogue-row {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ gap: 15px;
|
|
|
|
|
+ align-items: flex-start;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 角色选择 */
|
|
|
|
|
+.dialogue-role-select {
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 回复角色选择 */
|
|
|
|
|
+.dialogue-reply-select {
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 内容容器 */
|
|
|
|
|
+.dialogue-content-container {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ min-width: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+/* 添加对话按钮容器 */
|
|
|
|
|
+.add-dialogue-buttons {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+ margin-top: 15px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 添加对话按钮 */
|
|
|
|
|
+.add-dialogue-btn {
|
|
|
|
|
+ padding: 8px 16px;
|
|
|
|
|
+ border: none;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ transition: all 0.3s ease;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ gap: 5px;
|
|
|
|
|
+ min-width: 120px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.add-dialogue-btn.digital {
|
|
|
|
|
+ background-color: #83c2ff;
|
|
|
|
|
+ color: white;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.add-dialogue-btn.digital:hover {
|
|
|
|
|
+ background-color: #409EFF;
|
|
|
|
|
+ border-color: #4aa2fd;
|
|
|
|
|
+ color: white;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.add-dialogue-btn.user {
|
|
|
|
|
+ background-color: #85ce61;
|
|
|
|
|
+ color: white;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.add-dialogue-btn.user:hover {
|
|
|
|
|
+ background-color: #67C23A;
|
|
|
|
|
+ border-color: #67C23A;
|
|
|
|
|
+ color: white;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 预览对话样式 */
|
|
|
|
|
+.preview-dialogue {
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ margin-bottom: 20px;
|
|
|
|
|
+ padding: 15px;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ background-color: #f9f9f9;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.preview-dialogue.digital {
|
|
|
|
|
+ border-left: 4px solid #409EFF;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.preview-dialogue.user {
|
|
|
|
|
+ border-left: 4px solid #67C23A;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.preview-dialogue .dialogue-header {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ gap: 0;
|
|
|
|
|
+ margin-bottom: 10px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.preview-dialogue .dialogue-header-left {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 2px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.preview-dialogue .dialogue-type-tag {
|
|
|
|
|
+ position: static;
|
|
|
|
|
+ margin-right: 2px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 回复信息 */
|
|
|
|
|
+.reply-info {
|
|
|
|
|
+ margin-top: 10px;
|
|
|
|
|
+ padding-top: 10px;
|
|
|
|
|
+ border-top: 1px solid #e8e8e8;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ color: #666;
|
|
|
|
|
+ font-style: italic;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* AI输入容器 */
|
|
|
|
|
+.ai-input-container {
|
|
|
|
|
+ max-width: 800px;
|
|
|
|
|
+ margin: 0 auto;
|
|
|
|
|
+ padding: 30px;
|
|
|
|
|
+ background: white;
|
|
|
|
|
+ border-radius: 16px;
|
|
|
|
|
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ min-height: 500px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.selection-group {
|
|
|
|
|
+ margin-bottom: 30px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.ai-input-wrapper {
|
|
|
|
|
+ margin-bottom: 30px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.ai-input-container button.generate-btn.primary {
|
|
|
|
|
+ margin-top: 30px;
|
|
|
|
|
+ align-self: center;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.ai-input-wrapper {
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ margin-bottom: 30px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.ai-icon {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ top: 20px;
|
|
|
|
|
+ left: 20px;
|
|
|
|
|
+ font-size: 28px;
|
|
|
|
|
+ color: #409EFF;
|
|
|
|
|
+ background: rgba(64, 158, 255, 0.1);
|
|
|
|
|
+ width: 48px;
|
|
|
|
|
+ height: 48px;
|
|
|
|
|
+ border-radius: 12px;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ animation: pulse 2s infinite;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+@keyframes pulse {
|
|
|
|
|
+ 0% {
|
|
|
|
|
+ box-shadow: 0 0 0 0 rgba(64, 158, 255, 0.4);
|
|
|
|
|
+ }
|
|
|
|
|
+ 70% {
|
|
|
|
|
+ box-shadow: 0 0 0 10px rgba(64, 158, 255, 0);
|
|
|
|
|
+ }
|
|
|
|
|
+ 100% {
|
|
|
|
|
+ box-shadow: 0 0 0 0 rgba(64, 158, 255, 0);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.ai-textarea {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ min-height: 140px;
|
|
|
|
|
+ padding: 24px 24px 24px 84px;
|
|
|
|
|
+ border: 2px solid #e8e8e8;
|
|
|
|
|
+ border-radius: 16px;
|
|
|
|
|
+ font-size: 16px;
|
|
|
|
|
+ resize: vertical;
|
|
|
|
|
+ transition: all 0.3s ease;
|
|
|
|
|
+ background: #fafafa;
|
|
|
|
|
+ line-height: 1.6;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.ai-textarea:focus {
|
|
|
|
|
+ outline: none;
|
|
|
|
|
+ border-color: #409EFF;
|
|
|
|
|
+ background: white;
|
|
|
|
|
+ box-shadow: 0 0 0 3px rgba(64, 158, 255, 0.1);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.selection-group {
|
|
|
|
|
+ display: grid;
|
|
|
|
|
+ grid-template-columns: 1fr 1fr;
|
|
|
|
|
+ gap: 24px;
|
|
|
|
|
+ margin-bottom: 30px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.selection-item {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.selection-item label {
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #333;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ text-transform: uppercase;
|
|
|
|
|
+ letter-spacing: 0.5px;
|
|
|
|
|
+ color: #666;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.selection-item select {
|
|
|
|
|
+ padding: 14px 16px;
|
|
|
|
|
+ border: 2px solid #e8e8e8;
|
|
|
|
|
+ border-radius: 10px;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ transition: all 0.3s ease;
|
|
|
|
|
+ background: white;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.selection-item select:focus {
|
|
|
|
|
+ outline: none;
|
|
|
|
|
+ border-color: #409EFF;
|
|
|
|
|
+ box-shadow: 0 0 0 3px rgba(64, 158, 255, 0.1);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.multi-select {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-wrap: wrap;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+ padding: 12px;
|
|
|
|
|
+ border: 2px solid #e8e8e8;
|
|
|
|
|
+ border-radius: 10px;
|
|
|
|
|
+ background: white;
|
|
|
|
|
+ transition: all 0.3s ease;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.multi-select:focus-within {
|
|
|
|
|
+ border-color: #409EFF;
|
|
|
|
|
+ box-shadow: 0 0 0 3px rgba(64, 158, 255, 0.1);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.select-option {
|
|
|
|
|
+ padding: 8px 16px;
|
|
|
|
|
+ background: #f5f5f5;
|
|
|
|
|
+ border-radius: 20px;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ transition: all 0.2s ease;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ border: 1px solid transparent;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.select-option:hover {
|
|
|
|
|
+ background: #e6f7ff;
|
|
|
|
|
+ border-color: #91d5ff;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.select-option.selected {
|
|
|
|
|
+ background: #409EFF;
|
|
|
|
|
+ color: white;
|
|
|
|
|
+ border-color: #409EFF;
|
|
|
|
|
+ box-shadow: 0 2px 8px rgba(64, 158, 255, 0.3);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 加载遮挡层样式 */
|
|
|
|
|
+.loading-overlay {
|
|
|
|
|
+ position: fixed;
|
|
|
|
|
+ top: 0;
|
|
|
|
|
+ left: 0;
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ background-color: rgba(0, 0, 0, 0.1);
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ z-index: 9999;
|
|
|
|
|
+ pointer-events: auto;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.loading-content {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 16px;
|
|
|
|
|
+ background: white;
|
|
|
|
|
+ padding: 32px;
|
|
|
|
|
+ border-radius: 12px;
|
|
|
|
|
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
|
|
|
|
|
+ pointer-events: auto;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.loading-spinner {
|
|
|
|
|
+ width: 48px;
|
|
|
|
|
+ height: 48px;
|
|
|
|
|
+ border: 4px solid #f3f3f3;
|
|
|
|
|
+ border-top: 4px solid #409EFF;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ animation: spin 1s linear infinite;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+@keyframes spin {
|
|
|
|
|
+ 0% { transform: rotate(0deg); }
|
|
|
|
|
+ 100% { transform: rotate(360deg); }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.loading-text {
|
|
|
|
|
+ font-size: 16px;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ color: #333;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 按钮样式 */
|
|
|
|
|
+.generate-btn {
|
|
|
|
|
+ padding: 14px 8px;
|
|
|
|
|
+ border: none;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ font-weight: 400;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ transition: all 0.3s ease;
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.generate-btn.primary {
|
|
|
|
|
+ background: linear-gradient(135deg, #409EFF, #1890ff);
|
|
|
|
|
+ color: white;
|
|
|
|
|
+ box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.generate-btn.primary:hover {
|
|
|
|
|
+ transform: translateY(-2px);
|
|
|
|
|
+ box-shadow: 0 6px 16px rgba(64, 158, 255, 0.4);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.generate-btn.primary:disabled {
|
|
|
|
|
+ background: #91c5f7;
|
|
|
|
|
+ cursor: not-allowed;
|
|
|
|
|
+ transform: none;
|
|
|
|
|
+ box-shadow: none;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.generate-btn.small {
|
|
|
|
|
+ padding: 8px 16px;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.generate-btn.small:hover {
|
|
|
|
|
+ transform: translateY(-1px);
|
|
|
|
|
+ box-shadow: 0 3px 8px rgba(64, 158, 255, 0.3);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.generate-btn.small:disabled {
|
|
|
|
|
+ background: #91c5f7;
|
|
|
|
|
+ color: white;
|
|
|
|
|
+ cursor: not-allowed;
|
|
|
|
|
+ transform: none;
|
|
|
|
|
+ box-shadow: none;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.toolbar-btn:disabled {
|
|
|
|
|
+ background: #f0f0f0;
|
|
|
|
|
+ color: #c0c0c0;
|
|
|
|
|
+ border-color: #e0e0e0;
|
|
|
|
|
+ cursor: not-allowed;
|
|
|
|
|
+ transform: none;
|
|
|
|
|
+ box-shadow: none;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.toolbar-btn:disabled:hover {
|
|
|
|
|
+ background: #f0f0f0;
|
|
|
|
|
+ color: #c0c0c0;
|
|
|
|
|
+ border-color: #e0e0e0;
|
|
|
|
|
+ transform: none;
|
|
|
|
|
+ box-shadow: none;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 编辑器工具栏 */
|
|
|
|
|
+.editor-toolbar {
|
|
|
|
|
+ position: sticky;
|
|
|
|
|
+ top: 20px;
|
|
|
|
|
+ z-index: 100;
|
|
|
|
|
+ margin-bottom: 30px;
|
|
|
|
|
+ padding: 16px 20px;
|
|
|
|
|
+ background: rgba(250, 250, 250, 0.95);
|
|
|
|
|
+ backdrop-filter: blur(10px);
|
|
|
|
|
+ border-radius: 12px;
|
|
|
|
|
+ border: 1px solid #e8e8e8;
|
|
|
|
|
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
|
|
|
|
+ overflow-x: auto;
|
|
|
|
|
+ overflow-y: hidden;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.editor-toolbar::-webkit-scrollbar {
|
|
|
|
|
+ height: 4px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.editor-toolbar::-webkit-scrollbar-track {
|
|
|
|
|
+ background: #f1f1f1;
|
|
|
|
|
+ border-radius: 2px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.editor-toolbar::-webkit-scrollbar-thumb {
|
|
|
|
|
+ background: #c1c1c1;
|
|
|
|
|
+ border-radius: 2px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.editor-toolbar::-webkit-scrollbar-thumb:hover {
|
|
|
|
|
+ background: #a1a1a1;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.toolbar-btn {
|
|
|
|
|
+ padding: 12px 24px;
|
|
|
|
|
+ margin: 0 12px;
|
|
|
|
|
+ background: white;
|
|
|
|
|
+ border: 2px solid #e8e8e8;
|
|
|
|
|
+ border-radius: 10px;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ transition: all 0.3s ease;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ color: #333;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.toolbar-btn:hover {
|
|
|
|
|
+ background: #409EFF;
|
|
|
|
|
+ color: white;
|
|
|
|
|
+ border-color: #409EFF;
|
|
|
|
|
+ transform: translateY(-1px);
|
|
|
|
|
+ box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 脚本编辑器 */
|
|
|
|
|
+.script-section {
|
|
|
|
|
+ margin-top: 40px;
|
|
|
|
|
+ margin-bottom: 40px;
|
|
|
|
|
+ padding: 5px 24px 24px;
|
|
|
|
|
+ background: white;
|
|
|
|
|
+ border-radius: 16px;
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
|
|
|
|
+ border: 1px solid #f0f0f0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.section-header {
|
|
|
|
|
+ margin-bottom: 24px;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 16px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.section-name-container {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+ margin-top: 16px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.remove-section-btn {
|
|
|
|
|
+ background: #ff4d4f;
|
|
|
|
|
+ border: none;
|
|
|
|
|
+ color: white;
|
|
|
|
|
+ font-size: 16px;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ padding: 0;
|
|
|
|
|
+ width: 28px;
|
|
|
|
|
+ height: 28px;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ border-radius: 6px;
|
|
|
|
|
+ transition: all 0.3s ease;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.remove-section-btn:hover {
|
|
|
|
|
+ background: #ff7875;
|
|
|
|
|
+ transform: scale(1.1);
|
|
|
|
|
+ box-shadow: 0 2px 8px rgba(255, 77, 79, 0.3);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.section-name-label {
|
|
|
|
|
+ padding: 2px 15px;
|
|
|
|
|
+ border-radius: 10px;
|
|
|
|
|
+ font-size: 18px;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ color: white;
|
|
|
|
|
+ background-color: #c18484;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ height: 44px;
|
|
|
|
|
+ box-sizing: border-box;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.section-title {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ padding: 14px 16px;
|
|
|
|
|
+ border: 2px solid #e8e8e8;
|
|
|
|
|
+ border-radius: 10px;
|
|
|
|
|
+ font-size: 16px;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ transition: all 0.3s ease;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.section-title:focus {
|
|
|
|
|
+ outline: none;
|
|
|
|
|
+ border-color: #409EFF;
|
|
|
|
|
+ box-shadow: 0 0 0 3px rgba(64, 158, 255, 0.1);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.media-controls {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ background: linear-gradient(135deg, #f8f9fa, #ffffff);
|
|
|
|
|
+ padding: 16px 20px;
|
|
|
|
|
+ border-radius: 10px;
|
|
|
|
|
+ border: 1px solid #e8e8e8;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.media-item {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+ margin-right: 50px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.media-input-group {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.media-label {
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+ color: #666;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.media-prompt {
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ transition: all 0.3s ease;
|
|
|
|
|
+ width: 300px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.media-prompt .el-textarea {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ min-height: 60px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.media-prompt .el-textarea__inner {
|
|
|
|
|
+ padding: 8px 12px;
|
|
|
|
|
+ border: 2px solid #e8e8e8;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ transition: all 0.3s ease;
|
|
|
|
|
+ min-height: 60px;
|
|
|
|
|
+ resize: none;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.media-prompt .el-textarea__inner:focus {
|
|
|
|
|
+ outline: none;
|
|
|
|
|
+ border-color: #409EFF;
|
|
|
|
|
+ box-shadow: 0 0 0 3px rgba(64, 158, 255, 0.1);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.media-preview {
|
|
|
|
|
+ margin-top: 12px;
|
|
|
|
|
+ margin-left: 20px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.media-preview img {
|
|
|
|
|
+ max-width: 180px;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
|
|
|
+ transition: transform 0.3s ease;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.media-preview img:hover {
|
|
|
|
|
+ transform: scale(1.05);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.dialogues-container {
|
|
|
|
|
+ margin-top: 48px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.dialogue-item {
|
|
|
|
|
+ margin-bottom: 20px;
|
|
|
|
|
+ padding: 20px;
|
|
|
|
|
+ background: #fafafa;
|
|
|
|
|
+ border-radius: 12px;
|
|
|
|
|
+ border: 1px solid #e8e8e8;
|
|
|
|
|
+ transition: all 0.3s ease;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.dialogue-item:hover {
|
|
|
|
|
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
|
|
|
|
+ transform: translateY(-2px);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.dialogue-row {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: flex-start;
|
|
|
|
|
+ gap: 16px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.dialogue-role-select {
|
|
|
|
|
+ min-width: 140px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.dialogue-role-select select {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ padding: 10px 12px;
|
|
|
|
|
+ border: 2px solid #e8e8e8;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ transition: all 0.3s ease;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.dialogue-role-select select:focus {
|
|
|
|
|
+ outline: none;
|
|
|
|
|
+ border-color: #409EFF;
|
|
|
|
|
+ box-shadow: 0 0 0 3px rgba(64, 158, 255, 0.1);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.dialogue-content-container {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.dialogue-content:focus {
|
|
|
|
|
+ outline: none;
|
|
|
|
|
+ border-color: #409EFF;
|
|
|
|
|
+ box-shadow: 0 0 0 3px rgba(64, 158, 255, 0.1);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.remove-btn {
|
|
|
|
|
+ background: #ff4d4f;
|
|
|
|
|
+ border: none;
|
|
|
|
|
+ color: white;
|
|
|
|
|
+ font-size: 16px;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ padding: 0;
|
|
|
|
|
+ width: 28px;
|
|
|
|
|
+ height: 28px;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ border-radius: 6px;
|
|
|
|
|
+ transition: all 0.3s ease;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.remove-btn:hover {
|
|
|
|
|
+ background: #ff7875;
|
|
|
|
|
+ transform: scale(1.1);
|
|
|
|
|
+ box-shadow: 0 2px 8px rgba(255, 77, 79, 0.3);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.dialogue-header {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ margin-bottom: 8px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.dialogue-text {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ font-size: 16px;
|
|
|
|
|
+ line-height: 1.6;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.generate-btn.small {
|
|
|
|
|
+ padding: 8px;
|
|
|
|
|
+ background: linear-gradient(135deg, #409EFF, #1890ff);
|
|
|
|
|
+ color: white;
|
|
|
|
|
+ border: none;
|
|
|
|
|
+ border-radius: 6px;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ transition: all 0.3s ease;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ height: 28px;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.generate-btn.small:hover {
|
|
|
|
|
+ transform: scale(1.1);
|
|
|
|
|
+ box-shadow: 0 2px 8px rgba(64, 158, 255, 0.3);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.play-btn {
|
|
|
|
|
+ padding: 8px;
|
|
|
|
|
+ background: #52c41a;
|
|
|
|
|
+ color: white;
|
|
|
|
|
+ border: none;
|
|
|
|
|
+ border-radius: 6px;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ transition: all 0.3s ease;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ width: 28px;
|
|
|
|
|
+ height: 28px;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.play-btn:hover {
|
|
|
|
|
+ background: #73d13d;
|
|
|
|
|
+ transform: scale(1.1);
|
|
|
|
|
+ box-shadow: 0 2px 8px rgba(82, 196, 26, 0.3);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.play-btn.small {
|
|
|
|
|
+ padding: 8px;
|
|
|
|
|
+ width: 28px;
|
|
|
|
|
+ height: 28px;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.voice-icon {
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ line-height: 1;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.play-icon {
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ line-height: 1;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.action-buttons {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+ margin-left: 8px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.action-buttons-row {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: flex-end;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+.add-dialogue-btn,
|
|
|
|
|
+.add-section-btn {
|
|
|
|
|
+ padding: 12px 24px;
|
|
|
|
|
+ background: white;
|
|
|
|
|
+ border: 2px dashed #d9d9d9;
|
|
|
|
|
+ border-radius: 10px;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ transition: all 0.3s ease;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ color: #666;
|
|
|
|
|
+ margin-top: 16px;
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.add-dialogue-btn:hover,
|
|
|
|
|
+.add-section-btn:hover {
|
|
|
|
|
+ border-color: #c18484;
|
|
|
|
|
+ color: white;
|
|
|
|
|
+ background: rgba(193, 132, 132, 0.6);
|
|
|
|
|
+ transform: translateY(-1px);
|
|
|
|
|
+ box-shadow: 0 4px 12px rgba(64, 158, 255, 0.15);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+/* 预览页面 */
|
|
|
|
|
+.preview-container {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ background: white;
|
|
|
|
|
+ border-radius: 16px;
|
|
|
|
|
+ padding: 30px;
|
|
|
|
|
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.preview-content {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ overflow-y: auto;
|
|
|
|
|
+ margin-top: 20px;
|
|
|
|
|
+ margin-bottom: 30px;
|
|
|
|
|
+ padding-right: 10px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.preview-content::-webkit-scrollbar {
|
|
|
|
|
+ width: 6px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.preview-content::-webkit-scrollbar-track {
|
|
|
|
|
+ background: #f1f1f1;
|
|
|
|
|
+ border-radius: 3px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.preview-content::-webkit-scrollbar-thumb {
|
|
|
|
|
+ background: #c1c1c1;
|
|
|
|
|
+ border-radius: 3px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.preview-content::-webkit-scrollbar-thumb:hover {
|
|
|
|
|
+ background: #a1a1a1;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.preview-actions {
|
|
|
|
|
+ margin-top: auto;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: flex-end;
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.validation-result {
|
|
|
|
|
+ padding: 16px 20px;
|
|
|
|
|
+ border-radius: 10px;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.validation-result.valid {
|
|
|
|
|
+ background: #f6ffed;
|
|
|
|
|
+ border: 2px solid #b7eb8f;
|
|
|
|
|
+ color: #52c41a;
|
|
|
|
|
+ box-shadow: 0 2px 8px rgba(82, 196, 26, 0.1);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.validation-result.invalid {
|
|
|
|
|
+ background: #fff2f0;
|
|
|
|
|
+ border: 2px solid #ffccc7;
|
|
|
|
|
+ color: #ff4d4f;
|
|
|
|
|
+ box-shadow: 0 2px 8px rgba(255, 77, 79, 0.1);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.preview-section {
|
|
|
|
|
+ margin-bottom: 40px;
|
|
|
|
|
+ padding: 40px;
|
|
|
|
|
+ border-radius: 12px;
|
|
|
|
|
+ border: 1px solid #e8e8e8;
|
|
|
|
|
+ min-height: 300px;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.preview-section h4 {
|
|
|
|
|
+ margin-top: 0;
|
|
|
|
|
+ color: #333;
|
|
|
|
|
+ font-size: 18px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ margin-bottom: 20px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.preview-section-content {
|
|
|
|
|
+ background: rgba(255, 255, 255, 0.2);
|
|
|
|
|
+ padding: 20px;
|
|
|
|
|
+ border-radius: 12px;
|
|
|
|
|
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
|
|
|
+ backdrop-filter: blur(5px);
|
|
|
|
|
+ width: 80% ;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.preview-media {
|
|
|
|
|
+ margin: 20px 0;
|
|
|
|
|
+ padding: 16px;
|
|
|
|
|
+ background: rgba(255, 255, 255, 0.8);
|
|
|
|
|
+ border-radius: 10px;
|
|
|
|
|
+ border: 1px solid #e8e8e8;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ flex-wrap: wrap;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.preview-media-left {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.preview-media-right {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.preview-image img {
|
|
|
|
|
+ max-width: 250px;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
|
|
|
+ transition: transform 0.3s ease;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.preview-image img:hover {
|
|
|
|
|
+ transform: scale(1.05);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.preview-audio{
|
|
|
|
|
+ float: right;
|
|
|
|
|
+ margin-right: 20px;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.preview-dialogue {
|
|
|
|
|
+ margin-bottom: 20px;
|
|
|
|
|
+ padding: 16px;
|
|
|
|
|
+ background: white;
|
|
|
|
|
+ border-radius: 10px;
|
|
|
|
|
+ border: 1px solid #e8e8e8;
|
|
|
|
|
+ transition: all 0.3s ease;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.dialogue-header {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ margin-bottom: 8px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.preview-dialogue:hover {
|
|
|
|
|
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.dialogue-role {
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #409EFF;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.dialogue-text {
|
|
|
|
|
+ line-height: 1.6;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ color: #333;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* Markdown 样式 */
|
|
|
|
|
+.dialogue-text h1,
|
|
|
|
|
+.dialogue-text h2,
|
|
|
|
|
+.dialogue-text h3,
|
|
|
|
|
+.dialogue-text h4,
|
|
|
|
|
+.dialogue-text h5,
|
|
|
|
|
+.dialogue-text h6 {
|
|
|
|
|
+ margin: 16px 0 8px 0;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ line-height: 1.2;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.dialogue-text h1 { font-size: 24px; }
|
|
|
|
|
+.dialogue-text h2 { font-size: 20px; }
|
|
|
|
|
+.dialogue-text h3 { font-size: 18px; }
|
|
|
|
|
+.dialogue-text h4 { font-size: 16px; }
|
|
|
|
|
+.dialogue-text h5 { font-size: 14px; }
|
|
|
|
|
+.dialogue-text h6 { font-size: 12px; }
|
|
|
|
|
+
|
|
|
|
|
+.dialogue-text p {
|
|
|
|
|
+ margin: 8px 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.dialogue-text ul,
|
|
|
|
|
+.dialogue-text ol {
|
|
|
|
|
+ margin: 8px 0;
|
|
|
|
|
+ padding-left: 24px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.dialogue-text li {
|
|
|
|
|
+ margin: 4px 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.dialogue-text strong {
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.dialogue-text em {
|
|
|
|
|
+ font-style: italic;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.dialogue-text a {
|
|
|
|
|
+ color: #409EFF;
|
|
|
|
|
+ text-decoration: none;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.dialogue-text a:hover {
|
|
|
|
|
+ text-decoration: underline;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.dialogue-text code {
|
|
|
|
|
+ background: #f5f5f5;
|
|
|
|
|
+ padding: 2px 4px;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ font-family: 'Courier New', Courier, monospace;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.dialogue-text pre {
|
|
|
|
|
+ background: #f5f5f5;
|
|
|
|
|
+ padding: 12px;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ overflow-x: auto;
|
|
|
|
|
+ margin: 12px 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.dialogue-text pre code {
|
|
|
|
|
+ background: none;
|
|
|
|
|
+ padding: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.voiceover-preview {
|
|
|
|
|
+ margin-top: 12px;
|
|
|
|
|
+ padding-top: 12px;
|
|
|
|
|
+ border-top: 1px solid #f0f0f0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.preview-actions {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ gap: 20px;
|
|
|
|
|
+ padding-top: 24px;
|
|
|
|
|
+ border-top: 1px solid #e8e8e8;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.primary-btn,
|
|
|
|
|
+.secondary-btn {
|
|
|
|
|
+ padding: 14px 32px;
|
|
|
|
|
+ border: none;
|
|
|
|
|
+ border-radius: 12px;
|
|
|
|
|
+ font-size: 16px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ transition: all 0.3s ease;
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.primary-btn {
|
|
|
|
|
+ background: linear-gradient(135deg, #409EFF, #1890ff);
|
|
|
|
|
+ color: white;
|
|
|
|
|
+ box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.primary-btn:hover {
|
|
|
|
|
+ transform: translateY(-2px);
|
|
|
|
|
+ box-shadow: 0 6px 16px rgba(64, 158, 255, 0.4);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.primary-btn:disabled {
|
|
|
|
|
+ background: #91c5f7;
|
|
|
|
|
+ cursor: not-allowed;
|
|
|
|
|
+ transform: none;
|
|
|
|
|
+ box-shadow: none;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.secondary-btn {
|
|
|
|
|
+ background: white;
|
|
|
|
|
+ color: #333;
|
|
|
|
|
+ border: 2px solid #e8e8e8;
|
|
|
|
|
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.secondary-btn:hover {
|
|
|
|
|
+ background: #fafafa;
|
|
|
|
|
+ border-color: #409EFF;
|
|
|
|
|
+ color: #409EFF;
|
|
|
|
|
+ transform: translateY(-2px);
|
|
|
|
|
+ box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
|
|
|
|
|
+}
|
|
|
|
|
+</style>
|