From 54c8856adfc2d44a1d349ed2040d226f9ae846d6 Mon Sep 17 00:00:00 2001 From: GeekMaster Date: Wed, 3 Sep 2025 16:00:28 +0800 Subject: [PATCH] =?UTF-8?q?AI3D=20=E9=A1=B5=E9=9D=A2=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/core/types/ai3d.go | 7 + api/handler/admin/upload_handler.go | 4 +- api/handler/ai3d_handler.go | 188 +++++- api/service/ai3d/service.go | 2 +- api/service/ai3d/tencent_client.go | 4 +- api/store/vo/ai3d_job.go | 3 +- web/package.json | 2 +- web/pnpm-lock.yaml | 10 +- web/src/assets/css/ai3d.scss | 632 ++++++++++++++++++++ web/src/components/ThreeDPreview.vue | 388 ++++++++---- web/src/components/ui/CustomSwitch.vue | 237 ++++++++ web/src/router.js | 10 +- web/src/store/jimeng.js | 4 +- web/src/store/mobile/jimeng.js | 2 +- web/src/store/mobile/suno.js | 2 +- web/src/store/suno.js | 2 +- web/src/store/video.js | 2 +- web/src/views/AIThreeDCreate.vue | 784 ++++++++++++------------- web/src/views/mobile/ThreeDCreate.vue | 765 ------------------------ web/src/views/{ => test}/Test.vue | 0 web/src/views/test/Test3D.vue | 66 +++ 21 files changed, 1795 insertions(+), 1319 deletions(-) create mode 100644 web/src/assets/css/ai3d.scss create mode 100644 web/src/components/ui/CustomSwitch.vue delete mode 100644 web/src/views/mobile/ThreeDCreate.vue rename web/src/views/{ => test}/Test.vue (100%) create mode 100644 web/src/views/test/Test3D.vue diff --git a/api/core/types/ai3d.go b/api/core/types/ai3d.go index a32fee1c..7953c2ae 100644 --- a/api/core/types/ai3d.go +++ b/api/core/types/ai3d.go @@ -22,6 +22,13 @@ type Gitee3DConfig struct { Models []AI3DModel `json:"models,omitempty"` } +type AI3DTaskType string + +const ( + AI3DTaskTypeTencent AI3DTaskType = "tencent" + AI3DTaskTypeGitee AI3DTaskType = "gitee" +) + // AI3DJobResult 3D任务结果 type AI3DJobResult struct { JobId string `json:"job_id"` // 任务ID diff --git a/api/handler/admin/upload_handler.go b/api/handler/admin/upload_handler.go index 5d96335b..878c7776 100644 --- a/api/handler/admin/upload_handler.go +++ b/api/handler/admin/upload_handler.go @@ -31,12 +31,12 @@ func NewUploadHandler(app *core.AppServer, db *gorm.DB, manager *oss.UploaderMan // RegisterRoutes 注册路由 func (h *UploadHandler) RegisterRoutes() { - group := h.App.Engine.Group("/api/admin/upload/") + group := h.App.Engine.Group("/api/admin/upload") // 需要管理员授权的接口 group.Use(middleware.AdminAuthMiddleware(h.App.Config.AdminSession.SecretKey, h.App.Redis)) { - group.POST("upload", h.Upload) + group.POST("", h.Upload) } } diff --git a/api/handler/ai3d_handler.go b/api/handler/ai3d_handler.go index cec17e81..1c580c48 100644 --- a/api/handler/ai3d_handler.go +++ b/api/handler/ai3d_handler.go @@ -46,42 +46,61 @@ func (h *AI3DHandler) RegisterRoutes() { { group.POST("generate", h.Generate) group.GET("jobs", h.JobList) + group.GET("jobs/mock", h.ListMock) // 演示数据接口 group.GET("job/:id", h.JobDetail) - group.DELETE("job/:id", h.DeleteJob) + group.GET("job/delete", h.DeleteJob) group.GET("download/:id", h.Download) } } // Generate 创建3D生成任务 func (h *AI3DHandler) Generate(c *gin.Context) { - var request vo.AI3DJobCreate + var request struct { + // 通用参数 + Type types.AI3DTaskType `json:"type" binding:"required"` // API类型 (tencent/gitee) + Model string `json:"model" binding:"required"` // 3D模型类型 + Prompt string `json:"prompt"` // 文本提示词 + ImageURL string `json:"image_url"` // 输入图片URL + FileFormat string `json:"file_format"` // 输出文件格式 + // 腾讯3d专有参数 + EnablePBR bool `json:"enable_pbr"` // 是否开启PBR材质 + // Gitee3d专有参数 + Texture bool `json:"texture"` // 是否开启纹理 + Seed int `json:"seed"` // 随机种子 + NumInferenceSteps int `json:"num_inference_steps"` //迭代次数 + GuidanceScale float64 `json:"guidance_scale"` //引导系数 + OctreeResolution int `json:"octree_resolution"` // 3D 渲染精度,越高3D 细节越丰富 + } if err := c.ShouldBindJSON(&request); err != nil { resp.ERROR(c, "参数错误") return } - // 验证必填参数 - if request.Type == "" || request.Model == "" || request.Power <= 0 { - resp.ERROR(c, "缺少必要参数") + // 提示词和图片不能同时为空 + if request.Prompt == "" && request.ImageURL == "" { + resp.ERROR(c, "提示词和图片不能同时为空") return } - // 获取用户ID - userId := h.GetLoginUserId(c) - if userId == 0 { - resp.ERROR(c, "用户未登录") + // Gitee 只支持图片 + if request.Type == types.AI3DTaskTypeGitee && request.ImageURL == "" { + resp.ERROR(c, "Gitee 只支持图生3D") return } - // 创建任务 - job, err := h.service.CreateJob(uint(userId), request) - if err != nil { - resp.ERROR(c, fmt.Sprintf("创建任务失败: %v", err)) - return - } + logger.Infof("request: %+v", request) + + // // 获取用户ID + // userId := h.GetLoginUserId(c) + // // 创建任务 + // job, err := h.service.CreateJob(uint(userId), request) + // if err != nil { + // resp.ERROR(c, fmt.Sprintf("创建任务失败: %v", err)) + // return + // } resp.SUCCESS(c, gin.H{ - "job_id": job.Id, + "job_id": 0, "message": "任务创建成功", }) } @@ -147,7 +166,7 @@ func (h *AI3DHandler) JobDetail(c *gin.Context) { Type: job.Type, Power: job.Power, TaskId: job.TaskId, - ImgURL: job.FileURL, + FileURL: job.FileURL, PreviewURL: job.PreviewURL, Model: job.Model, Status: job.Status, @@ -163,24 +182,38 @@ func (h *AI3DHandler) JobDetail(c *gin.Context) { // DeleteJob 删除任务 func (h *AI3DHandler) DeleteJob(c *gin.Context) { userId := h.GetLoginUserId(c) - if userId == 0 { - resp.ERROR(c, "用户未登录") + id := c.Query("id") + if id == "" { + resp.ERROR(c, "任务ID不能为空") return } - idStr := c.Param("id") - id, err := strconv.ParseUint(idStr, 10, 32) + var job model.AI3DJob + err := h.DB.Where("id = ?", id).Where("user_id = ?", userId).First(&job).Error if err != nil { - resp.ERROR(c, "任务ID格式错误") + resp.ERROR(c, err.Error()) return } - err = h.service.DeleteJob(uint(id), uint(userId)) + err = h.DB.Delete(&job).Error if err != nil { - resp.ERROR(c, fmt.Sprintf("删除任务失败: %v", err)) + resp.ERROR(c, err.Error()) return } + // 失败的任务要退回算力 + if job.Status == types.AI3DJobStatusFailed { + err = h.userService.IncreasePower(userId, job.Power, model.PowerLog{ + Type: types.PowerRefund, + Model: job.Model, + Remark: fmt.Sprintf("删除任务,退回%d算力", job.Power), + }) + if err != nil { + resp.ERROR(c, err.Error()) + return + } + } + resp.SUCCESS(c, gin.H{"message": "删除成功"}) } @@ -252,3 +285,110 @@ func (h *AI3DHandler) GetConfigs(c *gin.Context) { resp.SUCCESS(c, config3d) } + +// ListMock 返回演示数据 +func (h *AI3DHandler) ListMock(c *gin.Context) { + // 创建各种状态的演示数据 + mockJobs := []vo.AI3DJob{ + { + Id: 1, + UserId: 1, + Type: "gitee", + Power: 10, + TaskId: "mock_task_1", + FileURL: "https://img.r9it.com/R03TQZ7PZ386RGL7PTMNGFOHAJW15WYF.glb", + PreviewURL: "/static/upload/2025/9/1756873317505073.png", + Model: "gitee-3d-v1", + Status: types.AI3DJobStatusCompleted, + ErrMsg: "", + Params: `{"prompt":"一只可爱的小猫","image_url":"","texture":true,"seed":42}`, + CreatedAt: 1704067200, // 2024-01-01 00:00:00 + UpdatedAt: 1704067800, // 2024-01-01 00:10:00 + }, + { + Id: 2, + UserId: 1, + Type: "tencent", + Power: 15, + TaskId: "mock_task_2", + FileURL: "", + PreviewURL: "/static/upload/2025/9/1756873317505073.png", + Model: "tencent-3d-v2", + Status: types.AI3DJobStatusProcessing, + ErrMsg: "", + Params: `{"prompt":"一个现代建筑模型","image_url":"","enable_pbr":true}`, + CreatedAt: 1704070800, // 2024-01-01 01:00:00 + UpdatedAt: 1704070800, // 2024-01-01 01:00:00 + }, + { + Id: 3, + UserId: 1, + Type: "gitee", + Power: 8, + TaskId: "mock_task_3", + FileURL: "", + PreviewURL: "", + Model: "gitee-3d-v1", + Status: types.AI3DJobStatusPending, + ErrMsg: "", + Params: `{"prompt":"一辆跑车模型","image_url":"https://example.com/car.jpg","texture":false}`, + CreatedAt: 1704074400, // 2024-01-01 02:00:00 + UpdatedAt: 1704074400, // 2024-01-01 02:00:00 + }, + { + Id: 4, + UserId: 1, + Type: "tencent", + Power: 12, + TaskId: "mock_task_4", + FileURL: "", + PreviewURL: "", + Model: "tencent-3d-v1", + Status: types.AI3DJobStatusFailed, + ErrMsg: "模型生成失败:输入图片质量不符合要求", + Params: `{"prompt":"一个机器人模型","image_url":"https://example.com/robot.jpg","enable_pbr":false}`, + CreatedAt: 1704078000, // 2024-01-01 03:00:00 + UpdatedAt: 1704078600, // 2024-01-01 03:10:00 + }, + { + Id: 5, + UserId: 1, + Type: "gitee", + Power: 20, + TaskId: "mock_task_5", + FileURL: "https://ai.gitee.com/a8c1af8e-26e9-4ca6-aa5c-6d4ba86bfdac", + PreviewURL: "https://ai.gitee.com/a8c1af8e-26e9-4ca6-aa5c-6d4ba86bfdac", + Model: "gitee-3d-v2", + Status: types.AI3DJobStatusCompleted, + ErrMsg: "", + Params: `{"prompt":"一个复杂的机械装置","image_url":"","texture":true,"octree_resolution":512}`, + CreatedAt: 1704081600, // 2024-01-01 04:00:00 + UpdatedAt: 1704082200, // 2024-01-01 04:10:00 + }, + { + Id: 6, + UserId: 1, + Type: "tencent", + Power: 18, + TaskId: "mock_task_6", + FileURL: "", + PreviewURL: "", + Model: "tencent-3d-v2", + Status: types.AI3DJobStatusProcessing, + ErrMsg: "", + Params: `{"prompt":"一个科幻飞船","image_url":"","enable_pbr":true}`, + CreatedAt: 1704085200, // 2024-01-01 05:00:00 + UpdatedAt: 1704085200, // 2024-01-01 05:00:00 + }, + } + + // 创建分页响应 + mockResponse := vo.ThreeDJobList{ + Page: 1, + PageSize: 10, + Total: len(mockJobs), + Items: mockJobs, + } + + resp.SUCCESS(c, mockResponse) +} diff --git a/api/service/ai3d/service.go b/api/service/ai3d/service.go index f0238902..ba0731ad 100644 --- a/api/service/ai3d/service.go +++ b/api/service/ai3d/service.go @@ -249,7 +249,7 @@ func (s *Service) GetJobList(userId uint, page, pageSize int) (*vo.Page, error) Type: job.Type, Power: job.Power, TaskId: job.TaskId, - ImgURL: job.FileURL, + FileURL: job.FileURL, PreviewURL: job.PreviewURL, Model: job.Model, Status: job.Status, diff --git a/api/service/ai3d/tencent_client.go b/api/service/ai3d/tencent_client.go index f309d69e..998159b6 100644 --- a/api/service/ai3d/tencent_client.go +++ b/api/service/ai3d/tencent_client.go @@ -136,7 +136,7 @@ func (c *Tencent3DClient) QueryJob(jobId string) (*types.AI3DJobResult, error) { if file.PreviewImageUrl != nil { result.PreviewURL = *file.PreviewImageUrl } - break // 取第一个文件 + // TODO 取第一个文件 } } case "FAIL": @@ -153,6 +153,6 @@ func (c *Tencent3DClient) QueryJob(jobId string) (*types.AI3DJobResult, error) { // GetSupportedModels 获取支持的模型列表 func (c *Tencent3DClient) GetSupportedModels() []types.AI3DModel { return []types.AI3DModel{ - {Name: "Hunyuan3D-3", Power: 500, Formats: []string{"OBJ", "GLB", "STL", "USDZ", "FBX", "MP4"}, Desc: "Hunyuan3D 是腾讯混元团队推出的高质量 3D 生成模型,具备高保真度、细节丰富和高效生成的特点,可快速将文本或图像转换为逼真的 3D 物体。"}, + {Name: "Hunyuan3D-3", Power: 500, Formats: []string{"GLB", "OBJ", "STL", "USDZ", "FBX", "MP4"}, Desc: "Hunyuan3D 是腾讯混元团队推出的高质量 3D 生成模型,具备高保真度、细节丰富和高效生成的特点,可快速将文本或图像转换为逼真的 3D 物体。"}, } } diff --git a/api/store/vo/ai3d_job.go b/api/store/vo/ai3d_job.go index e4ecebf1..c7599d0d 100644 --- a/api/store/vo/ai3d_job.go +++ b/api/store/vo/ai3d_job.go @@ -6,7 +6,7 @@ type AI3DJob struct { Type string `json:"type"` Power int `json:"power"` TaskId string `json:"task_id"` - ImgURL string `json:"img_url"` + FileURL string `json:"file_url"` PreviewURL string `json:"preview_url"` Model string `json:"model"` Status string `json:"status"` @@ -29,4 +29,5 @@ type ThreeDJobList struct { PageSize int `json:"page_size"` Total int `json:"total"` List []AI3DJob `json:"list"` + Items []AI3DJob `json:"items"` } diff --git a/web/package.json b/web/package.json index 1c70c74e..c8d4e3f2 100644 --- a/web/package.json +++ b/web/package.json @@ -41,7 +41,7 @@ "qrcode": "^1.5.3", "qs": "^6.11.1", "sortablejs": "^1.15.0", - "three": "^0.128.0", + "three": "^0.160.0", "unplugin-auto-import": "^0.18.5", "vant": "^4.5.0", "vue": "^3.2.13", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 84f88a05..a562643e 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -105,8 +105,8 @@ importers: specifier: ^1.15.0 version: 1.15.6 three: - specifier: ^0.128.0 - version: 0.128.0 + specifier: ^0.160.0 + version: 0.160.1 unplugin-auto-import: specifier: ^0.18.5 version: 0.18.6(@vueuse/core@9.13.0(vue@3.5.18))(rollup@4.46.1) @@ -2240,8 +2240,8 @@ packages: peerDependencies: tslib: ^2 - three@0.128.0: - resolution: {integrity: sha512-i0ap/E+OaSfzw7bD1TtYnPo3VEplkl70WX5fZqZnfZsE3k3aSFudqrrC9ldFZfYFkn1zwDmBcdGfiIm/hnbyZA==} + three@0.160.1: + resolution: {integrity: sha512-Bgl2wPJypDOZ1stAxwfWAcJ0WQf7QzlptsxkjYiURPz+n5k4RBDLsq+6f9Y75TYxn6aHLcWz+JNmwTOXWrQTBQ==} tiny-emitter@2.1.0: resolution: {integrity: sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==} @@ -4549,7 +4549,7 @@ snapshots: dependencies: tslib: 2.8.1 - three@0.128.0: {} + three@0.160.1: {} tiny-emitter@2.1.0: {} diff --git a/web/src/assets/css/ai3d.scss b/web/src/assets/css/ai3d.scss new file mode 100644 index 00000000..fa589488 --- /dev/null +++ b/web/src/assets/css/ai3d.scss @@ -0,0 +1,632 @@ +.page-threed { + display: flex; + height: 100vh; + background: #f5f5f5; +} + +.params-panel { + width: 400px; + background: white; + border-right: 1px solid #e4e7ed; + padding: 20px; + overflow-y: auto; +} + +.platform-tabs { + margin-bottom: 20px; +} + +.platform-info { + display: flex; + align-items: center; + gap: 8px; + + i { + font-size: 18px; + color: #409eff; + } +} + +.params-container { + .param-line { + margin-bottom: 16px; + + &.pt { + margin-top: 20px; + } + + .label { + display: block; + font-weight: 600; + color: #414141; + } + } + + .advanced-toggle-btn { + padding: 0; + font-size: 14px; + color: #409eff; + border: none; + background: none; + display: flex; + align-items: center; + gap: 4px; + + &:hover { + color: #66b1ff; + background: #f0f9ff; + border-radius: 4px; + } + } + + .advanced-params { + padding: 10px 16px; + border-radius: 8px; + border-left: 4px solid #2196f3; + } +} + +.power-display { + display: flex; + align-items: center; + gap: 8px; + + .power-value { + font-size: 24px; + font-weight: bold; + color: #409eff; + } + + .power-unit { + color: #666; + } +} + +.generate-section { + margin-top: 30px; + + .generate-btn { + width: 100%; + height: 44px; + font-size: 16px; + } +} + +.content-panel { + flex: 1; + padding: 20px; + overflow-y: auto; +} + +.task-list { + .list-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 30px; + padding: 20px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border-radius: 12px; + color: white; + + h3 { + margin: 0; + color: white; + font-size: 24px; + font-weight: 600; + display: flex; + align-items: center; + + &::before { + content: ''; + width: 4px; + height: 24px; + background: white; + margin-right: 12px; + border-radius: 2px; + } + } + + .el-button { + background: rgba(255, 255, 255, 0.2); + border: 1px solid rgba(255, 255, 255, 0.3); + color: white; + + &:hover { + background: rgba(255, 255, 255, 0.3); + border-color: rgba(255, 255, 255, 0.5); + } + } + } +} + +.task-items { + .task-card { + background: white; + border-radius: 12px; + padding: 16px; + margin-bottom: 16px; + border: 1px solid #e4e7ed; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + display: flex; + flex-direction: column; + transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out; + position: relative; + overflow: hidden; + + &:hover { + transform: translateY(-5px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + } + + &.task-card-completed { + border-left: 4px solid #67c23a; + background: #f0f9eb; + } + + &.task-card-processing { + border-left: 4px solid #409eff; + background: #ecf5ff; + } + + &.task-card-failed { + border-left: 4px solid #f56c6c; + background: #fef0f0; + } + + &.task-card-default { + border-left: 4px solid #909399; + background: #f4f4f4; + } + } + + .task-card-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; + padding-bottom: 12px; + border-bottom: 1px dashed #eee; + + .task-info { + display: flex; + align-items: center; + gap: 10px; + + .task-id { + font-size: 18px; + font-weight: bold; + color: #333; + display: flex; + align-items: center; + + i { + font-size: 20px; + color: #409eff; + } + } + + .task-platform { + font-size: 14px; + color: #666; + display: flex; + align-items: center; + + i { + font-size: 16px; + color: #409eff; + } + } + } + + .task-status-wrapper { + display: flex; + align-items: center; + gap: 10px; + + .task-status { + padding: 4px 10px; + border-radius: 6px; + font-size: 13px; + font-weight: bold; + display: flex; + align-items: center; + + &.pending { + background: #fffbe6; + color: #e6a23c; + } + + &.processing { + background: #e1f3d8; + color: #67c23a; + } + + &.completed { + background: #e1f3d8; + color: #67c23a; + } + + &.failed { + background: #fef0f0; + color: #f56c6c; + } + + i { + font-size: 14px; + margin-right: 4px; + } + } + + .task-power { + font-size: 14px; + color: #666; + display: flex; + align-items: center; + + i { + font-size: 14px; + margin-right: 4px; + color: #409eff; + } + } + } + } + + .task-card-content { + display: flex; + gap: 16px; + margin-bottom: 12px; + + .task-preview { + flex: 1; + position: relative; + border-radius: 8px; + overflow: hidden; + background: #f0f0f0; + display: flex; + align-items: center; + justify-content: center; + color: #666; + min-height: 120px; + max-width: 200px; + + .preview-image { + width: 100%; + height: 100%; + object-fit: cover; + position: relative; + + img { + width: 100%; + height: 100%; + object-fit: cover; + } + + .preview-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + color: white; + font-size: 24px; + opacity: 0; + transition: opacity 0.3s ease; + + &:hover { + opacity: 1; + } + } + } + + .input-image { + width: 100%; + height: 100%; + position: relative; + + img { + width: 100%; + height: 100%; + object-fit: cover; + } + + .input-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + color: white; + font-size: 24px; + opacity: 0; + transition: opacity 0.3s ease; + + &:hover { + opacity: 1; + } + } + } + + .prompt-placeholder { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + color: #999; + + i { + font-size: 48px; + margin-bottom: 12px; + } + + span { + font-size: 14px; + } + } + } + + .task-details { + flex: 1; + display: flex; + flex-direction: column; + justify-content: space-between; + + .task-model { + font-size: 16px; + font-weight: bold; + color: #333; + display: flex; + align-items: center; + + i { + font-size: 18px; + margin-right: 6px; + color: #409eff; + } + } + + .task-prompt { + font-size: 14px; + color: #666; + display: flex; + align-items: center; + margin-top: 4px; + + i { + font-size: 14px; + margin-right: 6px; + color: #909399; + } + } + + .task-params { + font-size: 14px; + color: #666; + display: flex; + align-items: center; + margin-top: 4px; + + i { + font-size: 14px; + margin-right: 6px; + color: #909399; + } + } + + .task-time { + font-size: 12px; + color: #999; + display: flex; + align-items: center; + margin-top: 4px; + + i { + font-size: 12px; + margin-right: 6px; + } + } + + .task-error { + font-size: 12px; + color: #f56c6c; + display: flex; + align-items: center; + margin-top: 4px; + + i { + font-size: 12px; + margin-right: 6px; + } + } + } + } + + .task-card-footer { + display: flex; + justify-content: flex-end; + gap: 8px; + padding-top: 12px; + border-top: 1px dashed #eee; + + .task-actions { + display: flex; + gap: 8px; + + .action-btn { + padding: 6px 12px; + font-size: 13px; + border-radius: 6px; + display: flex; + align-items: center; + justify-content: center; + gap: 4px; + + &.preview-btn { + background: #409eff; + color: white; + border: 1px solid #409eff; + + &:hover { + background: #66b1ff; + border-color: #66b1ff; + } + } + + &.download-btn { + background: #67c23a; + color: white; + border: 1px solid #67c23a; + + &:hover { + background: #85ce61; + border-color: #85ce61; + } + } + + &.delete-btn { + background: #f56c6c; + color: white; + border: 1px solid #f56c6c; + + &:hover { + background: #f78989; + border-color: #f78989; + } + } + + &.processing-btn { + background: #909399; + color: white; + border: 1px solid #909399; + cursor: not-allowed; + } + } + } + } + + .empty-state { + width: 100%; + height: 200px; + background: #f0f0f0; + border-radius: 12px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + color: #999; + font-size: 14px; + + i { + font-size: 48px; + margin-bottom: 12px; + } + } +} + +.pagination { + display: flex; + justify-content: center; + margin-top: 20px; +} + +.preview-container { + width: 100%; + height: 500px; + min-height: 500px; + background: #f0f0f0; + border-radius: 8px; + position: relative; + + .three-container { + width: 100%; + height: 500px; + background: #f0f0f0; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + color: #666; + } + + .preview-placeholder { + width: 100%; + height: 500px; + background: #f0f0f0; + border-radius: 8px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + color: #666; + + i { + font-size: 48px; + margin-bottom: 12px; + color: #999; + } + + p { + margin: 0; + font-size: 14px; + } + } +} + +@media (max-width: 768px) { + .page-threed { + flex-direction: column; + } + + .params-panel { + width: 100%; + border-right: none; + border-bottom: 1px solid #e4e7ed; + } + + .task-card-content { + flex-direction: column; + gap: 12px; + } + + .task-preview { + max-width: 100%; + min-height: 150px; + } + + .task-card-header { + flex-direction: column; + align-items: flex-start; + gap: 12px; + } + + .task-status-wrapper { + align-self: flex-end; + } +} + +@media (max-width: 480px) { + .task-card { + padding: 12px; + } + + .task-actions { + flex-wrap: wrap; + justify-content: center; + } + + .action-btn { + flex: 1; + min-width: 80px; + } +} diff --git a/web/src/components/ThreeDPreview.vue b/web/src/components/ThreeDPreview.vue index b0c985e5..8f4b61fe 100644 --- a/web/src/components/ThreeDPreview.vue +++ b/web/src/components/ThreeDPreview.vue @@ -5,26 +5,28 @@
- - + +
+ + + + {{ scale.toFixed(1) }}x + + + +
- - -
- -
- 重置视角 - - {{ autoRotate ? '停止旋转' : '自动旋转' }} - + +
+ +
@@ -33,6 +35,12 @@

