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:
2026-03-27 15:29:50 +08:00
parent e3ff55b4db
commit 032c43525a
41 changed files with 3756 additions and 81 deletions

View 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">字节跳动火山引擎文生图 APISeedream 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>