Files
YuShiSheJiShi/frontend/src/views/admin/ConfigManage.vue
1d94ec114a feat(video): 集成可灵AI多图参考生视频生成服务
- 替换视频生成服务为可灵AI多图参考生视频API,支持1-4张多视角图片输入
- 调整图片拼接逻辑,生成横向长图传入即梦API备用
- 实现基于JWT认证的可灵API请求和轮询机制,支持高品质1:1正方形视频生成
- 在设计详情页新增视频展示区域及生成、重新生成和下载视频操作
- 更新后台系统配置,支持配置可灵AI Access Key和Secret Key
- 删除即梦视频相关配置及逻辑,所有视频生成功能切换到可灵AI实现
- 优化视频生成提示词,提升视频质感和展示效果
- 增加视频文件本地存储和路径管理,保证视频可访问和下载
- 前端增加视频生成状态管理和用户界面交互提示
- 后端添加PyJWT依赖,支持JWT认证流程
2026-03-28 00:20:48 +08:00

530 lines
17 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="config-page">
<!-- 默认模型选择 -->
<div class="section-card">
<h3 class="section-title">默认生图模型</h3>
<div class="model-switch">
<div
class="model-option"
:class="{ active: defaultModel === 'flux-dev' }"
@click="setDefaultModel('flux-dev')"
>
<div class="model-badge">默认</div>
<div class="model-name">SiliconFlow Kolors</div>
<div class="model-price">~0.04 /</div>
<div class="model-tag">性价比高</div>
</div>
<div
class="model-option"
:class="{ active: defaultModel === 'seedream-5.0' }"
@click="setDefaultModel('seedream-5.0')"
>
<div class="model-badge">备选</div>
<div class="model-name">火山引擎 Seedream 5.0 lite</div>
<div class="model-price">~0.04 /</div>
<div class="model-tag">高质量</div>
</div>
</div>
</div>
<!-- SiliconFlow 配置卡片 -->
<div class="section-card">
<div class="card-header">
<div class="card-title-row">
<h3 class="section-title">SiliconFlow Kolors</h3>
<el-tag :type="siliconflowStatus" size="small">
{{ siliconflowStatusText }}
</el-tag>
</div>
<p class="card-desc">硅基流动文生图 API基于 Kolors 开源模型性价比高</p>
</div>
<el-form label-width="120px" class="config-form">
<el-form-item label="API Key">
<el-input
v-model="siliconflowKey"
type="password"
show-password
placeholder="请输入 SiliconFlow API Key"
clearable
/>
</el-form-item>
<el-form-item label="接口地址">
<el-input v-model="siliconflowUrl" placeholder="https://api.siliconflow.cn/v1" clearable />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="testSiliconflow" :loading="testingSiliconflow">
测试连接
</el-button>
<span v-if="siliconflowTestResult" :class="['test-result', siliconflowTestResult.ok ? 'success' : 'fail']">
{{ siliconflowTestResult.msg }}
</span>
</el-form-item>
</el-form>
</div>
<!-- 火山引擎配置卡片 -->
<div class="section-card">
<div class="card-header">
<div class="card-title-row">
<h3 class="section-title">火山引擎 Seedream 5.0 lite</h3>
<el-tag :type="volcengineStatus" size="small">
{{ volcengineStatusText }}
</el-tag>
</div>
<p class="card-desc">字节跳动火山引擎文生图 APISeedream 5.0 lite 模型支持中英文提示词高质量输出</p>
</div>
<el-form label-width="120px" class="config-form">
<el-form-item label="API Key">
<el-input
v-model="volcengineKey"
type="password"
show-password
placeholder="请输入火山引擎 API Key"
clearable
/>
</el-form-item>
<el-form-item label="接口地址">
<el-input v-model="volcengineUrl" placeholder="https://ark.cn-beijing.volces.com/api/v3" clearable />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="testVolcengine" :loading="testingVolcengine">
测试连接
</el-button>
<span v-if="volcengineTestResult" :class="['test-result', volcengineTestResult.ok ? 'success' : 'fail']">
{{ volcengineTestResult.msg }}
</span>
</el-form-item>
</el-form>
</div>
<!-- 可灵AI 视频生成配置卡片 -->
<div class="section-card">
<div class="card-header">
<div class="card-title-row">
<h3 class="section-title">可灵 AI 视频生成</h3>
<el-tag :type="klingVideoStatus" size="small">
{{ klingVideoStatusText }}
</el-tag>
</div>
<p class="card-desc">可灵 AI 多图参考生视频 API支持传入多张参考图1-4AI 理解为同一物体多角度参考生成单品 360 度旋转展示视频</p>
</div>
<el-form label-width="120px" class="config-form">
<el-form-item label="Access Key">
<el-input
v-model="klingAccessKey"
type="password"
show-password
placeholder="请输入可灵 AI Access Key"
clearable
/>
</el-form-item>
<el-form-item label="Secret Key">
<el-input
v-model="klingSecretKey"
type="password"
show-password
placeholder="请输入可灵 AI Secret Key"
clearable
/>
</el-form-item>
<el-form-item>
<span class="form-tip">获取方式访问 <a href="https://klingai.com" target="_blank">可灵AI开放平台</a> 注册/登录 控制台 API密钥管理</span>
</el-form-item>
<el-form-item label="视频提示词">
<el-input
v-model="videoPrompt"
type="textarea"
:rows="3"
placeholder="玉雕作品在摄影棚内缓慢旋转360度展示全貌..."
/>
<span class="form-tip">用于控制视频生成效果留空使用默认提示词</span>
</el-form-item>
</el-form>
</div>
<!-- 腾讯混元3D 配置卡片 -->
<div class="section-card">
<div class="card-header">
<div class="card-title-row">
<h3 class="section-title">腾讯混元3D 模型生成</h3>
<el-tag :type="hunyuan3dStatus" size="small">
{{ hunyuan3dStatusText }}
</el-tag>
</div>
<p class="card-desc">腾讯混元3D 图生 3D 模型 API将设计图转换为可交互的 3D 模型.glb 格式</p>
</div>
<el-form label-width="120px" class="config-form">
<el-form-item label="SecretId">
<el-input
v-model="tencentSecretId"
type="password"
show-password
placeholder="请输入腾讯云 SecretId"
clearable
/>
</el-form-item>
<el-form-item label="SecretKey">
<el-input
v-model="tencentSecretKey"
type="password"
show-password
placeholder="请输入腾讯云 SecretKey"
clearable
/>
</el-form-item>
<el-form-item>
<span class="form-tip">获取方式访问 <a href="https://console.cloud.tencent.com/cam/capi" target="_blank">https://console.cloud.tencent.com/cam/capi</a> 创建密钥,并在 <a href="https://console.cloud.tencent.com/ai3d" target="_blank">混元3D控制台</a> 开通服务</span>
</el-form-item>
<el-form-item label="3D提示词">
<el-input
v-model="model3dPrompt"
type="textarea"
:rows="3"
placeholder="可选用于控制3D模型生成效果"
/>
<span class="form-tip">可选用于描述3D模型的生成效果要求</span>
</el-form-item>
</el-form>
</div>
<!-- 通用设置 -->
<div class="section-card">
<h3 class="section-title">通用设置</h3>
<el-form label-width="120px" class="config-form">
<el-form-item label="图片尺寸">
<el-select v-model="imageSize" style="width: 200px">
<el-option label="512 x 512" value="512" />
<el-option label="768 x 768" value="768" />
<el-option label="1024 x 1024" value="1024" />
</el-select>
<span class="form-tip">生成图片的宽高像素</span>
</el-form-item>
</el-form>
</div>
<!-- 保存按钮 -->
<div class="save-bar">
<el-button type="primary" size="large" @click="handleSave" :loading="saving">
保存所有配置
</el-button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { getConfigs, updateConfigsBatch } from '@/api/admin'
import request from '@/api/request'
// 配置值
const defaultModel = ref('flux-dev')
const siliconflowKey = ref('')
const siliconflowUrl = ref('https://api.siliconflow.cn/v1')
const volcengineKey = ref('')
const volcengineUrl = ref('https://ark.cn-beijing.volces.com/api/v3')
const klingAccessKey = ref('')
const klingSecretKey = ref('')
const tencentSecretId = ref('')
const tencentSecretKey = ref('')
const videoPrompt = ref('')
const model3dPrompt = ref('')
const imageSize = ref('1024')
const saving = ref(false)
// 后端是否已配置 API Key脱敏值也算已配置
const siliconflowConfigured = ref(false)
const volcengineConfigured = ref(false)
const klingAccessKeyConfigured = ref(false)
const klingSecretKeyConfigured = ref(false)
const tencentSecretIdConfigured = ref(false)
const tencentSecretKeyConfigured = ref(false)
// 测试状态
const testingSiliconflow = ref(false)
const testingVolcengine = ref(false)
const siliconflowTestResult = ref<{ ok: boolean; msg: string } | null>(null)
const volcengineTestResult = ref<{ ok: boolean; msg: string } | null>(null)
// 状态计算:输入框有值 或 后端已配置 都算"已配置"
const siliconflowStatus = computed(() => (siliconflowKey.value || siliconflowConfigured.value) ? 'success' : 'info')
const siliconflowStatusText = computed(() => (siliconflowKey.value || siliconflowConfigured.value) ? '已配置' : '未配置')
const volcengineStatus = computed(() => (volcengineKey.value || volcengineConfigured.value) ? 'success' : 'info')
const volcengineStatusText = computed(() => (volcengineKey.value || volcengineConfigured.value) ? '已配置' : '未配置')
const klingVideoStatus = computed(() => ((klingAccessKey.value || klingAccessKeyConfigured.value) && (klingSecretKey.value || klingSecretKeyConfigured.value)) ? 'success' : 'info')
const klingVideoStatusText = computed(() => ((klingAccessKey.value || klingAccessKeyConfigured.value) && (klingSecretKey.value || klingSecretKeyConfigured.value)) ? '已配置' : '未配置')
const hunyuan3dStatus = computed(() => ((tencentSecretId.value || tencentSecretIdConfigured.value) && (tencentSecretKey.value || tencentSecretKeyConfigured.value)) ? 'success' : 'info')
const hunyuan3dStatusText = computed(() => ((tencentSecretId.value || tencentSecretIdConfigured.value) && (tencentSecretKey.value || tencentSecretKeyConfigured.value)) ? '已配置' : '未配置')
// 加载配置
const loadConfigs = async () => {
try {
const data = await getConfigs() as any
const items: any[] = data.items || []
const map: Record<string, string> = {}
for (const item of items) {
map[item.config_key] = item.config_value || ''
}
defaultModel.value = map['AI_IMAGE_MODEL'] || 'flux-dev'
// 注意API Key 是脱敏的(****),不回填到输入框
// 只有完整值才回填
// 记录后端是否已有 API Key脱敏值也算已配置
siliconflowConfigured.value = !!map['SILICONFLOW_API_KEY']
volcengineConfigured.value = !!map['VOLCENGINE_API_KEY']
// 脱敏值不回填输入框,只有完整值才回填
if (map['SILICONFLOW_API_KEY'] && !map['SILICONFLOW_API_KEY'].includes('****')) {
siliconflowKey.value = map['SILICONFLOW_API_KEY']
}
siliconflowUrl.value = map['SILICONFLOW_BASE_URL'] || 'https://api.siliconflow.cn/v1'
if (map['VOLCENGINE_API_KEY'] && !map['VOLCENGINE_API_KEY'].includes('****')) {
volcengineKey.value = map['VOLCENGINE_API_KEY']
}
volcengineUrl.value = map['VOLCENGINE_BASE_URL'] || 'https://ark.cn-beijing.volces.com/api/v3'
// 可灵 AI Access Key / Secret Key
klingAccessKeyConfigured.value = !!map['KLING_ACCESS_KEY']
klingSecretKeyConfigured.value = !!map['KLING_SECRET_KEY']
if (map['KLING_ACCESS_KEY'] && !map['KLING_ACCESS_KEY'].includes('****')) {
klingAccessKey.value = map['KLING_ACCESS_KEY']
}
if (map['KLING_SECRET_KEY'] && !map['KLING_SECRET_KEY'].includes('****')) {
klingSecretKey.value = map['KLING_SECRET_KEY']
}
// 腾讯云 SecretId / SecretKey
tencentSecretIdConfigured.value = !!map['TENCENT_SECRET_ID']
tencentSecretKeyConfigured.value = !!map['TENCENT_SECRET_KEY']
if (map['TENCENT_SECRET_ID'] && !map['TENCENT_SECRET_ID'].includes('****')) {
tencentSecretId.value = map['TENCENT_SECRET_ID']
}
if (map['TENCENT_SECRET_KEY'] && !map['TENCENT_SECRET_KEY'].includes('****')) {
tencentSecretKey.value = map['TENCENT_SECRET_KEY']
}
// 提示词配置
videoPrompt.value = map['VIDEO_PROMPT'] || ''
model3dPrompt.value = map['MODEL3D_PROMPT'] || ''
imageSize.value = map['AI_IMAGE_SIZE'] || '1024'
} catch (e) {
console.error('加载配置失败', e)
}
}
// 切换默认模型
const setDefaultModel = (model: string) => {
defaultModel.value = model
}
// 保存配置
const handleSave = async () => {
saving.value = true
try {
const configs: Record<string, string> = {
AI_IMAGE_MODEL: defaultModel.value,
SILICONFLOW_BASE_URL: siliconflowUrl.value,
VOLCENGINE_BASE_URL: volcengineUrl.value,
AI_IMAGE_SIZE: imageSize.value,
}
// API Key 只有用户填写了才提交(留空不覆盖)
if (siliconflowKey.value) {
configs['SILICONFLOW_API_KEY'] = siliconflowKey.value
}
if (volcengineKey.value) {
configs['VOLCENGINE_API_KEY'] = volcengineKey.value
}
if (klingAccessKey.value) {
configs['KLING_ACCESS_KEY'] = klingAccessKey.value
}
if (klingSecretKey.value) {
configs['KLING_SECRET_KEY'] = klingSecretKey.value
}
if (tencentSecretId.value) {
configs['TENCENT_SECRET_ID'] = tencentSecretId.value
}
if (tencentSecretKey.value) {
configs['TENCENT_SECRET_KEY'] = tencentSecretKey.value
}
// 提示词始终提交(包括空值,允许清空)
configs['VIDEO_PROMPT'] = videoPrompt.value
configs['MODEL3D_PROMPT'] = model3dPrompt.value
await updateConfigsBatch(configs)
ElMessage.success('配置已保存')
await loadConfigs()
} catch (e) {
console.error('保存失败', e)
} finally {
saving.value = false
}
}
// 测试 SiliconFlow 连接
const testSiliconflow = async () => {
if (!siliconflowKey.value) {
siliconflowTestResult.value = { ok: false, msg: '请先填写 API Key' }
return
}
testingSiliconflow.value = true
siliconflowTestResult.value = null
try {
const resp = await request.post('/admin/configs/test', {
provider: 'siliconflow',
api_key: siliconflowKey.value,
base_url: siliconflowUrl.value,
}) as any
siliconflowTestResult.value = { ok: true, msg: resp.message || '连接成功' }
} catch (e: any) {
const msg = e.response?.data?.detail || '连接失败'
siliconflowTestResult.value = { ok: false, msg }
} finally {
testingSiliconflow.value = false
}
}
// 测试火山引擎连接
const testVolcengine = async () => {
if (!volcengineKey.value) {
volcengineTestResult.value = { ok: false, msg: '请先填写 API Key' }
return
}
testingVolcengine.value = true
volcengineTestResult.value = null
try {
const resp = await request.post('/admin/configs/test', {
provider: 'volcengine',
api_key: volcengineKey.value,
base_url: volcengineUrl.value,
}) as any
volcengineTestResult.value = { ok: true, msg: resp.message || '连接成功' }
} catch (e: any) {
const msg = e.response?.data?.detail || '连接失败'
volcengineTestResult.value = { ok: false, msg }
} finally {
testingVolcengine.value = false
}
}
onMounted(loadConfigs)
</script>
<style scoped lang="scss">
.section-card {
background: #fff;
border-radius: 10px;
padding: 24px;
margin-bottom: 20px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
}
.section-title {
font-size: 16px;
font-weight: 600;
color: #1d1e2c;
margin: 0 0 4px;
}
.card-header {
margin-bottom: 20px;
.card-title-row {
display: flex;
align-items: center;
gap: 12px;
}
.card-desc {
font-size: 13px;
color: #999;
margin: 6px 0 0;
}
}
// 模型切换
.model-switch {
display: flex;
gap: 20px;
margin-top: 16px;
}
.model-option {
flex: 1;
position: relative;
border: 2px solid #e8e8e8;
border-radius: 12px;
padding: 24px;
cursor: pointer;
transition: all 0.25s;
text-align: center;
&:hover {
border-color: #a8d5ba;
box-shadow: 0 2px 12px rgba(91, 126, 107, 0.1);
}
&.active {
border-color: #5B7E6B;
background: linear-gradient(135deg, #f0f7f3 0%, #fff 100%);
box-shadow: 0 4px 16px rgba(91, 126, 107, 0.15);
.model-badge {
background: #5B7E6B;
color: #fff;
}
}
.model-badge {
position: absolute;
top: -10px;
left: 16px;
background: #ddd;
color: #666;
font-size: 11px;
padding: 2px 10px;
border-radius: 10px;
font-weight: 500;
}
.model-name {
font-size: 15px;
font-weight: 600;
color: #333;
margin-bottom: 8px;
}
.model-price {
font-size: 22px;
font-weight: 700;
color: #5B7E6B;
margin-bottom: 6px;
}
.model-tag {
font-size: 12px;
color: #999;
}
}
// 配置表单
.config-form {
margin-top: 16px;
max-width: 600px;
}
.form-tip {
font-size: 12px;
color: #999;
margin-left: 12px;
}
// 测试结果
.test-result {
margin-left: 12px;
font-size: 13px;
&.success { color: #67c23a; }
&.fail { color: #f56c6c; }
}
// 保存栏
.save-bar {
text-align: center;
padding: 20px 0;
}
</style>