修复部分BUG 前端页面添加英文

This commit is contained in:
Connor
2026-01-14 12:40:45 +08:00
parent d628283ef6
commit 0a5b39249e
28 changed files with 2204 additions and 599 deletions

View File

@@ -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'))
}
}
}