加载3D模型中...

+
+
+
+
+ {{ loadingProgress.toFixed(1) }}% +
@@ -48,20 +56,20 @@ - diff --git a/web/src/router.js b/web/src/router.js index c83c2f43..d45aad08 100644 --- a/web/src/router.js +++ b/web/src/router.js @@ -98,6 +98,12 @@ const routes = [ meta: { title: 'AI3D模型生成' }, component: () => import('@/views/AIThreeDCreate.vue'), }, + { + name: 'test3d', + path: '/test3d', + meta: { title: '3D预览测试' }, + component: () => import('@/views/test/Test3D.vue'), + }, { name: 'ExternalLink', path: '/external', @@ -476,7 +482,7 @@ const routes = [ path: '/mobile/3d', name: 'mobile-3d', meta: { title: '3D模型生成' }, - component: () => import('@/views/mobile/ThreeDCreate.vue'), + component: () => import('@/views/AIThreeDCreate.vue'), }, ], }, @@ -485,7 +491,7 @@ const routes = [ name: 'test', path: '/test', meta: { title: '测试页面' }, - component: () => import('@/views/Test.vue'), + component: () => import('@/views/test/Test.vue'), }, { diff --git a/web/src/store/jimeng.js b/web/src/store/jimeng.js index 8be97d0e..0a9ca489 100644 --- a/web/src/store/jimeng.js +++ b/web/src/store/jimeng.js @@ -12,7 +12,7 @@ import { httpDownload, httpGet, httpPost } from '@/utils/http' import { replaceImg, substr } from '@/utils/libs' import { ElMessageBox } from 'element-plus' import { defineStore } from 'pinia' -import { computed, nextTick, reactive, ref } from 'vue' +import { computed, reactive, ref } from 'vue' export const useJimengStore = defineStore('jimeng', () => { // 当前激活的功能分类和具体功能 @@ -431,7 +431,7 @@ export const useJimengStore = defineStore('jimeng', () => { const downloadFile = async (item) => { const url = replaceImg(item.video_url || item.img_url) - const downloadURL = `${import.meta.env.VITE_API_HOST}/api/download?url=${url}` + const downloadURL = `/api/download?url=${url}` const urlObj = new URL(url) const fileName = urlObj.pathname.split('/').pop() diff --git a/web/src/store/mobile/jimeng.js b/web/src/store/mobile/jimeng.js index 8f5f50b3..300ce5cf 100644 --- a/web/src/store/mobile/jimeng.js +++ b/web/src/store/mobile/jimeng.js @@ -352,7 +352,7 @@ export const useJimengStore = defineStore('mobile-jimeng', () => { const downloadFile = async (item) => { const url = replaceImg(item.video_url || item.img_url) - const downloadURL = `${import.meta.env.VITE_API_HOST}/api/download?url=${url}` + const downloadURL = `/api/download?url=${url}` const urlObj = new URL(url) const fileName = urlObj.pathname.split('/').pop() diff --git a/web/src/store/mobile/suno.js b/web/src/store/mobile/suno.js index 6808e316..35352a95 100644 --- a/web/src/store/mobile/suno.js +++ b/web/src/store/mobile/suno.js @@ -262,7 +262,7 @@ export const useSunoStore = defineStore('suno', () => { } const download = (item) => { const url = replaceImg(item.audio_url) - const downloadURL = `${import.meta.env.VITE_API_HOST}/api/download?url=${url}` + const downloadURL = `/api/download?url=${url}` const urlObj = new URL(url) const fileName = urlObj.pathname.split('/').pop() item.downloading = true diff --git a/web/src/store/suno.js b/web/src/store/suno.js index dbb413f1..a9ddd99f 100644 --- a/web/src/store/suno.js +++ b/web/src/store/suno.js @@ -179,7 +179,7 @@ export const useSunoStore = defineStore('suno', () => { const download = async (item) => { const url = replaceImg(item.audio_url) - const downloadURL = `${import.meta.env.VITE_API_HOST}/api/download?url=${url}` + const downloadURL = `/api/download?url=${url}` const urlObj = new URL(url) const fileName = urlObj.pathname.split('/').pop() diff --git a/web/src/store/video.js b/web/src/store/video.js index c90698e9..c025892e 100644 --- a/web/src/store/video.js +++ b/web/src/store/video.js @@ -464,7 +464,7 @@ export const useVideoStore = defineStore('video', () => { // 视频下载 const downloadVideo = async (item) => { const url = replaceImg(item.video_url) - const downloadURL = `${import.meta.env.VITE_API_HOST}/api/download?url=${url}` + const downloadURL = `/api/download?url=${url}` const urlObj = new URL(url) const fileName = urlObj.pathname.split('/').pop() diff --git a/web/src/views/AIThreeDCreate.vue b/web/src/views/AIThreeDCreate.vue index ebd8c1ed..9215659a 100644 --- a/web/src/views/AIThreeDCreate.vue +++ b/web/src/views/AIThreeDCreate.vue @@ -16,35 +16,20 @@
- 上传图片: + *上传图片:
- -
- 提示词: -
-
- -
-
- 输出格式: + *模型选择:
+
+ + {{ giteeForm.model_desc }} + +
+ + +
+ *输出格式: + + + +
+ + +
+ 生成纹理: + +
@@ -64,9 +73,7 @@ > 高级参数设置 @@ -75,14 +82,9 @@
- -
- 启用纹理 -
-
- 随机种子: + 随机种子:
- 迭代次数: + 迭代次数:
- 引导系数: + 引导系数:
- 3D渲染精度: + 3D渲染精度: @@ -138,38 +140,63 @@
- -
- 上传图片: -
-
- +
+ 生成模式: + + + +
-
- 提示词: +
+
+ *提示词: +
+
+ +
-
- +
+ +
+ *上传图片: +
+
+ +
- 输出格式: + *模型选择:
- - -
- 高级参数: -
- -
- 启用PBR材质 + + {{ tencentForm.model_desc }} +
- 文件格式: + *输出格式: - - - - +
+ + +
+ 启用PBR材质: + +
@@ -232,34 +262,140 @@
-
- #{{ task.id }} - - {{ getStatusText(task.status) }} - -
- -
-
- {{ task.params?.prompt }} + +
+
+
+ + #{{ task.id }} +
+
+ + {{ getPlatformName(task.type) }} +
-
- +
+
+ + {{ getStatusText(task.status) }} +
+
+ + {{ task.power }} +
-
- 预览 - 下载 + +
+ +
+
+ +
+ +
+
+
+ +
+ +
+
+
+ + {{ getTaskPrompt(task) }} +
+
+ + +
+
+ + {{ task.model }} +
+ +
+ + {{ getTaskPrompt(task) }} +
+ +
+ + {{ getTaskParams(task) }} +
+ +
+ + {{ formatTime(task.created_at) }} +
+ +
+ + {{ task.err_msg }} +
+
-
- 删除 + +
+ + +
+ +

暂无任务,开始创建你的第一个3D模型吧!

+
@@ -281,9 +417,8 @@
@@ -293,7 +428,14 @@ @@ -303,12 +445,15 @@ diff --git a/web/src/views/mobile/ThreeDCreate.vue b/web/src/views/mobile/ThreeDCreate.vue deleted file mode 100644 index 801c9dbd..00000000 --- a/web/src/views/mobile/ThreeDCreate.vue +++ /dev/null @@ -1,765 +0,0 @@ - - - - - diff --git a/web/src/views/Test.vue b/web/src/views/test/Test.vue similarity index 100% rename from web/src/views/Test.vue rename to web/src/views/test/Test.vue diff --git a/web/src/views/test/Test3D.vue b/web/src/views/test/Test3D.vue new file mode 100644 index 00000000..8cea8522 --- /dev/null +++ b/web/src/views/test/Test3D.vue @@ -0,0 +1,66 @@ + + + + +