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 @@
加载3D模型中...
+暂无任务,开始创建你的第一个3D模型吧!
+3D模型预览
-没有测试模型URL
+