feat(ai): 支持双模型多视角AI设计生图与后台管理系统
- 实现AI多视角设计图生成功能,支持6个可选设计参数配置 - 集成SiliconFlow FLUX.1与火山引擎Seedream 4.5双模型切换 - 构建专业中文转英文prompt系统,提升AI生成质量 - 前端设计预览支持多视角切换与视角指示器展示 - 增加多视角设计图片DesignImage模型关联及存储 - 后端设计服务异步调用AI接口,失败时降级生成mock图 - 新增管理员后台管理路由及完整的权限校验机制 - 实现后台模块:仪表盘、系统配置、用户/品类/设计管理 - 配置数据库系统配置表,支持动态AI配置及热更新 - 增加用户管理员标识字段,管理后台登录鉴权支持 - 更新API接口支持多视角设计参数及后台管理接口 - 优化设计删除逻辑,删除多视角相关图片文件 - 前端新增管理后台页面与路由,布局样式独立分离 - 更新环境变量增加AI模型相关Key与参数配置说明 - 引入httpx异步HTTP客户端用于AI接口调用及图片下载 - README文档完善AI多视角生图与后台管理详细功能与流程说明
This commit is contained in:
381
frontend/src/views/admin/ConfigManage.vue
Normal file
381
frontend/src/views/admin/ConfigManage.vue
Normal file
@@ -0,0 +1,381 @@
|
||||
<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 FLUX.1 [dev]</div>
|
||||
<div class="model-price">~0.13 元/张</div>
|
||||
<div class="model-tag">性价比高</div>
|
||||
</div>
|
||||
<div
|
||||
class="model-option"
|
||||
:class="{ active: defaultModel === 'seedream-4.5' }"
|
||||
@click="setDefaultModel('seedream-4.5')"
|
||||
>
|
||||
<div class="model-badge">备选</div>
|
||||
<div class="model-name">火山引擎 Seedream 4.5</div>
|
||||
<div class="model-price">~0.30 元/张</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 FLUX.1 [dev]</h3>
|
||||
<el-tag :type="siliconflowStatus" size="small">
|
||||
{{ siliconflowStatusText }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<p class="card-desc">硅基流动文生图 API,基于 FLUX.1 开源模型,性价比高</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 4.5</h3>
|
||||
<el-tag :type="volcengineStatus" size="small">
|
||||
{{ volcengineStatusText }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<p class="card-desc">字节跳动火山引擎文生图 API,Seedream 4.5 模型,高质量输出</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>
|
||||
|
||||
<!-- 通用设置 -->
|
||||
<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 imageSize = ref('1024')
|
||||
const saving = 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 ? 'success' : 'info')
|
||||
const siliconflowStatusText = computed(() => siliconflowKey.value ? '已配置' : '未配置')
|
||||
const volcengineStatus = computed(() => volcengineKey.value ? 'success' : 'info')
|
||||
const volcengineStatusText = computed(() => volcengineKey.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 是脱敏的(****),不回填到输入框
|
||||
// 只有完整值才回填
|
||||
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'
|
||||
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
|
||||
}
|
||||
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>
|
||||
Reference in New Issue
Block a user