diff --git a/backend/cmd/main.go b/backend/cmd/main.go index d4f15f0..820646d 100755 --- a/backend/cmd/main.go +++ b/backend/cmd/main.go @@ -25,7 +25,7 @@ func main() { gin.SetMode(cfg.Server.Mode) // 初始化 SQLite 数据库 - if err := db.Init(cfg.Database.DSN, &model.User{}); err != nil { + if err := db.Init(cfg.Database.DSN, &model.User{}, &model.Project{}, &model.ProjectStyleRecord{}, &model.Task{}); err != nil { slog.Error("db init failed", "error", err) return } @@ -41,6 +41,9 @@ func main() { storageSvc := service.NewStorageService(cfg.Qiniu) handler.InitStorageService(storageSvc) + // 初始化项目服务 + service.InitProjectService(db.GetDB()) + r := gin.New() r.Use(mildware.Logger()) r.Use(mildware.Recovery()) @@ -57,9 +60,20 @@ func main() { v1Auth := r.Group("/api/v1") v1Auth.Use(mildware.AuthMiddleware(cfg.JWT.Secret)) { + // 工程管理 + v1Auth.GET("/projects", handler.ListProjects) // 获取工程列表 + v1Auth.POST("/projects", handler.CreateProject) // 创建工程 + v1Auth.GET("/projects/:projectId", handler.GetProject) // 获取工程详情 + v1Auth.PUT("/projects/:projectId", handler.UpdateProject) // 更新工程信息 + v1Auth.DELETE("/projects/:projectId", handler.DeleteProject) // 删除工程 + v1Auth.GET("/projects/:projectId/style", handler.GetStyle) // 获取工程风格 + v1Auth.PUT("/projects/:projectId/style", handler.UpdateStyle) // 更新工程风格 + v1Auth.GET("/projects/:projectId/tasks", handler.GetProjectTasks) // 获取工程任务列表 + // 素材生成 v1Auth.POST("/generate", handler.Generate) // 素材生成管线 v1Auth.GET("/tasks/:taskId", handler.GetTask) // 查询任务 v1Auth.GET("/tasks/:taskId/assets", handler.GetAssets) // 查询任务素材 + // 图片编辑 v1Auth.POST("/images/edit", handler.EditImage) // 图片编辑 } diff --git a/backend/internal/db/db.go b/backend/internal/db/db.go index 0fdbf7b..c75cdc9 100755 --- a/backend/internal/db/db.go +++ b/backend/internal/db/db.go @@ -18,3 +18,8 @@ func Init(dsn string, models ...any) error { return DB.AutoMigrate(models...) } + +// GetDB 获取全局数据库实例。 +func GetDB() *gorm.DB { + return DB +} diff --git a/backend/internal/handler/project.go b/backend/internal/handler/project.go new file mode 100644 index 0000000..11a39de --- /dev/null +++ b/backend/internal/handler/project.go @@ -0,0 +1,280 @@ +package handler + +import ( + "net/http" + "strconv" + "strings" + + "gen2d/internal/logger" + "gen2d/internal/model" + "gen2d/internal/service" + + "github.com/gin-gonic/gin" +) + +// CreateProjectRequest 创建工程请求。 +type CreateProjectRequest struct { + Name string `json:"name" binding:"required,min=1,max=100"` + Style map[string]string `json:"style"` +} + +// UpdateProjectRequest 更新工程请求。 +type UpdateProjectRequest struct { + Name string `json:"name" binding:"omitempty,min=1,max=100"` +} + +// UpdateStyleRequest 更新风格请求。 +type UpdateStyleRequest struct { + KvPairs map[string]string `json:"kvPairs"` +} + +// CreateProject 创建工程。 +func CreateProject(c *gin.Context) { + userID := c.GetUint("userID") + if userID == 0 { + c.JSON(http.StatusUnauthorized, model.Fail(http.StatusUnauthorized, "未认证")) + return + } + + var req CreateProjectRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, model.Fail(http.StatusBadRequest, "参数错误: "+err.Error())) + return + } + + projectSvc := service.GetProjectService() + proj, err := projectSvc.CreateProject(c.Request.Context(), userID, req.Name, req.Style) + if err != nil { + logger.FromCtx(c.Request.Context()).Error("create project failed", "error", err) + c.JSON(http.StatusInternalServerError, model.Fail(http.StatusInternalServerError, "创建工程失败")) + return + } + + c.JSON(http.StatusCreated, model.OK(proj)) +} + +// ListProjects 获取工程列表。 +func ListProjects(c *gin.Context) { + userID := c.GetUint("userID") + if userID == 0 { + c.JSON(http.StatusUnauthorized, model.Fail(http.StatusUnauthorized, "未认证")) + return + } + + pageStr := c.DefaultQuery("page", "1") + pageSizeStr := c.DefaultQuery("pageSize", "20") + + page, err := strconv.Atoi(pageStr) + if err != nil || page < 1 { + page = 1 + } + + pageSize, err := strconv.Atoi(pageSizeStr) + if err != nil || pageSize < 1 || pageSize > 100 { + pageSize = 20 + } + + projectSvc := service.GetProjectService() + result, err := projectSvc.ListProjects(c.Request.Context(), userID, page, pageSize) + if err != nil { + logger.FromCtx(c.Request.Context()).Error("list projects failed", "error", err) + c.JSON(http.StatusInternalServerError, model.Fail(http.StatusInternalServerError, "获取工程列表失败")) + return + } + + c.JSON(http.StatusOK, model.OK(result)) +} + +// GetProject 获取工程详情。 +func GetProject(c *gin.Context) { + userID := c.GetUint("userID") + if userID == 0 { + c.JSON(http.StatusUnauthorized, model.Fail(http.StatusUnauthorized, "未认证")) + return + } + + projectIDStr := c.Param("projectId") + projectID, err := strconv.ParseUint(projectIDStr, 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, model.Fail(http.StatusBadRequest, "无效的工程ID")) + return + } + + projectSvc := service.GetProjectService() + proj, err := projectSvc.GetProject(c.Request.Context(), userID, uint(projectID)) + if err != nil { + logger.FromCtx(c.Request.Context()).Error("get project failed", "error", err) + if strings.Contains(err.Error(), "not found") { + c.JSON(http.StatusNotFound, model.Fail(http.StatusNotFound, "工程不存在")) + return + } + c.JSON(http.StatusInternalServerError, model.Fail(http.StatusInternalServerError, "获取工程详情失败")) + return + } + + c.JSON(http.StatusOK, model.OK(proj)) +} + +// UpdateProject 更新工程信息。 +func UpdateProject(c *gin.Context) { + userID := c.GetUint("userID") + if userID == 0 { + c.JSON(http.StatusUnauthorized, model.Fail(http.StatusUnauthorized, "未认证")) + return + } + + projectIDStr := c.Param("projectId") + projectID, err := strconv.ParseUint(projectIDStr, 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, model.Fail(http.StatusBadRequest, "无效的工程ID")) + return + } + + var req UpdateProjectRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, model.Fail(http.StatusBadRequest, "参数错误: "+err.Error())) + return + } + + projectSvc := service.GetProjectService() + proj, err := projectSvc.UpdateProject(c.Request.Context(), userID, uint(projectID), req.Name) + if err != nil { + logger.FromCtx(c.Request.Context()).Error("update project failed", "error", err) + if strings.Contains(err.Error(), "not found") { + c.JSON(http.StatusNotFound, model.Fail(http.StatusNotFound, "工程不存在")) + return + } + c.JSON(http.StatusInternalServerError, model.Fail(http.StatusInternalServerError, "更新工程信息失败")) + return + } + + c.JSON(http.StatusOK, model.OK(proj)) +} + +// DeleteProject 删除工程。 +func DeleteProject(c *gin.Context) { + userID := c.GetUint("userID") + if userID == 0 { + c.JSON(http.StatusUnauthorized, model.Fail(http.StatusUnauthorized, "未认证")) + return + } + + projectIDStr := c.Param("projectId") + projectID, err := strconv.ParseUint(projectIDStr, 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, model.Fail(http.StatusBadRequest, "无效的工程ID")) + return + } + + projectSvc := service.GetProjectService() + err = projectSvc.DeleteProject(c.Request.Context(), userID, uint(projectID)) + if err != nil { + logger.FromCtx(c.Request.Context()).Error("delete project failed", "error", err) + if strings.Contains(err.Error(), "not found") { + c.JSON(http.StatusNotFound, model.Fail(http.StatusNotFound, "工程不存在")) + return + } + c.JSON(http.StatusInternalServerError, model.Fail(http.StatusInternalServerError, "删除工程失败")) + return + } + + c.JSON(http.StatusOK, model.OK(nil)) +} + +// GetStyle 获取工程风格。 +func GetStyle(c *gin.Context) { + userID := c.GetUint("userID") + if userID == 0 { + c.JSON(http.StatusUnauthorized, model.Fail(http.StatusUnauthorized, "未认证")) + return + } + + projectIDStr := c.Param("projectId") + projectID, err := strconv.ParseUint(projectIDStr, 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, model.Fail(http.StatusBadRequest, "无效的工程ID")) + return + } + + projectSvc := service.GetProjectService() + style, err := projectSvc.GetStyle(c.Request.Context(), userID, uint(projectID)) + if err != nil { + logger.FromCtx(c.Request.Context()).Error("get style failed", "error", err) + if strings.Contains(err.Error(), "not found") { + c.JSON(http.StatusNotFound, model.Fail(http.StatusNotFound, "工程不存在")) + return + } + c.JSON(http.StatusInternalServerError, model.Fail(http.StatusInternalServerError, "获取工程风格失败")) + return + } + + c.JSON(http.StatusOK, model.OK(map[string]interface{}{ + "kvPairs": style, + })) +} + +// UpdateStyle 更新工程风格。 +func UpdateStyle(c *gin.Context) { + userID := c.GetUint("userID") + if userID == 0 { + c.JSON(http.StatusUnauthorized, model.Fail(http.StatusUnauthorized, "未认证")) + return + } + + projectIDStr := c.Param("projectId") + projectID, err := strconv.ParseUint(projectIDStr, 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, model.Fail(http.StatusBadRequest, "无效的工程ID")) + return + } + + var req UpdateStyleRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, model.Fail(http.StatusBadRequest, "参数错误: "+err.Error())) + return + } + + projectSvc := service.GetProjectService() + err = projectSvc.UpdateStyle(c.Request.Context(), userID, uint(projectID), req.KvPairs) + if err != nil { + logger.FromCtx(c.Request.Context()).Error("update style failed", "error", err) + if strings.Contains(err.Error(), "not found") { + c.JSON(http.StatusNotFound, model.Fail(http.StatusNotFound, "工程不存在")) + return + } + c.JSON(http.StatusInternalServerError, model.Fail(http.StatusInternalServerError, "更新工程风格失败")) + return + } + + c.JSON(http.StatusOK, model.OK(nil)) +} + +// GetProjectTasks 获取工程下的任务列表。 +func GetProjectTasks(c *gin.Context) { + userID := c.GetUint("userID") + if userID == 0 { + c.JSON(http.StatusUnauthorized, model.Fail(http.StatusUnauthorized, "未认证")) + return + } + + projectIDStr := c.Param("projectId") + projectID, err := strconv.ParseUint(projectIDStr, 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, model.Fail(http.StatusBadRequest, "无效的工程ID")) + return + } + + projectSvc := service.GetProjectService() + tasks, err := projectSvc.GetTasks(c.Request.Context(), userID, uint(projectID)) + if err != nil { + logger.FromCtx(c.Request.Context()).Error("get tasks failed", "error", err) + if strings.Contains(err.Error(), "not found") { + c.JSON(http.StatusNotFound, model.Fail(http.StatusNotFound, "工程不存在")) + return + } + c.JSON(http.StatusInternalServerError, model.Fail(http.StatusInternalServerError, "获取任务列表失败")) + return + } + + c.JSON(http.StatusOK, model.OK(tasks)) +} diff --git a/backend/internal/model/project.go b/backend/internal/model/project.go new file mode 100644 index 0000000..d298c3f --- /dev/null +++ b/backend/internal/model/project.go @@ -0,0 +1,43 @@ +package model + +import "time" + +// Project 工程数据模型,对应数据库表。 +type Project struct { + ID uint `gorm:"primaryKey" json:"id"` + UserID uint `gorm:"index" json:"-"` // 用户ID,用于权限控制 + Name string `gorm:"size:100;not null" json:"name"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt,omitempty"` +} + +// ProjectStyleRecord 工程风格记录,用于存储键值对。 +type ProjectStyleRecord struct { + ID uint `gorm:"primaryKey" json:"id"` + ProjectID uint `gorm:"index" json:"-"` + Key string `gorm:"size:50;not null" json:"-"` + Value string `gorm:"size:100;not null" json:"-"` + CreatedAt time.Time `json:"-"` +} + +// ProjectResponse 工程响应,包含风格键值对和元数据。 +type ProjectResponse struct { + ID string `json:"id"` + Name string `json:"name"` + Style *ProjectStyleResponse `json:"style"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt,omitempty"` + TaskCount int `json:"taskCount,omitempty"` + LastActivityAt string `json:"lastActivityAt,omitempty"` +} + +// ProjectStyleResponse 工程风格响应。 +type ProjectStyleResponse struct { + KvPairs map[string]string `json:"kvPairs"` +} + +// ProjectsListResponse 工程列表响应。 +type ProjectsListResponse struct { + Total int `json:"total"` + Projects []ProjectResponse `json:"projects"` +} diff --git a/backend/internal/model/task.go b/backend/internal/model/task.go new file mode 100644 index 0000000..a62ab60 --- /dev/null +++ b/backend/internal/model/task.go @@ -0,0 +1,33 @@ +package model + +import "time" + +// Task 任务数据模型,对应数据库表。 +type Task struct { + ID uint `gorm:"primaryKey" json:"id"` + ProjectID uint `gorm:"index" json:"-"` + Prompt string `gorm:"type:text" json:"prompt"` + AssetType string `gorm:"size:50" json:"assetType"` + Status string `gorm:"size:20;default:'pending'" json:"status"` + Stage string `gorm:"size:50" json:"stage,omitempty"` + Progress int `gorm:"default:0" json:"progress"` + RetryCount int `gorm:"default:0" json:"retryCount"` + Error string `gorm:"type:text" json:"error,omitempty"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt,omitempty"` +} + +// TaskResponse 任务响应。 +type TaskResponse struct { + ID string `json:"id"` + ProjectID string `json:"projectId"` + Prompt string `json:"prompt"` + AssetType string `json:"assetType"` + Status string `json:"status"` + Stage string `json:"stage,omitempty"` + Progress int `json:"progress"` + RetryCount int `json:"retryCount"` + Error string `json:"error,omitempty"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt,omitempty"` +} diff --git a/backend/internal/service/project.go b/backend/internal/service/project.go new file mode 100644 index 0000000..14138f3 --- /dev/null +++ b/backend/internal/service/project.go @@ -0,0 +1,272 @@ +package service + +import ( + "context" + "errors" + "fmt" + "sync" + "time" + + "gen2d/internal/model" + "gorm.io/gorm" +) + +var ( + projectService *ProjectService + projectServiceOnce sync.Once +) + +// ProjectService 工程管理服务。 +type ProjectService struct { + db *gorm.DB +} + +// InitProjectService 初始化工程管理服务。 +func InitProjectService(db *gorm.DB) { + projectServiceOnce.Do(func() { + projectService = &ProjectService{db: db} + }) +} + +// GetProjectService 获取工程管理服务实例。 +func GetProjectService() *ProjectService { + return projectService +} + +// CreateProject 创建工程。 +func (s *ProjectService) CreateProject(ctx context.Context, userID uint, name string, style map[string]string) (*model.ProjectResponse, error) { + project := &model.Project{ + UserID: userID, + Name: name, + CreatedAt: time.Now(), + } + + if err := s.db.WithContext(ctx).Create(project).Error; err != nil { + return nil, fmt.Errorf("create project failed: %w", err) + } + + // 保存风格键值对 + if err := s.saveStyle(ctx, project.ID, style); err != nil { + // 回滚工程创建 + s.db.WithContext(ctx).Delete(project) + return nil, fmt.Errorf("save style failed: %w", err) + } + + return s.toProjectResponse(project, style), nil +} + +// ListProjects 获取用户工程列表。 +func (s *ProjectService) ListProjects(ctx context.Context, userID uint, page, pageSize int) (*model.ProjectsListResponse, error) { + var projects []model.Project + var total int64 + + offset := (page - 1) * pageSize + + if err := s.db.WithContext(ctx).Model(&model.Project{}).Where("user_id = ?", userID).Count(&total).Error; err != nil { + return nil, fmt.Errorf("count projects failed: %w", err) + } + + if err := s.db.WithContext(ctx). + Where("user_id = ?", userID). + Order("created_at DESC"). + Limit(pageSize). + Offset(offset). + Find(&projects).Error; err != nil { + return nil, fmt.Errorf("list projects failed: %w", err) + } + + response := &model.ProjectsListResponse{ + Total: int(total), + Projects: make([]model.ProjectResponse, len(projects)), + } + + for i, p := range projects { + style, _ := s.getStyle(ctx, p.ID) + response.Projects[i] = *s.toProjectResponse(&p, style) + } + + return response, nil +} + +// GetProject 获取工程详情。 +func (s *ProjectService) GetProject(ctx context.Context, userID uint, projectID uint) (*model.ProjectResponse, error) { + var project model.Project + if err := s.db.WithContext(ctx).Where("id = ? AND user_id = ?", projectID, userID).First(&project).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.New("project not found") + } + return nil, fmt.Errorf("get project failed: %w", err) + } + + style, _ := s.getStyle(ctx, project.ID) + return s.toProjectResponse(&project, style), nil +} + +// UpdateProject 更新工程信息。 +func (s *ProjectService) UpdateProject(ctx context.Context, userID uint, projectID uint, name string) (*model.ProjectResponse, error) { + var project model.Project + if err := s.db.WithContext(ctx).Where("id = ? AND user_id = ?", projectID, userID).First(&project).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.New("project not found") + } + return nil, fmt.Errorf("get project failed: %w", err) + } + + project.Name = name + + if err := s.db.WithContext(ctx).Save(&project).Error; err != nil { + return nil, fmt.Errorf("update project failed: %w", err) + } + + style, _ := s.getStyle(ctx, project.ID) + return s.toProjectResponse(&project, style), nil +} + +// DeleteProject 删除工程。 +func (s *ProjectService) DeleteProject(ctx context.Context, userID uint, projectID uint) error { + var project model.Project + if err := s.db.WithContext(ctx).Where("id = ? AND user_id = ?", projectID, userID).First(&project).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return errors.New("project not found") + } + return fmt.Errorf("get project failed: %w", err) + } + + return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + // 删除风格记录 + if err := tx.Where("project_id = ?", projectID).Delete(&model.ProjectStyleRecord{}).Error; err != nil { + return fmt.Errorf("delete style records failed: %w", err) + } + + // TODO: 删除任务和素材记录 + // TODO: 删除七牛云对象 + + // 删除工程 + if err := tx.Delete(&project).Error; err != nil { + return fmt.Errorf("delete project failed: %w", err) + } + + return nil + }) +} + +// GetStyle 获取工程风格。 +func (s *ProjectService) GetStyle(ctx context.Context, userID uint, projectID uint) (map[string]string, error) { + var project model.Project + if err := s.db.WithContext(ctx).Where("id = ? AND user_id = ?", projectID, userID).First(&project).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.New("project not found") + } + return nil, fmt.Errorf("get project failed: %w", err) + } + + style, _ := s.getStyle(ctx, project.ID) + return style, nil +} + +// UpdateStyle 更新工程风格。 +func (s *ProjectService) UpdateStyle(ctx context.Context, userID uint, projectID uint, kvPairs map[string]string) error { + var project model.Project + if err := s.db.WithContext(ctx).Where("id = ? AND user_id = ?", projectID, userID).First(&project).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return errors.New("project not found") + } + return fmt.Errorf("get project failed: %w", err) + } + + return s.saveStyle(ctx, projectID, kvPairs) +} + +// GetTasks 获取工程下的任务列表。 +func (s *ProjectService) GetTasks(ctx context.Context, userID uint, projectID uint) ([]model.TaskResponse, error) { + var project model.Project + if err := s.db.WithContext(ctx).Where("id = ? AND user_id = ?", projectID, userID).First(&project).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.New("project not found") + } + return nil, fmt.Errorf("get project failed: %w", err) + } + + var tasks []model.Task + if err := s.db.WithContext(ctx). + Where("project_id = ?", projectID). + Order("created_at DESC"). + Find(&tasks).Error; err != nil { + return nil, fmt.Errorf("get tasks failed: %w", err) + } + + response := make([]model.TaskResponse, len(tasks)) + for i, t := range tasks { + response[i] = model.TaskResponse{ + ID: fmt.Sprintf("%d", t.ID), + ProjectID: fmt.Sprintf("%d", t.ProjectID), + Prompt: t.Prompt, + AssetType: t.AssetType, + Status: t.Status, + Stage: t.Stage, + Progress: t.Progress, + RetryCount: t.RetryCount, + Error: t.Error, + CreatedAt: t.CreatedAt.Format("2006-01-02T15:04:05Z07:00"), + UpdatedAt: t.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"), + } + } + + return response, nil +} + +func (s *ProjectService) saveStyle(ctx context.Context, projectID uint, style map[string]string) error { + return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + // 删除现有风格记录 + if err := tx.Where("project_id = ?", projectID).Delete(&model.ProjectStyleRecord{}).Error; err != nil { + return err + } + + // 插入新记录 + for key, value := range style { + record := &model.ProjectStyleRecord{ + ProjectID: projectID, + Key: key, + Value: value, + } + if err := tx.Create(record).Error; err != nil { + return err + } + } + + return nil + }) +} + +func (s *ProjectService) getStyle(ctx context.Context, projectID uint) (map[string]string, error) { + var records []model.ProjectStyleRecord + if err := s.db.WithContext(ctx).Where("project_id = ?", projectID).Find(&records).Error; err != nil { + return nil, err + } + + style := make(map[string]string) + for _, r := range records { + style[r.Key] = r.Value + } + + return style, nil +} + +func (s *ProjectService) toProjectResponse(project *model.Project, style map[string]string) *model.ProjectResponse { + // 获取任务数量 + var taskCount int64 + s.db.Model(&model.Task{}).Where("project_id = ?", project.ID).Count(&taskCount) + + styleResp := &model.ProjectStyleResponse{ + KvPairs: style, + } + + return &model.ProjectResponse{ + ID: fmt.Sprintf("%d", project.ID), + Name: project.Name, + Style: styleResp, + CreatedAt: project.CreatedAt.Format("2006-01-02T15:04:05Z07:00"), + UpdatedAt: project.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"), + TaskCount: int(taskCount), + } +} diff --git a/docs/api.md b/docs/api.md index 06948af..ecae802 100755 --- a/docs/api.md +++ b/docs/api.md @@ -217,12 +217,12 @@ POST /api/v1/prompt/optimize | 状态 | 方法 | 路径 | 说明 | |------|------|------|------| -| [ ] | GET | `/api/v1/projects` | 获取当前用户的工程列表(前端已对接 mock) | -| [ ] | POST | `/api/v1/projects` | 创建工程(前端已对接 mock) | -| [ ] | GET | `/api/v1/projects/:projectId` | 获取工程信息 | -| [ ] | PUT | `/api/v1/projects/:projectId` | 更新工程信息(如名称) | -| [ ] | DELETE | `/api/v1/projects/:projectId` | 删除工程(级联删除任务和素材) | -| [ ] | GET | `/api/v1/projects/:projectId/tasks` | 获取工程下的任务列表 | +| [x] | GET | `/api/v1/projects` | 获取当前用户的工程列表(前端已对接 mock) | +| [x] | POST | `/api/v1/projects` | 创建工程(前端已对接 mock) | +| [x] | GET | `/api/v1/projects/:projectId` | 获取工程信息 | +| [x] | PUT | `/api/v1/projects/:projectId` | 更新工程信息(如名称) | +| [x] | DELETE | `/api/v1/projects/:projectId` | 删除工程(级联删除任务和素材) | +| [x] | GET | `/api/v1/projects/:projectId/tasks` | 获取工程下的任务列表 | ### GET /api/v1/projects @@ -355,8 +355,8 @@ POST /api/v1/prompt/optimize | 状态 | 方法 | 路径 | 说明 | |------|------|------|------| -| [ ] | GET | `/api/v1/projects/:projectId/style` | 获取工程风格 | -| [ ] | PUT | `/api/v1/projects/:projectId/style` | 更新工程风格 | +| [x] | GET | `/api/v1/projects/:projectId/style` | 获取工程风格 | +| [x] | PUT | `/api/v1/projects/:projectId/style` | 更新工程风格 | 请求体示例 (PUT /api/v1/projects/:projectId/style): diff --git a/frontend/src/api/edit.ts b/frontend/src/api/edit.ts new file mode 100644 index 0000000..a14525b --- /dev/null +++ b/frontend/src/api/edit.ts @@ -0,0 +1,22 @@ +import { post } from './client' + +export interface EditImageRequest { + image: string // base64 编码 + prompt: string + count?: number +} + +export interface EditAssetResponse { + data: string // base64 编码 + format: string +} + +export interface EditImageResponse { + assets: EditAssetResponse[] +} + +export async function editImage( + req: EditImageRequest, +): Promise { + return post('/api/v1/images/edit', req) +} diff --git a/frontend/src/api/health.ts b/frontend/src/api/health.ts new file mode 100644 index 0000000..79c6764 --- /dev/null +++ b/frontend/src/api/health.ts @@ -0,0 +1,9 @@ +import { get } from './client' + +export interface HealthResponse { + status: string +} + +export async function getHealth(): Promise { + return get('/api/v1/health') +} diff --git a/frontend/src/api/project.ts b/frontend/src/api/project.ts index a87d19c..5b68e67 100755 --- a/frontend/src/api/project.ts +++ b/frontend/src/api/project.ts @@ -1,64 +1,60 @@ import type { Project, Task } from './types' -import { - mockListProjects, - mockGetProject, - mockCreateProject, - mockDeleteProject, - mockUpdateProject, - mockGetStyle, - mockSaveStyle, - mockGetTasks, -} from './mock' +import { get, post, put, del } from './client' -const USE_MOCK = true +export async function listProjects( + page?: number, + pageSize?: number +): Promise { + const params = new URLSearchParams() + if (page !== undefined) params.set('page', String(page)) + if (pageSize !== undefined) params.set('pageSize', String(pageSize)) -export async function listProjects(): Promise { - if (USE_MOCK) return mockListProjects() - throw new Error('Not implemented') + const url = `/api/v1/projects${params.toString() ? '?' + params : ''}` + const resp = await get<{ + total: number + projects: Project[] + }>(url) + return resp.projects } export async function getProject(projectId: string): Promise { - if (USE_MOCK) return mockGetProject(projectId) - throw new Error('Not implemented') + return get(`/api/v1/projects/${projectId}`) } export async function createProject( name: string, style?: Record ): Promise { - if (USE_MOCK) return mockCreateProject(name, style) - throw new Error('Not implemented') + return post('/api/v1/projects', { name, style }) } export async function deleteProject(projectId: string): Promise { - if (USE_MOCK) return mockDeleteProject(projectId) - throw new Error('Not implemented') + return del(`/api/v1/projects/${projectId}`) } export async function updateProject( projectId: string, - data: { name?: string } + data: { name: string } ): Promise { - if (USE_MOCK) return mockUpdateProject(projectId, data) - throw new Error('Not implemented') + return put(`/api/v1/projects/${projectId}`, data) } export async function getStyle( projectId: string ): Promise> { - if (USE_MOCK) return mockGetStyle(projectId) - throw new Error('Not implemented') + const resp = await get<{ kvPairs: Record }>( + `/api/v1/projects/${projectId}/style` + ) + return resp.kvPairs } export async function saveStyle( projectId: string, kvPairs: Record ): Promise { - if (USE_MOCK) return mockSaveStyle(projectId, kvPairs) - throw new Error('Not implemented') + return put(`/api/v1/projects/${projectId}/style`, { kvPairs }) } export async function getTasks(projectId: string): Promise { - if (USE_MOCK) return mockGetTasks(projectId) - throw new Error('Not implemented') + return get(`/api/v1/projects/${projectId}/tasks`) }