diff --git a/api/handler/ai3d_handler.go b/api/handler/ai3d_handler.go index 28ad5ff2..cec17e81 100644 --- a/api/handler/ai3d_handler.go +++ b/api/handler/ai3d_handler.go @@ -7,7 +7,9 @@ import ( "geekai/core/types" "geekai/service" "geekai/service/ai3d" + "geekai/store/model" "geekai/store/vo" + "geekai/utils" "geekai/utils/resp" "strconv" @@ -34,10 +36,10 @@ func NewAI3DHandler(app *core.AppServer, db *gorm.DB, service *ai3d.Service, use // RegisterRoutes 注册路由 func (h *AI3DHandler) RegisterRoutes() { - group := h.App.Engine.Group("/api/3d/") + group := h.App.Engine.Group("/api/ai3d/") // 公开接口,不需要授权 - group.GET("models/:type", h.GetModels) + group.GET("configs", h.GetConfigs) // 需要用户授权的接口 group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis)) @@ -224,13 +226,29 @@ func (h *AI3DHandler) Download(c *gin.Context) { c.Redirect(302, job.FileURL) } -// GetModels 获取支持的模型列表 -func (h *AI3DHandler) GetModels(c *gin.Context) { - models := h.service.GetSupportedModels() - if len(models) == 0 { - resp.ERROR(c, "无可用3D模型") +// GetConfigs 获取3D生成配置 +func (h *AI3DHandler) GetConfigs(c *gin.Context) { + var config model.Config + err := h.DB.Where("name", types.ConfigKeyAI3D).First(&config).Error + if err != nil { + resp.ERROR(c, err.Error()) return } + var config3d types.AI3DConfig + err = utils.JsonDecode(config.Value, &config3d) + if err != nil { + resp.ERROR(c, err.Error()) + return + } + models := h.service.GetSupportedModels() + if len(config3d.Gitee.Models) == 0 { + config3d.Gitee.Models = models["gitee"] + } + if len(config3d.Tencent.Models) == 0 { + config3d.Tencent.Models = models["tencent"] + } - resp.SUCCESS(c, models) + logger.Info("config3d: ", config3d) + + resp.SUCCESS(c, config3d) } diff --git a/web/src/views/AIThreeDCreate.vue b/web/src/views/AIThreeDCreate.vue index bedf46fa..ebd8c1ed 100644 --- a/web/src/views/AIThreeDCreate.vue +++ b/web/src/views/AIThreeDCreate.vue @@ -4,90 +4,219 @@
- - -
- - Gitee AI 3D生成 + + + + +
+ +
+ 上传图片: +
+
+ +
+ + +
+ 提示词: +
+
+ +
+ + +
+ 输出格式: +
+
+ + + +
+ + +
+ + + 高级参数设置 + +
+ + +
+ +
+ 启用纹理 +
+ + +
+ 随机种子: + +
+ + +
+ 迭代次数: + +
+ + +
+ 引导系数: + +
+ + +
+ 3D渲染精度: + + + + + +
+
- -
- - 腾讯云混元3D生成 + + + +
+ +
+ 上传图片: +
+
+ +
+ + +
+ 提示词: +
+
+ +
+ + +
+ 输出格式: +
+
+ + + +
+ + +
+ 高级参数: +
+ + +
+ 启用PBR材质 +
+ + +
+ 文件格式: + + + + + + +
+ +
+ +
- - -
- -
- 上传图片: -
-
- -
- - -
- 提示词: -
-
- -
- - -
- 输出格式: -
-
- - - -
- - -
- 算力消耗: -
-
- {{ currentPower }} - -
- - -
- - {{ generating ? '生成中...' : '开始生成' }} - -
-
@@ -115,7 +244,7 @@
- {{ task.params ? getPromptFromParams(task.params) : '' }} + {{ task.params?.prompt }}
@@ -124,7 +253,7 @@
预览 - 下载 + 下载
@@ -141,8 +270,8 @@ :page-sizes="[10, 20, 50]" :total="total" layout="total, sizes, prev, pager, next, jumper" - @size-change="handleSizeChange" - @current-change="handleCurrentChange" + @size-change="handlePageSizeChange" + @current-change="handleCurrentPageChange" />
@@ -179,83 +308,101 @@ import CustomTabs from '@/components/ui/CustomTabs.vue' import { httpGet, httpPost } from '@/utils/http' import { ElMessage, ElMessageBox } from 'element-plus' import { computed, onMounted, ref } from 'vue' +import { checkSession } from '@/store/cache' // 响应式数据 const activePlatform = ref('gitee') -const currentImage = ref([]) -const currentPrompt = ref('') -const selectedModel = ref('obj') -const generating = ref(false) +const loading = ref(false) const previewVisible = ref(false) const currentPage = ref(1) const pageSize = ref(10) const total = ref(0) const taskList = ref([]) const currentPreviewTask = ref(null) +const giteeAdvancedVisible = ref(false) // 控制Gitee高级参数显示状态 +const tencentForm = ref({ + prompt: '', + image_url: '', + model: '', + power: 0, + file_format: '', // 输出文件格式 + enable_pbr: false, // 是否开启PBR材质 +}) +const giteeForm = ref({ + prompt: '', + image_url: '', + model: '', + power: 0, + file_format: '', // 输出文件格式 + texture: false, // 是否开启纹理 + seed: 1234, // 随机种子 + num_inference_steps: 5, //迭代次数 + guidance_scale: 7.5, //引导系数 + octree_resolution: 128, // 3D 渲染精度,越高3D 细节越丰富 +}) +const currentPower = ref(0) -// 平台配置 -const platformConfig = { - gitee: { - name: '魔力方舟', - models: { - obj: { name: 'OBJ格式', power: 45 }, - glb: { name: 'GLB格式', power: 55 }, - stl: { name: 'STL格式', power: 35 }, - usdz: { name: 'USDZ格式', power: 65 }, - fbx: { name: 'FBX格式', power: 75 }, - mp4: { name: 'MP4格式', power: 85 }, - }, - }, - tencent: { - name: '腾讯混元', - models: { - obj: { name: 'OBJ格式', power: 50 }, - glb: { name: 'GLB格式', power: 60 }, - stl: { name: 'STL格式', power: 40 }, - usdz: { name: 'USDZ格式', power: 70 }, - fbx: { name: 'FBX格式', power: 80 }, - mp4: { name: 'MP4格式', power: 90 }, - }, - }, +// 计算属性:获取当前活跃平台的表单数据 +const currentForm = computed(() => { + return activePlatform.value === 'tencent' ? tencentForm.value : giteeForm.value +}) + +const selectedModel = computed(() => { + return currentForm.value.model +}) + +const currentPrompt = computed(() => { + return currentForm.value.prompt +}) + +const currentImage = computed(() => { + return currentForm.value.image_url ? [{ url: currentForm.value.image_url }] : [] +}) + +const configs = ref({ + gitee: { models: [] }, + tencent: { models: [] }, +}) + +const loadConfigs = async () => { + const response = await httpGet('/api/ai3d/configs') + configs.value = response.data } -// 计算属性 -const availableModels = computed(() => { - return platformConfig[activePlatform.value]?.models || {} -}) - -const currentPower = computed(() => { - return availableModels.value[selectedModel.value]?.power || 0 -}) - -const canGenerate = computed(() => { - return currentPrompt.value.trim() && currentImage.value.length > 0 && selectedModel.value -}) - -// 方法 -const handlePlatformChange = (platform) => { - // 切换平台时重置模型选择 - if (!availableModels.value[selectedModel.value]) { - selectedModel.value = Object.keys(availableModels.value)[0] +const handleModelChange = (value) => { + if (activePlatform.value === 'tencent') { + const model = configs.value.tencent.models.find((model) => model.name === value) + currentPower.value = model.power + tencentForm.value.power = model.power + } else { + const model = configs.value.gitee.models.find((model) => model.name === value) + currentPower.value = model.power + giteeForm.value.power = model.power } } -const handleImageChange = (files) => { - currentImage.value = files -} - -const handleModelChange = () => { - // 模型改变时的处理逻辑 +const handlePlatformChange = (value) => { + currentPower.value = value === 'tencent' ? tencentForm.value.power : giteeForm.value.power } const generate3D = async () => { - if (!canGenerate.value) { + if (currentPower.value === 0) { ElMessage.warning('请完善生成参数') return } + if (!currentPrompt.value.trim()) { + ElMessage.warning('请输入提示词') + return + } + + if (!selectedModel.value) { + ElMessage.warning('请选择输出格式') + return + } + try { - generating.value = true + loading.value = true const requestData = { type: activePlatform.value, @@ -263,15 +410,35 @@ const generate3D = async () => { prompt: currentPrompt.value, image_url: currentImage.value[0]?.url || '', power: currentPower.value, + ...currentForm.value, // 包含所有表单参数 } - const response = await httpPost('/api/3d/generate', requestData) + const response = await httpPost('/api/ai3d/generate', requestData) if (response.code === 0) { ElMessage.success('任务创建成功') // 清空表单 - currentImage.value = [] - currentPrompt.value = '' + tencentForm.value = { + prompt: '', + image_url: '', + model: '', + power: 0, + file_format: '', + enable_pbr: false, + } + giteeForm.value = { + prompt: '', + image_url: '', + model: '', + power: 0, + file_format: '', + texture: false, + seed: 1234, + num_inference_steps: 5, + guidance_scale: 7.5, + octree_resolution: 128, + } + currentPower.value = 0 // 刷新任务列表 loadTasks() } else { @@ -280,13 +447,13 @@ const generate3D = async () => { } catch (error) { ElMessage.error('创建任务失败:' + error.message) } finally { - generating.value = false + loading.value = false } } const loadTasks = async () => { try { - const response = await httpGet('/api/3d/jobs', { + const response = await httpGet('/api/ai3d/jobs', { page: currentPage.value, page_size: pageSize.value, }) @@ -304,13 +471,13 @@ const refreshTasks = () => { loadTasks() } -const handleSizeChange = (size) => { +const handlePageSizeChange = (size) => { pageSize.value = size currentPage.value = 1 loadTasks() } -const handleCurrentChange = (page) => { +const handleCurrentPageChange = (page) => { currentPage.value = page loadTasks() } @@ -323,7 +490,7 @@ const deleteTask = async (taskId) => { type: 'warning', }) - const response = await httpGet(`/api/3d/job/${taskId}/delete`) + const response = await httpGet(`/api/ai3d/job/${taskId}/delete`) if (response.code === 0) { ElMessage.success('删除成功') loadTasks() @@ -347,7 +514,7 @@ const closePreview = () => { currentPreviewTask.value = null } -const download3D = async (task) => { +const download = async (task) => { if (!task.img_url) { ElMessage.warning('模型文件不存在') return @@ -372,7 +539,7 @@ const download3D = async (task) => { const downloadCurrentModel = () => { if (currentPreviewTask.value) { - download3D(currentPreviewTask.value) + download(currentPreviewTask.value) } } @@ -386,18 +553,14 @@ const getStatusText = (status) => { return statusMap[status] || status } -const getPromptFromParams = (paramsStr) => { - try { - const params = JSON.parse(paramsStr) - return params.prompt || '' - } catch { - return '' - } -} - // 生命周期 onMounted(() => { - loadTasks() + loadConfigs() + checkSession() + .then(() => { + loadTasks() + }) + .catch(() => {}) }) @@ -446,6 +609,37 @@ onMounted(() => { color: #333; } } + + .advanced-toggle-btn { + padding: 0; + font-size: 14px; + color: #409eff; + border: none; + background: none; + display: flex; + align-items: center; + gap: 4px; + transition: all 0.3s ease; + + &:hover { + color: #66b1ff; + background: #f0f9ff; + border-radius: 4px; + padding: 4px 8px; + } + + i { + font-size: 12px; + transition: transform 0.3s ease; + } + } + + .advanced-params { + margin-left: 16px; + padding: 10px 16px; + border-left: 3px solid #e4e7ed; + margin-top: 8px; + } } .power-display {