- 替换视频生成服务为可灵AI多图参考生视频API,支持1-4张多视角图片输入 - 调整图片拼接逻辑,生成横向长图传入即梦API备用 - 实现基于JWT认证的可灵API请求和轮询机制,支持高品质1:1正方形视频生成 - 在设计详情页新增视频展示区域及生成、重新生成和下载视频操作 - 更新后台系统配置,支持配置可灵AI Access Key和Secret Key - 删除即梦视频相关配置及逻辑,所有视频生成功能切换到可灵AI实现 - 优化视频生成提示词,提升视频质感和展示效果 - 增加视频文件本地存储和路径管理,保证视频可访问和下载 - 前端增加视频生成状态管理和用户界面交互提示 - 后端添加PyJWT依赖,支持JWT认证流程
530 lines
17 KiB
Vue
530 lines
17 KiB
Vue
<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">字节跳动火山引擎文生图 API,Seedream 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-4张),AI 理解为同一物体多角度参考,生成单品 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>
|