修复部分BUG 前端页面添加英文
This commit is contained in:
@@ -14,12 +14,14 @@ import (
|
||||
|
||||
type ImageGenerationHandler struct {
|
||||
imageService *services.ImageGenerationService
|
||||
taskService *services.TaskService
|
||||
log *logger.Logger
|
||||
}
|
||||
|
||||
func NewImageGenerationHandler(db *gorm.DB, cfg *config.Config, log *logger.Logger, transferService *services.ResourceTransferService, localStorage *storage.LocalStorage) *ImageGenerationHandler {
|
||||
return &ImageGenerationHandler{
|
||||
imageService: services.NewImageGenerationService(db, transferService, localStorage, log),
|
||||
taskService: services.NewTaskService(db, log),
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
@@ -71,18 +73,57 @@ func (h *ImageGenerationHandler) GetBackgroundsForEpisode(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (h *ImageGenerationHandler) ExtractBackgroundsForEpisode(c *gin.Context) {
|
||||
|
||||
episodeID := c.Param("episode_id")
|
||||
|
||||
// 同步执行场景提取
|
||||
backgrounds, err := h.imageService.ExtractBackgroundsForEpisode(episodeID)
|
||||
// 创建异步任务
|
||||
task, err := h.taskService.CreateTask("background_extraction", episodeID)
|
||||
if err != nil {
|
||||
h.log.Errorw("Failed to extract backgrounds", "error", err)
|
||||
h.log.Errorw("Failed to create task", "error", err)
|
||||
response.InternalError(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, backgrounds)
|
||||
// 启动后台goroutine处理
|
||||
go h.processBackgroundExtraction(task.ID, episodeID)
|
||||
|
||||
// 立即返回任务ID
|
||||
response.Success(c, gin.H{
|
||||
"task_id": task.ID,
|
||||
"status": "pending",
|
||||
"message": "场景提取任务已创建,正在后台处理...",
|
||||
})
|
||||
}
|
||||
|
||||
// processBackgroundExtraction 后台处理场景提取
|
||||
func (h *ImageGenerationHandler) processBackgroundExtraction(taskID, episodeID string) {
|
||||
h.log.Infow("Starting background extraction", "task_id", taskID, "episode_id", episodeID)
|
||||
|
||||
// 更新任务状态为处理中
|
||||
if err := h.taskService.UpdateTaskStatus(taskID, "processing", 10, "开始提取场景..."); err != nil {
|
||||
h.log.Errorw("Failed to update task status", "error", err)
|
||||
}
|
||||
|
||||
// 调用实际的提取逻辑
|
||||
backgrounds, err := h.imageService.ExtractBackgroundsForEpisode(episodeID)
|
||||
if err != nil {
|
||||
h.log.Errorw("Failed to extract backgrounds", "error", err, "task_id", taskID)
|
||||
if updateErr := h.taskService.UpdateTaskError(taskID, err); updateErr != nil {
|
||||
h.log.Errorw("Failed to update task error", "error", updateErr)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 更新任务结果
|
||||
result := gin.H{
|
||||
"backgrounds": backgrounds,
|
||||
"total": len(backgrounds),
|
||||
}
|
||||
if err := h.taskService.UpdateTaskResult(taskID, result); err != nil {
|
||||
h.log.Errorw("Failed to update task result", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
h.log.Infow("Background extraction completed", "task_id", taskID, "total", len(backgrounds))
|
||||
}
|
||||
|
||||
func (h *ImageGenerationHandler) BatchGenerateForEpisode(c *gin.Context) {
|
||||
|
||||
@@ -11,12 +11,14 @@ import (
|
||||
|
||||
type ScriptGenerationHandler struct {
|
||||
scriptService *services.ScriptGenerationService
|
||||
taskService *services.TaskService
|
||||
log *logger.Logger
|
||||
}
|
||||
|
||||
func NewScriptGenerationHandler(db *gorm.DB, cfg *config.Config, log *logger.Logger) *ScriptGenerationHandler {
|
||||
return &ScriptGenerationHandler{
|
||||
scriptService: services.NewScriptGenerationService(db, log),
|
||||
taskService: services.NewTaskService(db, log),
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
@@ -46,15 +48,55 @@ func (h *ScriptGenerationHandler) GenerateCharacters(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 同步执行角色生成
|
||||
characters, err := h.scriptService.GenerateCharacters(&req)
|
||||
// 创建异步任务
|
||||
task, err := h.taskService.CreateTask("character_generation", req.DramaID)
|
||||
if err != nil {
|
||||
h.log.Errorw("Failed to generate characters", "error", err)
|
||||
response.InternalError(c, "生成角色失败")
|
||||
h.log.Errorw("Failed to create task", "error", err)
|
||||
response.InternalError(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, characters)
|
||||
// 启动后台goroutine处理
|
||||
go h.processCharacterGeneration(task.ID, &req)
|
||||
|
||||
// 立即返回任务ID
|
||||
response.Success(c, gin.H{
|
||||
"task_id": task.ID,
|
||||
"status": "pending",
|
||||
"message": "角色生成任务已创建,正在后台处理...",
|
||||
})
|
||||
}
|
||||
|
||||
// processCharacterGeneration 后台处理角色生成
|
||||
func (h *ScriptGenerationHandler) processCharacterGeneration(taskID string, req *services.GenerateCharactersRequest) {
|
||||
h.log.Infow("Starting character generation", "task_id", taskID, "drama_id", req.DramaID)
|
||||
|
||||
// 更新任务状态为处理中
|
||||
if err := h.taskService.UpdateTaskStatus(taskID, "processing", 10, "开始生成角色..."); err != nil {
|
||||
h.log.Errorw("Failed to update task status", "error", err)
|
||||
}
|
||||
|
||||
// 调用实际的生成逻辑
|
||||
characters, err := h.scriptService.GenerateCharacters(req)
|
||||
if err != nil {
|
||||
h.log.Errorw("Failed to generate characters", "error", err, "task_id", taskID)
|
||||
if updateErr := h.taskService.UpdateTaskError(taskID, err); updateErr != nil {
|
||||
h.log.Errorw("Failed to update task error", "error", updateErr)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 更新任务结果
|
||||
result := gin.H{
|
||||
"characters": characters,
|
||||
"total": len(characters),
|
||||
}
|
||||
if err := h.taskService.UpdateTaskResult(taskID, result); err != nil {
|
||||
h.log.Errorw("Failed to update task result", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
h.log.Infow("Character generation completed", "task_id", taskID, "total", len(characters))
|
||||
}
|
||||
|
||||
func (h *ScriptGenerationHandler) GenerateEpisodes(c *gin.Context) {
|
||||
|
||||
@@ -317,14 +317,14 @@ func (s *AIService) TestConnection(req *TestConnectionRequest) error {
|
||||
|
||||
func (s *AIService) GetDefaultConfig(serviceType string) (*models.AIServiceConfig, error) {
|
||||
var config models.AIServiceConfig
|
||||
// 按优先级降序获取第一个启用的配置
|
||||
err := s.db.Where("service_type = ? AND is_active = ?", serviceType, true).
|
||||
// 按优先级降序获取第一个配置
|
||||
err := s.db.Where("service_type = ?", serviceType).
|
||||
Order("priority DESC, created_at DESC").
|
||||
First(&config).Error
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errors.New("no active config found")
|
||||
return nil, errors.New("no config found")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
@@ -332,10 +332,10 @@ func (s *AIService) GetDefaultConfig(serviceType string) (*models.AIServiceConfi
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
// GetConfigForModel 根据服务类型和模型名称获取优先级最高的启用配置
|
||||
// GetConfigForModel 根据服务类型和模型名称获取优先级最高的配置
|
||||
func (s *AIService) GetConfigForModel(serviceType string, modelName string) (*models.AIServiceConfig, error) {
|
||||
var configs []models.AIServiceConfig
|
||||
err := s.db.Where("service_type = ? AND is_active = ?", serviceType, true).
|
||||
err := s.db.Where("service_type = ?", serviceType).
|
||||
Order("priority DESC, created_at DESC").
|
||||
Find(&configs).Error
|
||||
|
||||
|
||||
@@ -41,9 +41,10 @@ type ChatfireSoraRequest struct {
|
||||
type ChatfireDoubaoRequest struct {
|
||||
Model string `json:"model"`
|
||||
Content []struct {
|
||||
Type string `json:"type"`
|
||||
Text string `json:"text,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
Type string `json:"type"`
|
||||
Text string `json:"text,omitempty"`
|
||||
ImageURL map[string]interface{} `json:"image_url,omitempty"`
|
||||
Role string `json:"role,omitempty"`
|
||||
} `json:"content"`
|
||||
}
|
||||
|
||||
@@ -70,6 +71,9 @@ type ChatfireTaskResponse struct {
|
||||
Status string `json:"status,omitempty"`
|
||||
VideoURL string `json:"video_url,omitempty"`
|
||||
} `json:"data,omitempty"`
|
||||
Content struct {
|
||||
VideoURL string `json:"video_url,omitempty"`
|
||||
} `json:"content,omitempty"`
|
||||
}
|
||||
|
||||
// getErrorMessage 从 error 字段提取错误信息(支持字符串或对象)
|
||||
@@ -144,18 +148,83 @@ func (c *ChatfireClient) GenerateVideo(imageURL, prompt string, opts ...VideoOpt
|
||||
}
|
||||
// 添加文本内容
|
||||
reqBody.Content = append(reqBody.Content, struct {
|
||||
Type string `json:"type"`
|
||||
Text string `json:"text,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
Type string `json:"type"`
|
||||
Text string `json:"text,omitempty"`
|
||||
ImageURL map[string]interface{} `json:"image_url,omitempty"`
|
||||
Role string `json:"role,omitempty"`
|
||||
}{Type: "text", Text: prompt})
|
||||
|
||||
// 如果有图片URL,添加图片内容
|
||||
if imageURL != "" {
|
||||
// 处理不同的图片模式
|
||||
// 1. 组图模式(多个reference_image)
|
||||
if len(options.ReferenceImageURLs) > 0 {
|
||||
for _, refURL := range options.ReferenceImageURLs {
|
||||
reqBody.Content = append(reqBody.Content, struct {
|
||||
Type string `json:"type"`
|
||||
Text string `json:"text,omitempty"`
|
||||
ImageURL map[string]interface{} `json:"image_url,omitempty"`
|
||||
Role string `json:"role,omitempty"`
|
||||
}{
|
||||
Type: "image_url",
|
||||
ImageURL: map[string]interface{}{
|
||||
"url": refURL,
|
||||
},
|
||||
Role: "reference_image",
|
||||
})
|
||||
}
|
||||
} else if options.FirstFrameURL != "" && options.LastFrameURL != "" {
|
||||
// 2. 首尾帧模式
|
||||
reqBody.Content = append(reqBody.Content, struct {
|
||||
Type string `json:"type"`
|
||||
Text string `json:"text,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
}{Type: "image_url", URL: imageURL})
|
||||
Type string `json:"type"`
|
||||
Text string `json:"text,omitempty"`
|
||||
ImageURL map[string]interface{} `json:"image_url,omitempty"`
|
||||
Role string `json:"role,omitempty"`
|
||||
}{
|
||||
Type: "image_url",
|
||||
ImageURL: map[string]interface{}{
|
||||
"url": options.FirstFrameURL,
|
||||
},
|
||||
Role: "first_frame",
|
||||
})
|
||||
reqBody.Content = append(reqBody.Content, struct {
|
||||
Type string `json:"type"`
|
||||
Text string `json:"text,omitempty"`
|
||||
ImageURL map[string]interface{} `json:"image_url,omitempty"`
|
||||
Role string `json:"role,omitempty"`
|
||||
}{
|
||||
Type: "image_url",
|
||||
ImageURL: map[string]interface{}{
|
||||
"url": options.LastFrameURL,
|
||||
},
|
||||
Role: "last_frame",
|
||||
})
|
||||
} else if imageURL != "" {
|
||||
// 3. 单图模式(默认)
|
||||
reqBody.Content = append(reqBody.Content, struct {
|
||||
Type string `json:"type"`
|
||||
Text string `json:"text,omitempty"`
|
||||
ImageURL map[string]interface{} `json:"image_url,omitempty"`
|
||||
Role string `json:"role,omitempty"`
|
||||
}{
|
||||
Type: "image_url",
|
||||
ImageURL: map[string]interface{}{
|
||||
"url": imageURL,
|
||||
},
|
||||
// 单图模式不需要role
|
||||
})
|
||||
} else if options.FirstFrameURL != "" {
|
||||
// 4. 只有首帧
|
||||
reqBody.Content = append(reqBody.Content, struct {
|
||||
Type string `json:"type"`
|
||||
Text string `json:"text,omitempty"`
|
||||
ImageURL map[string]interface{} `json:"image_url,omitempty"`
|
||||
Role string `json:"role,omitempty"`
|
||||
}{
|
||||
Type: "image_url",
|
||||
ImageURL: map[string]interface{}{
|
||||
"url": options.FirstFrameURL,
|
||||
},
|
||||
Role: "first_frame",
|
||||
})
|
||||
}
|
||||
|
||||
jsonData, err = json.Marshal(reqBody)
|
||||
@@ -286,6 +355,9 @@ func (c *ChatfireClient) GetTaskStatus(taskID string) (*VideoResult, error) {
|
||||
return nil, fmt.Errorf("read response: %w", err)
|
||||
}
|
||||
|
||||
// 调试日志:打印响应内容
|
||||
fmt.Printf("[Chatfire] GetTaskStatus Response body: %s\n", string(body))
|
||||
|
||||
var result ChatfireTaskResponse
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
return nil, fmt.Errorf("parse response: %w, body: %s", err, string(body))
|
||||
@@ -307,10 +379,16 @@ func (c *ChatfireClient) GetTaskStatus(taskID string) (*VideoResult, error) {
|
||||
status = result.Data.Status
|
||||
}
|
||||
|
||||
// 按优先级获取 video_url:VideoURL -> Data.VideoURL -> Content.VideoURL
|
||||
videoURL := result.VideoURL
|
||||
if videoURL == "" && result.Data.VideoURL != "" {
|
||||
videoURL = result.Data.VideoURL
|
||||
}
|
||||
if videoURL == "" && result.Content.VideoURL != "" {
|
||||
videoURL = result.Content.VideoURL
|
||||
}
|
||||
|
||||
fmt.Printf("[Chatfire] Parsed result - TaskID: %s, Status: %s, VideoURL: %s\n", responseTaskID, status, videoURL)
|
||||
|
||||
videoResult := &VideoResult{
|
||||
TaskID: responseTaskID,
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"element-plus": "^2.5.0",
|
||||
"pinia": "^2.1.0",
|
||||
"vue": "^3.4.0",
|
||||
"vue-i18n": "^9.14.5",
|
||||
"vue-router": "^4.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -72,7 +72,7 @@ export const dramaAPI = {
|
||||
},
|
||||
|
||||
extractBackgrounds(episodeId: string) {
|
||||
return request.post(`/images/episode/${episodeId}/backgrounds/extract`)
|
||||
return request.post<{ task_id: string; status: string; message: string }>(`/images/episode/${episodeId}/backgrounds/extract`)
|
||||
},
|
||||
|
||||
batchGenerateBackgrounds(episodeId: string) {
|
||||
|
||||
@@ -13,7 +13,7 @@ export const generationAPI = {
|
||||
},
|
||||
|
||||
generateCharacters(data: GenerateCharactersRequest) {
|
||||
return request.post<Character[]>('/generation/characters', data)
|
||||
return request.post<{ task_id: string; status: string; message: string }>('/generation/characters', data)
|
||||
},
|
||||
|
||||
generateEpisodes(data: GenerateEpisodesRequest) {
|
||||
|
||||
64
web/src/components/LanguageSwitcher.vue
Normal file
64
web/src/components/LanguageSwitcher.vue
Normal file
@@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<el-dropdown @command="handleCommand">
|
||||
<span class="language-switcher">
|
||||
<el-icon><Switch /></el-icon>
|
||||
<span class="lang-text">{{ currentLangText }}</span>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="zh-CN" :disabled="currentLang === 'zh-CN'">
|
||||
🇨🇳 简体中文
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="en-US" :disabled="currentLang === 'en-US'">
|
||||
🇺🇸 English
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { setLanguage } from '@/locales'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const { locale } = useI18n()
|
||||
|
||||
const currentLang = ref(locale.value)
|
||||
|
||||
const currentLangText = computed(() => {
|
||||
return currentLang.value === 'zh-CN' ? '中文' : 'English'
|
||||
})
|
||||
|
||||
const handleCommand = (lang: string) => {
|
||||
setLanguage(lang)
|
||||
currentLang.value = lang
|
||||
ElMessage.success(
|
||||
lang === 'zh-CN'
|
||||
? '语言已切换为中文'
|
||||
: 'Language switched to English'
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.language-switcher {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.language-switcher:hover {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.lang-text {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
}
|
||||
</style>
|
||||
@@ -4,8 +4,8 @@
|
||||
<div class="editor-toolbar">
|
||||
<div class="toolbar-left">
|
||||
<el-button-group>
|
||||
<el-button :icon="VideoPlay" @click="playTimeline" :disabled="timelineClips.length === 0">播放</el-button>
|
||||
<el-button :icon="VideoPause" @click="pauseTimeline">暂停</el-button>
|
||||
<el-button :icon="VideoPlay" @click="playTimeline" :disabled="timelineClips.length === 0">{{ $t('common.play') }}</el-button>
|
||||
<el-button :icon="VideoPause" @click="pauseTimeline">{{ $t('common.pause') }}</el-button>
|
||||
</el-button-group>
|
||||
<span class="time-display">{{ formatTime(currentTime) }} / {{ formatTime(totalDuration) }}</span>
|
||||
</div>
|
||||
@@ -17,7 +17,7 @@
|
||||
:disabled="timelineClips.length === 0"
|
||||
:loading="serverMerging"
|
||||
>
|
||||
合成视频
|
||||
{{ $t('video.merge') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -42,7 +42,7 @@
|
||||
:style="{ animationDuration: transitionState.duration + 's' }"
|
||||
></div>
|
||||
<div class="preview-overlay" v-if="!currentPreviewUrl">
|
||||
<el-empty description="将场景拖拽到时间线开始编辑" />
|
||||
<el-empty :description="$t('video.dragToTimeline')" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="preview-controls">
|
||||
@@ -59,8 +59,8 @@
|
||||
<div class="media-library">
|
||||
<div class="library-header">
|
||||
<div class="header-left">
|
||||
<h4>视频素材库</h4>
|
||||
<span>{{ availableStoryboards.length }} 个视频</span>
|
||||
<h4>{{ $t('video.mediaLibrary') }}</h4>
|
||||
<span>{{ $t('video.videoCount', { count: availableStoryboards.length }) }}</span>
|
||||
</div>
|
||||
<el-button
|
||||
type="primary"
|
||||
@@ -69,7 +69,7 @@
|
||||
@click="addAllScenesInOrder"
|
||||
:disabled="availableStoryboards.length === 0"
|
||||
>
|
||||
一键添加全部
|
||||
{{ $t('common.addAll') }}
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="media-grid">
|
||||
@@ -98,12 +98,12 @@
|
||||
:icon="Plus"
|
||||
@click.stop="addClipToTimeline(scene)"
|
||||
>
|
||||
添加到时间线
|
||||
{{ $t('common.addToTimeline') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="media-info">
|
||||
<div class="media-title">镜头 #{{ scene.storyboard_num || scene.assetId }}</div>
|
||||
<div class="media-title">{{ $t('storyboard.shot') }} #{{ scene.storyboard_num || scene.assetId }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -116,7 +116,7 @@
|
||||
<div class="zoom-controls">
|
||||
<el-button-group size="small">
|
||||
<el-button @click="zoomOut">-</el-button>
|
||||
<el-button @click="zoomReset">重置</el-button>
|
||||
<el-button @click="zoomReset">{{ $t('common.reset') }}</el-button>
|
||||
<el-button @click="zoomIn">+</el-button>
|
||||
</el-button-group>
|
||||
<span class="zoom-level">{{ Math.round(zoom * 100) }}%</span>
|
||||
@@ -154,7 +154,7 @@
|
||||
@dragover.prevent
|
||||
@click="clickTimeline($event)"
|
||||
>
|
||||
<div class="track-label">视频轨道</div>
|
||||
<div class="track-label">{{ $t('video.videoTrack') }}</div>
|
||||
<div class="track-clips">
|
||||
<!-- 视频片段 -->
|
||||
<div
|
||||
@@ -171,7 +171,7 @@
|
||||
<video :src="clip.video_url" />
|
||||
</div>
|
||||
<div class="clip-info">
|
||||
<div class="clip-title">场景{{ clip.storyboard_number }}</div>
|
||||
<div class="clip-title">{{ $t('storyboard.scene') }} {{ clip.storyboard_number }}</div>
|
||||
<div class="clip-duration">{{ clip.duration.toFixed(1) }}s</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -204,13 +204,13 @@
|
||||
@click="clickTimeline($event)"
|
||||
>
|
||||
<div class="track-label">
|
||||
<span>音频轨道</span>
|
||||
<span>{{ $t('video.audioTrack') }}</span>
|
||||
<el-button
|
||||
type="text"
|
||||
size="small"
|
||||
@click.stop="extractAllAudio"
|
||||
:disabled="timelineClips.length === 0"
|
||||
title="从所有视频片段提取音频"
|
||||
:title="$t('video.extractAudio')"
|
||||
>
|
||||
<el-icon><Headset /></el-icon>
|
||||
</el-button>
|
||||
@@ -231,7 +231,7 @@
|
||||
<el-icon><Microphone /></el-icon>
|
||||
</div>
|
||||
<div class="clip-info">
|
||||
<div class="clip-title">音频 {{ audio.order + 1 }}</div>
|
||||
<div class="clip-title">{{ $t('video.audio') }} {{ audio.order + 1 }}</div>
|
||||
<div class="clip-duration">{{ audio.duration.toFixed(1) }}s</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -253,8 +253,8 @@
|
||||
width="500px"
|
||||
>
|
||||
<el-form label-width="100px">
|
||||
<el-form-item label="转场类型">
|
||||
<el-select v-model="editingTransition.type" placeholder="选择转场效果">
|
||||
<el-form-item :label="$t('video.transitionType')">
|
||||
<el-select v-model="editingTransition.type" :placeholder="$t('video.selectTransition')">
|
||||
<el-option label="无转场" value="none" />
|
||||
<!-- 淡入淡出类 -->
|
||||
<el-option label="淡入淡出" value="fade" />
|
||||
@@ -283,7 +283,7 @@
|
||||
<el-option label="垂直关闭" value="vertclose" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="转场时长" v-if="editingTransition.type !== 'none'">
|
||||
<el-form-item :label="$t('video.transitionDuration')" v-if="editingTransition.type !== 'none'">
|
||||
<el-slider
|
||||
v-model="editingTransition.duration"
|
||||
:min="0.3"
|
||||
|
||||
699
web/src/locales/en-US.ts
Normal file
699
web/src/locales/en-US.ts
Normal file
@@ -0,0 +1,699 @@
|
||||
export default {
|
||||
nav: {
|
||||
home: 'Home',
|
||||
characters: 'Characters',
|
||||
storyboard: 'Storyboard',
|
||||
videos: 'Videos',
|
||||
assets: 'Assets',
|
||||
settings: 'Settings',
|
||||
dramas: 'Drama Projects'
|
||||
},
|
||||
dashboard: {
|
||||
title: '🎬 Drama Generator',
|
||||
welcome: 'Welcome to AI Drama Generation Platform',
|
||||
subtitle: 'One-stop drama creation tool from script to video',
|
||||
stats: {
|
||||
projects: 'Drama Projects',
|
||||
images: 'Generated Images',
|
||||
videos: 'Generated Videos',
|
||||
tasks: 'Processing Tasks'
|
||||
},
|
||||
quickStart: 'Quick Start',
|
||||
actions: {
|
||||
newProject: 'Create New Project',
|
||||
newProjectDesc: 'Start a brand new drama project',
|
||||
myProjects: 'My Projects',
|
||||
myProjectsDesc: 'View and manage existing projects'
|
||||
}
|
||||
},
|
||||
common: {
|
||||
create: 'Create',
|
||||
edit: 'Edit',
|
||||
delete: 'Delete',
|
||||
save: 'Save',
|
||||
cancel: 'Cancel',
|
||||
confirm: 'Confirm',
|
||||
search: 'Search',
|
||||
filter: 'Filter',
|
||||
reset: 'Reset',
|
||||
submit: 'Submit',
|
||||
close: 'Close',
|
||||
back: 'Back',
|
||||
next: 'Next',
|
||||
previous: 'Previous',
|
||||
selectAll: 'Select All',
|
||||
loading: 'Loading...',
|
||||
success: 'Success',
|
||||
error: 'Error',
|
||||
warning: 'Warning',
|
||||
info: 'Info',
|
||||
actions: 'Actions',
|
||||
status: 'Status',
|
||||
name: 'Name',
|
||||
description: 'Description',
|
||||
createdAt: 'Created At',
|
||||
updatedAt: 'Updated At'
|
||||
},
|
||||
settings: {
|
||||
title: 'Settings',
|
||||
aiConfig: 'AI Configuration',
|
||||
general: 'General',
|
||||
language: 'Language',
|
||||
theme: 'Theme'
|
||||
},
|
||||
aiConfig: {
|
||||
title: 'AI Service Configuration',
|
||||
addConfig: 'Add Configuration',
|
||||
editConfig: 'Edit Configuration',
|
||||
back: 'Back',
|
||||
empty: 'No configurations yet, click Add Configuration to get started',
|
||||
enabled: 'Enabled',
|
||||
disabled: 'Disabled',
|
||||
enable: 'Enable',
|
||||
disable: 'Disable',
|
||||
endpoint: 'Endpoint',
|
||||
queryEndpoint: 'Query Endpoint',
|
||||
tabs: {
|
||||
text: 'Text Generation',
|
||||
image: 'Image Generation',
|
||||
video: 'Video Generation'
|
||||
},
|
||||
form: {
|
||||
name: 'Configuration Name',
|
||||
namePlaceholder: 'e.g., OpenAI GPT-4',
|
||||
provider: 'Provider',
|
||||
providerPlaceholder: 'Select a provider',
|
||||
providerTip: 'Select AI service provider',
|
||||
priority: 'Priority',
|
||||
priorityTip: 'Higher values have higher priority. For the same model, higher priority configurations are used first',
|
||||
model: 'Model',
|
||||
modelPlaceholder: 'Enter or select model name',
|
||||
modelTip: 'Enter model name directly or select from list, supports multiple models',
|
||||
baseUrl: 'Base URL',
|
||||
baseUrlPlaceholder: 'https://api.openai.com',
|
||||
baseUrlTip: 'API service base address, e.g., Chatfire: https://api.chatfire.site/v1, Gemini: https://generativelanguage.googleapis.com (no /v1 needed)',
|
||||
fullEndpoint: 'Full endpoint path',
|
||||
apiKey: 'API Key',
|
||||
apiKeyPlaceholder: 'sk-...',
|
||||
apiKeyTip: 'Your API key',
|
||||
isActive: 'Active Status'
|
||||
},
|
||||
actions: {
|
||||
test: 'Test Connection',
|
||||
delete: 'Delete',
|
||||
edit: 'Edit'
|
||||
},
|
||||
messages: {
|
||||
deleteConfirm: 'Are you sure to delete this configuration?',
|
||||
testSuccess: 'Connection test successful!',
|
||||
testFailed: 'Connection test failed'
|
||||
}
|
||||
},
|
||||
drama: {
|
||||
title: 'Drama Management',
|
||||
create: 'Create Project',
|
||||
totalProjects: 'Total {count} projects',
|
||||
createNew: 'Create New Project',
|
||||
aiConfig: 'AI Configuration',
|
||||
aiConfigTip: 'Please configure AI services before creating a project',
|
||||
empty: 'No projects yet, click the button above to create a new project',
|
||||
status: {
|
||||
draft: 'Draft',
|
||||
production: 'In Production',
|
||||
completed: 'Completed'
|
||||
},
|
||||
actions: {
|
||||
edit: 'Edit',
|
||||
view: 'View',
|
||||
delete: 'Delete'
|
||||
},
|
||||
management: {
|
||||
overview: 'Project Overview',
|
||||
episodes: 'Episode Management',
|
||||
characters: 'Character Management',
|
||||
scenes: 'Scene Management',
|
||||
projectInfo: 'Project Information',
|
||||
projectName: 'Project Name',
|
||||
projectDesc: 'Project Description',
|
||||
noDescription: 'No description',
|
||||
episodeStats: 'Episode Statistics',
|
||||
characterStats: 'Character Statistics',
|
||||
sceneStats: 'Scene Statistics',
|
||||
episodesCreated: 'Episodes Created',
|
||||
charactersCreated: 'Characters Created',
|
||||
sceneLibraryCount: 'Scene Library Count',
|
||||
startFirstEpisode: 'Start creating your first episode!',
|
||||
noEpisodesYet: 'Your project has no episodes yet. Please create an episode to start production.',
|
||||
createFirstEpisode: 'Create First Episode Now',
|
||||
episodeList: 'Episode List',
|
||||
createNewEpisode: 'Create New Episode',
|
||||
noEpisodes: 'No episodes yet',
|
||||
clickToCreate: 'Click the button above to create your first episode',
|
||||
episodeNumber: 'Episode {number}',
|
||||
goToEdit: 'Go to Edit',
|
||||
characterList: 'Character List',
|
||||
noCharacters: 'No characters yet',
|
||||
charactersTip: 'Characters will be automatically created during script generation',
|
||||
sceneList: 'Scene List',
|
||||
noScenes: 'No scenes yet',
|
||||
scenesTip: 'Scenes will be automatically created during storyboard generation'
|
||||
}
|
||||
},
|
||||
character: {
|
||||
title: 'Character Management',
|
||||
create: 'Create Character',
|
||||
edit: 'Edit Character',
|
||||
add: 'Add Character',
|
||||
list: 'Character List',
|
||||
name: 'Character Name',
|
||||
role: 'Role',
|
||||
personality: 'Personality',
|
||||
appearance: 'Appearance',
|
||||
background: 'Background',
|
||||
description: 'Description',
|
||||
image: 'Character Image',
|
||||
generate: 'Generate Character Image',
|
||||
extracting: 'Extracting...',
|
||||
generateImage: 'Generate Image',
|
||||
batch: 'Batch Operations',
|
||||
empty: 'Characters were created during script generation. You can view and edit them here',
|
||||
backToProject: 'Back to Project',
|
||||
saveChanges: 'Save Changes',
|
||||
nextStep: 'Next Step: Generate Character Images'
|
||||
},
|
||||
scriptGenerationPage: {
|
||||
prevStep: 'Previous',
|
||||
characterList: 'Character List',
|
||||
characterName: 'Character Name',
|
||||
position: 'Position',
|
||||
appearanceDesc: 'Appearance Description',
|
||||
personality: 'Personality',
|
||||
uploadScript: 'Upload Script',
|
||||
uploadContent: 'Upload Content',
|
||||
aiParse: 'AI Parse',
|
||||
confirmSave: 'Confirm & Save',
|
||||
uploadNotice: 'Paste or upload your script file, the system will automatically identify and split into episodes and scenes',
|
||||
uploadMethod: 'Upload Method',
|
||||
dragFilesHere: 'Drag files here or',
|
||||
clickUpload: 'click to upload',
|
||||
supportedFormats: 'Supports .txt, .md, .doc, .docx formats',
|
||||
characterListEditable: 'Character List (Editable)',
|
||||
addCharacter: '+ Add Character',
|
||||
characterType: 'Character Type',
|
||||
mainCharacter: 'Main Character',
|
||||
supportingCharacter: 'Supporting Character',
|
||||
minorCharacter: 'Minor Character',
|
||||
characterDesc: 'Character Description',
|
||||
appearanceFeatures: 'Appearance Features',
|
||||
operations: 'Operations',
|
||||
delete: 'Delete',
|
||||
episodeCount: 'Episode Count',
|
||||
generateFullScript: 'Generate complete episode scripts based on outline',
|
||||
outlineCreatedEpisodes: 'The outline has created {count} episodes, but you can reset the episode count and regenerate',
|
||||
episodePreview: 'Episode Preview (Total {count} episodes)',
|
||||
regenerate: 'Regenerate',
|
||||
episodeNumber: 'Episode',
|
||||
title: 'Title',
|
||||
summary: 'Summary',
|
||||
durationSeconds: 'Duration (seconds)',
|
||||
autoGenerateCharacters: 'Auto-generate character list from outline',
|
||||
charactersCreatedInOutline: 'Characters have been created during outline generation, click "Next" to view and edit'
|
||||
},
|
||||
script: {
|
||||
title: 'Script Generation',
|
||||
backToProject: 'Back to Project',
|
||||
aiGenerate: 'AI Generate Script',
|
||||
uploadScript: 'Upload Script',
|
||||
steps: {
|
||||
outline: 'Generate Outline',
|
||||
characters: 'Generate Characters',
|
||||
episodes: 'Generate Episodes'
|
||||
},
|
||||
form: {
|
||||
theme: 'Creative Theme',
|
||||
themePlaceholder: 'Describe the theme and story concept of the drama you want to create',
|
||||
genre: 'Genre Preference',
|
||||
genrePlaceholder: 'Select a genre',
|
||||
style: 'Style Requirements',
|
||||
stylePlaceholder: 'e.g., Light and humorous, Tense and thrilling, Warm and healing',
|
||||
episodeCount: 'Episode Count',
|
||||
randomGenerate: 'Random Generate',
|
||||
title: 'Title',
|
||||
titlePlaceholder: 'Enter script title',
|
||||
summary: 'Summary',
|
||||
summaryPlaceholder: 'Enter script summary',
|
||||
genreExample: 'e.g., Urban, Costume',
|
||||
tags: 'Tags',
|
||||
newTag: 'New Tag'
|
||||
},
|
||||
notice: 'Please enter the creative theme and requirements, AI will generate a script outline for you',
|
||||
generateFailed: 'Generation Failed',
|
||||
generating: 'Generating...',
|
||||
nextStep: 'Next Step',
|
||||
prevStep: 'Previous Step',
|
||||
complete: 'Complete',
|
||||
regenerate: 'Regenerate',
|
||||
regenerateOutline: 'Regenerate Outline',
|
||||
outlinePreview: 'Outline Preview (Editable)'
|
||||
},
|
||||
imageDialog: {
|
||||
title: 'AI Image Generation',
|
||||
selectDrama: 'Select Drama',
|
||||
selectScene: 'Select Scene',
|
||||
selectSceneOptional: 'Select Scene (Optional)',
|
||||
sceneLabel: 'Scene {number}: {title}',
|
||||
prompt: 'Prompt',
|
||||
promptPlaceholder: 'Describe the image you want to generate\nFor example: A beautiful landscape with mountains and rivers at sunset, cinematic lighting, highly detailed',
|
||||
negativePrompt: 'Negative Prompt',
|
||||
negativePromptPlaceholder: 'Describe elements you don\'t want (optional)\nFor example: blurry, low quality, watermark',
|
||||
aiService: 'AI Service',
|
||||
selectService: 'Select service',
|
||||
imageSize: 'Image Size',
|
||||
selectSize: 'Select size',
|
||||
square: 'Square',
|
||||
landscape: 'Landscape',
|
||||
portrait: 'Portrait',
|
||||
imageQuality: 'Image Quality',
|
||||
standard: 'Standard',
|
||||
hd: 'HD',
|
||||
style: 'Style',
|
||||
vivid: 'Vivid',
|
||||
natural: 'Natural',
|
||||
advancedSettings: 'Advanced Settings',
|
||||
samplingSteps: 'Sampling Steps',
|
||||
promptRelevance: 'Prompt Relevance',
|
||||
randomSeed: 'Random Seed',
|
||||
leaveBlankRandom: 'Leave blank for random',
|
||||
seedTip: 'Set the same seed to reproduce the image',
|
||||
generate: 'Generate Image',
|
||||
pleaseSelectDrama: 'Please select a drama',
|
||||
pleaseEnterPrompt: 'Please enter a prompt',
|
||||
promptMinLength: 'Prompt must be at least 5 characters',
|
||||
taskSubmitted: 'Image generation task submitted, please check results later',
|
||||
generateFailed: 'Generation failed',
|
||||
weak: 'Weak',
|
||||
moderate: 'Moderate',
|
||||
strong: 'Strong',
|
||||
veryStrong: 'Very Strong'
|
||||
},
|
||||
image: {
|
||||
title: 'AI Image Generation',
|
||||
generate: 'Generate Image',
|
||||
loadFailed: 'Load Failed',
|
||||
generating: 'Generating...',
|
||||
generateFailed: 'Generation Failed'
|
||||
},
|
||||
dramaWorkflow: {
|
||||
returnToList: 'Back',
|
||||
episodeScript: 'Episode {number} Script',
|
||||
storyboardBreakdown: 'Storyboard Breakdown',
|
||||
characterImages: 'Character Images',
|
||||
createChapterPrompt: 'Please create the first chapter to start production',
|
||||
createChapter: 'Create Chapter {number}',
|
||||
nextStepCharacterImages: 'Next: Character Images',
|
||||
nextStep: 'Next',
|
||||
reGenerateShots: 'Re-split',
|
||||
reGenerateShotsConfirm: 'Re-splitting will overwrite existing shots, are you sure?',
|
||||
pleaseWriteScript: 'Please write script content first',
|
||||
splitStoryboardFirst: 'Please split storyboard first',
|
||||
aiSplitting: 'AI Splitting...',
|
||||
aiAutoSplit: 'AI Auto Split',
|
||||
selected: 'Selected',
|
||||
characterCount: 'Characters',
|
||||
generated: 'Generated',
|
||||
batchGenerate: 'Batch Generate'
|
||||
},
|
||||
workflow: {
|
||||
backToProject: 'Back to Project',
|
||||
episodeProduction: 'Episode {number} Production',
|
||||
steps: {
|
||||
content: 'Episode Content',
|
||||
generateImages: 'Generate Images',
|
||||
splitStoryboard: 'Split Storyboard'
|
||||
},
|
||||
scriptPlaceholder: 'Enter episode content...',
|
||||
saveChapter: 'Save Chapter',
|
||||
chapterContent: 'Chapter {number} Content',
|
||||
saved: 'Saved',
|
||||
extractedData: 'Extracted Data',
|
||||
characters: 'Characters',
|
||||
scenes: 'Scenes',
|
||||
extractedCharacters: 'Extracted Characters (This Episode)',
|
||||
extractedScenes: 'Extracted Scenes (This Episode)',
|
||||
extractCharactersAndScenes: 'Extract Characters and Scenes',
|
||||
reExtract: 'Re-extract Characters and Scenes',
|
||||
nextStepGenerateImages: 'Next Step: Generate Images',
|
||||
extractWarning: 'Please click "Extract Characters and Scenes" first, then you can generate images after extraction is complete',
|
||||
characterImages: 'Character Images',
|
||||
sceneImages: 'Scene Images',
|
||||
characterCount: '{count} characters need to generate images',
|
||||
sceneCount: '{count} scenes need to generate images',
|
||||
selectAll: 'Select All',
|
||||
batchGenerate: 'Batch Generate',
|
||||
modelConfig: 'AI Model Configuration',
|
||||
editPrompt: 'Edit Prompt',
|
||||
aiGenerate: 'AI Generate',
|
||||
uploadImage: 'Upload Image',
|
||||
selectFromLibrary: 'Select from Library',
|
||||
shotList: 'Shot List',
|
||||
dragFilesHere: 'Drop files here, or',
|
||||
clickToUpload: 'Click to Upload',
|
||||
prevStep: 'Previous Step',
|
||||
nextStepSplitShots: 'Next Step: Split Shots',
|
||||
reExtractConfirmTitle: 'Re-extract Confirmation',
|
||||
reExtractConfirmMessage: 'Re-extraction will overwrite extracted characters and scenes (including generated images). Continue?',
|
||||
startReExtracting: 'Starting re-extraction, please wait...',
|
||||
regenerateShots: 'Regenerate Shots',
|
||||
batchGenerateSelected: 'Batch Generate Selected Scenes',
|
||||
generateAllImagesFirst: 'Please generate all character and scene images before splitting shots',
|
||||
sceneImageGenerating: 'Scene image generating, please wait...',
|
||||
sceneImageComplete: 'Scene image generation completed!',
|
||||
sceneImageStarted: 'Scene image generation started',
|
||||
reSplitShots: 'Re-split Shots',
|
||||
enterProfessional: 'Enter Professional Production',
|
||||
editShot: 'Edit Shot',
|
||||
splitSuccess: 'Shot splitting successful! Entering professional production interface...',
|
||||
reSplitConfirm: 'Are you sure you want to re-split the shots?',
|
||||
deleteCharacter: 'Delete Character',
|
||||
splitStoryboardFirst: 'Please split the storyboard first',
|
||||
aiSplitting: 'AI Splitting...',
|
||||
aiAutoSplit: 'AI Auto Split',
|
||||
batchTaskSubmitted: 'Batch generation task submitted!',
|
||||
batchGenerateFailed: 'Batch generation failed',
|
||||
batchCompleteSuccess: 'Batch generation completed! Successfully generated {count} scenes',
|
||||
batchCompletePartial: 'Generation completed: {success} succeeded, {fail} failed',
|
||||
addToLibrary: 'Add to Character Library',
|
||||
addToLibraryConfirm: 'Are you sure you want to add character "{name}" to the global character library? Once added, this character can be used in all projects.',
|
||||
addedToLibrary: 'Added to character library!',
|
||||
addFailed: 'Add failed',
|
||||
shotTitle: 'Shot Title',
|
||||
shotTitlePlaceholder: 'Enter shot title',
|
||||
shotType: 'Shot Type',
|
||||
selectShotType: 'Select shot type',
|
||||
longShot: 'Long Shot',
|
||||
fullShot: 'Full Shot',
|
||||
mediumShot: 'Medium Shot',
|
||||
closeUp: 'Close-up',
|
||||
extremeCloseUp: 'Extreme Close-up',
|
||||
cameraAngle: 'Camera Angle',
|
||||
selectAngle: 'Select angle',
|
||||
eyeLevel: 'Eye Level',
|
||||
lowAngle: 'Low Angle',
|
||||
highAngle: 'High Angle',
|
||||
location: 'Location',
|
||||
locationPlaceholder: 'Scene location',
|
||||
shotDescription: 'Shot Description',
|
||||
shotDescriptionPlaceholder: 'Overall shot description',
|
||||
cameraMovement: 'Camera Movement',
|
||||
selectMovement: 'Select movement',
|
||||
staticShot: 'Static Shot',
|
||||
pushIn: 'Push In',
|
||||
pullOut: 'Pull Out',
|
||||
followShot: 'Follow Shot',
|
||||
sideView: 'Side View',
|
||||
time: 'Time',
|
||||
timeSetting: 'Time Setting',
|
||||
actionDescription: 'Action Description',
|
||||
detailedAction: 'Detailed action description',
|
||||
dialogue: 'Dialogue',
|
||||
characterDialogue: 'Character dialogue',
|
||||
generateImageFirst: 'Please generate character images first',
|
||||
saveAndGenerate: 'Save and Generate',
|
||||
saveConfig: 'Save Configuration',
|
||||
play: 'Play',
|
||||
pause: 'Pause',
|
||||
addAll: 'Add All',
|
||||
addToTimeline: 'Add to Timeline',
|
||||
deleteAsset: 'Delete Asset',
|
||||
confirmDelete: 'Confirm Delete',
|
||||
tip: 'Tip',
|
||||
edit: 'Edit'
|
||||
},
|
||||
tooltip: {
|
||||
editPrompt: 'Edit Prompt',
|
||||
aiGenerate: 'AI Generate',
|
||||
uploadImage: 'Upload Image',
|
||||
selectFromLibrary: 'Select from Library',
|
||||
shotList: 'Shot List',
|
||||
dragFilesHere: 'Drop files here, or',
|
||||
prevStep: 'Previous Step',
|
||||
nextStepSplitShots: 'Next Step: Split Shots',
|
||||
reExtractConfirmTitle: 'Re-extract Confirmation',
|
||||
reExtractConfirmMessage: 'Re-extraction will overwrite extracted characters and scenes (including generated images). Continue?',
|
||||
startReExtracting: 'Starting re-extraction, please wait...',
|
||||
regenerateShots: 'Regenerate Shots',
|
||||
batchGenerateSelected: 'Batch Generate Selected Scenes',
|
||||
generateAllImagesFirst: 'Please generate all character and scene images before splitting shots',
|
||||
sceneImageGenerating: 'Scene image generating, please wait...',
|
||||
sceneImageComplete: 'Scene image generation completed!',
|
||||
sceneImageStarted: 'Scene image generation started',
|
||||
reSplitShots: 'Re-split Shots',
|
||||
editShot: 'Edit Shot',
|
||||
splitSuccess: 'Shot splitting successful! Entering professional production interface...',
|
||||
reSplitConfirm: 'Are you sure you want to re-split the shots?',
|
||||
deleteCharacter: 'Delete Character',
|
||||
splitStoryboardFirst: 'Please split the storyboard first',
|
||||
aiSplitting: 'AI Splitting...',
|
||||
aiAutoSplit: 'AI Auto Split',
|
||||
batchTaskSubmitted: 'Batch generation task submitted!',
|
||||
batchGenerateFailed: 'Batch generation failed',
|
||||
batchCompleteSuccess: 'Batch generation completed! Successfully generated {count} scenes',
|
||||
batchCompletePartial: 'Generation completed: {success} succeeded, {fail} failed',
|
||||
addToLibrary: 'Add to Character Library',
|
||||
addToLibraryConfirm: 'Are you sure you want to add character "{name}" to the global character library? Once added, this character can be used in all projects.',
|
||||
addedToLibrary: 'Added to character library!',
|
||||
addFailed: 'Add failed',
|
||||
shotTitle: 'Shot Title',
|
||||
shotTitlePlaceholder: 'Enter shot title',
|
||||
shotType: 'Shot Type',
|
||||
selectShotType: 'Select shot type',
|
||||
longShot: 'Long Shot',
|
||||
fullShot: 'Full Shot',
|
||||
mediumShot: 'Medium Shot',
|
||||
closeUp: 'Close-up',
|
||||
extremeCloseUp: 'Extreme Close-up',
|
||||
cameraAngle: 'Camera Angle',
|
||||
selectAngle: 'Select angle',
|
||||
eyeLevel: 'Eye Level',
|
||||
lowAngle: 'Low Angle',
|
||||
highAngle: 'High Angle',
|
||||
location: 'Location',
|
||||
locationPlaceholder: 'Scene location',
|
||||
shotDescription: 'Shot Description',
|
||||
shotDescriptionPlaceholder: 'Overall shot description',
|
||||
cameraMovement: 'Camera Movement',
|
||||
selectMovement: 'Select movement',
|
||||
staticShot: 'Static Shot',
|
||||
pushIn: 'Push In',
|
||||
pullOut: 'Pull Out',
|
||||
followShot: 'Follow Shot',
|
||||
sideView: 'Side View',
|
||||
time: 'Time',
|
||||
timeSetting: 'Time setting',
|
||||
actionDescription: 'Action Description',
|
||||
detailedAction: 'Detailed action description',
|
||||
dialogue: 'Dialogue',
|
||||
characterDialogue: 'Character dialogue',
|
||||
generateImageFirst: 'Please generate character image first',
|
||||
result: 'Result',
|
||||
actionResult: 'Action result',
|
||||
atmosphere: 'Atmosphere',
|
||||
atmosphereDescription: 'Atmosphere description',
|
||||
loadLibraryFailed: 'Failed to load character library',
|
||||
imagePrompt: 'Image Prompt',
|
||||
imagePromptPlaceholder: 'Prompt for AI image generation',
|
||||
videoPrompt: 'Video Prompt',
|
||||
videoPromptPlaceholder: 'Prompt for AI video generation',
|
||||
bgmHint: 'BGM Hint',
|
||||
bgmAtmosphere: 'BGM atmosphere description',
|
||||
soundEffect: 'Sound Effect',
|
||||
soundEffectDescription: 'Sound effect description',
|
||||
durationSeconds: 'Duration (seconds)',
|
||||
emptyLibrary: 'Character library is empty, please generate or upload character images first',
|
||||
textModelTip: 'Used to generate episode content, characters, scenes and other text',
|
||||
uploadFormatTip: 'Supports jpg/png formats, file size should not exceed 10MB',
|
||||
aiModelConfig: 'AI Model Configuration',
|
||||
textGenModel: 'Text Generation Model',
|
||||
imageGenModel: 'Image Generation Model',
|
||||
selectTextModel: 'Select text generation model',
|
||||
selectImageModel: 'Select image generation model',
|
||||
modelConfigTip: 'For generating character and scene images',
|
||||
modelConfigSaved: 'Model configuration saved',
|
||||
pleaseSelectModels: 'Please select text and image generation models'
|
||||
},
|
||||
professionalEditor: {
|
||||
duration: 'Duration',
|
||||
seconds: 's',
|
||||
videoDuration: 'Video Duration',
|
||||
downloadVideo: 'Download Video'
|
||||
},
|
||||
storyboard: {
|
||||
title: 'Storyboard',
|
||||
edit: 'Storyboard Edit',
|
||||
create: 'Create Storyboard',
|
||||
script: 'Script',
|
||||
scene: 'Scene',
|
||||
shot: 'Shot',
|
||||
shotNumber: 'Shot {number}',
|
||||
untitled: 'Untitled Shot',
|
||||
scriptStructure: 'Script Structure',
|
||||
add: 'Add',
|
||||
noStoryboard: 'No Storyboards',
|
||||
shotProperties: 'Shot Properties',
|
||||
selectScene: 'Select Scene',
|
||||
inDevelopment: 'Feature under development...',
|
||||
generateScript: 'Generate Script',
|
||||
generateImage: 'Generate Image',
|
||||
generateVideo: 'Generate Video',
|
||||
table: {
|
||||
number: 'No.',
|
||||
title: 'Title',
|
||||
shotType: 'Shot Type',
|
||||
movement: 'Movement',
|
||||
location: 'Location',
|
||||
character: 'Character',
|
||||
dialogue: 'Dialogue',
|
||||
action: 'Action',
|
||||
duration: 'Duration',
|
||||
operations: 'Operations'
|
||||
}
|
||||
},
|
||||
timeline: {
|
||||
title: 'Timeline Editor',
|
||||
backToEditor: 'Back',
|
||||
noScenes: 'No available scenes',
|
||||
loadFailed: 'Failed to load storyboards'
|
||||
},
|
||||
editor: {
|
||||
backToEpisode: 'Back to Episode Edit',
|
||||
episode: 'Episode {number}',
|
||||
settings: 'Settings',
|
||||
basicInfo: 'Basic Info',
|
||||
sceneProduction: 'Scene Production',
|
||||
sceneId: 'Scene ID',
|
||||
sceneGenerating: 'Scene image generating...',
|
||||
noBackground: 'No background linked',
|
||||
cast: 'Cast',
|
||||
addCharacter: 'Add Character',
|
||||
removeCharacter: 'Remove Character',
|
||||
noCharacters: 'No characters specified',
|
||||
visualSettings: 'Visual Settings',
|
||||
shotType: 'Shot Type',
|
||||
shotTypePlaceholder: 'Select shot type',
|
||||
movement: 'Camera Movement',
|
||||
movementPlaceholder: 'Camera movement',
|
||||
angle: 'Camera Angle',
|
||||
anglePlaceholder: 'Camera angle',
|
||||
action: 'Action',
|
||||
actionPlaceholder: 'Describe the action...',
|
||||
result: 'Result',
|
||||
resultPlaceholder: 'Describe the result...',
|
||||
dialogue: 'Dialogue',
|
||||
dialoguePlaceholder: 'Enter dialogue...',
|
||||
soundEffects: 'Sound Effects',
|
||||
soundEffectsPlaceholder: 'Describe sound effects...',
|
||||
transitions: 'Transitions',
|
||||
transitionsPlaceholder: 'Select transition',
|
||||
duration: 'Duration',
|
||||
seconds: 's',
|
||||
description: 'Description',
|
||||
descriptionPlaceholder: 'Overall shot description...',
|
||||
bgmPrompt: 'BGM Prompt',
|
||||
bgmPromptPlaceholder: 'Describe BGM atmosphere, e.g., Intense background music',
|
||||
atmosphere: 'Atmosphere',
|
||||
atmospherePlaceholder: 'Describe environment atmosphere, e.g., Dark and oppressive, Bright and warm',
|
||||
lightingEffect: 'Lighting Effect',
|
||||
specialEffects: 'Special Effects',
|
||||
props: 'Props',
|
||||
emotionalTone: 'Emotional Tone',
|
||||
shotImage: 'Shot Image',
|
||||
noShotSelected: 'No shot selected',
|
||||
selectFrameType: 'Select Frame Type',
|
||||
firstFrame: 'First Frame',
|
||||
lastFrame: 'Last Frame',
|
||||
panelFrame: 'Panel',
|
||||
actionSequence: 'Action Sequence',
|
||||
keyFrame: 'Key Frame',
|
||||
panelCount: 'Panel Count',
|
||||
prompt: 'Prompt',
|
||||
extractPrompt: 'Extract Prompt',
|
||||
promptPlaceholder: 'Click Extract Prompt button, the system will generate image prompts based on storyboard content...',
|
||||
generating: 'Generating...',
|
||||
generateImage: 'Generate Image',
|
||||
uploadImage: 'Upload Image',
|
||||
generationResult: 'Generation Result'
|
||||
},
|
||||
video: {
|
||||
title: 'AI Video Generation',
|
||||
generate: 'Generate Video',
|
||||
merge: 'Merge Video',
|
||||
mediaLibrary: 'Video Media Library',
|
||||
videoCount: '{count} videos',
|
||||
dragToTimeline: 'Drag scenes to timeline to start editing',
|
||||
videoTrack: 'Video Track',
|
||||
audioTrack: 'Audio Track',
|
||||
soundAndMusic: 'Sound & Music',
|
||||
soundMusicInDev: 'Sound & Music feature in development',
|
||||
noMergeYet: 'No videos merged yet',
|
||||
mergeInstructions: 'Arrange videos in the timeline editor and click "Merge Video" to proceed',
|
||||
selectVideoModel: 'Please select a video model',
|
||||
mergeComplete: 'Video merge completed and downloaded!',
|
||||
mergeTaskSubmitted: 'Video merge task submitted, processing in background...',
|
||||
audio: 'Audio',
|
||||
extractAudio: 'Extract audio from all video clips',
|
||||
model: 'Model',
|
||||
videoGeneration: 'Video Generation',
|
||||
soundAndMusicTab: 'Sound & Music',
|
||||
videoMerge: 'Video Merge',
|
||||
noMergeRecords: 'No merge records',
|
||||
transitionType: 'Transition Type',
|
||||
transitionDuration: 'Transition Duration',
|
||||
selectTransition: 'Select transition',
|
||||
filter: {
|
||||
drama: 'Script',
|
||||
allDramas: 'All Scripts',
|
||||
status: 'Status',
|
||||
allStatus: 'All Status',
|
||||
query: 'Query',
|
||||
reset: 'Reset'
|
||||
},
|
||||
status: {
|
||||
pending: 'Pending',
|
||||
processing: 'Processing',
|
||||
completed: 'Completed',
|
||||
failed: 'Failed'
|
||||
},
|
||||
prompt: 'Prompt',
|
||||
duration: 'Duration',
|
||||
createdAt: 'Created At',
|
||||
actions: {
|
||||
view: 'View Details',
|
||||
download: 'Download',
|
||||
delete: 'Delete'
|
||||
}
|
||||
},
|
||||
asset: {
|
||||
title: 'Asset Library',
|
||||
type: 'Asset Type',
|
||||
upload: 'Upload',
|
||||
import: 'Import',
|
||||
export: 'Export'
|
||||
},
|
||||
genres: {
|
||||
urban: 'Urban',
|
||||
costume: 'Costume',
|
||||
mystery: 'Mystery',
|
||||
romance: 'Romance',
|
||||
comedy: 'Comedy'
|
||||
},
|
||||
message: {
|
||||
deleteConfirm: 'Are you sure to delete?',
|
||||
deleteSuccess: 'Deleted successfully',
|
||||
createSuccess: 'Created successfully',
|
||||
updateSuccess: 'Updated successfully',
|
||||
operationSuccess: 'Operation successful',
|
||||
operationFailed: 'Operation failed',
|
||||
loadingFailed: 'Loading failed',
|
||||
networkError: 'Network error'
|
||||
}
|
||||
}
|
||||
36
web/src/locales/index.ts
Normal file
36
web/src/locales/index.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import zhCN from './zh-CN'
|
||||
import enUS from './en-US'
|
||||
|
||||
// 从 localStorage 获取保存的语言,默认为中文
|
||||
const getStoredLanguage = (): string => {
|
||||
const stored = localStorage.getItem('language')
|
||||
if (stored) return stored
|
||||
|
||||
// 自动检测浏览器语言
|
||||
const browserLang = navigator.language.toLowerCase()
|
||||
if (browserLang.startsWith('zh')) return 'zh-CN'
|
||||
return 'en-US'
|
||||
}
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false, // 使用 Composition API 模式
|
||||
locale: getStoredLanguage(),
|
||||
fallbackLocale: 'zh-CN',
|
||||
messages: {
|
||||
'zh-CN': zhCN,
|
||||
'en-US': enUS
|
||||
}
|
||||
})
|
||||
|
||||
export default i18n
|
||||
|
||||
// 导出语言切换函数
|
||||
export const setLanguage = (lang: string) => {
|
||||
i18n.global.locale.value = lang as any
|
||||
localStorage.setItem('language', lang)
|
||||
}
|
||||
|
||||
export const getCurrentLanguage = () => {
|
||||
return i18n.global.locale.value
|
||||
}
|
||||
607
web/src/locales/zh-CN.ts
Normal file
607
web/src/locales/zh-CN.ts
Normal file
@@ -0,0 +1,607 @@
|
||||
export default {
|
||||
nav: {
|
||||
home: '首页',
|
||||
characters: '角色管理',
|
||||
storyboard: '分镜制作',
|
||||
videos: '视频管理',
|
||||
assets: '资源库',
|
||||
settings: '设置',
|
||||
dramas: '短剧项目'
|
||||
},
|
||||
dashboard: {
|
||||
title: '🎬 Drama Generator',
|
||||
welcome: '欢迎使用 AI 短剧生成平台',
|
||||
subtitle: '从剧本到视频,一站式短剧创作工具',
|
||||
stats: {
|
||||
projects: '短剧项目',
|
||||
images: '生成图片',
|
||||
videos: '生成视频',
|
||||
tasks: '处理中任务'
|
||||
},
|
||||
quickStart: '快速开始',
|
||||
actions: {
|
||||
newProject: '创建新项目',
|
||||
newProjectDesc: '开始一个全新的短剧项目',
|
||||
myProjects: '我的项目',
|
||||
myProjectsDesc: '查看和管理已有项目'
|
||||
}
|
||||
},
|
||||
common: {
|
||||
create: '创建',
|
||||
edit: '编辑',
|
||||
delete: '删除',
|
||||
save: '保存',
|
||||
cancel: '取消',
|
||||
confirm: '确定',
|
||||
search: '搜索',
|
||||
filter: '筛选',
|
||||
reset: '重置',
|
||||
submit: '提交',
|
||||
close: '关闭',
|
||||
back: '返回',
|
||||
next: '下一步',
|
||||
previous: '上一步',
|
||||
selectAll: '全选',
|
||||
loading: '加载中...',
|
||||
success: '成功',
|
||||
failed: '失败',
|
||||
noData: '暂无数据',
|
||||
pleaseSelect: '请选择',
|
||||
add: '添加',
|
||||
view: '查看',
|
||||
upload: '上传',
|
||||
download: '下载',
|
||||
generating: '生成中...',
|
||||
notGenerated: '未生成',
|
||||
generateFailed: '生成失败',
|
||||
clickToRegenerate: '点击重新生成',
|
||||
queuing: '排队中',
|
||||
processing: '处理中',
|
||||
saveAndGenerate: '保存并生成',
|
||||
saveConfig: '保存配置',
|
||||
play: '播放',
|
||||
pause: '暂停',
|
||||
addAll: '一键添加全部',
|
||||
addToTimeline: '添加到时间线',
|
||||
deleteAsset: '删除素材',
|
||||
confirmDelete: '确认删除',
|
||||
tip: '提示',
|
||||
edit: '编辑',
|
||||
status: '状态',
|
||||
createdAt: '创建时间',
|
||||
updatedAt: '更新时间'
|
||||
},
|
||||
settings: {
|
||||
title: '设置',
|
||||
aiConfig: 'AI配置',
|
||||
general: '通用设置',
|
||||
language: '语言',
|
||||
theme: '主题'
|
||||
},
|
||||
aiConfig: {
|
||||
title: 'AI 服务配置',
|
||||
addConfig: '添加配置',
|
||||
editConfig: '编辑配置',
|
||||
back: '返回',
|
||||
empty: '暂无配置,点击添加配置开始使用',
|
||||
enabled: '已启用',
|
||||
disabled: '已禁用',
|
||||
enable: '启用',
|
||||
disable: '禁用',
|
||||
endpoint: '端点',
|
||||
queryEndpoint: '查询端点',
|
||||
tabs: {
|
||||
text: '文本生成',
|
||||
image: '图片生成',
|
||||
video: '视频生成'
|
||||
},
|
||||
form: {
|
||||
name: '配置名称',
|
||||
namePlaceholder: '例如:OpenAI GPT-4',
|
||||
provider: '厂商',
|
||||
providerPlaceholder: '请选择厂商',
|
||||
providerTip: '选择AI服务提供商',
|
||||
priority: '优先级',
|
||||
priorityTip: '数值越大优先级越高,相同模型时优先使用高优先级配置',
|
||||
model: '模型',
|
||||
modelPlaceholder: '输入或选择模型名称',
|
||||
modelTip: '可直接输入模型名称或从列表选择,支持多个模型',
|
||||
baseUrl: 'Base URL',
|
||||
baseUrlPlaceholder: 'https://api.openai.com',
|
||||
baseUrlTip: 'API 服务的基础地址,如 Chatfire: https://api.chatfire.site/v1,Gemini: https://generativelanguage.googleapis.com(无需 /v1)',
|
||||
fullEndpoint: '完整调用路径',
|
||||
apiKey: 'API Key',
|
||||
apiKeyPlaceholder: 'sk-...',
|
||||
apiKeyTip: '您的 API 密钥',
|
||||
isActive: '启用状态'
|
||||
},
|
||||
actions: {
|
||||
test: '测试连接',
|
||||
delete: '删除',
|
||||
edit: '编辑'
|
||||
},
|
||||
messages: {
|
||||
deleteConfirm: '确定要删除此配置吗?',
|
||||
testSuccess: '连接测试成功!',
|
||||
testFailed: '连接测试失败'
|
||||
}
|
||||
},
|
||||
drama: {
|
||||
title: '短剧管理',
|
||||
create: '创建项目',
|
||||
totalProjects: '共 {count} 个项目',
|
||||
createNew: '创建新项目',
|
||||
aiConfig: 'AI配置',
|
||||
aiConfigTip: '请先配置 AI 服务后再创建项目',
|
||||
empty: '暂无项目,点击上方按钮创建新项目',
|
||||
status: {
|
||||
draft: '草稿',
|
||||
production: '制作中',
|
||||
completed: '已完成'
|
||||
},
|
||||
actions: {
|
||||
edit: '编辑',
|
||||
view: '查看',
|
||||
delete: '删除'
|
||||
},
|
||||
management: {
|
||||
overview: '项目概览',
|
||||
episodes: '章节管理',
|
||||
characters: '角色管理',
|
||||
scenes: '场景管理',
|
||||
projectInfo: '项目信息',
|
||||
projectName: '项目名称',
|
||||
projectDesc: '项目描述',
|
||||
noDescription: '暂无描述',
|
||||
episodeStats: '章节统计',
|
||||
characterStats: '角色统计',
|
||||
sceneStats: '场景统计',
|
||||
episodesCreated: '已创建章节',
|
||||
charactersCreated: '已创建角色',
|
||||
sceneLibraryCount: '场景库数量',
|
||||
startFirstEpisode: '开始创作您的第一个章节!',
|
||||
noEpisodesYet: '您的项目还没有章节。请先创建一个章节开始制作。',
|
||||
createFirstEpisode: '立即创建第一个章节',
|
||||
episodeList: '章节列表',
|
||||
createNewEpisode: '创建新章节',
|
||||
noEpisodes: '还没有章节',
|
||||
clickToCreate: '点击上方按钮创建第一个章节',
|
||||
episodeNumber: '第 {number} 章',
|
||||
goToEdit: '进入编辑',
|
||||
characterList: '角色列表',
|
||||
noCharacters: '还没有角色',
|
||||
charactersTip: '角色将在剧本生成阶段自动创建',
|
||||
sceneList: '场景列表',
|
||||
noScenes: '还没有场景',
|
||||
scenesTip: '场景将在分镜生成阶段自动创建'
|
||||
}
|
||||
},
|
||||
character: {
|
||||
title: '角色管理',
|
||||
create: '创建角色',
|
||||
edit: '编辑角色',
|
||||
add: '添加角色',
|
||||
list: '角色列表',
|
||||
name: '角色名称',
|
||||
role: '角色',
|
||||
personality: '性格',
|
||||
appearance: '外貌',
|
||||
background: '背景',
|
||||
description: '角色描述',
|
||||
image: '角色形象',
|
||||
generate: '生成角色形象',
|
||||
extracting: '提取中...',
|
||||
generateImage: '生成形象',
|
||||
batch: '批量操作',
|
||||
empty: '角色已在剧本生成阶段创建,您可以在此查看和编辑',
|
||||
backToProject: '返回项目',
|
||||
saveChanges: '保存修改',
|
||||
nextStep: '下一步:生成角色图片'
|
||||
},
|
||||
script: {
|
||||
title: '剧本生成',
|
||||
backToProject: '返回项目',
|
||||
aiGenerate: 'AI 生成剧本',
|
||||
uploadScript: '上传剧本',
|
||||
steps: {
|
||||
outline: '生成大纲',
|
||||
characters: '生成角色',
|
||||
episodes: '生成剧集'
|
||||
},
|
||||
form: {
|
||||
theme: '创作主题',
|
||||
themePlaceholder: '描述你想创作的短剧主题和故事概念',
|
||||
genre: '类型偏好',
|
||||
genrePlaceholder: '选择类型',
|
||||
style: '风格要求',
|
||||
stylePlaceholder: '例如:轻松幽默、紧张刺激、温馨治愈',
|
||||
episodeCount: '剧集数量',
|
||||
randomGenerate: '随机生成',
|
||||
title: '标题',
|
||||
titlePlaceholder: '请输入剧本标题',
|
||||
summary: '概要',
|
||||
summaryPlaceholder: '请输入剧本概要',
|
||||
genreExample: '例如:都市、古装',
|
||||
tags: '标签',
|
||||
newTag: '新标签'
|
||||
},
|
||||
notice: '请输入创作主题和相关要求,AI将为您生成剧本大纲',
|
||||
generateFailed: '生成失败',
|
||||
generating: '生成中...',
|
||||
nextStep: '下一步',
|
||||
prevStep: '上一步',
|
||||
complete: '完成',
|
||||
regenerate: '重新生成',
|
||||
regenerateOutline: '重新生成大纲',
|
||||
outlinePreview: '大纲预览(可编辑)'
|
||||
},
|
||||
imageDialog: {
|
||||
title: 'AI 图片生成',
|
||||
selectDrama: '选择剧本',
|
||||
selectScene: '选择场景',
|
||||
selectSceneOptional: '选择场景(可选)',
|
||||
sceneLabel: '场景{number}: {title}',
|
||||
prompt: '提示词',
|
||||
promptPlaceholder: '描述你想生成的图片\n例如:A beautiful landscape with mountains and rivers at sunset, cinematic lighting, highly detailed',
|
||||
negativePrompt: '反向提示词',
|
||||
negativePromptPlaceholder: '描述不希望出现的元素(可选)\n例如:blurry, low quality, watermark',
|
||||
aiService: 'AI 服务',
|
||||
selectService: '选择服务',
|
||||
imageSize: '图片尺寸',
|
||||
selectSize: '选择尺寸',
|
||||
square: '正方形',
|
||||
landscape: '横向',
|
||||
portrait: '纵向',
|
||||
imageQuality: '图片质量',
|
||||
standard: '标准',
|
||||
hd: '高清',
|
||||
style: '风格',
|
||||
vivid: '鲜艳',
|
||||
natural: '自然',
|
||||
advancedSettings: '高级设置',
|
||||
samplingSteps: '采样步数',
|
||||
promptRelevance: '提示词相关性',
|
||||
randomSeed: '随机种子',
|
||||
leaveBlankRandom: '留空随机',
|
||||
seedTip: '设置相同种子可复现图片',
|
||||
generate: '生成图片',
|
||||
pleaseSelectDrama: '请选择剧本',
|
||||
pleaseEnterPrompt: '请输入提示词',
|
||||
promptMinLength: '提示词至少5个字符',
|
||||
taskSubmitted: '图片生成任务已提交,请稍后查看结果',
|
||||
generateFailed: '生成失败',
|
||||
weak: '弱',
|
||||
moderate: '适中',
|
||||
strong: '强',
|
||||
veryStrong: '很强'
|
||||
},
|
||||
image: {
|
||||
title: 'AI 图片生成',
|
||||
generate: '生成图片',
|
||||
loadFailed: '加载失败',
|
||||
generating: '生成中...',
|
||||
generateFailed: '生成失败'
|
||||
},
|
||||
dramaWorkflow: {
|
||||
returnToList: '返回',
|
||||
episodeScript: '第{number}集剧本',
|
||||
storyboardBreakdown: '分镜拆解',
|
||||
characterImages: '角色图片',
|
||||
createChapterPrompt: '请创建第一章开始制作',
|
||||
createChapter: '创建第{number}章',
|
||||
nextStepCharacterImages: '下一步:角色图片',
|
||||
nextStep: '下一步',
|
||||
reGenerateShots: '重新拆分',
|
||||
reGenerateShotsConfirm: '重新拂分将覆盖现有镜头,确定继续吗?',
|
||||
pleaseWriteScript: '请先创作剧本内容',
|
||||
splitStoryboardFirst: '请先对剧本进行分镜拆解',
|
||||
aiSplitting: 'AI拆分中...',
|
||||
aiAutoSplit: 'AI自动拆分',
|
||||
selected: '已选',
|
||||
characterCount: '角色数',
|
||||
generated: '已生成',
|
||||
batchGenerate: '批量生成'
|
||||
},
|
||||
workflow: {
|
||||
backToProject: '返回项目',
|
||||
episodeProduction: '第{number}章制作',
|
||||
steps: {
|
||||
content: '章节内容',
|
||||
generateImages: '生成图片',
|
||||
splitStoryboard: '拆分分镜'
|
||||
},
|
||||
scriptPlaceholder: '请输入章节内容...',
|
||||
saveChapter: '保存章节',
|
||||
chapterContent: '第{number}章内容',
|
||||
saved: '已保存',
|
||||
extractedData: '已提取数据',
|
||||
characters: '角色',
|
||||
scenes: '场景',
|
||||
extractedCharacters: '提取的角色(本集)',
|
||||
extractedScenes: '提取的场景(本集)',
|
||||
extractCharactersAndScenes: '提取角色和场景',
|
||||
reExtract: '重新提取角色和场景',
|
||||
nextStepGenerateImages: '下一步:生成图片',
|
||||
extractWarning: '请先点击“提取角色和场景”按钮,完成提取后才能生成图片',
|
||||
characterImages: '角色图片',
|
||||
sceneImages: '场景图片',
|
||||
characterCount: '共 {count} 个角色需要生成图片',
|
||||
sceneCount: '共 {count} 个场景需要生成图片',
|
||||
selectAll: '全选',
|
||||
batchGenerate: '批量生成',
|
||||
modelConfig: 'AI模型配置',
|
||||
editPrompt: '修改提示词',
|
||||
aiGenerate: 'AI生成',
|
||||
uploadImage: '上传图片',
|
||||
selectFromLibrary: '从角色库选择',
|
||||
shotList: '镜头列表',
|
||||
dragFilesHere: '将文件拖到此处,或',
|
||||
clickToUpload: '点击上传',
|
||||
prevStep: '上一步',
|
||||
nextStepSplitShots: '下一步:拆分分镜',
|
||||
reExtractConfirmTitle: '重新提取确认',
|
||||
reExtractConfirmMessage: '重新提取将覆盖已提取的角色和场景(包括已生成的图片),确定继续吗?',
|
||||
startReExtracting: '开始重新提取,请稍候...',
|
||||
regenerateShots: '重新生成分镜',
|
||||
batchGenerateSelected: '批量生成选中场景',
|
||||
generateAllImagesFirst: '请先生成所有角色和场景图片后再进行分镜拆分',
|
||||
sceneImageGenerating: '场景图片生成中,请稍候...',
|
||||
sceneImageComplete: '场景图片生成完成!',
|
||||
sceneImageStarted: '场景图片生成已启动',
|
||||
reSplitShots: '重新拆分',
|
||||
enterProfessional: '进入专业制作',
|
||||
editShot: '编辑镜头',
|
||||
splitSuccess: '分镜拆分成功!正在进入专业制作界面...',
|
||||
reSplitConfirm: '确定要重新拂分分镜吗?',
|
||||
deleteCharacter: '删除角色',
|
||||
splitStoryboardFirst: '请先对章节进行分镜拆解',
|
||||
aiSplitting: 'AI拆分中...',
|
||||
aiAutoSplit: 'AI自动拆分',
|
||||
batchTaskSubmitted: '批量生成任务已提交!',
|
||||
batchGenerateFailed: '批量生成失败',
|
||||
batchCompleteSuccess: '批量生成完成!成功生成 {count} 个场景',
|
||||
batchCompletePartial: '生成完成:成功 {success} 个,失败 {fail} 个',
|
||||
addToLibrary: '添加到角色库',
|
||||
addToLibraryConfirm: '确定要将角色“{name}”添加到全局角色库吗?添加后可以在所有项目中使用该角色形象。',
|
||||
addedToLibrary: '已添加到角色库!',
|
||||
addFailed: '添加失败',
|
||||
shotTitle: '镜头标题',
|
||||
shotTitlePlaceholder: '请输入镜头标题',
|
||||
shotType: '景别',
|
||||
selectShotType: '选择景别',
|
||||
longShot: '远景',
|
||||
fullShot: '全景',
|
||||
mediumShot: '中景',
|
||||
closeUp: '近景',
|
||||
extremeCloseUp: '特写',
|
||||
cameraAngle: '镜头角度',
|
||||
selectAngle: '选择角度',
|
||||
eyeLevel: '平视',
|
||||
lowAngle: '仰视',
|
||||
highAngle: '俯视',
|
||||
location: '地点',
|
||||
locationPlaceholder: '场景地点',
|
||||
shotDescription: '镜头描述',
|
||||
shotDescriptionPlaceholder: '镜头整体描述',
|
||||
cameraMovement: '运镜方式',
|
||||
selectMovement: '选择运镜',
|
||||
staticShot: '固定镜头',
|
||||
pushIn: '推镜',
|
||||
pullOut: '拉镜',
|
||||
followShot: '跟镜',
|
||||
sideView: '侧面',
|
||||
time: '时间',
|
||||
timeSetting: '时间设定',
|
||||
actionDescription: '动作描述',
|
||||
detailedAction: '详细动作描述',
|
||||
dialogue: '对白',
|
||||
characterDialogue: '角色对白',
|
||||
generateImageFirst: '请先生成角色图片',
|
||||
result: '画面结果',
|
||||
actionResult: '动作结果',
|
||||
atmosphere: '环境氛围',
|
||||
atmosphereDescription: '环境氛围描述',
|
||||
loadLibraryFailed: '获取角色库失败',
|
||||
imagePrompt: '图片提示词',
|
||||
imagePromptPlaceholder: '用于AI生成图片的提示词',
|
||||
videoPrompt: '视频提示词',
|
||||
videoPromptPlaceholder: '用于AI生成视频的提示词',
|
||||
bgmHint: '配乐提示',
|
||||
bgmAtmosphere: '配乐氛围描述',
|
||||
soundEffect: '音效',
|
||||
soundEffectDescription: '音效描述',
|
||||
durationSeconds: '时长(秒)',
|
||||
emptyLibrary: '角色库为空,请先生成或上传角色图片',
|
||||
textModelTip: '用于生成章节内容、角色、场景等文本',
|
||||
uploadFormatTip: '支持 jpg/png 格式,文件大小不超过 10MB',
|
||||
aiModelConfig: 'AI模型配置',
|
||||
textGenModel: '文本生成模型',
|
||||
imageGenModel: '图片生成模型',
|
||||
selectTextModel: '选择文本生成模型',
|
||||
selectImageModel: '选择图片生成模型',
|
||||
modelConfigTip: '用于生成角色和场景图片',
|
||||
modelConfigSaved: '模型配置已保存',
|
||||
pleaseSelectModels: '请选择文本和图片生成模型'
|
||||
},
|
||||
professionalEditor: {
|
||||
duration: '时长',
|
||||
seconds: '秒',
|
||||
videoDuration: '视频时长',
|
||||
downloadVideo: '下载视频'
|
||||
},
|
||||
storyboard: {
|
||||
title: '分镜制作',
|
||||
edit: '分镜编辑',
|
||||
create: '创建分镜',
|
||||
script: '剧本',
|
||||
scene: '场景',
|
||||
shot: '镜头',
|
||||
shotNumber: '镜头 {number}',
|
||||
untitled: '未命名镜头',
|
||||
scriptStructure: '剧本结构',
|
||||
add: '添加',
|
||||
noStoryboard: '暂无分镜',
|
||||
shotProperties: '镜头属性',
|
||||
selectScene: '选择场景',
|
||||
inDevelopment: '功能开发中...',
|
||||
generateScript: '生成分镜脚本',
|
||||
generateImage: '生成分镜图片',
|
||||
generateVideo: '生成视频',
|
||||
table: {
|
||||
number: '编号',
|
||||
title: '标题',
|
||||
shotType: '景别',
|
||||
movement: '运镜',
|
||||
location: '地点',
|
||||
character: '角色',
|
||||
dialogue: '对白',
|
||||
action: '动作',
|
||||
duration: '时长',
|
||||
operations: '操作'
|
||||
}
|
||||
},
|
||||
timeline: {
|
||||
title: '时间线编辑器',
|
||||
backToEditor: '返回',
|
||||
noScenes: '暂无可用场景',
|
||||
loadFailed: '加载分镜失败'
|
||||
},
|
||||
editor: {
|
||||
backToEpisode: '返回剧集编辑',
|
||||
episode: '第{number}集',
|
||||
settings: '设置',
|
||||
basicInfo: '基础信息',
|
||||
sceneProduction: '场景制作',
|
||||
sceneId: '场景ID',
|
||||
sceneGenerating: '场景图片生成中...',
|
||||
noBackground: '未关联背景',
|
||||
cast: '登场角色',
|
||||
addCharacter: '添加角色',
|
||||
removeCharacter: '移除角色',
|
||||
noCharacters: '未指定角色',
|
||||
visualSettings: '视效设置',
|
||||
shotType: '景别',
|
||||
shotTypePlaceholder: '选择景别',
|
||||
movement: '运镜方式',
|
||||
movementPlaceholder: '运镜方式',
|
||||
angle: '镜头角度',
|
||||
anglePlaceholder: '镜头角度',
|
||||
action: '动作描述',
|
||||
actionPlaceholder: '描述角色的动作过程...',
|
||||
result: '动作结果',
|
||||
resultPlaceholder: '描述动作的结果...',
|
||||
dialogue: '对白',
|
||||
dialoguePlaceholder: '输入角色对白...',
|
||||
soundEffects: '音效',
|
||||
soundEffectsPlaceholder: '描述音效...',
|
||||
transitions: '转场效果',
|
||||
transitionsPlaceholder: '选择转场',
|
||||
duration: '时长',
|
||||
seconds: '秒',
|
||||
description: '镜头描述',
|
||||
descriptionPlaceholder: '整体镜头描述...',
|
||||
bgmPrompt: '配乐提示',
|
||||
bgmPromptPlaceholder: '描述配乐氛围,如:紧张激烈的背景音乐',
|
||||
atmosphere: '环境氛围',
|
||||
atmospherePlaceholder: '描述环境氛围,如:昱暗压抑、明亮温馨',
|
||||
lightingEffect: '光照效果',
|
||||
specialEffects: '特效',
|
||||
props: '道具',
|
||||
emotionalTone: '情绪色调',
|
||||
shotImage: '镜头图片',
|
||||
noShotSelected: '未选择镜头',
|
||||
selectFrameType: '选择帧类型',
|
||||
firstFrame: '首帧',
|
||||
lastFrame: '尾帧',
|
||||
panelFrame: '分镜板',
|
||||
actionSequence: '动作序列',
|
||||
keyFrame: '关键帧',
|
||||
panelCount: '格数',
|
||||
prompt: '提示词',
|
||||
extractPrompt: '提取提示词',
|
||||
promptPlaceholder: '点击提取提示词按钮,系统将根据分镜内容生成图片提示词...',
|
||||
generating: '生成中...',
|
||||
generateImage: '生成图片',
|
||||
uploadImage: '上传图片',
|
||||
generationResult: '生成结果'
|
||||
},
|
||||
video: {
|
||||
title: 'AI 视频生成',
|
||||
generate: '生成视频',
|
||||
merge: '合成视频',
|
||||
mediaLibrary: '视频素材库',
|
||||
videoCount: '{count} 个视频',
|
||||
dragToTimeline: '将场景拖拽到时间线开始编辑',
|
||||
videoTrack: '视频轨道',
|
||||
audioTrack: '音频轨道',
|
||||
soundAndMusic: '音效与配乐',
|
||||
soundMusicInDev: '音效与配乐功能开发中',
|
||||
noMergeYet: '还没有合成过视频',
|
||||
mergeInstructions: '在时间线编辑器中排列好视频后点击“合成视频”即可',
|
||||
selectVideoModel: '请选择视频模型',
|
||||
mergeComplete: '视频合成完成并已下载!',
|
||||
mergeTaskSubmitted: '视频合成任务已提交,正在后台处理...',
|
||||
audio: '音频',
|
||||
extractAudio: '从所有视频片段提取音频',
|
||||
model: '模型',
|
||||
videoGeneration: '视频生成',
|
||||
soundAndMusicTab: '音效与配乐',
|
||||
videoMerge: '视频合成',
|
||||
noMergeRecords: '暂无视频合成记录',
|
||||
transitionType: '转场类型',
|
||||
transitionDuration: '转场时长',
|
||||
selectTransition: '选择转场效果',
|
||||
filter: {
|
||||
drama: '剧本',
|
||||
allDramas: '全部剧本',
|
||||
status: '状态',
|
||||
allStatus: '全部状态',
|
||||
query: '查询',
|
||||
reset: '重置'
|
||||
},
|
||||
status: {
|
||||
pending: '等待中',
|
||||
processing: '生成中',
|
||||
completed: '已完成',
|
||||
failed: '失败'
|
||||
},
|
||||
prompt: '提示词',
|
||||
duration: '时长',
|
||||
createdAt: '创建时间',
|
||||
actions: {
|
||||
view: '查看详情',
|
||||
download: '下载',
|
||||
delete: '删除'
|
||||
}
|
||||
},
|
||||
asset: {
|
||||
title: '资源库',
|
||||
type: '资源类型',
|
||||
upload: '上传',
|
||||
import: '导入',
|
||||
export: '导出'
|
||||
},
|
||||
genres: {
|
||||
urban: '都市',
|
||||
costume: '古装',
|
||||
mystery: '悬疑',
|
||||
romance: '爱情',
|
||||
comedy: '喜剧'
|
||||
},
|
||||
tooltip: {
|
||||
editPrompt: '修改提示词',
|
||||
aiGenerate: 'AI生成',
|
||||
uploadImage: '上传图片',
|
||||
selectFromLibrary: '从角色库选择'
|
||||
},
|
||||
message: {
|
||||
deleteConfirm: '确定要删除吗?',
|
||||
deleteSuccess: '删除成功',
|
||||
createSuccess: '创建成功',
|
||||
updateSuccess: '更新成功',
|
||||
operationSuccess: '操作成功',
|
||||
operationFailed: '操作失败',
|
||||
loadingFailed: '加载失败',
|
||||
networkError: '网络错误'
|
||||
}
|
||||
}
|
||||
@@ -6,12 +6,14 @@ import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import i18n from './locales'
|
||||
import './assets/styles/main.css'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(createPinia())
|
||||
app.use(router)
|
||||
app.use(i18n)
|
||||
app.use(ElementPlus)
|
||||
|
||||
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||
|
||||
@@ -3,14 +3,15 @@
|
||||
<el-container>
|
||||
<el-header class="header">
|
||||
<div class="header-content">
|
||||
<h2>🎬 Drama Generator</h2>
|
||||
<h2>{{ $t('dashboard.title') }}</h2>
|
||||
<LanguageSwitcher />
|
||||
</div>
|
||||
</el-header>
|
||||
|
||||
<el-main>
|
||||
<div class="welcome-section">
|
||||
<h1>欢迎使用 AI 短剧生成平台</h1>
|
||||
<p>从剧本到视频,一站式短剧创作工具</p>
|
||||
<h1>{{ $t('dashboard.welcome') }}</h1>
|
||||
<p>{{ $t('dashboard.subtitle') }}</p>
|
||||
</div>
|
||||
|
||||
<el-row :gutter="20" class="stats-row">
|
||||
@@ -19,7 +20,7 @@
|
||||
<div class="stat-item">
|
||||
<el-icon :size="40" color="#409eff"><Document /></el-icon>
|
||||
<h3>0</h3>
|
||||
<p>短剧项目</p>
|
||||
<p>{{ $t('dashboard.stats.projects') }}</p>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
@@ -29,7 +30,7 @@
|
||||
<div class="stat-item">
|
||||
<el-icon :size="40" color="#67c23a"><Picture /></el-icon>
|
||||
<h3>0</h3>
|
||||
<p>生成图片</p>
|
||||
<p>{{ $t('dashboard.stats.images') }}</p>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
@@ -39,7 +40,7 @@
|
||||
<div class="stat-item">
|
||||
<el-icon :size="40" color="#e6a23c"><VideoPlay /></el-icon>
|
||||
<h3>0</h3>
|
||||
<p>生成视频</p>
|
||||
<p>{{ $t('dashboard.stats.videos') }}</p>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
@@ -49,28 +50,28 @@
|
||||
<div class="stat-item">
|
||||
<el-icon :size="40" color="#f56c6c"><Clock /></el-icon>
|
||||
<h3>0</h3>
|
||||
<p>处理中任务</p>
|
||||
<p>{{ $t('dashboard.stats.tasks') }}</p>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<div class="quick-actions">
|
||||
<h2>快速开始</h2>
|
||||
<h2>{{ $t('dashboard.quickStart') }}</h2>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8">
|
||||
<el-card shadow="hover" class="action-card" @click="goToDramas">
|
||||
<el-icon :size="50" color="#409eff"><Plus /></el-icon>
|
||||
<h3>创建新项目</h3>
|
||||
<p>开始一个全新的短剧项目</p>
|
||||
<h3>{{ $t('dashboard.actions.newProject') }}</h3>
|
||||
<p>{{ $t('dashboard.actions.newProjectDesc') }}</p>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="8">
|
||||
<el-card shadow="hover" class="action-card" @click="goToDramas">
|
||||
<el-icon :size="50" color="#67c23a"><FolderOpened /></el-icon>
|
||||
<h3>我的项目</h3>
|
||||
<p>查看和管理已有项目</p>
|
||||
<h3>{{ $t('dashboard.actions.myProjects') }}</h3>
|
||||
<p>{{ $t('dashboard.actions.myProjectsDesc') }}</p>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
@@ -84,6 +85,7 @@
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Document, Picture, VideoPlay, Clock, Plus, FolderOpened, Setting } from '@element-plus/icons-vue'
|
||||
import LanguageSwitcher from '@/components/LanguageSwitcher.vue'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
<div class="page-header-wrapper">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>我的短剧项目</h2>
|
||||
<p class="subtitle">共 {{ total }} 个项目</p>
|
||||
<h2>{{ $t('drama.title') }}</h2>
|
||||
<p class="subtitle">{{ $t('drama.totalProjects', { count: total }) }}</p>
|
||||
</div>
|
||||
<div style="display: flex; gap: 12px; align-items: center;">
|
||||
<LanguageSwitcher />
|
||||
<el-alert
|
||||
type="info"
|
||||
:closable="false"
|
||||
@@ -14,17 +15,17 @@
|
||||
style="padding: 8px 12px;"
|
||||
>
|
||||
<template #title>
|
||||
<span style="font-size: 13px;">使用前请先配置 AI 服务</span>
|
||||
<span style="font-size: 13px;">{{ $t('drama.aiConfigTip') }}</span>
|
||||
</template>
|
||||
</el-alert>
|
||||
<el-button type="warning" @click="goToAIConfig" :icon="Setting">AI 配置</el-button>
|
||||
<el-button type="primary" @click="handleCreate" :icon="Plus">创建新项目</el-button>
|
||||
<el-button type="warning" @click="goToAIConfig" :icon="Setting">{{ $t('drama.aiConfig') }}</el-button>
|
||||
<el-button type="primary" @click="handleCreate" :icon="Plus">{{ $t('drama.createNew') }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-loading="loading" class="dramas-grid">
|
||||
<el-empty v-if="!loading && dramas.length === 0" description="暂无项目,点击创建新项目开始吧" />
|
||||
<el-empty v-if="!loading && dramas.length === 0" :description="$t('drama.empty')" />
|
||||
|
||||
<el-card
|
||||
v-for="drama in dramas"
|
||||
@@ -38,7 +39,7 @@
|
||||
<img v-if="drama.thumbnail" :src="drama.thumbnail" :alt="drama.title" />
|
||||
<div v-else class="cover-placeholder">
|
||||
<el-icon :size="64"><Film /></el-icon>
|
||||
<p>暂无封面</p>
|
||||
<p>{{ $t('drama.noCover') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -48,7 +49,7 @@
|
||||
<el-tag v-if="drama.genre" class="genre-tag" size="small">{{ drama.genre }}</el-tag>
|
||||
</div>
|
||||
|
||||
<p class="drama-description">{{ drama.description || '暂无描述' }}</p>
|
||||
<p class="drama-description">{{ drama.description || $t('drama.noDescription') }}</p>
|
||||
|
||||
<div class="drama-footer">
|
||||
<div class="meta-info">
|
||||
@@ -66,7 +67,7 @@
|
||||
<el-icon><View /></el-icon>
|
||||
</el-button>
|
||||
<el-popconfirm
|
||||
title="确定删除该项目吗?"
|
||||
:title="$t('drama.deleteConfirm')"
|
||||
@confirm="deleteDrama(drama.id)"
|
||||
>
|
||||
<template #reference>
|
||||
@@ -127,6 +128,7 @@ import { ElMessage } from 'element-plus'
|
||||
import { Plus, Clock, Film, Setting } from '@element-plus/icons-vue'
|
||||
import { dramaAPI } from '@/api/drama'
|
||||
import type { Drama, DramaListQuery, DramaStatus } from '@/types/drama'
|
||||
import LanguageSwitcher from '@/components/LanguageSwitcher.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const loading = ref(false)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="drama-management">
|
||||
<div class="page-header">
|
||||
<div class="header-left">
|
||||
<el-button :icon="ArrowLeft" @click="router.back()">返回</el-button>
|
||||
<el-button :icon="ArrowLeft" @click="router.back()">{{ $t('common.back') }}</el-button>
|
||||
<div class="drama-info">
|
||||
<h1>{{ drama?.title }}</h1>
|
||||
</div>
|
||||
@@ -11,19 +11,19 @@
|
||||
|
||||
<el-tabs v-model="activeTab" class="management-tabs">
|
||||
<!-- 项目概览 -->
|
||||
<el-tab-pane label="项目概览" name="overview">
|
||||
<el-tab-pane :label="$t('drama.management.overview')" name="overview">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8">
|
||||
<el-card shadow="hover">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<el-icon :size="24" color="#409eff"><Document /></el-icon>
|
||||
<span>章节统计</span>
|
||||
<span>{{ $t('drama.management.episodeStats') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="stat-content">
|
||||
<div class="stat-number">{{ episodesCount }}</div>
|
||||
<div class="stat-label">已创建章节</div>
|
||||
<div class="stat-label">{{ $t('drama.management.episodesCreated') }}</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
@@ -32,12 +32,12 @@
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<el-icon :size="24" color="#67c23a"><User /></el-icon>
|
||||
<span>角色统计</span>
|
||||
<span>{{ $t('drama.management.characterStats') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="stat-content">
|
||||
<div class="stat-number">{{ charactersCount }}</div>
|
||||
<div class="stat-label">已创建角色</div>
|
||||
<div class="stat-label">{{ $t('drama.management.charactersCreated') }}</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
@@ -46,12 +46,12 @@
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<el-icon :size="24" color="#e6a23c"><Picture /></el-icon>
|
||||
<span>场景统计</span>
|
||||
<span>{{ $t('drama.management.sceneStats') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="stat-content">
|
||||
<div class="stat-number">{{ scenesCount }}</div>
|
||||
<div class="stat-label">场景库数量</div>
|
||||
<div class="stat-label">{{ $t('drama.management.sceneLibraryCount') }}</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
@@ -60,79 +60,79 @@
|
||||
<!-- 引导卡片:无章节时显示 -->
|
||||
<el-alert
|
||||
v-if="episodesCount === 0"
|
||||
title="开始创作您的第一个章节!"
|
||||
:title="$t('drama.management.startFirstEpisode')"
|
||||
type="info"
|
||||
:closable="false"
|
||||
style="margin-top: 20px;"
|
||||
>
|
||||
<template #default>
|
||||
<p style="margin: 8px 0;">您的项目还没有章节。请先创建一个章节开始制作。</p>
|
||||
<p style="margin: 8px 0;">{{ $t('drama.management.noEpisodesYet') }}</p>
|
||||
<el-button type="primary" :icon="Plus" @click="createNewEpisode" style="margin-top: 8px;">
|
||||
立即创建第一个章节
|
||||
{{ $t('drama.management.createFirstEpisode') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-alert>
|
||||
|
||||
<el-card shadow="never" style="margin-top: 20px;">
|
||||
<template #header>
|
||||
<h3>项目信息</h3>
|
||||
<h3>{{ $t('drama.management.projectInfo') }}</h3>
|
||||
</template>
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="项目名称">{{ drama?.title }}</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">{{ formatDate(drama?.created_at) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="项目描述" :span="2">
|
||||
{{ drama?.description || '暂无描述' }}
|
||||
<el-descriptions-item :label="$t('drama.management.projectName')">{{ drama?.title }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('common.createdAt')">{{ formatDate(drama?.created_at) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('drama.management.projectDesc')" :span="2">
|
||||
{{ drama?.description || $t('drama.management.noDescription') }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 章节管理 -->
|
||||
<el-tab-pane label="章节管理" name="episodes">
|
||||
<el-tab-pane :label="$t('drama.management.episodes')" name="episodes">
|
||||
<div class="tab-header">
|
||||
<h2>章节列表</h2>
|
||||
<el-button type="primary" :icon="Plus" @click="createNewEpisode">创建新章节</el-button>
|
||||
<h2>{{ $t('drama.management.episodeList') }}</h2>
|
||||
<el-button type="primary" :icon="Plus" @click="createNewEpisode">{{ $t('drama.management.createNewEpisode') }}</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 空状态引导 -->
|
||||
<el-empty
|
||||
v-if="episodesCount === 0"
|
||||
description="还没有章节"
|
||||
:description="$t('drama.management.noEpisodes')"
|
||||
style="margin-top: 40px;"
|
||||
>
|
||||
<template #image>
|
||||
<el-icon :size="80" color="#409eff"><Document /></el-icon>
|
||||
</template>
|
||||
<el-button type="primary" :icon="Plus" @click="createNewEpisode">
|
||||
创建第一个章节
|
||||
{{ $t('drama.management.createFirstEpisode') }}
|
||||
</el-button>
|
||||
</el-empty>
|
||||
|
||||
<el-table v-else :data="sortedEpisodes" border stripe style="margin-top: 16px;">
|
||||
<el-table-column type="index" label="序号" width="80" />
|
||||
<el-table-column prop="title" label="章节标题" min-width="200" />
|
||||
<el-table-column label="状态" width="120">
|
||||
<el-table-column type="index" :label="$t('storyboard.table.number')" width="80" />
|
||||
<el-table-column prop="title" :label="$t('drama.management.episodeList')" min-width="200" />
|
||||
<el-table-column :label="$t('common.status')" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getEpisodeStatusType(row)">{{ getEpisodeStatusText(row) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="分镜数" width="100">
|
||||
<el-table-column label="Shots" width="100">
|
||||
<template #default="{ row }">
|
||||
{{ row.shots?.length || 0 }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" width="180">
|
||||
<el-table-column :label="$t('common.createdAt')" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.created_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="220" fixed="right">
|
||||
<el-table-column :label="$t('storyboard.table.operations')" width="220" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" @click="enterEpisodeWorkflow(row)">
|
||||
进入制作
|
||||
{{ $t('drama.management.goToEdit') }}
|
||||
</el-button>
|
||||
<el-button size="small" type="danger" @click="deleteEpisode(row)">
|
||||
删除
|
||||
{{ $t('common.delete') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -140,10 +140,10 @@
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 角色管理 -->
|
||||
<el-tab-pane label="角色管理" name="characters">
|
||||
<el-tab-pane :label="$t('drama.management.characters')" name="characters">
|
||||
<div class="tab-header">
|
||||
<h2>角色列表</h2>
|
||||
<el-button type="primary" :icon="Plus" @click="openAddCharacterDialog">添加角色</el-button>
|
||||
<h2>{{ $t('drama.management.characterList') }}</h2>
|
||||
<el-button type="primary" :icon="Plus" @click="openAddCharacterDialog">{{ $t('character.add') }}</el-button>
|
||||
</div>
|
||||
|
||||
<el-row :gutter="16" style="margin-top: 16px;">
|
||||
@@ -157,27 +157,27 @@
|
||||
<div class="character-info">
|
||||
<h4>{{ character.name }}</h4>
|
||||
<el-tag :type="character.role === 'main' ? 'danger' : 'info'" size="small">
|
||||
{{ character.role === 'main' ? '主角' : character.role === 'supporting' ? '配角' : '次要' }}
|
||||
{{ character.role === 'main' ? 'Main' : character.role === 'supporting' ? 'Supporting' : 'Minor' }}
|
||||
</el-tag>
|
||||
<p class="desc">{{ character.appearance || character.description }}</p>
|
||||
</div>
|
||||
|
||||
<div class="character-actions">
|
||||
<el-button size="small" @click="editCharacter(character)">编辑</el-button>
|
||||
<el-button size="small" type="danger" @click="deleteCharacter(character)">删除</el-button>
|
||||
<el-button size="small" @click="editCharacter(character)">{{ $t('common.edit') }}</el-button>
|
||||
<el-button size="small" type="danger" @click="deleteCharacter(character)">{{ $t('common.delete') }}</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-empty v-if="!drama?.characters || drama.characters.length === 0" description="暂无角色" />
|
||||
<el-empty v-if="!drama?.characters || drama.characters.length === 0" :description="$t('drama.management.noCharacters')" />
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 场景库管理 -->
|
||||
<el-tab-pane label="场景库" name="scenes">
|
||||
<el-tab-pane :label="$t('drama.management.sceneList')" name="scenes">
|
||||
<div class="tab-header">
|
||||
<h2>场景库</h2>
|
||||
<el-button type="primary" :icon="Plus" @click="openAddSceneDialog">添加场景</el-button>
|
||||
<h2>{{ $t('drama.management.sceneList') }}</h2>
|
||||
<el-button type="primary" :icon="Plus" @click="openAddSceneDialog">{{ $t('common.add') }}</el-button>
|
||||
</div>
|
||||
|
||||
<el-row :gutter="16" style="margin-top: 16px;">
|
||||
@@ -196,59 +196,59 @@
|
||||
</div>
|
||||
|
||||
<div class="scene-actions">
|
||||
<el-button size="small" @click="editScene(scene)">编辑</el-button>
|
||||
<el-button size="small" type="danger" @click="deleteScene(scene)">删除</el-button>
|
||||
<el-button size="small" @click="editScene(scene)">{{ $t('common.edit') }}</el-button>
|
||||
<el-button size="small" type="danger" @click="deleteScene(scene)">{{ $t('common.delete') }}</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-empty v-if="scenes.length === 0" description="暂无场景" />
|
||||
<el-empty v-if="scenes.length === 0" :description="$t('drama.management.noScenes')" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<!-- 添加角色对话框 -->
|
||||
<el-dialog v-model="addCharacterDialogVisible" title="添加角色" width="600px">
|
||||
<el-dialog v-model="addCharacterDialogVisible" :title="$t('character.add')" width="600px">
|
||||
<el-form :model="newCharacter" label-width="100px">
|
||||
<el-form-item label="角色名称">
|
||||
<el-input v-model="newCharacter.name" placeholder="请输入角色名称" />
|
||||
<el-form-item :label="$t('character.name')">
|
||||
<el-input v-model="newCharacter.name" :placeholder="$t('character.name')" />
|
||||
</el-form-item>
|
||||
<el-form-item label="角色类型">
|
||||
<el-select v-model="newCharacter.role" placeholder="请选择角色类型">
|
||||
<el-option label="主角" value="main" />
|
||||
<el-option label="配角" value="supporting" />
|
||||
<el-option label="次要角色" value="minor" />
|
||||
<el-form-item :label="$t('character.role')">
|
||||
<el-select v-model="newCharacter.role" :placeholder="$t('common.pleaseSelect')">
|
||||
<el-option label="Main" value="main" />
|
||||
<el-option label="Supporting" value="supporting" />
|
||||
<el-option label="Minor" value="minor" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="外貌特征">
|
||||
<el-input v-model="newCharacter.appearance" type="textarea" :rows="3" placeholder="描述角色的外貌特征" />
|
||||
<el-form-item :label="$t('character.appearance')">
|
||||
<el-input v-model="newCharacter.appearance" type="textarea" :rows="3" :placeholder="$t('character.appearance')" />
|
||||
</el-form-item>
|
||||
<el-form-item label="性格特点">
|
||||
<el-input v-model="newCharacter.personality" type="textarea" :rows="3" placeholder="描述角色的性格特点" />
|
||||
<el-form-item :label="$t('character.personality')">
|
||||
<el-input v-model="newCharacter.personality" type="textarea" :rows="3" :placeholder="$t('character.personality')" />
|
||||
</el-form-item>
|
||||
<el-form-item label="角色描述">
|
||||
<el-input v-model="newCharacter.description" type="textarea" :rows="3" placeholder="其他描述信息" />
|
||||
<el-form-item :label="$t('character.description')">
|
||||
<el-input v-model="newCharacter.description" type="textarea" :rows="3" :placeholder="$t('common.description')" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="addCharacterDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="addCharacter">确定</el-button>
|
||||
<el-button @click="addCharacterDialogVisible = false">{{ $t('common.cancel') }}</el-button>
|
||||
<el-button type="primary" @click="addCharacter">{{ $t('common.confirm') }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 添加场景对话框 -->
|
||||
<el-dialog v-model="addSceneDialogVisible" title="添加场景" width="600px">
|
||||
<el-dialog v-model="addSceneDialogVisible" :title="$t('common.add')" width="600px">
|
||||
<el-form :model="newScene" label-width="100px">
|
||||
<el-form-item label="场景名称">
|
||||
<el-input v-model="newScene.name" placeholder="请输入场景名称" />
|
||||
<el-form-item :label="$t('common.name')">
|
||||
<el-input v-model="newScene.name" :placeholder="$t('common.name')" />
|
||||
</el-form-item>
|
||||
<el-form-item label="场景描述">
|
||||
<el-input v-model="newScene.description" type="textarea" :rows="4" placeholder="描述场景的特征" />
|
||||
<el-form-item :label="$t('common.description')">
|
||||
<el-input v-model="newScene.description" type="textarea" :rows="4" :placeholder="$t('common.description')" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="addSceneDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="addScene">确定</el-button>
|
||||
<el-button @click="addSceneDialogVisible = false">{{ $t('common.cancel') }}</el-button>
|
||||
<el-button type="primary" @click="addScene">{{ $t('common.confirm') }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<div class="header-left-section">
|
||||
<el-button text @click="goBack" class="back-btn">
|
||||
<el-icon><ArrowLeft /></el-icon>
|
||||
<span>返回</span>
|
||||
<span>{{ $t('dramaWorkflow.returnToList') }}</span>
|
||||
</el-button>
|
||||
<h2 class="drama-title">{{ drama?.title }}</h2>
|
||||
<el-tag :type="getStatusType(drama?.status)" size="small">{{ getStatusText(drama?.status) }}</el-tag>
|
||||
@@ -16,17 +16,17 @@
|
||||
<div class="custom-steps">
|
||||
<div class="step-item" :class="{ active: currentStep >= 0, current: currentStep === 0 }">
|
||||
<div class="step-circle">1</div>
|
||||
<span class="step-text">第{{ currentEpisodeNumber }}集剧本</span>
|
||||
<span class="step-text">{{ $t('dramaWorkflow.episodeScript', { number: currentEpisodeNumber }) }}</span>
|
||||
</div>
|
||||
<el-icon class="step-arrow"><ArrowRight /></el-icon>
|
||||
<div class="step-item" :class="{ active: currentStep >= 1, current: currentStep === 1 }">
|
||||
<div class="step-circle">2</div>
|
||||
<span class="step-text">分镜拆解</span>
|
||||
<span class="step-text">{{ $t('dramaWorkflow.storyboardBreakdown') }}</span>
|
||||
</div>
|
||||
<el-icon class="step-arrow"><ArrowRight /></el-icon>
|
||||
<div class="step-item" :class="{ active: currentStep >= 2, current: currentStep === 2 }">
|
||||
<div class="step-circle">3</div>
|
||||
<span class="step-text">角色图片</span>
|
||||
<span class="step-text">{{ $t('dramaWorkflow.characterImages') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -40,14 +40,14 @@
|
||||
<div class="stage-body stage-body-fullscreen">
|
||||
<!-- 初始状态:显示创建第一章按钮 -->
|
||||
<div v-if="!hasScript && !showScriptInput" class="create-chapter-prompt">
|
||||
<el-empty description="请创建第一章开始制作">
|
||||
<el-empty :description="$t('dramaWorkflow.createChapterPrompt')">
|
||||
<el-button
|
||||
type="primary"
|
||||
size="large"
|
||||
@click="startCreateChapter"
|
||||
:icon="Document"
|
||||
>
|
||||
创建第{{ currentEpisodeNumber }}章
|
||||
{{ $t('dramaWorkflow.createChapter', { number: currentEpisodeNumber }) }}
|
||||
</el-button>
|
||||
</el-empty>
|
||||
</div>
|
||||
@@ -179,14 +179,14 @@
|
||||
@click="regenerateShots"
|
||||
:icon="MagicStick"
|
||||
>
|
||||
重新拆分
|
||||
{{ $t('dramaWorkflow.reGenerateShots') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
type="success"
|
||||
@click="nextStep"
|
||||
:disabled="!hasCharacters"
|
||||
>
|
||||
下一步:角色图片
|
||||
{{ $t('dramaWorkflow.nextStepCharacterImages') }}
|
||||
<el-icon><ArrowRight /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
@@ -194,14 +194,14 @@
|
||||
|
||||
<!-- 未拆分时显示 -->
|
||||
<div v-else class="empty-shots">
|
||||
<el-empty description="请先对剧本进行分镜拆解">
|
||||
<el-empty :description="$t('dramaWorkflow.splitStoryboardFirst')">
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="generateShots"
|
||||
:loading="generatingShots"
|
||||
:icon="MagicStick"
|
||||
>
|
||||
{{ generatingShots ? 'AI拆分中...' : 'AI自动拆分' }}
|
||||
{{ generatingShots ? $t('dramaWorkflow.aiSplitting') : $t('dramaWorkflow.aiAutoSplit') }}
|
||||
</el-button>
|
||||
</el-empty>
|
||||
</div>
|
||||
@@ -215,12 +215,12 @@
|
||||
<div class="batch-toolbar-compact">
|
||||
<div class="toolbar-left">
|
||||
<el-checkbox v-model="selectAllCharacters" @change="handleSelectAllCharacters" :indeterminate="isCharacterIndeterminate">
|
||||
全选
|
||||
{{ $t('common.selectAll') }}
|
||||
</el-checkbox>
|
||||
<span class="selection-info">已选 {{ selectedCharacterIds.length }}/{{ drama?.characters?.length || 0 }}</span>
|
||||
<span class="selection-info">{{ $t('dramaWorkflow.selected') }} {{ selectedCharacterIds.length }}/{{ drama?.characters?.length || 0 }}</span>
|
||||
</div>
|
||||
<div class="toolbar-right">
|
||||
<span class="stats-compact">角色数: {{ drama?.characters?.length || 0 }} | 已生成: {{ characterImagesCount || 0 }}</span>
|
||||
<span class="stats-compact">{{ $t('dramaWorkflow.characterCount') }}: {{ drama?.characters?.length || 0 }} | {{ $t('dramaWorkflow.generated') }}: {{ characterImagesCount || 0 }}</span>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
@@ -229,7 +229,7 @@
|
||||
@click="batchGenerateCharacterImages"
|
||||
:icon="MagicStick"
|
||||
>
|
||||
批量生成({{ selectedCharacterIds.length }})
|
||||
{{ $t('dramaWorkflow.batchGenerate') }}({{ selectedCharacterIds.length }})
|
||||
</el-button>
|
||||
<el-button
|
||||
type="success"
|
||||
@@ -237,7 +237,7 @@
|
||||
@click="nextStep"
|
||||
:disabled="!allCharactersHaveImages"
|
||||
>
|
||||
下一步
|
||||
{{ $t('dramaWorkflow.nextStep') }}
|
||||
<el-icon><ArrowRight /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
@@ -539,8 +539,9 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
MagicStick,
|
||||
@@ -569,6 +570,7 @@ import type { Drama, DramaStatus } from '@/types/drama'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const { t } = useI18n()
|
||||
const drama = ref<Drama>()
|
||||
const currentStep = ref(0)
|
||||
const currentEpisodeNumber = ref(1) // 当前正在创作的集数
|
||||
@@ -920,7 +922,7 @@ const editCurrentEpisodeScript = () => {
|
||||
// AI自动拆分分镜
|
||||
const generateShots = async () => {
|
||||
if (!currentEpisode.value?.script_content) {
|
||||
ElMessage.warning('请先创作剧本内容')
|
||||
ElMessage.warning(t('dramaWorkflow.pleaseWriteScript'))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -946,11 +948,11 @@ const generateShots = async () => {
|
||||
// 重新拆分分镜
|
||||
const regenerateShots = async () => {
|
||||
await ElMessageBox.confirm(
|
||||
'重新拆分将覆盖现有镜头,确定继续吗?',
|
||||
'重新拆分',
|
||||
t('dramaWorkflow.reGenerateShotsConfirm'),
|
||||
t('dramaWorkflow.reGenerateShots'),
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
confirmButtonText: t('common.confirm'),
|
||||
cancelButtonText: t('common.cancel'),
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
|
||||
@@ -2,29 +2,29 @@
|
||||
<div class="episode-workflow-container">
|
||||
<div class="workflow-header">
|
||||
<div class="header-left">
|
||||
<el-button :icon="ArrowLeft" @click="goBack">返回项目</el-button>
|
||||
<h1>第{{ episodeNumber }}章制作</h1>
|
||||
<el-button :icon="ArrowLeft" @click="goBack">{{ $t('workflow.backToProject') }}</el-button>
|
||||
<h1>{{ $t('workflow.episodeProduction', { number: episodeNumber }) }}</h1>
|
||||
</div>
|
||||
<div class="steps-inline">
|
||||
<div class="custom-steps">
|
||||
<div class="step-item" :class="{ active: currentStep >= 0, current: currentStep === 0 }">
|
||||
<div class="step-circle">1</div>
|
||||
<span class="step-text">章节内容</span>
|
||||
<span class="step-text">{{ $t('workflow.steps.content') }}</span>
|
||||
</div>
|
||||
<el-icon class="step-arrow"><ArrowRight /></el-icon>
|
||||
<div class="step-item" :class="{ active: currentStep >= 1, current: currentStep === 1 }">
|
||||
<div class="step-circle">2</div>
|
||||
<span class="step-text">生成图片</span>
|
||||
<span class="step-text">{{ $t('workflow.steps.generateImages') }}</span>
|
||||
</div>
|
||||
<el-icon class="step-arrow"><ArrowRight /></el-icon>
|
||||
<div class="step-item" :class="{ active: currentStep >= 2, current: currentStep === 2 }">
|
||||
<div class="step-circle">3</div>
|
||||
<span class="step-text">拆分分镜</span>
|
||||
<span class="step-text">{{ $t('workflow.steps.splitStoryboard') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<el-button :icon="Setting" circle @click="showModelConfigDialog" title="AI模型配置" />
|
||||
<el-button :icon="Setting" circle @click="showModelConfigDialog" :title="$t('workflow.modelConfig')" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<el-input
|
||||
v-model="scriptContent"
|
||||
type="textarea"
|
||||
placeholder="请输入章节内容..."
|
||||
:placeholder="$t('workflow.scriptPlaceholder')"
|
||||
class="script-textarea script-textarea-fullscreen"
|
||||
/>
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
:disabled="!scriptContent.trim() || generatingScript"
|
||||
>
|
||||
<el-icon><Check /></el-icon>
|
||||
<span>保存章节</span>
|
||||
<span>{{ $t('workflow.saveChapter') }}</span>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -56,8 +56,8 @@
|
||||
<!-- 已保存时显示内容 -->
|
||||
<div v-if="hasScript" class="overview-section">
|
||||
<div class="episode-info">
|
||||
<h3>第{{ episodeNumber }}章内容</h3>
|
||||
<el-tag type="success" size="large">已保存</el-tag>
|
||||
<h3>{{ $t('workflow.chapterContent', { number: episodeNumber }) }}</h3>
|
||||
<el-tag type="success" size="large">{{ $t('workflow.saved') }}</el-tag>
|
||||
</div>
|
||||
<div class="overview-content">
|
||||
<el-input
|
||||
@@ -80,16 +80,16 @@
|
||||
>
|
||||
<template #title>
|
||||
<div style="display: flex; align-items: center; gap: 16px;">
|
||||
<span>✅ 已提取数据</span>
|
||||
<el-tag v-if="hasCharacters" type="success">角色: {{ charactersCount }}</el-tag>
|
||||
<el-tag v-if="currentEpisode?.scenes" type="success">场景: {{ currentEpisode.scenes.length }}</el-tag>
|
||||
<span>✅ {{ $t('workflow.extractedData') }}</span>
|
||||
<el-tag v-if="hasCharacters" type="success">{{ $t('workflow.characters') }}: {{ charactersCount }}</el-tag>
|
||||
<el-tag v-if="currentEpisode?.scenes" type="success">{{ $t('workflow.scenes') }}: {{ currentEpisode.scenes.length }}</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
</el-alert>
|
||||
|
||||
<!-- 角色列表 -->
|
||||
<div v-if="hasCharacters" style="margin-bottom: 16px;">
|
||||
<h4 style="margin-bottom: 8px; color: #606266;">提取的角色(本集):</h4>
|
||||
<h4 style="margin-bottom: 8px; color: #606266;">{{ $t('workflow.extractedCharacters') }}:</h4>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 8px;">
|
||||
<el-tag
|
||||
v-for="char in currentEpisode?.characters"
|
||||
@@ -103,7 +103,7 @@
|
||||
|
||||
<!-- 场景列表 -->
|
||||
<div v-if="currentEpisode?.scenes && currentEpisode.scenes.length > 0">
|
||||
<h4 style="margin-bottom: 8px; color: #606266;">提取的场景(本集):</h4>
|
||||
<h4 style="margin-bottom: 8px; color: #606266;">{{ $t('workflow.extractedScenes') }}:</h4>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 8px;">
|
||||
<el-tag
|
||||
v-for="scene in currentEpisode.scenes"
|
||||
@@ -127,7 +127,7 @@
|
||||
:disabled="!hasScript"
|
||||
>
|
||||
<el-icon><MagicStick /></el-icon>
|
||||
{{ hasExtractedData ? '重新提取角色和场景' : '提取角色和场景' }}
|
||||
{{ hasExtractedData ? $t('workflow.reExtract') : $t('workflow.extractCharactersAndScenes') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
type="success"
|
||||
@@ -135,14 +135,14 @@
|
||||
@click="nextStep"
|
||||
:disabled="!hasExtractedData"
|
||||
>
|
||||
下一步:生成图片
|
||||
{{ $t('workflow.nextStepGenerateImages') }}
|
||||
<el-icon><ArrowRight /></el-icon>
|
||||
</el-button>
|
||||
<div v-if="!hasExtractedData" style="margin-top: 8px;">
|
||||
<el-alert type="warning" :closable="false" style="display: inline-block;">
|
||||
<template #title>
|
||||
<span style="font-size: 12px;">
|
||||
请先点击"提取角色和场景"按钮,完成提取后才能生成图片
|
||||
{{ $t('workflow.extractWarning') }}
|
||||
</span>
|
||||
</template>
|
||||
</el-alert>
|
||||
@@ -161,14 +161,14 @@
|
||||
<div class="section-title">
|
||||
<h3>
|
||||
<el-icon><User /></el-icon>
|
||||
角色图片
|
||||
{{ $t('workflow.characterImages') }}
|
||||
</h3>
|
||||
<el-alert
|
||||
type="info"
|
||||
:closable="false"
|
||||
style="margin: 0;"
|
||||
>
|
||||
共 {{ charactersCount }} 个角色需要生成图片
|
||||
{{ $t('workflow.characterCount', { count: charactersCount }) }}
|
||||
</el-alert>
|
||||
</div>
|
||||
<div class="section-actions">
|
||||
@@ -177,7 +177,7 @@
|
||||
@change="toggleSelectAllCharacters"
|
||||
style="margin-right: 12px;"
|
||||
>
|
||||
全选
|
||||
{{ $t('workflow.selectAll') }}
|
||||
</el-checkbox>
|
||||
<el-button
|
||||
type="primary"
|
||||
@@ -186,7 +186,7 @@
|
||||
:disabled="selectedCharacterIds.length === 0"
|
||||
size="default"
|
||||
>
|
||||
批量生成选中角色 ({{ selectedCharacterIds.length }})
|
||||
{{ $t('workflow.batchGenerate') }} ({{ selectedCharacterIds.length }})
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -210,7 +210,7 @@
|
||||
:icon="Delete"
|
||||
circle
|
||||
@click="deleteCharacter(char.id)"
|
||||
title="删除角色"
|
||||
:title="$t('workflow.deleteCharacter')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -220,22 +220,22 @@
|
||||
</div>
|
||||
<div v-else-if="char.image_generation_status === 'pending' || char.image_generation_status === 'processing' || generatingCharacterImages[char.id]" class="char-placeholder generating">
|
||||
<el-icon :size="64" class="rotating"><Loading /></el-icon>
|
||||
<span>生成中...</span>
|
||||
<el-tag type="warning" size="small" style="margin-top: 8px;">{{ char.image_generation_status === 'pending' ? '排队中' : '处理中' }}</el-tag>
|
||||
<span>{{ $t('common.generating') }}</span>
|
||||
<el-tag type="warning" size="small" style="margin-top: 8px;">{{ char.image_generation_status === 'pending' ? $t('common.queuing') : $t('common.processing') }}</el-tag>
|
||||
</div>
|
||||
<div v-else-if="char.image_generation_status === 'failed'" class="char-placeholder failed">
|
||||
<el-icon :size="64"><WarningFilled /></el-icon>
|
||||
<span>生成失败</span>
|
||||
<el-tag type="danger" size="small" style="margin-top: 8px;">点击重新生成</el-tag>
|
||||
<span>{{ $t('common.generateFailed') }}</span>
|
||||
<el-tag type="danger" size="small" style="margin-top: 8px;">{{ $t('common.clickToRegenerate') }}</el-tag>
|
||||
</div>
|
||||
<div v-else class="char-placeholder">
|
||||
<el-icon :size="64"><User /></el-icon>
|
||||
<span>未生成</span>
|
||||
<span>{{ $t('common.notGenerated') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-actions">
|
||||
<el-tooltip content="修改提示词" placement="top">
|
||||
<el-tooltip :content="$t('tooltip.editPrompt')" placement="top">
|
||||
<el-button
|
||||
size="small"
|
||||
@click="openPromptDialog(char, 'character')"
|
||||
@@ -243,7 +243,7 @@
|
||||
circle
|
||||
/>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="AI生成" placement="top">
|
||||
<el-tooltip :content="$t('tooltip.aiGenerate')" placement="top">
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
@@ -253,7 +253,7 @@
|
||||
circle
|
||||
/>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="上传图片" placement="top">
|
||||
<el-tooltip :content="$t('tooltip.uploadImage')" placement="top">
|
||||
<el-button
|
||||
size="small"
|
||||
@click="uploadCharacterImage(char.id)"
|
||||
@@ -261,7 +261,7 @@
|
||||
circle
|
||||
/>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="从角色库选择" placement="top">
|
||||
<el-tooltip :content="$t('tooltip.selectFromLibrary')" placement="top">
|
||||
<el-button
|
||||
size="small"
|
||||
@click="selectFromLibrary(char.id)"
|
||||
@@ -269,7 +269,7 @@
|
||||
circle
|
||||
/>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="添加到角色库" placement="top">
|
||||
<el-tooltip :content="$t('workflow.addToLibrary')" placement="top">
|
||||
<el-button
|
||||
size="small"
|
||||
@click="addToCharacterLibrary(char)"
|
||||
@@ -292,14 +292,14 @@
|
||||
<div class="section-title">
|
||||
<h3>
|
||||
<el-icon><Place /></el-icon>
|
||||
场景图片
|
||||
{{ $t('workflow.sceneImages') }}
|
||||
</h3>
|
||||
<el-alert
|
||||
type="info"
|
||||
:closable="false"
|
||||
style="margin: 0;"
|
||||
>
|
||||
共 {{ drama?.scenes?.length || 0 }} 个场景需要生成图片
|
||||
{{ $t('workflow.sceneCount', { count: drama?.scenes?.length || 0 }) }}
|
||||
</el-alert>
|
||||
</div>
|
||||
<div class="section-actions">
|
||||
@@ -308,7 +308,7 @@
|
||||
@change="toggleSelectAllScenes"
|
||||
style="margin-right: 12px;"
|
||||
>
|
||||
全选
|
||||
{{ $t('workflow.selectAll') }}
|
||||
</el-checkbox>
|
||||
<el-button
|
||||
type="primary"
|
||||
@@ -317,7 +317,7 @@
|
||||
:disabled="selectedSceneIds.length === 0"
|
||||
size="default"
|
||||
>
|
||||
批量生成选中场景 ({{ selectedSceneIds.length }})
|
||||
{{ $t('workflow.batchGenerateSelected') }} ({{ selectedSceneIds.length }})
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -343,22 +343,22 @@
|
||||
</div>
|
||||
<div v-else-if="scene.image_generation_status === 'pending' || scene.image_generation_status === 'processing' || generatingSceneImages[scene.id]" class="scene-placeholder generating">
|
||||
<el-icon :size="64" class="rotating"><Loading /></el-icon>
|
||||
<span>生成中...</span>
|
||||
<el-tag type="warning" size="small" style="margin-top: 8px;">{{ scene.image_generation_status === 'pending' ? '排队中' : '处理中' }}</el-tag>
|
||||
<span>{{ $t('common.generating') }}</span>
|
||||
<el-tag type="warning" size="small" style="margin-top: 8px;">{{ scene.image_generation_status === 'pending' ? $t('common.queuing') : $t('common.processing') }}</el-tag>
|
||||
</div>
|
||||
<div v-else-if="scene.image_generation_status === 'failed'" class="scene-placeholder failed" @click="generateSceneImage(scene.id)" style="cursor: pointer;">
|
||||
<el-icon :size="64"><WarningFilled /></el-icon>
|
||||
<span>生成失败</span>
|
||||
<el-tag type="danger" size="small" style="margin-top: 8px;">点击重新生成</el-tag>
|
||||
<span>{{ $t('common.generateFailed') }}</span>
|
||||
<el-tag type="danger" size="small" style="margin-top: 8px;">{{ $t('common.clickToRegenerate') }}</el-tag>
|
||||
</div>
|
||||
<div v-else class="scene-placeholder">
|
||||
<el-icon :size="64"><Place /></el-icon>
|
||||
<span>未生成</span>
|
||||
<span>{{ $t('common.notGenerated') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-actions">
|
||||
<el-tooltip content="修改提示词" placement="top">
|
||||
<el-tooltip :content="$t('tooltip.editPrompt')" placement="top">
|
||||
<el-button
|
||||
size="small"
|
||||
@click="openPromptDialog(scene, 'scene')"
|
||||
@@ -366,7 +366,7 @@
|
||||
circle
|
||||
/>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="AI生成" placement="top">
|
||||
<el-tooltip :content="$t('tooltip.aiGenerate')" placement="top">
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
@@ -376,7 +376,7 @@
|
||||
circle
|
||||
/>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="上传图片" placement="top">
|
||||
<el-tooltip :content="$t('tooltip.uploadImage')" placement="top">
|
||||
<el-button
|
||||
size="small"
|
||||
@click="uploadSceneImage(scene.id)"
|
||||
@@ -395,7 +395,7 @@
|
||||
<div class="action-buttons">
|
||||
<el-button size="large" @click="prevStep">
|
||||
<el-icon><ArrowLeft /></el-icon>
|
||||
上一步
|
||||
{{ $t('workflow.prevStep') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
type="success"
|
||||
@@ -403,14 +403,14 @@
|
||||
@click="nextStep"
|
||||
:disabled="!allImagesGenerated"
|
||||
>
|
||||
下一步:拆分分镜
|
||||
{{ $t('workflow.nextStepSplitShots') }}
|
||||
<el-icon><ArrowRight /></el-icon>
|
||||
</el-button>
|
||||
<div v-if="!allImagesGenerated" style="margin-top: 8px;">
|
||||
<el-alert type="warning" :closable="false" style="display: inline-block;">
|
||||
<template #title>
|
||||
<span style="font-size: 12px;">
|
||||
请先生成所有角色和场景图片后再进行分镜拆分
|
||||
{{ $t('workflow.generateAllImagesFirst') }}
|
||||
</span>
|
||||
</template>
|
||||
</el-alert>
|
||||
@@ -425,32 +425,32 @@
|
||||
<!-- 分镜列表 -->
|
||||
<div v-if="currentEpisode?.storyboards && currentEpisode.storyboards.length > 0" class="shots-list">
|
||||
<div class="shots-header">
|
||||
<h3>镜头列表</h3>
|
||||
<h3>{{ $t('workflow.shotList') }}</h3>
|
||||
</div>
|
||||
|
||||
<el-table :data="currentEpisode.storyboards" border stripe style="margin-top: 16px;">
|
||||
<el-table-column type="index" label="编号" width="60" />
|
||||
<el-table-column label="标题" width="120" show-overflow-tooltip>
|
||||
<el-table-column type="index" :label="$t('storyboard.table.number')" width="60" />
|
||||
<el-table-column :label="$t('storyboard.table.title')" width="120" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
{{ row.title || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="景别" width="80">
|
||||
<el-table-column :label="$t('storyboard.table.shotType')" width="80">
|
||||
<template #default="{ row }">
|
||||
{{ row.shot_type || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="运镜" width="80">
|
||||
<el-table-column :label="$t('storyboard.table.movement')" width="80">
|
||||
<template #default="{ row }">
|
||||
{{ row.movement || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="地点" width="150" show-overflow-tooltip>
|
||||
<el-table-column :label="$t('storyboard.table.location')" width="150" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
{{ row.location || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="角色" width="100">
|
||||
<el-table-column :label="$t('storyboard.table.character')" width="100">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.characters && row.characters.length > 0">
|
||||
{{ row.characters.map(c => c.name || c).join(', ') }}
|
||||
@@ -458,24 +458,24 @@
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="动作" show-overflow-tooltip>
|
||||
<el-table-column :label="$t('storyboard.table.action')" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
{{ row.action || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="时长" width="80">
|
||||
<el-table-column :label="$t('storyboard.table.duration')" width="80">
|
||||
<template #default="{ row }">
|
||||
{{ row.duration || '-' }}秒
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="100" fixed="right">
|
||||
<el-table-column :label="$t('storyboard.table.operations')" width="100" fixed="right">
|
||||
<template #default="{ row, $index }">
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="editShot(row, $index)"
|
||||
>
|
||||
编辑
|
||||
{{ $t('common.edit') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -484,20 +484,20 @@
|
||||
<div class="action-buttons" style="margin-top: 24px;">
|
||||
<el-button size="large" @click="prevStep">
|
||||
<el-icon><ArrowLeft /></el-icon>
|
||||
上一步
|
||||
{{ $t('workflow.prevStep') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
@click="regenerateShots"
|
||||
:icon="MagicStick"
|
||||
>
|
||||
重新拆分
|
||||
{{ $t('workflow.reSplitShots') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
type="success"
|
||||
size="large"
|
||||
@click="goToProfessionalUI"
|
||||
>
|
||||
进入专业制作
|
||||
{{ $t('workflow.enterProfessional') }}
|
||||
<el-icon><ArrowRight /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
@@ -505,14 +505,14 @@
|
||||
|
||||
<!-- 未拆分时显示 -->
|
||||
<div v-else class="empty-shots">
|
||||
<el-empty description="请先对章节进行分镜拆解">
|
||||
<el-empty :description="$t('workflow.splitStoryboardFirst')">
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="generateShots"
|
||||
:loading="generatingShots"
|
||||
:icon="MagicStick"
|
||||
>
|
||||
{{ generatingShots ? 'AI拆分中...' : 'AI自动拆分' }}
|
||||
{{ generatingShots ? $t('workflow.aiSplitting') : $t('workflow.aiAutoSplit') }}
|
||||
</el-button>
|
||||
|
||||
<!-- 任务进度显示 -->
|
||||
@@ -536,45 +536,44 @@
|
||||
<!-- 镜头编辑对话框 -->
|
||||
<el-dialog
|
||||
v-model="shotEditDialogVisible"
|
||||
title="编辑镜头"
|
||||
:title="$t('workflow.editShot')"
|
||||
width="800px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form v-if="editingShot" label-width="100px" size="default">
|
||||
<el-form-item label="镜头标题">
|
||||
<el-input v-model="editingShot.title" placeholder="请输入镜头标题" />
|
||||
<el-form-item :label="$t('workflow.shotTitle')">
|
||||
<el-input v-model="editingShot.title" :placeholder="$t('workflow.shotTitlePlaceholder')" />
|
||||
</el-form-item>
|
||||
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="景别">
|
||||
<el-select v-model="editingShot.shot_type" placeholder="选择景别">
|
||||
<el-option label="远景" value="远景" />
|
||||
<el-option label="全景" value="全景" />
|
||||
<el-option label="中景" value="中景" />
|
||||
<el-option label="近景" value="近景" />
|
||||
<el-option label="特写" value="特写" />
|
||||
<el-form-item :label="$t('workflow.shotType')">
|
||||
<el-select v-model="editingShot.shot_type" :placeholder="$t('workflow.selectShotType')">
|
||||
<el-option :label="$t('workflow.longShot')" value="远景" />
|
||||
<el-option :label="$t('workflow.fullShot')" value="全景" />
|
||||
<el-option :label="$t('workflow.mediumShot')" value="中景" />
|
||||
<el-option :label="$t('workflow.closeUp')" value="近景" />
|
||||
<el-option :label="$t('workflow.extremeCloseUp')" value="特写" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="镜头角度">
|
||||
<el-select v-model="editingShot.angle" placeholder="选择角度">
|
||||
<el-option label="平视" value="平视" />
|
||||
<el-option label="仰视" value="仰视" />
|
||||
<el-option label="俯视" value="俯视" />
|
||||
<el-option label="侧面" value="侧面" />
|
||||
<el-form-item :label="$t('workflow.cameraAngle')">
|
||||
<el-select v-model="editingShot.angle" :placeholder="$t('workflow.selectAngle')">
|
||||
<el-option :label="$t('workflow.eyeLevel')" value="平视" />
|
||||
<el-option :label="$t('workflow.lowAngle')" value="仰视" />
|
||||
<el-option :label="$t('workflow.highAngle')" value="俯视" />
|
||||
<el-option :label="$t('workflow.sideView')" value="侧面" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="运镜方式">
|
||||
<el-select v-model="editingShot.movement" placeholder="选择运镜">
|
||||
<el-option label="固定镜头" value="固定镜头" />
|
||||
<el-option label="推镜" value="推镜" />
|
||||
<el-option label="拉镜" value="拉镜" />
|
||||
<el-option label="摇镜" value="摇镜" />
|
||||
<el-option label="跟镜" value="跟镜" />
|
||||
<el-form-item :label="$t('workflow.cameraMovement')">
|
||||
<el-select v-model="editingShot.movement" :placeholder="$t('workflow.selectMovement')">
|
||||
<el-option :label="$t('workflow.staticShot')" value="固定镜头" />
|
||||
<el-option :label="$t('workflow.pushIn')" value="推镜" />
|
||||
<el-option :label="$t('workflow.pullOut')" value="拉镜" />
|
||||
<el-option :label="$t('workflow.followShot')" value="跟镜" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
@@ -582,98 +581,98 @@
|
||||
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="地点">
|
||||
<el-input v-model="editingShot.location" placeholder="场景地点" />
|
||||
<el-form-item :label="$t('workflow.location')">
|
||||
<el-input v-model="editingShot.location" :placeholder="$t('workflow.locationPlaceholder')" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="时间">
|
||||
<el-input v-model="editingShot.time" placeholder="时间设定" />
|
||||
<el-form-item :label="$t('workflow.time')">
|
||||
<el-input v-model="editingShot.time" :placeholder="$t('workflow.timeSetting')" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-form-item label="镜头描述">
|
||||
<el-input v-model="editingShot.description" type="textarea" :rows="2" placeholder="镜头整体描述" />
|
||||
<el-form-item :label="$t('workflow.shotDescription')">
|
||||
<el-input v-model="editingShot.description" type="textarea" :rows="2" :placeholder="$t('workflow.shotDescriptionPlaceholder')" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="动作描述">
|
||||
<el-input v-model="editingShot.action" type="textarea" :rows="3" placeholder="详细动作描述" />
|
||||
<el-form-item :label="$t('workflow.actionDescription')">
|
||||
<el-input v-model="editingShot.action" type="textarea" :rows="3" :placeholder="$t('workflow.detailedAction')" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="对白">
|
||||
<el-input v-model="editingShot.dialogue" type="textarea" :rows="2" placeholder="角色对白" />
|
||||
<el-form-item :label="$t('workflow.dialogue')">
|
||||
<el-input v-model="editingShot.dialogue" type="textarea" :rows="2" :placeholder="$t('workflow.characterDialogue')" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="画面结果">
|
||||
<el-input v-model="editingShot.result" type="textarea" :rows="2" placeholder="动作结果" />
|
||||
<el-form-item :label="$t('workflow.result')">
|
||||
<el-input v-model="editingShot.result" type="textarea" :rows="2" :placeholder="$t('workflow.actionResult')" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="环境氛围">
|
||||
<el-input v-model="editingShot.atmosphere" type="textarea" :rows="2" placeholder="环境氛围描述" />
|
||||
<el-form-item :label="$t('workflow.atmosphere')">
|
||||
<el-input v-model="editingShot.atmosphere" type="textarea" :rows="2" :placeholder="$t('workflow.atmosphereDescription')" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="图片提示词">
|
||||
<el-input v-model="editingShot.image_prompt" type="textarea" :rows="3" placeholder="用于AI生成图片的提示词" />
|
||||
<el-form-item :label="$t('workflow.imagePrompt')">
|
||||
<el-input v-model="editingShot.image_prompt" type="textarea" :rows="3" :placeholder="$t('workflow.imagePromptPlaceholder')" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="视频提示词">
|
||||
<el-input v-model="editingShot.video_prompt" type="textarea" :rows="3" placeholder="用于AI生成视频的提示词" />
|
||||
<el-form-item :label="$t('workflow.videoPrompt')">
|
||||
<el-input v-model="editingShot.video_prompt" type="textarea" :rows="3" :placeholder="$t('workflow.videoPromptPlaceholder')" />
|
||||
</el-form-item>
|
||||
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="配乐提示">
|
||||
<el-input v-model="editingShot.bgm_prompt" placeholder="配乐氛围描述" />
|
||||
<el-form-item :label="$t('workflow.bgmHint')">
|
||||
<el-input v-model="editingShot.bgm_prompt" :placeholder="$t('workflow.bgmAtmosphere')" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="音效">
|
||||
<el-input v-model="editingShot.sound_effect" placeholder="音效描述" />
|
||||
<el-form-item :label="$t('workflow.soundEffect')">
|
||||
<el-input v-model="editingShot.sound_effect" :placeholder="$t('workflow.soundEffectDescription')" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-form-item label="时长(秒)">
|
||||
<el-form-item :label="$t('workflow.durationSeconds')">
|
||||
<el-input-number v-model="editingShot.duration" :min="1" :max="60" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="shotEditDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="saveShotEdit" :loading="savingShot">保存</el-button>
|
||||
<el-button @click="shotEditDialogVisible = false">{{ $t('common.cancel') }}</el-button>
|
||||
<el-button type="primary" @click="saveShotEdit" :loading="savingShot">{{ $t('common.save') }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 提示词编辑对话框 -->
|
||||
<el-dialog
|
||||
v-model="promptDialogVisible"
|
||||
title="修改提示词"
|
||||
:title="$t('workflow.editPrompt')"
|
||||
width="600px"
|
||||
>
|
||||
<el-form label-width="80px">
|
||||
<el-form-item label="名称">
|
||||
<el-form-item :label="$t('common.name')">
|
||||
<el-input v-model="currentEditItem.name" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="提示词">
|
||||
<el-form-item label="Prompt">
|
||||
<el-input
|
||||
v-model="editPrompt"
|
||||
type="textarea"
|
||||
:rows="6"
|
||||
placeholder="请输入AI生成图片的提示词..."
|
||||
placeholder="Enter AI image generation prompt..."
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="promptDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="savePrompt">保存并生成</el-button>
|
||||
<el-button @click="promptDialogVisible = false">{{ $t('common.cancel') }}</el-button>
|
||||
<el-button type="primary" @click="savePrompt">{{ $t('common.saveAndGenerate') }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 角色库选择对话框 -->
|
||||
<el-dialog
|
||||
v-model="libraryDialogVisible"
|
||||
title="从角色库选择"
|
||||
:title="$t('workflow.selectFromLibrary')"
|
||||
width="800px"
|
||||
>
|
||||
<div class="library-grid">
|
||||
@@ -688,20 +687,20 @@
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="libraryItems.length === 0" class="empty-library">
|
||||
<el-empty description="角色库为空,请先生成或上传角色图片" />
|
||||
<el-empty :description="$t('workflow.emptyLibrary')" />
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<!-- AI模型配置对话框 -->
|
||||
<el-dialog
|
||||
v-model="modelConfigDialogVisible"
|
||||
title="AI模型配置"
|
||||
:title="$t('workflow.aiModelConfig')"
|
||||
width="600px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form label-width="120px">
|
||||
<el-form-item label="文本生成模型">
|
||||
<el-select v-model="selectedTextModel" placeholder="选择文本生成模型" style="width: 100%">
|
||||
<el-form-item :label="$t('workflow.textGenModel')">
|
||||
<el-select v-model="selectedTextModel" :placeholder="$t('workflow.selectTextModel')" style="width: 100%">
|
||||
<el-option
|
||||
v-for="model in textModels"
|
||||
:key="model.modelName"
|
||||
@@ -710,12 +709,12 @@
|
||||
/>
|
||||
</el-select>
|
||||
<div style="margin-top: 8px; font-size: 12px; color: #909399;">
|
||||
用于生成章节内容、角色、场景等文本
|
||||
{{ $t('workflow.textModelTip') }}
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="图片生成模型">
|
||||
<el-select v-model="selectedImageModel" placeholder="选择图片生成模型" style="width: 100%">
|
||||
<el-form-item :label="$t('workflow.imageGenModel')">
|
||||
<el-select v-model="selectedImageModel" :placeholder="$t('workflow.selectImageModel')" style="width: 100%">
|
||||
<el-option
|
||||
v-for="model in imageModels"
|
||||
:key="model.modelName"
|
||||
@@ -724,21 +723,21 @@
|
||||
/>
|
||||
</el-select>
|
||||
<div style="margin-top: 8px; font-size: 12px; color: #909399;">
|
||||
用于生成角色和场景图片
|
||||
{{ $t('workflow.modelConfigTip') }}
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="modelConfigDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="saveModelConfig">保存配置</el-button>
|
||||
<el-button @click="modelConfigDialogVisible = false">{{ $t('common.cancel') }}</el-button>
|
||||
<el-button type="primary" @click="saveModelConfig">{{ $t('common.saveConfig') }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 图片上传对话框 -->
|
||||
<el-dialog
|
||||
v-model="uploadDialogVisible"
|
||||
title="上传图片"
|
||||
:title="$t('tooltip.uploadImage')"
|
||||
width="500px"
|
||||
>
|
||||
<el-upload
|
||||
@@ -753,11 +752,11 @@
|
||||
>
|
||||
<el-icon class="el-icon--upload"><Upload /></el-icon>
|
||||
<div class="el-upload__text">
|
||||
将文件拖到此处,或<em>点击上传</em>
|
||||
{{ $t('workflow.dragFilesHere') }}<em>{{ $t('workflow.clickToUpload') }}</em>
|
||||
</div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">
|
||||
支持 jpg/png 格式,文件大小不超过 10MB
|
||||
{{ $t('workflow.uploadFormatTip') }}
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
@@ -768,6 +767,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
User,
|
||||
@@ -797,6 +797,7 @@ import type { Drama } from '@/types/drama'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const { t: $t } = useI18n()
|
||||
const dramaId = route.params.id as string
|
||||
const episodeNumber = parseInt(route.params.episodeNumber as string)
|
||||
|
||||
@@ -907,12 +908,8 @@ const loadAIConfigs = async () => {
|
||||
aiAPI.list('image')
|
||||
])
|
||||
|
||||
// 将配置展开为模型列表,只显示启用的配置
|
||||
const activeTextConfigs = textList.filter(c => c.is_active)
|
||||
const activeImageConfigs = imageList.filter(c => c.is_active)
|
||||
|
||||
// 展开模型列表并去重(保留优先级最高的)
|
||||
const allTextModels = activeTextConfigs.flatMap(config => {
|
||||
const allTextModels = textList.flatMap(config => {
|
||||
const models = Array.isArray(config.model) ? config.model : [config.model]
|
||||
return models.map(modelName => ({
|
||||
modelName,
|
||||
@@ -931,7 +928,7 @@ const loadAIConfigs = async () => {
|
||||
})
|
||||
textModels.value = Array.from(textModelMap.values())
|
||||
|
||||
const allImageModels = activeImageConfigs.flatMap(config => {
|
||||
const allImageModels = imageList.flatMap(config => {
|
||||
const models = Array.isArray(config.model) ? config.model : [config.model]
|
||||
return models.map(modelName => ({
|
||||
modelName,
|
||||
@@ -971,7 +968,7 @@ const showModelConfigDialog = () => {
|
||||
// 保存模型配置
|
||||
const saveModelConfig = () => {
|
||||
if (!selectedTextModel.value || !selectedImageModel.value) {
|
||||
ElMessage.warning('请选择文本和图片生成模型')
|
||||
ElMessage.warning($t('workflow.pleaseSelectModels'))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -979,7 +976,7 @@ const saveModelConfig = () => {
|
||||
localStorage.setItem(`ai_text_model_${dramaId}`, selectedTextModel.value)
|
||||
localStorage.setItem(`ai_image_model_${dramaId}`, selectedImageModel.value)
|
||||
|
||||
ElMessage.success('模型配置已保存')
|
||||
ElMessage.success($t('workflow.modelConfigSaved'))
|
||||
modelConfigDialogVisible.value = false
|
||||
}
|
||||
|
||||
@@ -1135,11 +1132,11 @@ const handleExtractCharactersAndBackgrounds = async () => {
|
||||
if (hasExtractedData.value) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
'重新提取将覆盖已提取的角色和场景(包括已生成的图片),确定继续吗?',
|
||||
'重新提取确认',
|
||||
$t('workflow.reExtractConfirmMessage'),
|
||||
$t('workflow.reExtractConfirmTitle'),
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
confirmButtonText: $t('common.confirm'),
|
||||
cancelButtonText: $t('common.cancel'),
|
||||
type: 'warning',
|
||||
distinguishCancelAndClose: true
|
||||
}
|
||||
@@ -1152,7 +1149,7 @@ const handleExtractCharactersAndBackgrounds = async () => {
|
||||
|
||||
// 显示即将开始的提示
|
||||
if (hasExtractedData.value) {
|
||||
ElMessage.info('开始重新提取,请稍候...')
|
||||
ElMessage.info($t('workflow.startReExtracting'))
|
||||
}
|
||||
|
||||
await extractCharactersAndBackgrounds()
|
||||
@@ -1160,8 +1157,8 @@ const handleExtractCharactersAndBackgrounds = async () => {
|
||||
|
||||
// 轮询检查图片生成状态
|
||||
const pollImageStatus = async (imageGenId: number, onComplete: () => Promise<void>) => {
|
||||
const maxAttempts = 60 // 最多轮询60次
|
||||
const pollInterval = 3000 // 每3秒轮询一次
|
||||
const maxAttempts = 100 // 最多轮询100次
|
||||
const pollInterval = 6000 // 每6秒轮询一次
|
||||
|
||||
for (let i = 0; i < maxAttempts; i++) {
|
||||
try {
|
||||
@@ -1190,7 +1187,6 @@ const pollImageStatus = async (imageGenId: number, onComplete: () => Promise<voi
|
||||
}
|
||||
|
||||
const extractCharactersAndBackgrounds = async () => {
|
||||
|
||||
if (!currentEpisode.value?.id) {
|
||||
ElMessage.error('章节信息不存在')
|
||||
return
|
||||
@@ -1201,47 +1197,38 @@ const extractCharactersAndBackgrounds = async () => {
|
||||
try {
|
||||
const episodeId = currentEpisode.value.id
|
||||
|
||||
// 并行执行角色生成和场景提取
|
||||
const [characters] = await Promise.all([
|
||||
// 并行创建异步任务
|
||||
const [characterTask, backgroundTask] = await Promise.all([
|
||||
generationAPI.generateCharacters({
|
||||
drama_id: dramaId.toString(),
|
||||
outline: currentEpisode.value.script_content || '',
|
||||
count: 0
|
||||
}).then(result => {
|
||||
return result
|
||||
}).catch(err => {
|
||||
console.error('[提取] 角色生成API失败:', err)
|
||||
throw err
|
||||
}),
|
||||
dramaAPI.extractBackgrounds(episodeId).then(result => {
|
||||
return result
|
||||
}).catch(err => {
|
||||
console.error('[提取] 场景提取API失败:', err)
|
||||
throw err
|
||||
})
|
||||
dramaAPI.extractBackgrounds(episodeId)
|
||||
])
|
||||
|
||||
// 保存生成的角色到数据库
|
||||
if (characters && characters.length > 0) {
|
||||
await dramaAPI.saveCharacters(dramaId, characters, currentEpisode.value?.id)
|
||||
}
|
||||
ElMessage.success('任务已创建,正在后台处理...')
|
||||
|
||||
// 并行轮询两个任务
|
||||
await Promise.all([
|
||||
pollExtractTask(characterTask.task_id, 'character'),
|
||||
pollExtractTask(backgroundTask.task_id, 'background')
|
||||
])
|
||||
|
||||
ElMessage.success('角色和场景提取成功!')
|
||||
await loadDramaData()
|
||||
} catch (error: any) {
|
||||
console.error('角色和场景提取失败:', error)
|
||||
|
||||
// 从接口返回的数据结构中提取错误信息
|
||||
const errorData = error.response?.data?.error
|
||||
const errorMsg = errorData?.message || error.message || '提取失败'
|
||||
|
||||
// 检查是否是AI配置缺失的错误
|
||||
if (errorMsg.includes('no active config found') ||
|
||||
if (errorMsg.includes('no config found') ||
|
||||
errorMsg.includes('AI client') ||
|
||||
errorMsg.includes('failed to get AI client')) {
|
||||
ElMessage({
|
||||
type: 'warning',
|
||||
message: '未配置AI服务或当前配置未激活,请前往"设置 > AI服务配置"添加并激活文本生成服务',
|
||||
message: '未配置AI服务,请前往"设置 > AI服务配置"添加文本生成服务',
|
||||
duration: 5000,
|
||||
showClose: true
|
||||
})
|
||||
@@ -1253,6 +1240,41 @@ const extractCharactersAndBackgrounds = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 轮询提取任务状态
|
||||
const pollExtractTask = async (taskId: string, type: 'character' | 'background') => {
|
||||
const maxAttempts = 60 // 最多轮询60次(2分钟)
|
||||
const interval = 2000 // 每2秒查询一次
|
||||
|
||||
for (let i = 0; i < maxAttempts; i++) {
|
||||
await new Promise(resolve => setTimeout(resolve, interval))
|
||||
|
||||
try {
|
||||
const task = await generationAPI.getTaskStatus(taskId)
|
||||
|
||||
if (task.status === 'completed') {
|
||||
// 任务完成
|
||||
if (type === 'character' && task.result) {
|
||||
// 解析角色数据并保存
|
||||
const result = typeof task.result === 'string' ? JSON.parse(task.result) : task.result
|
||||
if (result.characters && result.characters.length > 0) {
|
||||
await dramaAPI.saveCharacters(dramaId, result.characters, currentEpisode.value?.id)
|
||||
}
|
||||
}
|
||||
return
|
||||
} else if (task.status === 'failed') {
|
||||
// 任务失败
|
||||
throw new Error(task.error || `${type === 'character' ? '角色生成' : '场景提取'}失败`)
|
||||
}
|
||||
// 否则继续轮询
|
||||
} catch (error: any) {
|
||||
console.error(`轮询${type}任务状态失败:`, error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`${type === 'character' ? '角色生成' : '场景提取'}超时`)
|
||||
}
|
||||
|
||||
|
||||
const generateCharacterImage = async (characterId: number) => {
|
||||
generatingCharacterImages.value[characterId] = true
|
||||
@@ -1314,10 +1336,10 @@ const batchGenerateCharacterImages = async () => {
|
||||
model
|
||||
)
|
||||
|
||||
ElMessage.success('批量生成任务已提交!')
|
||||
ElMessage.success($t('workflow.batchTaskSubmitted'))
|
||||
await loadDramaData()
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || '批量生成失败')
|
||||
ElMessage.error(error.message || $t('workflow.batchGenerateFailed'))
|
||||
} finally {
|
||||
batchGeneratingCharacters.value = false
|
||||
}
|
||||
@@ -1336,14 +1358,14 @@ const generateSceneImage = async (sceneId: string) => {
|
||||
const imageGenId = response.image_generation?.id
|
||||
|
||||
if (imageGenId) {
|
||||
ElMessage.info('场景图片生成中,请稍候...')
|
||||
ElMessage.info($t('workflow.sceneImageGenerating'))
|
||||
// 轮询检查生成状态
|
||||
await pollImageStatus(imageGenId, async () => {
|
||||
await loadDramaData()
|
||||
ElMessage.success('场景图片生成完成!')
|
||||
ElMessage.success($t('workflow.sceneImageComplete'))
|
||||
})
|
||||
} else {
|
||||
ElMessage.success('场景图片生成已启动')
|
||||
ElMessage.success($t('workflow.sceneImageStarted'))
|
||||
await loadDramaData()
|
||||
}
|
||||
} catch (error: any) {
|
||||
@@ -1370,12 +1392,12 @@ const batchGenerateSceneImages = async () => {
|
||||
const failCount = results.filter(r => r.status === 'rejected').length
|
||||
|
||||
if (failCount === 0) {
|
||||
ElMessage.success(`批量生成完成!成功生成 ${successCount} 个场景`)
|
||||
ElMessage.success($t('workflow.batchCompleteSuccess', { count: successCount }))
|
||||
} else {
|
||||
ElMessage.warning(`生成完成:成功 ${successCount} 个,失败 ${failCount} 个`)
|
||||
ElMessage.warning($t('workflow.batchCompletePartial', { success: successCount, fail: failCount }))
|
||||
}
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || '批量生成失败')
|
||||
ElMessage.error(error.message || $t('workflow.batchGenerateFailed'))
|
||||
} finally {
|
||||
batchGeneratingScenes.value = false
|
||||
}
|
||||
@@ -1427,7 +1449,7 @@ const pollTaskStatus = async (taskId: string) => {
|
||||
}
|
||||
generatingShots.value = false
|
||||
|
||||
ElMessage.success('分镜拆分成功!正在进入专业制作界面...')
|
||||
ElMessage.success($t('workflow.splitSuccess'))
|
||||
|
||||
// 跳转到专业编辑器页面
|
||||
router.push({
|
||||
@@ -1465,7 +1487,7 @@ const pollTaskStatus = async (taskId: string) => {
|
||||
}
|
||||
|
||||
const regenerateShots = async () => {
|
||||
await ElMessageBox.confirm('确定要重新拆分分镜吗?', '提示', {
|
||||
await ElMessageBox.confirm($t('workflow.reSplitConfirm'), $t('common.tip'), {
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
@@ -1550,32 +1572,32 @@ const selectFromLibrary = async (characterId: number) => {
|
||||
currentUploadTarget.value = characterId
|
||||
libraryDialogVisible.value = true
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || '获取角色库失败')
|
||||
ElMessage.error(error.message || $t('workflow.loadLibraryFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
const addToCharacterLibrary = async (character: any) => {
|
||||
if (!character.image_url) {
|
||||
ElMessage.warning('请先生成角色图片')
|
||||
ElMessage.warning($t('workflow.generateImageFirst'))
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要将角色"${character.name}"添加到全局角色库吗?添加后可以在所有项目中使用该角色形象。`,
|
||||
'添加到角色库',
|
||||
$t('workflow.addToLibraryConfirm', { name: character.name }),
|
||||
$t('workflow.addToLibrary'),
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
confirmButtonText: $t('common.confirm'),
|
||||
cancelButtonText: $t('common.cancel'),
|
||||
type: 'info'
|
||||
}
|
||||
)
|
||||
|
||||
await characterLibraryAPI.addCharacterToLibrary(character.id.toString())
|
||||
ElMessage.success('已添加到角色库!')
|
||||
ElMessage.success($t('workflow.addedToLibrary'))
|
||||
} catch (error: any) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error(error.message || '添加失败')
|
||||
ElMessage.error(error.message || $t('workflow.addFailed'))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
<div class="toolbar-left">
|
||||
<el-button link @click="goBack" class="back-btn">
|
||||
<el-icon><ArrowLeft /></el-icon>
|
||||
返回剧集编辑
|
||||
{{ $t('editor.backToEpisode') }}
|
||||
</el-button>
|
||||
<el-divider direction="vertical" />
|
||||
<span class="episode-title">{{ drama?.title }} - 第{{ episodeNumber }}集</span>
|
||||
<span class="episode-title">{{ drama?.title }} - {{ $t('editor.episode', { number: episodeNumber }) }}</span>
|
||||
</div>
|
||||
|
||||
<div class="toolbar-right">
|
||||
@@ -21,8 +21,8 @@
|
||||
<!-- 左侧分镜列表 -->
|
||||
<div class="storyboard-panel">
|
||||
<div class="panel-header">
|
||||
<h3>剧本结构</h3>
|
||||
<el-button text :icon="Plus" @click="handleAddStoryboard">添加</el-button>
|
||||
<h3>{{ $t('storyboard.scriptStructure') }}</h3>
|
||||
<el-button text :icon="Plus" @click="handleAddStoryboard">{{ $t('storyboard.add') }}</el-button>
|
||||
</div>
|
||||
|
||||
<div class="storyboard-list">
|
||||
@@ -36,8 +36,8 @@
|
||||
<div class="shot-content">
|
||||
<div class="shot-header">
|
||||
<div class="shot-title-row">
|
||||
<span class="shot-number">镜头 {{ shot.storyboard_number }}</span>
|
||||
<span class="shot-title">{{ shot.title || '未命名镜头' }}</span>
|
||||
<span class="shot-number">{{ $t('storyboard.shotNumber', { number: shot.storyboard_number }) }}</span>
|
||||
<span class="shot-title">{{ shot.title || $t('storyboard.untitled') }}</span>
|
||||
</div>
|
||||
<div class="shot-duration">{{ shot.duration }}s</div>
|
||||
</div>
|
||||
@@ -60,39 +60,39 @@
|
||||
@asset-deleted="loadVideoAssets"
|
||||
@merge-completed="handleMergeCompleted"
|
||||
/>
|
||||
<el-empty v-else description="暂无分镜" class="empty-timeline" />
|
||||
<el-empty v-else :description="$t('storyboard.noStoryboard')" class="empty-timeline" />
|
||||
</div>
|
||||
|
||||
<!-- 右侧编辑面板 -->
|
||||
<div class="edit-panel">
|
||||
<el-tabs v-model="activeTab" class="edit-tabs">
|
||||
<!-- 镜头属性标签 -->
|
||||
<el-tab-pane label="镜头属性" name="shot" v-if="currentStoryboard">
|
||||
<el-tab-pane :label="$t('storyboard.shotProperties')" name="shot" v-if="currentStoryboard">
|
||||
<div v-if="currentStoryboard" class="shot-editor-new">
|
||||
<!-- 场景(Scene) -->
|
||||
<div class="scene-section">
|
||||
<div class="section-label">
|
||||
场景 (Scene)
|
||||
<el-button size="small" text @click="showSceneSelector = true">选择场景</el-button>
|
||||
{{ $t('storyboard.scene') }} (Scene)
|
||||
<el-button size="small" text @click="showSceneSelector = true">{{ $t('storyboard.selectScene') }}</el-button>
|
||||
</div>
|
||||
<div class="scene-preview" v-if="currentStoryboard.background?.image_url" @click="showSceneImage">
|
||||
<img :src="currentStoryboard.background.image_url" alt="场景" style="cursor: pointer;" />
|
||||
<div class="scene-info">
|
||||
<div>{{ currentStoryboard.background.location }} · {{ currentStoryboard.background.time }}</div>
|
||||
<div class="scene-id">场景ID: {{ currentStoryboard.scene_id || 'N/A' }}</div>
|
||||
<div class="scene-id">{{ $t('editor.sceneId') }}: {{ currentStoryboard.scene_id || 'N/A' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="scene-preview-empty" v-else>
|
||||
<el-icon :size="48" color="#666"><Picture /></el-icon>
|
||||
<div>{{ currentStoryboard.background ? '场景图片生成中...' : '未关联背景' }}</div>
|
||||
<div>{{ currentStoryboard.background ? $t('editor.sceneGenerating') : $t('editor.noBackground') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 登场角色(Cast) -->
|
||||
<div class="cast-section">
|
||||
<div class="section-label">
|
||||
登场角色 (Cast)
|
||||
<el-button size="small" text :icon="Plus" @click="showCharacterSelector = true">添加角色</el-button>
|
||||
{{ $t('editor.cast') }} (Cast)
|
||||
<el-button size="small" text :icon="Plus" @click="showCharacterSelector = true">{{ $t('editor.addCharacter') }}</el-button>
|
||||
</div>
|
||||
<div class="cast-list">
|
||||
<div
|
||||
@@ -105,23 +105,23 @@
|
||||
<span v-else>{{ char.name?.[0] || '?' }}</span>
|
||||
</div>
|
||||
<div class="cast-name">{{ char.name }}</div>
|
||||
<div class="cast-remove" @click.stop="toggleCharacterInShot(char.id)" title="移除角色">
|
||||
<div class="cast-remove" @click.stop="toggleCharacterInShot(char.id)" :title="$t('editor.removeCharacter')">
|
||||
<el-icon :size="14"><Close /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!currentStoryboard?.characters || currentStoryboard.characters.length === 0" class="cast-empty">
|
||||
未指定角色
|
||||
{{ $t('editor.noCharacters') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 视效设置 -->
|
||||
<div class="settings-section">
|
||||
<div class="section-label">视效设置</div>
|
||||
<div class="section-label">{{ $t('editor.visualSettings') }}</div>
|
||||
<div class="settings-grid">
|
||||
<div class="setting-item">
|
||||
<label>景别</label>
|
||||
<el-select v-model="currentStoryboard.shot_type" size="small" placeholder="选择景别" @change="saveStoryboardField('shot_type')">
|
||||
<label>{{ $t('editor.shotType') }}</label>
|
||||
<el-select v-model="currentStoryboard.shot_type" size="small" :placeholder="$t('editor.shotTypePlaceholder')" @change="saveStoryboardField('shot_type')">
|
||||
<el-option label="大远景" value="大远景" />
|
||||
<el-option label="远景" value="远景" />
|
||||
<el-option label="全景" value="全景" />
|
||||
@@ -135,8 +135,8 @@
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<label>运镜方式</label>
|
||||
<el-select v-model="currentStoryboard.movement" size="small" placeholder="运镜方式" @change="saveStoryboardField('movement')">
|
||||
<label>{{ $t('editor.movement') }}</label>
|
||||
<el-select v-model="currentStoryboard.movement" size="small" :placeholder="$t('editor.movementPlaceholder')" @change="saveStoryboardField('movement')">
|
||||
<el-option label="固定镜头" value="固定镜头" />
|
||||
<el-option label="推镜" value="推镜" />
|
||||
<el-option label="拉镜" value="拉镜" />
|
||||
@@ -155,8 +155,8 @@
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<label>镜头角度</label>
|
||||
<el-select v-model="currentStoryboard.angle" size="small" placeholder="镜头角度" @change="saveStoryboardField('angle')">
|
||||
<label>{{ $t('editor.angle') }}</label>
|
||||
<el-select v-model="currentStoryboard.angle" size="small" :placeholder="$t('editor.anglePlaceholder')" @change="saveStoryboardField('angle')">
|
||||
<el-option label="平视" value="平视" />
|
||||
<el-option label="俯视" value="俯视" />
|
||||
<el-option label="仰视" value="仰视" />
|
||||
@@ -175,52 +175,52 @@
|
||||
|
||||
<!-- 叙事内容 -->
|
||||
<div class="narrative-section">
|
||||
<div class="section-label">动作描述 (Action)</div>
|
||||
<div class="section-label">{{ $t('editor.action') }} (Action)</div>
|
||||
<el-input
|
||||
v-model="currentStoryboard.action"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="描述角色的动作过程..."
|
||||
:placeholder="$t('editor.actionPlaceholder')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="narrative-section">
|
||||
<div class="section-label">动作结果 (Result)</div>
|
||||
<div class="section-label">{{ $t('editor.result') }} (Result)</div>
|
||||
<el-input
|
||||
v-model="currentStoryboard.result"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="描述动作完成后的状态..."
|
||||
:placeholder="$t('editor.resultPlaceholder')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="dialogue-section">
|
||||
<div class="section-label">对白 (Dialogue)</div>
|
||||
<div class="section-label">{{ $t('editor.dialogue') }} (Dialogue)</div>
|
||||
<el-input
|
||||
v-model="currentStoryboard.dialogue"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="角色对白内容..."
|
||||
:placeholder="$t('editor.dialoguePlaceholder')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="narrative-section">
|
||||
<div class="section-label">镜头描述 (Description)</div>
|
||||
<div class="section-label">{{ $t('editor.description') }} (Description)</div>
|
||||
<el-input
|
||||
v-model="currentStoryboard.description"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="整体镜头描述..."
|
||||
:placeholder="$t('editor.descriptionPlaceholder')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 音效设置 -->
|
||||
<div class="settings-section">
|
||||
<div class="section-label">音效</div>
|
||||
<div class="section-label">{{ $t('editor.soundEffects') }}</div>
|
||||
<div class="audio-controls">
|
||||
<el-input
|
||||
v-model="currentStoryboard.sound_effect"
|
||||
placeholder="描述音效,如:脚步声、关门声等"
|
||||
:placeholder="$t('editor.soundEffectsPlaceholder')"
|
||||
size="small"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
@@ -230,11 +230,11 @@
|
||||
|
||||
<!-- 配乐设置 -->
|
||||
<div class="settings-section">
|
||||
<div class="section-label">配乐提示</div>
|
||||
<div class="section-label">{{ $t('editor.bgmPrompt') }}</div>
|
||||
<div class="audio-controls">
|
||||
<el-input
|
||||
v-model="currentStoryboard.bgm_prompt"
|
||||
placeholder="描述配乐氛围,如:紧张激烈的背景音乐"
|
||||
:placeholder="$t('editor.bgmPromptPlaceholder')"
|
||||
size="small"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
@@ -244,11 +244,11 @@
|
||||
|
||||
<!-- 氛围设置 -->
|
||||
<div class="settings-section">
|
||||
<div class="section-label">环境氛围</div>
|
||||
<div class="section-label">{{ $t('editor.atmosphere') }}</div>
|
||||
<div class="audio-controls">
|
||||
<el-input
|
||||
v-model="currentStoryboard.atmosphere"
|
||||
placeholder="描述环境氛围,如:昏暗压抑、明亮温馨"
|
||||
:placeholder="$t('editor.atmospherePlaceholder')"
|
||||
size="small"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
@@ -256,22 +256,22 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-empty v-else description="未选择镜头" />
|
||||
<el-empty v-else :description="$t('editor.noShotSelected')" />
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 图片生成标签 -->
|
||||
<el-tab-pane label="镜头图片" name="image">
|
||||
<el-tab-pane :label="$t('editor.shotImage')" name="image">
|
||||
<div class="tab-content" v-if="currentStoryboard">
|
||||
<div class="image-generation-section">
|
||||
<!-- 帧类型选择 -->
|
||||
<div class="frame-type-selector">
|
||||
<div class="section-label">选择帧类型</div>
|
||||
<div class="section-label">{{ $t('editor.selectFrameType') }}</div>
|
||||
<el-radio-group v-model="selectedFrameType" size="small">
|
||||
<el-radio-button label="first">首帧</el-radio-button>
|
||||
<el-radio-button label="last">尾帧</el-radio-button>
|
||||
<el-radio-button label="panel">分镜板</el-radio-button>
|
||||
<el-radio-button label="action">动作序列</el-radio-button>
|
||||
<el-radio-button label="key">关键帧</el-radio-button>
|
||||
<el-radio-button label="first">{{ $t('editor.firstFrame') }}</el-radio-button>
|
||||
<el-radio-button label="last">{{ $t('editor.lastFrame') }}</el-radio-button>
|
||||
<el-radio-button label="panel">{{ $t('editor.panelFrame') }}</el-radio-button>
|
||||
<el-radio-button label="action">{{ $t('editor.actionSequence') }}</el-radio-button>
|
||||
<el-radio-button label="key">{{ $t('editor.keyFrame') }}</el-radio-button>
|
||||
</el-radio-group>
|
||||
<el-input-number
|
||||
v-if="selectedFrameType === 'panel'"
|
||||
@@ -282,13 +282,13 @@
|
||||
class="panel-count-input"
|
||||
style="margin-left: 10px; margin-top: 12px;"
|
||||
/>
|
||||
<span v-if="selectedFrameType === 'panel'" style="margin-left: 5px; font-size: 12px; color: #999;">格数</span>
|
||||
<span v-if="selectedFrameType === 'panel'" style="margin-left: 5px; font-size: 12px; color: #999;">{{ $t('editor.panelCount') }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 提示词区域 -->
|
||||
<div class="prompt-section">
|
||||
<div class="section-label">
|
||||
提示词
|
||||
{{ $t('editor.prompt') }}
|
||||
<el-button
|
||||
size="small"
|
||||
type="primary"
|
||||
@@ -297,14 +297,14 @@
|
||||
@click="extractFramePrompt"
|
||||
style="margin-left: 10px;"
|
||||
>
|
||||
提取提示词
|
||||
{{ $t('editor.extractPrompt') }}
|
||||
</el-button>
|
||||
</div>
|
||||
<el-input
|
||||
v-model="currentFramePrompt"
|
||||
type="textarea"
|
||||
:rows="8"
|
||||
placeholder="点击提取提示词按钮,系统将根据分镜内容生成图片提示词..."
|
||||
:placeholder="$t('editor.promptPlaceholder')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -317,14 +317,14 @@
|
||||
:disabled="!currentFramePrompt"
|
||||
@click="generateFrameImage"
|
||||
>
|
||||
{{ generatingImage ? '生成中...' : '生成图片' }}
|
||||
{{ generatingImage ? $t('editor.generating') : $t('editor.generateImage') }}
|
||||
</el-button>
|
||||
<el-button :icon="Upload" @click="uploadImage">上传图片</el-button>
|
||||
<el-button :icon="Upload" @click="uploadImage">{{ $t('editor.uploadImage') }}</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 生成结果 -->
|
||||
<div class="generation-result" v-if="generatedImages.length > 0">
|
||||
<div class="section-label">生成结果 ({{ generatedImages.length }})</div>
|
||||
<div class="section-label">{{ $t('editor.generationResult') }} ({{ generatedImages.length }})</div>
|
||||
<div class="image-grid">
|
||||
<div
|
||||
v-for="img in generatedImages"
|
||||
@@ -356,7 +356,7 @@
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 视频生成标签 -->
|
||||
<el-tab-pane label="视频生成" name="video">
|
||||
<el-tab-pane :label="$t('video.videoGeneration')" name="video">
|
||||
<div class="tab-content" v-if="currentStoryboard">
|
||||
<div class="video-generation-section">
|
||||
<!-- 生成提示词展示 -->
|
||||
@@ -367,8 +367,8 @@
|
||||
<!-- 视频参数设置 -->
|
||||
<div class="video-params-section">
|
||||
<div style="margin-bottom: 12px; display: flex; align-items: center; gap: 12px;">
|
||||
<span style="min-width: 60px; font-size: 14px; color: #606266;">模型</span>
|
||||
<el-select v-model="selectedVideoModel" placeholder="请选择视频生成模型" size="default" style="flex: 1;">
|
||||
<span style="min-width: 60px; font-size: 14px; color: #606266;">{{ $t('video.model') }}</span>
|
||||
<el-select v-model="selectedVideoModel" :placeholder="$t('video.selectVideoModel')" size="default" style="flex: 1;">
|
||||
<el-option
|
||||
v-for="model in videoModelCapabilities"
|
||||
:key="model.id"
|
||||
@@ -406,10 +406,10 @@
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 8px; display: flex; align-items: center; gap: 12px;">
|
||||
<span style="min-width: 60px; font-size: 14px; color: #606266;">时长</span>
|
||||
<span style="min-width: 60px; font-size: 14px; color: #606266;">{{ $t('professionalEditor.duration') }}</span>
|
||||
<div style="flex: 1; display: flex; align-items: center;">
|
||||
<el-slider v-model="videoDuration" :min="4" :max="10" :step="1" show-stops style="flex: 1;" />
|
||||
<span style="margin-left: 10px; min-width: 40px;">{{ videoDuration }}秒</span>
|
||||
<span style="margin-left: 10px; min-width: 40px;">{{ videoDuration }}{{ $t('professionalEditor.seconds') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -685,21 +685,21 @@
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 音效与配乐标签 -->
|
||||
<el-tab-pane label="音效与配乐" name="audio">
|
||||
<el-tab-pane :label="$t('video.soundAndMusicTab')" name="audio">
|
||||
<div class="tab-content">
|
||||
<el-empty description="音效与配乐功能开发中" />
|
||||
<el-empty :description="$t('video.soundMusicInDev')" />
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 视频合成列表标签 -->
|
||||
<el-tab-pane label="视频合成" name="merges">
|
||||
<el-tab-pane :label="$t('video.videoMerge')" name="merges">
|
||||
<div class="tab-content">
|
||||
<div class="merges-list" v-loading="loadingMerges">
|
||||
<el-empty v-if="videoMerges.length === 0" description="暂无视频合成记录" :image-size="120">
|
||||
<el-empty v-if="videoMerges.length === 0" :description="$t('video.noMergeRecords')" :image-size="120">
|
||||
<template #description>
|
||||
<div style="color: #909399; font-size: 14px; margin-top: 12px;">
|
||||
<p style="margin: 0;">还没有合成过视频</p>
|
||||
<p style="margin: 8px 0 0 0; font-size: 12px;">在时间线编辑器中排列好视频后点击"合成视频"即可</p>
|
||||
<p style="margin: 0;">{{ $t('video.noMergeYet') }}</p>
|
||||
<p style="margin: 8px 0 0 0; font-size: 12px;">{{ $t('video.mergeInstructions') }}</p>
|
||||
</div>
|
||||
</template>
|
||||
</el-empty>
|
||||
@@ -738,8 +738,8 @@
|
||||
<el-icon :size="16"><Timer /></el-icon>
|
||||
</div>
|
||||
<div class="detail-content">
|
||||
<div class="detail-label">视频时长</div>
|
||||
<div class="detail-value">{{ merge.duration ? `${merge.duration} 秒` : '-' }}</div>
|
||||
<div class="detail-label">{{ $t('professionalEditor.videoDuration') }}</div>
|
||||
<div class="detail-value">{{ merge.duration ? `${merge.duration} ${$t('professionalEditor.seconds')}` : '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
@@ -906,10 +906,10 @@
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<div>
|
||||
<el-tag :type="getStatusType(previewVideo.status)" size="small">{{ getStatusText(previewVideo.status) }}</el-tag>
|
||||
<span v-if="previewVideo.duration" style="margin-left: 12px; color: #606266; font-size: 14px;">时长: {{ previewVideo.duration }}秒</span>
|
||||
<span v-if="previewVideo.duration" style="margin-left: 12px; color: #606266; font-size: 14px;">{{ $t('professionalEditor.duration') }}: {{ previewVideo.duration }}{{ $t('professionalEditor.seconds') }}</span>
|
||||
</div>
|
||||
<el-button v-if="previewVideo.video_url" size="small" @click="window.open(previewVideo.video_url, '_blank')">
|
||||
下载视频
|
||||
{{ $t('professionalEditor.downloadVideo') }}
|
||||
</el-button>
|
||||
</div>
|
||||
<div v-if="previewVideo.prompt" style="margin-top: 12px; font-size: 12px; color: #606266; line-height: 1.6;">
|
||||
@@ -922,8 +922,9 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, watch, onBeforeUnmount } from 'vue'
|
||||
import { ref, computed, watch, onMounted, onBeforeUnmount, nextTick } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
ArrowLeft, Plus, Picture, VideoPlay, VideoPause, View, Setting,
|
||||
@@ -947,6 +948,7 @@ import type { Drama, Episode, Storyboard } from '@/types/drama'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const { t: $t } = useI18n()
|
||||
|
||||
const dramaId = Number(route.params.dramaId)
|
||||
const episodeNumber = Number(route.params.episodeNumber)
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
<div class="editor-header">
|
||||
<el-button link @click="goBack" class="back-button">
|
||||
<el-icon><ArrowLeft /></el-icon>
|
||||
返回
|
||||
{{ $t('timeline.backToEditor') }}
|
||||
</el-button>
|
||||
<h2>时间线编辑器</h2>
|
||||
<h2>{{ $t('timeline.title') }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="editor-content">
|
||||
@@ -14,7 +14,7 @@
|
||||
:scenes="scenes"
|
||||
:episode-id="episodeId"
|
||||
/>
|
||||
<el-empty v-else description="暂无可用场景" />
|
||||
<el-empty v-else :description="$t('timeline.noScenes')" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -38,7 +38,7 @@ const loadScenes = async () => {
|
||||
const res = await dramaAPI.getStoryboards(episodeId)
|
||||
scenes.value = res.storyboards || []
|
||||
} catch (error: any) {
|
||||
ElMessage.error('加载分镜失败')
|
||||
ElMessage.error($t('timeline.loadFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,21 +3,21 @@
|
||||
<el-page-header @back="goBack" class="page-header">
|
||||
<template #content>
|
||||
<div class="header-content">
|
||||
<h2>AI 图片生成</h2>
|
||||
<h2>{{ $t('image.title') }}</h2>
|
||||
</div>
|
||||
</template>
|
||||
<template #extra>
|
||||
<el-button type="primary" @click="showGenerateDialog = true">
|
||||
<el-icon><Plus /></el-icon>
|
||||
生成图片
|
||||
{{ $t('image.generate') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-page-header>
|
||||
|
||||
<el-card shadow="never" class="filter-card">
|
||||
<el-form inline>
|
||||
<el-form-item label="剧本">
|
||||
<el-select v-model="filters.drama_id" placeholder="全部剧本" clearable>
|
||||
<el-form-item :label="$t('video.filter.drama')">
|
||||
<el-select v-model="filters.drama_id" :placeholder="$t('video.filter.allDramas')" clearable>
|
||||
<el-option
|
||||
v-for="drama in dramas"
|
||||
:key="drama.id"
|
||||
@@ -27,17 +27,17 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="filters.status" placeholder="全部状态" clearable>
|
||||
<el-option label="生成中" value="processing" />
|
||||
<el-option label="已完成" value="completed" />
|
||||
<el-option label="失败" value="failed" />
|
||||
<el-form-item :label="$t('video.filter.status')">
|
||||
<el-select v-model="filters.status" :placeholder="$t('video.filter.allStatus')" clearable>
|
||||
<el-option :label="$t('video.status.processing')" value="processing" />
|
||||
<el-option :label="$t('video.status.completed')" value="completed" />
|
||||
<el-option :label="$t('video.status.failed')" value="failed" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="loadImages">查询</el-button>
|
||||
<el-button @click="resetFilters">重置</el-button>
|
||||
<el-button type="primary" @click="loadImages">{{ $t('video.filter.query') }}</el-button>
|
||||
<el-button @click="resetFilters">{{ $t('video.filter.reset') }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
@@ -63,19 +63,19 @@
|
||||
<template #error>
|
||||
<div class="image-placeholder">
|
||||
<el-icon><PictureFilled /></el-icon>
|
||||
<span>加载失败</span>
|
||||
<span>{{ $t('image.loadFailed') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
|
||||
<div v-else-if="image.status === 'processing'" class="image-placeholder processing">
|
||||
<el-icon class="loading-icon"><Loading /></el-icon>
|
||||
<span>生成中...</span>
|
||||
<span>{{ $t('image.generating') }}</span>
|
||||
</div>
|
||||
|
||||
<div v-else-if="image.status === 'failed'" class="image-placeholder failed">
|
||||
<el-icon><CircleClose /></el-icon>
|
||||
<span>生成失败</span>
|
||||
<span>{{ $t('image.generateFailed') }}</span>
|
||||
</div>
|
||||
|
||||
<div v-else class="image-placeholder">
|
||||
|
||||
@@ -3,21 +3,21 @@
|
||||
<el-page-header @back="goBack" class="page-header">
|
||||
<template #content>
|
||||
<div class="header-content">
|
||||
<h2>AI 视频生成</h2>
|
||||
<h2>{{ $t('video.title') }}</h2>
|
||||
</div>
|
||||
</template>
|
||||
<template #extra>
|
||||
<el-button type="primary" @click="showGenerateDialog = true">
|
||||
<el-icon><VideoPlay /></el-icon>
|
||||
生成视频
|
||||
{{ $t('video.generate') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-page-header>
|
||||
|
||||
<el-card shadow="never" class="filter-card">
|
||||
<el-form inline>
|
||||
<el-form-item label="剧本">
|
||||
<el-select v-model="filters.drama_id" placeholder="全部剧本" clearable>
|
||||
<el-form-item :label="$t('video.filter.drama')">
|
||||
<el-select v-model="filters.drama_id" :placeholder="$t('video.filter.allDramas')" clearable>
|
||||
<el-option
|
||||
v-for="drama in dramas"
|
||||
:key="drama.id"
|
||||
@@ -27,17 +27,17 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="filters.status" placeholder="全部状态" clearable>
|
||||
<el-option label="生成中" value="processing" />
|
||||
<el-option label="已完成" value="completed" />
|
||||
<el-option label="失败" value="failed" />
|
||||
<el-form-item :label="$t('video.filter.status')">
|
||||
<el-select v-model="filters.status" :placeholder="$t('video.filter.allStatus')" clearable>
|
||||
<el-option :label="$t('video.status.processing')" value="processing" />
|
||||
<el-option :label="$t('video.status.completed')" value="completed" />
|
||||
<el-option :label="$t('video.status.failed')" value="failed" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="loadVideos">查询</el-button>
|
||||
<el-button @click="resetFilters">重置</el-button>
|
||||
<el-button type="primary" @click="loadVideos">{{ $t('video.filter.query') }}</el-button>
|
||||
<el-button @click="resetFilters">{{ $t('video.filter.reset') }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
title="AI 图片生成"
|
||||
:title="$t('imageDialog.title')"
|
||||
width="700px"
|
||||
:close-on-click-modal="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form :model="form" :rules="rules" ref="formRef" label-width="120px">
|
||||
<el-form-item label="选择剧本" prop="drama_id">
|
||||
<el-select v-model="form.drama_id" placeholder="选择剧本" @change="onDramaChange">
|
||||
<el-form-item :label="$t('imageDialog.selectDrama')" prop="drama_id">
|
||||
<el-select v-model="form.drama_id" :placeholder="$t('imageDialog.selectDrama')" @change="onDramaChange">
|
||||
<el-option
|
||||
v-for="drama in dramas"
|
||||
:key="drama.id"
|
||||
@@ -18,95 +18,95 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="选择场景" prop="scene_id">
|
||||
<el-form-item :label="$t('imageDialog.selectScene')" prop="scene_id">
|
||||
<el-select
|
||||
v-model="form.scene_id"
|
||||
placeholder="选择场景(可选)"
|
||||
:placeholder="$t('imageDialog.selectSceneOptional')"
|
||||
clearable
|
||||
@change="onSceneChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="scene in scenes"
|
||||
:key="scene.id"
|
||||
:label="`场景${scene.storyboard_number}: ${scene.title}`"
|
||||
:label="$t('imageDialog.sceneLabel', { number: scene.storyboard_number, title: scene.title })"
|
||||
:value="scene.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="提示词" prop="prompt">
|
||||
<el-form-item :label="$t('imageDialog.prompt')" prop="prompt">
|
||||
<el-input
|
||||
v-model="form.prompt"
|
||||
type="textarea"
|
||||
:rows="6"
|
||||
placeholder="描述你想生成的图片 例如:A beautiful landscape with mountains and rivers at sunset, cinematic lighting, highly detailed"
|
||||
:placeholder="$t('imageDialog.promptPlaceholder')"
|
||||
maxlength="2000"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="反向提示词">
|
||||
<el-form-item :label="$t('imageDialog.negativePrompt')">
|
||||
<el-input
|
||||
v-model="form.negative_prompt"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="描述不希望出现的元素(可选) 例如:blurry, low quality, watermark"
|
||||
:placeholder="$t('imageDialog.negativePromptPlaceholder')"
|
||||
maxlength="1000"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="AI 服务">
|
||||
<el-select v-model="form.provider" placeholder="选择服务">
|
||||
<el-form-item :label="$t('imageDialog.aiService')">
|
||||
<el-select v-model="form.provider" :placeholder="$t('imageDialog.selectService')">
|
||||
<el-option label="OpenAI/DALL-E" value="openai" />
|
||||
<el-option label="Stable Diffusion" value="stable_diffusion" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="图片尺寸">
|
||||
<el-select v-model="form.size" placeholder="选择尺寸">
|
||||
<el-option label="1024x1024 (正方形)" value="1024x1024" />
|
||||
<el-option label="1792x1024 (横向)" value="1792x1024" />
|
||||
<el-option label="1024x1792 (纵向)" value="1024x1792" />
|
||||
<el-form-item :label="$t('imageDialog.imageSize')">
|
||||
<el-select v-model="form.size" :placeholder="$t('imageDialog.selectSize')">
|
||||
<el-option :label="`1024x1024 (${$t('imageDialog.square')})`" value="1024x1024" />
|
||||
<el-option :label="`1792x1024 (${$t('imageDialog.landscape')})`" value="1792x1024" />
|
||||
<el-option :label="`1024x1792 (${$t('imageDialog.portrait')})`" value="1024x1792" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="图片质量" v-if="form.provider === 'openai'">
|
||||
<el-form-item :label="$t('imageDialog.imageQuality')" v-if="form.provider === 'openai'">
|
||||
<el-radio-group v-model="form.quality">
|
||||
<el-radio label="standard">标准</el-radio>
|
||||
<el-radio label="hd">高清</el-radio>
|
||||
<el-radio label="standard">{{ $t('imageDialog.standard') }}</el-radio>
|
||||
<el-radio label="hd">{{ $t('imageDialog.hd') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="风格" v-if="form.provider === 'openai'">
|
||||
<el-form-item :label="$t('imageDialog.style')" v-if="form.provider === 'openai'">
|
||||
<el-radio-group v-model="form.style">
|
||||
<el-radio label="vivid">鲜艳</el-radio>
|
||||
<el-radio label="natural">自然</el-radio>
|
||||
<el-radio label="vivid">{{ $t('imageDialog.vivid') }}</el-radio>
|
||||
<el-radio label="natural">{{ $t('imageDialog.natural') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-collapse v-if="form.provider === 'stable_diffusion'">
|
||||
<el-collapse-item title="高级设置" name="advanced">
|
||||
<el-form-item label="采样步数">
|
||||
<el-collapse-item :title="$t('imageDialog.advancedSettings')" name="advanced">
|
||||
<el-form-item :label="$t('imageDialog.samplingSteps')">
|
||||
<el-slider v-model="form.steps" :min="10" :max="50" :marks="stepsMarks" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="提示词相关性">
|
||||
<el-form-item :label="$t('imageDialog.promptRelevance')">
|
||||
<el-slider v-model="form.cfg_scale" :min="1" :max="20" :step="0.5" :marks="cfgMarks" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="随机种子">
|
||||
<el-input-number v-model="form.seed" :min="-1" placeholder="留空随机" />
|
||||
<span class="form-tip">设置相同种子可复现图片</span>
|
||||
<el-form-item :label="$t('imageDialog.randomSeed')">
|
||||
<el-input-number v-model="form.seed" :min="-1" :placeholder="$t('imageDialog.leaveBlankRandom')" />
|
||||
<span class="form-tip">{{ $t('imageDialog.seedTip') }}</span>
|
||||
</el-form-item>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button @click="handleClose">{{ $t('common.cancel') }}</el-button>
|
||||
<el-button type="primary" :loading="generating" @click="handleGenerate">
|
||||
生成图片
|
||||
{{ $t('imageDialog.generate') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
@@ -115,6 +115,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, watch } from 'vue'
|
||||
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { imageAPI } from '@/api/image'
|
||||
import { dramaAPI } from '@/api/drama'
|
||||
import type { Drama, Scene } from '@/types/drama'
|
||||
@@ -131,6 +132,8 @@ const emit = defineEmits<{
|
||||
success: []
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
@@ -157,11 +160,11 @@ const form = reactive<GenerateImageRequest>({
|
||||
|
||||
const rules: FormRules = {
|
||||
drama_id: [
|
||||
{ required: true, message: '请选择剧本', trigger: 'change' }
|
||||
{ required: true, message: t('imageDialog.pleaseSelectDrama'), trigger: 'change' }
|
||||
],
|
||||
prompt: [
|
||||
{ required: true, message: '请输入提示词', trigger: 'blur' },
|
||||
{ min: 5, message: '提示词至少5个字符', trigger: 'blur' }
|
||||
{ required: true, message: t('imageDialog.pleaseEnterPrompt'), trigger: 'blur' },
|
||||
{ min: 5, message: t('imageDialog.promptMinLength'), trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -174,10 +177,10 @@ const stepsMarks = {
|
||||
}
|
||||
|
||||
const cfgMarks = {
|
||||
1: '弱',
|
||||
7.5: '适中',
|
||||
15: '强',
|
||||
20: '很强'
|
||||
1: t('imageDialog.weak'),
|
||||
7.5: t('imageDialog.moderate'),
|
||||
15: t('imageDialog.strong'),
|
||||
20: t('imageDialog.veryStrong')
|
||||
}
|
||||
|
||||
watch(() => props.modelValue, (val) => {
|
||||
@@ -274,11 +277,11 @@ const handleGenerate = async () => {
|
||||
|
||||
await imageAPI.generateImage(params)
|
||||
|
||||
ElMessage.success('图片生成任务已提交,请稍后查看结果')
|
||||
ElMessage.success(t('imageDialog.taskSubmitted'))
|
||||
emit('success')
|
||||
handleClose()
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || '生成失败')
|
||||
ElMessage.error(error.message || t('imageDialog.generateFailed'))
|
||||
} finally {
|
||||
generating.value = false
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
<template>
|
||||
<div class="ai-config-container">
|
||||
<el-page-header @back="goBack" title="返回">
|
||||
<el-page-header @back="goBack" :title="$t('aiConfig.back')">
|
||||
<template #content>
|
||||
<div class="page-header">
|
||||
<h2>AI 服务配置</h2>
|
||||
<el-button type="primary" @click="showCreateDialog" :icon="Plus">添加配置</el-button>
|
||||
<h2>{{ $t('aiConfig.title') }}</h2>
|
||||
<el-button type="primary" @click="showCreateDialog" :icon="Plus">{{ $t('aiConfig.addConfig') }}</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-page-header>
|
||||
|
||||
<el-tabs v-model="activeTab" @tab-change="handleTabChange">
|
||||
<el-tab-pane label="文本生成" name="text">
|
||||
<el-tab-pane :label="$t('aiConfig.tabs.text')" name="text">
|
||||
<ConfigList
|
||||
:configs="configs"
|
||||
:loading="loading"
|
||||
@@ -22,7 +22,7 @@
|
||||
/>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="图片生成" name="image">
|
||||
<el-tab-pane :label="$t('aiConfig.tabs.image')" name="image">
|
||||
<ConfigList
|
||||
:configs="configs"
|
||||
:loading="loading"
|
||||
@@ -33,7 +33,7 @@
|
||||
/>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="视频生成" name="video">
|
||||
<el-tab-pane :label="$t('aiConfig.tabs.video')" name="video">
|
||||
<ConfigList
|
||||
:configs="configs"
|
||||
:loading="loading"
|
||||
@@ -47,7 +47,7 @@
|
||||
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="isEdit ? '编辑配置' : '添加配置'"
|
||||
:title="isEdit ? $t('aiConfig.editConfig') : $t('aiConfig.addConfig')"
|
||||
width="600px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
@@ -57,14 +57,14 @@
|
||||
:rules="rules"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item label="配置名称" prop="name">
|
||||
<el-input v-model="form.name" placeholder="例如:OpenAI GPT-4" />
|
||||
<el-form-item :label="$t('aiConfig.form.name')" prop="name">
|
||||
<el-input v-model="form.name" :placeholder="$t('aiConfig.form.namePlaceholder')" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="厂商" prop="provider">
|
||||
<el-form-item :label="$t('aiConfig.form.provider')" prop="provider">
|
||||
<el-select
|
||||
v-model="form.provider"
|
||||
placeholder="请选择厂商"
|
||||
:placeholder="$t('aiConfig.form.providerPlaceholder')"
|
||||
@change="handleProviderChange"
|
||||
style="width: 100%"
|
||||
>
|
||||
@@ -76,10 +76,10 @@
|
||||
:disabled="provider.disabled"
|
||||
/>
|
||||
</el-select>
|
||||
<div class="form-tip">选择AI服务提供商</div>
|
||||
<div class="form-tip">{{ $t('aiConfig.form.providerTip') }}</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="优先级" prop="priority">
|
||||
<el-form-item :label="$t('aiConfig.form.priority')" prop="priority">
|
||||
<el-input-number
|
||||
v-model="form.priority"
|
||||
:min="0"
|
||||
@@ -87,13 +87,13 @@
|
||||
:step="1"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<div class="form-tip">数值越大优先级越高,相同模型时优先使用高优先级配置</div>
|
||||
<div class="form-tip">{{ $t('aiConfig.form.priorityTip') }}</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="模型" prop="model">
|
||||
<el-form-item :label="$t('aiConfig.form.model')" prop="model">
|
||||
<el-select
|
||||
v-model="form.model"
|
||||
placeholder="输入或选择模型名称"
|
||||
:placeholder="$t('aiConfig.form.modelPlaceholder')"
|
||||
multiple
|
||||
filterable
|
||||
allow-create
|
||||
@@ -109,38 +109,38 @@
|
||||
:value="model"
|
||||
/>
|
||||
</el-select>
|
||||
<div class="form-tip">可直接输入模型名称或从列表选择,支持多个模型</div>
|
||||
<div class="form-tip">{{ $t('aiConfig.form.modelTip') }}</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="Base URL" prop="base_url">
|
||||
<el-input v-model="form.base_url" placeholder="https://api.openai.com" />
|
||||
<el-form-item :label="$t('aiConfig.form.baseUrl')" prop="base_url">
|
||||
<el-input v-model="form.base_url" :placeholder="$t('aiConfig.form.baseUrlPlaceholder')" />
|
||||
<div class="form-tip">
|
||||
API 服务的基础地址,如 Chatfire: https://api.chatfire.site/v1,Gemini: https://generativelanguage.googleapis.com(无需 /v1)
|
||||
{{ $t('aiConfig.form.baseUrlTip') }}
|
||||
<br>
|
||||
完整调用路径: {{ fullEndpointExample }}
|
||||
{{ $t('aiConfig.form.fullEndpoint') }}: {{ fullEndpointExample }}
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="API Key" prop="api_key">
|
||||
<el-form-item :label="$t('aiConfig.form.apiKey')" prop="api_key">
|
||||
<el-input
|
||||
v-model="form.api_key"
|
||||
type="password"
|
||||
show-password
|
||||
placeholder="sk-..."
|
||||
:placeholder="$t('aiConfig.form.apiKeyPlaceholder')"
|
||||
/>
|
||||
<div class="form-tip">您的 API 密钥</div>
|
||||
<div class="form-tip">{{ $t('aiConfig.form.apiKeyTip') }}</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="isEdit" label="启用状态">
|
||||
<el-form-item v-if="isEdit" :label="$t('aiConfig.form.isActive')">
|
||||
<el-switch v-model="form.is_active" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button v-if="form.service_type === 'text'" @click="testConnection" :loading="testing">测试连接</el-button>
|
||||
<el-button @click="dialogVisible = false">{{ $t('common.cancel') }}</el-button>
|
||||
<el-button v-if="form.service_type === 'text'" @click="testConnection" :loading="testing">{{ $t('aiConfig.actions.test') }}</el-button>
|
||||
<el-button type="primary" @click="handleSubmit" :loading="submitting">
|
||||
{{ isEdit ? '保存' : '创建' }}
|
||||
{{ isEdit ? $t('common.save') : $t('common.create') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div v-loading="loading" class="config-list">
|
||||
<el-empty v-if="!loading && configs.length === 0" description="暂无配置,点击添加配置开始使用" />
|
||||
<el-empty v-if="!loading && configs.length === 0" :description="$t('aiConfig.empty')" />
|
||||
|
||||
<el-card
|
||||
v-for="config in configs"
|
||||
@@ -11,30 +11,30 @@
|
||||
<div class="config-header">
|
||||
<div class="config-title">
|
||||
<h3>{{ config.name }}</h3>
|
||||
<el-tag v-if="config.is_active" type="success" size="small">已启用</el-tag>
|
||||
<el-tag v-else type="info" size="small">已禁用</el-tag>
|
||||
<el-tag v-if="config.is_active" type="success" size="small">{{ $t('aiConfig.enabled') }}</el-tag>
|
||||
<el-tag v-else type="info" size="small">{{ $t('aiConfig.disabled') }}</el-tag>
|
||||
</div>
|
||||
<div class="config-actions">
|
||||
<el-button v-if="showTestButton" text @click="$emit('test', config)" :icon="Connection">
|
||||
测试
|
||||
{{ $t('aiConfig.actions.test') }}
|
||||
</el-button>
|
||||
<el-button text @click="$emit('edit', config)" :icon="Edit">
|
||||
编辑
|
||||
{{ $t('common.edit') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
text
|
||||
:type="config.is_active ? 'warning' : 'success'"
|
||||
@click="$emit('toggle-active', config)"
|
||||
>
|
||||
{{ config.is_active ? '禁用' : '启用' }}
|
||||
{{ config.is_active ? $t('aiConfig.disable') : $t('aiConfig.enable') }}
|
||||
</el-button>
|
||||
<el-popconfirm
|
||||
title="确定删除该配置吗?"
|
||||
:title="$t('aiConfig.messages.deleteConfirm')"
|
||||
@confirm="$emit('delete', config)"
|
||||
>
|
||||
<template #reference>
|
||||
<el-button text type="danger" :icon="Delete">
|
||||
删除
|
||||
{{ $t('common.delete') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
@@ -48,12 +48,12 @@
|
||||
</div>
|
||||
|
||||
<div class="info-item">
|
||||
<label>端点:</label>
|
||||
<label>{{ $t('aiConfig.endpoint') }}:</label>
|
||||
<span>{{ config.endpoint || '/v1/chat/completions' }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="config.service_type === 'video' && config.query_endpoint" class="info-item">
|
||||
<label>查询端点:</label>
|
||||
<label>{{ $t('aiConfig.queryEndpoint') }}:</label>
|
||||
<span>{{ config.query_endpoint }}</span>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<div class="storyboard-edit-container">
|
||||
<el-page-header @back="goBack" title="返回">
|
||||
<el-page-header @back="goBack" :title="$t('common.back')">
|
||||
<template #content>
|
||||
<h2>分镜编辑</h2>
|
||||
<h2>{{ $t('storyboard.edit') }}</h2>
|
||||
</template>
|
||||
</el-page-header>
|
||||
<p>功能开发中...</p>
|
||||
<p>{{ $t('storyboard.inDevelopment') }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
<template>
|
||||
<div class="character-extraction-container">
|
||||
<el-page-header @back="goBack" title="返回项目">
|
||||
<el-page-header @back="goBack" :title="$t('character.backToProject')">
|
||||
<template #content>
|
||||
<h2>角色管理</h2>
|
||||
<h2>{{ $t('character.title') }}</h2>
|
||||
</template>
|
||||
</el-page-header>
|
||||
|
||||
<el-card shadow="never" class="main-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<h3>角色列表</h3>
|
||||
<h3>{{ $t('character.list') }}</h3>
|
||||
<div class="header-actions">
|
||||
<el-button @click="addCharacter">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加角色
|
||||
{{ $t('character.add') }}
|
||||
</el-button>
|
||||
<el-button type="primary" @click="saveCharacters" :loading="saving">
|
||||
保存修改
|
||||
{{ $t('character.saveChanges') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-empty v-if="characters.length === 0" description="角色已在剧本生成阶段创建,您可以在此查看和编辑" />
|
||||
<el-empty v-if="characters.length === 0" :description="$t('character.empty')" />
|
||||
|
||||
<el-row :gutter="20" v-else>
|
||||
<el-col :span="8" v-for="character in characters" :key="character.id">
|
||||
@@ -38,16 +38,16 @@
|
||||
</template>
|
||||
|
||||
<div class="character-details">
|
||||
<p><strong>性格:</strong>{{ character.personality }}</p>
|
||||
<p><strong>外貌:</strong>{{ character.appearance }}</p>
|
||||
<p><strong>背景:</strong>{{ character.background }}</p>
|
||||
<p><strong>{{ $t('character.personality') }}:</strong>{{ character.personality }}</p>
|
||||
<p><strong>{{ $t('character.appearance') }}:</strong>{{ character.appearance }}</p>
|
||||
<p><strong>{{ $t('character.background') }}:</strong>{{ character.background }}</p>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<el-button-group style="width: 100%">
|
||||
<el-button size="small" @click="editCharacter(character)">编辑</el-button>
|
||||
<el-button size="small" @click="editCharacter(character)">{{ $t('common.edit') }}</el-button>
|
||||
<el-button size="small" type="primary" @click="generateCharacterImage(character)">
|
||||
生成形象
|
||||
{{ $t('character.generateImage') }}
|
||||
</el-button>
|
||||
</el-button-group>
|
||||
</template>
|
||||
@@ -57,7 +57,7 @@
|
||||
|
||||
<div class="actions" v-if="characters.length > 0">
|
||||
<el-button type="success" size="large" @click="goToNextStep">
|
||||
下一步:生成角色图片
|
||||
{{ $t('character.nextStep') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
<template>
|
||||
<div class="script-generation-container">
|
||||
<el-page-header @back="goBack" title="返回项目">
|
||||
<el-page-header @back="goBack" :title="$t('script.backToProject')">
|
||||
<template #content>
|
||||
<h2>剧本生成</h2>
|
||||
<h2>{{ $t('script.title') }}</h2>
|
||||
</template>
|
||||
</el-page-header>
|
||||
|
||||
<el-card shadow="never" class="main-card">
|
||||
<el-tabs v-model="activeTab" @tab-change="handleTabChange" class="custom-tabs">
|
||||
<!-- AI 生成剧本 -->
|
||||
<el-tab-pane label="AI 生成剧本" name="ai">
|
||||
<el-tab-pane :label="$t('script.aiGenerate')" name="ai">
|
||||
<!-- 步骤进度条 -->
|
||||
<div class="steps-wrapper">
|
||||
<el-steps :active="aiCurrentStep" finish-status="success" align-center process-status="process">
|
||||
<el-step title="生成大纲" description="" />
|
||||
<el-step title="生成角色" description="" />
|
||||
<el-step title="生成剧集" description="" />
|
||||
<el-step :title="$t('script.steps.outline')" description="" />
|
||||
<el-step :title="$t('script.steps.characters')" description="" />
|
||||
<el-step :title="$t('script.steps.episodes')" description="" />
|
||||
</el-steps>
|
||||
</div>
|
||||
|
||||
@@ -23,13 +23,13 @@
|
||||
<!-- 步骤 0: 生成大纲 -->
|
||||
<div v-show="aiCurrentStep === 0" class="step-panel">
|
||||
<el-form :model="outlineForm" label-width="100px">
|
||||
<el-form-item label="创作主题" required>
|
||||
<el-form-item :label="$t('script.form.theme')" required>
|
||||
<div class="theme-input-wrapper">
|
||||
<el-input
|
||||
v-model="outlineForm.theme"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder="描述你想创作的短剧主题和故事概念"
|
||||
:placeholder="$t('script.form.themePlaceholder')"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
/>
|
||||
@@ -39,35 +39,35 @@
|
||||
@click="generateRandomTheme"
|
||||
class="random-btn"
|
||||
>
|
||||
随机生成
|
||||
{{ $t('script.form.randomGenerate') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="类型偏好">
|
||||
<el-select v-model="outlineForm.genre" placeholder="选择类型" clearable>
|
||||
<el-option label="都市" value="都市" />
|
||||
<el-option label="古装" value="古装" />
|
||||
<el-option label="悬疑" value="悬疑" />
|
||||
<el-option label="爱情" value="爱情" />
|
||||
<el-option label="喜剧" value="喜剧" />
|
||||
<el-form-item :label="$t('script.form.genre')">
|
||||
<el-select v-model="outlineForm.genre" :placeholder="$t('script.form.genrePlaceholder')" clearable>
|
||||
<el-option :label="$t('genres.urban')" value="都市" />
|
||||
<el-option :label="$t('genres.costume')" value="古装" />
|
||||
<el-option :label="$t('genres.mystery')" value="悬疑" />
|
||||
<el-option :label="$t('genres.romance')" value="爱情" />
|
||||
<el-option :label="$t('genres.comedy')" value="喜剧" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="风格要求">
|
||||
<el-form-item :label="$t('script.form.style')">
|
||||
<el-input
|
||||
v-model="outlineForm.style"
|
||||
placeholder="例如:轻松幽默、紧张刺激、温馨治愈"
|
||||
:placeholder="$t('script.form.stylePlaceholder')"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="剧集数量">
|
||||
<el-form-item :label="$t('script.form.episodeCount')">
|
||||
<el-input-number v-model="outlineForm.length" :min="3" :max="20" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div class="stage-notice">
|
||||
<p>请输入创作主题和相关要求,AI将为您生成剧本大纲</p>
|
||||
<p>{{ $t('script.notice') }}</p>
|
||||
</div>
|
||||
|
||||
<el-alert
|
||||
@@ -77,17 +77,17 @@
|
||||
show-icon
|
||||
class="error-alert"
|
||||
>
|
||||
<template #title>生成失败</template>
|
||||
<template #title>{{ $t('script.generateFailed') }}</template>
|
||||
<div>{{ generationError }}</div>
|
||||
<el-button type="primary" size="small" @click="retryGeneration" style="margin-top: 12px;">
|
||||
<el-icon><RefreshRight /></el-icon>
|
||||
重新生成
|
||||
{{ $t('script.regenerate') }}
|
||||
</el-button>
|
||||
</el-alert>
|
||||
|
||||
<div v-if="outlineResult" class="result-preview">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
|
||||
<h4 style="margin: 0;">大纲预览(可编辑)</h4>
|
||||
<h4 style="margin: 0;">{{ $t('script.outlinePreview') }}</h4>
|
||||
<el-button
|
||||
type="warning"
|
||||
size="small"
|
||||
@@ -95,25 +95,25 @@
|
||||
:loading="generating"
|
||||
>
|
||||
<el-icon><RefreshRight /></el-icon>
|
||||
重新生成大纲
|
||||
{{ $t('script.regenerateOutline') }}
|
||||
</el-button>
|
||||
</div>
|
||||
<el-form label-width="80px" class="outline-edit-form">
|
||||
<el-form-item label="标题">
|
||||
<el-input v-model="outlineResult.title" placeholder="请输入剧本标题" />
|
||||
<el-form-item :label="$t('script.form.title')">
|
||||
<el-input v-model="outlineResult.title" :placeholder="$t('script.form.titlePlaceholder')" />
|
||||
</el-form-item>
|
||||
<el-form-item label="概要">
|
||||
<el-form-item :label="$t('script.form.summary')">
|
||||
<el-input
|
||||
v-model="outlineResult.summary"
|
||||
type="textarea"
|
||||
:rows="8"
|
||||
placeholder="请输入剧本概要"
|
||||
:placeholder="$t('script.form.summaryPlaceholder')"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="类型">
|
||||
<el-input v-model="outlineResult.genre" placeholder="例如:都市、古装" />
|
||||
<el-form-item :label="$t('script.form.genre')">
|
||||
<el-input v-model="outlineResult.genre" :placeholder="$t('script.form.genreExample')" />
|
||||
</el-form-item>
|
||||
<el-form-item label="标签">
|
||||
<el-form-item :label="$t('script.form.tags')">
|
||||
<el-tag
|
||||
v-for="(tag, index) in outlineResult.tags"
|
||||
:key="index"
|
||||
@@ -132,7 +132,7 @@
|
||||
@keyup.enter="addTag"
|
||||
@blur="addTag"
|
||||
/>
|
||||
<el-button v-else size="small" @click="showTagInput">+ 新标签</el-button>
|
||||
<el-button v-else size="small" @click="showTagInput">+ {{ $t('script.form.newTag') }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
@@ -141,48 +141,48 @@
|
||||
<!-- 步骤 1: 生成角色 -->
|
||||
<div v-show="aiCurrentStep === 1" class="step-panel">
|
||||
<div v-if="charactersResult.length === 0" class="stage-notice">
|
||||
<p>基于大纲自动生成剧本中的角色列表</p>
|
||||
<p>{{ $t('scriptGenerationPage.autoGenerateCharacters') }}</p>
|
||||
<el-alert
|
||||
title="提示"
|
||||
type="info"
|
||||
:closable="false"
|
||||
style="margin-top: 16px;"
|
||||
>
|
||||
角色已在大纲生成时创建,点击"下一步"查看并编辑
|
||||
{{ $t('scriptGenerationPage.charactersCreatedInOutline') }}
|
||||
</el-alert>
|
||||
</div>
|
||||
|
||||
<div v-if="charactersResult.length > 0" class="result-preview">
|
||||
<h4>角色列表(可编辑)</h4>
|
||||
<el-button type="primary" size="small" @click="addCharacter" class="add-btn">+ 添加角色</el-button>
|
||||
<h4>{{ $t('scriptGenerationPage.characterListEditable') }}</h4>
|
||||
<el-button type="primary" size="small" @click="addCharacter" class="add-btn">{{ $t('scriptGenerationPage.addCharacter') }}</el-button>
|
||||
<el-table :data="charactersResult" border max-height="400" class="editable-table">
|
||||
<el-table-column prop="name" label="角色名" width="120">
|
||||
<el-table-column prop="name" :label="$t('scriptGenerationPage.characterName')" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.name" size="small" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="role" label="角色类型" width="120">
|
||||
<el-table-column prop="role" :label="$t('scriptGenerationPage.characterType')" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-select v-model="row.role" size="small">
|
||||
<el-option label="主角" value="main" />
|
||||
<el-option label="配角" value="supporting" />
|
||||
<el-option label="次要角色" value="minor" />
|
||||
<el-option :label="$t('scriptGenerationPage.mainCharacter')" value="main" />
|
||||
<el-option :label="$t('scriptGenerationPage.supportingCharacter')" value="supporting" />
|
||||
<el-option :label="$t('scriptGenerationPage.minorCharacter')" value="minor" />
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="description" label="角色描述">
|
||||
<el-table-column prop="description" :label="$t('scriptGenerationPage.characterDesc')">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.description" size="small" type="textarea" :rows="2" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="appearance" label="外貌特征" width="200">
|
||||
<el-table-column prop="appearance" :label="$t('scriptGenerationPage.appearanceFeatures')" width="200">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.appearance" size="small" type="textarea" :rows="2" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="80" fixed="right">
|
||||
<el-table-column :label="$t('scriptGenerationPage.operations')" width="80" fixed="right">
|
||||
<template #default="{ $index }">
|
||||
<el-button type="danger" size="small" text @click="deleteCharacter($index)">删除</el-button>
|
||||
<el-button type="danger" size="small" text @click="deleteCharacter($index)">{{ $t('scriptGenerationPage.delete') }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -193,13 +193,13 @@
|
||||
<div v-show="aiCurrentStep === 2" class="step-panel">
|
||||
<div v-if="episodesResult.length === 0">
|
||||
<el-form :model="episodesForm" label-width="100px">
|
||||
<el-form-item label="剧集数量" required>
|
||||
<el-form-item :label="$t('scriptGenerationPage.episodeCount')" required>
|
||||
<el-input-number v-model="episodesForm.episode_count" :min="1" :max="10" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div class="stage-notice">
|
||||
<p>根据大纲生成完整的分集剧本</p>
|
||||
<p>{{ $t('scriptGenerationPage.generateFullScript') }}</p>
|
||||
<el-alert
|
||||
v-if="outlineResult && outlineResult.episodes && outlineResult.episodes.length > 0"
|
||||
title="提示"
|
||||
@@ -207,21 +207,21 @@
|
||||
:closable="false"
|
||||
style="margin-top: 16px;"
|
||||
>
|
||||
大纲生成时已创建 {{ outlineResult.episodes.length }} 集剧情,但您可以重新设置剧集数量并生成
|
||||
{{ $t('scriptGenerationPage.outlineCreatedEpisodes', { count: outlineResult.episodes.length }) }}
|
||||
</el-alert>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="episodesResult.length > 0" class="result-preview">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
|
||||
<h4>剧集预览(共{{ episodesResult.length }}集)</h4>
|
||||
<el-button size="small" @click="regenerateEpisodes">重新生成</el-button>
|
||||
<h4>{{ $t('scriptGenerationPage.episodePreview', { count: episodesResult.length }) }}</h4>
|
||||
<el-button size="small" @click="regenerateEpisodes">{{ $t('scriptGenerationPage.regenerate') }}</el-button>
|
||||
</div>
|
||||
<el-table :data="episodesResult" border max-height="400">
|
||||
<el-table-column prop="episode_number" label="集数" width="80" />
|
||||
<el-table-column prop="title" label="标题" width="200" />
|
||||
<el-table-column prop="summary" label="简介" />
|
||||
<el-table-column prop="duration" label="时长(秒)" width="100" />
|
||||
<el-table-column prop="episode_number" :label="$t('scriptGenerationPage.episodeNumber')" width="80" />
|
||||
<el-table-column prop="title" :label="$t('scriptGenerationPage.title')" width="200" />
|
||||
<el-table-column prop="summary" :label="$t('scriptGenerationPage.summary')" />
|
||||
<el-table-column prop="duration" :label="$t('scriptGenerationPage.durationSeconds')" width="100" />
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
@@ -229,13 +229,13 @@
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 方式2: 上传剧本 -->
|
||||
<el-tab-pane label="上传剧本" name="upload">
|
||||
<el-tab-pane :label="$t('scriptGenerationPage.uploadScript')" name="upload">
|
||||
<!-- 上传流程步骤 -->
|
||||
<div class="steps-wrapper">
|
||||
<el-steps :active="uploadCurrentStep" finish-status="success" align-center process-status="process">
|
||||
<el-step title="上传内容" description="" />
|
||||
<el-step title="AI解析" description="" />
|
||||
<el-step title="确认保存" description="" />
|
||||
<el-step :title="$t('scriptGenerationPage.uploadContent')" description="" />
|
||||
<el-step :title="$t('scriptGenerationPage.aiParse')" description="" />
|
||||
<el-step :title="$t('scriptGenerationPage.confirmSave')" description="" />
|
||||
</el-steps>
|
||||
</div>
|
||||
|
||||
@@ -243,11 +243,11 @@
|
||||
<!-- 步骤 0: 上传内容 -->
|
||||
<div v-show="uploadCurrentStep === 0" class="step-panel">
|
||||
<div class="stage-notice">
|
||||
<p>粘贴或上传您的剧本文件,系统将自动识别并拆分为剧集和场景</p>
|
||||
<p>{{ $t('scriptGenerationPage.uploadNotice') }}</p>
|
||||
</div>
|
||||
|
||||
<el-form :model="uploadForm" label-width="100px">
|
||||
<el-form-item label="上传方式">
|
||||
<el-form-item :label="$t('scriptGenerationPage.uploadMethod')">
|
||||
<div class="upload-options">
|
||||
<el-upload
|
||||
ref="uploadRef"
|
||||
@@ -260,10 +260,10 @@
|
||||
>
|
||||
<el-icon class="el-icon--upload"><UploadFilled /></el-icon>
|
||||
<div class="el-upload__text">
|
||||
拖拽文件到此处 或 <em>点击上传</em>
|
||||
{{ $t('scriptGenerationPage.dragFilesHere') }} <em>{{ $t('scriptGenerationPage.clickUpload') }}</em>
|
||||
</div>
|
||||
<div class="el-upload__tip">
|
||||
支持 .txt, .md, .doc, .docx 格式
|
||||
{{ $t('scriptGenerationPage.supportedFormats') }}
|
||||
</div>
|
||||
</el-upload>
|
||||
</div>
|
||||
@@ -331,12 +331,12 @@
|
||||
</div>
|
||||
|
||||
<div v-if="parseResult.characters && parseResult.characters.length > 0" class="characters-list" style="margin-top: 20px;">
|
||||
<h4>角色列表</h4>
|
||||
<h4>{{ $t('scriptGenerationPage.characterList') }}</h4>
|
||||
<el-table :data="parseResult.characters" border max-height="300">
|
||||
<el-table-column prop="name" label="角色名" width="120" />
|
||||
<el-table-column prop="role" label="定位" width="100" />
|
||||
<el-table-column prop="description" label="外貌描述" show-overflow-tooltip />
|
||||
<el-table-column prop="personality" label="性格" width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="name" :label="$t('scriptGenerationPage.characterName')" width="120" />
|
||||
<el-table-column prop="role" :label="$t('scriptGenerationPage.position')" width="100" />
|
||||
<el-table-column prop="description" :label="$t('scriptGenerationPage.appearanceDesc')" show-overflow-tooltip />
|
||||
<el-table-column prop="personality" :label="$t('scriptGenerationPage.personality')" width="200" show-overflow-tooltip />
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
@@ -348,7 +348,7 @@
|
||||
<!-- 底部导航按钮 -->
|
||||
<div class="navigation-buttons">
|
||||
<el-button size="large" @click="handlePrevStep" :disabled="!canGoPrev">
|
||||
上一步
|
||||
{{ $t('scriptGenerationPage.prevStep') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
size="large"
|
||||
@@ -367,6 +367,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted, nextTick } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { Loading, RefreshRight, MagicStick, UploadFilled } from '@element-plus/icons-vue';
|
||||
import { generationAPI } from '@/api/generation';
|
||||
@@ -375,6 +376,7 @@ import type { OutlineResult, ParseScriptResult } from '@/types/generation';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const { t: $t } = useI18n();
|
||||
const dramaId = route.params.id as string;
|
||||
|
||||
// 从URL query参数恢复状态,如果没有则使用默认值
|
||||
|
||||
Reference in New Issue
Block a user