diff --git a/backend/middleware/rbac.go b/backend/middleware/rbac.go index 01b5852..1998dcb 100644 --- a/backend/middleware/rbac.go +++ b/backend/middleware/rbac.go @@ -94,9 +94,10 @@ func (m *RBACMiddleware) RequireProjectAccess(level string, paramKey string, idT c.Abort() return } + projectID = task.ProjectID default: - c.JSON(http.StatusInternalServerError, gin.H{"error": "Invalid ID type for RBAC"}) + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID type for RBAC"}) c.Abort() return } diff --git a/backend/routes/routes.go b/backend/routes/routes.go index 7f863ec..0fc5250 100644 --- a/backend/routes/routes.go +++ b/backend/routes/routes.go @@ -10,8 +10,8 @@ import ( "progress-wall-backend/handlers/column" "progress-wall-backend/handlers/project" "progress-wall-backend/handlers/task" - "progress-wall-backend/handlers/user" "progress-wall-backend/handlers/team" + "progress-wall-backend/handlers/user" "progress-wall-backend/middleware" "progress-wall-backend/services" @@ -31,10 +31,19 @@ func SetupRoutes(db *gorm.DB, cfg *config.Config) *gin.Engine { // 配置CORS corsConfig := cors.DefaultConfig() - corsConfig.AllowOrigins = strings.Split(cfg.CORS.AllowOrigins, ",") corsConfig.AllowMethods = []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"} corsConfig.AllowHeaders = []string{"Origin", "Content-Type", "Accept", "Authorization"} corsConfig.AllowCredentials = true + + // 在开发环境下允许所有Origin,避免跨域问题 + if cfg.Server.Mode == "release" { + corsConfig.AllowOrigins = strings.Split(cfg.CORS.AllowOrigins, ",") + } else { + corsConfig.AllowOriginFunc = func(origin string) bool { + return true + } + } + r.Use(cors.New(corsConfig)) permService := services.NewPermissionService(db) @@ -95,38 +104,38 @@ func SetupRoutes(db *gorm.DB, cfg *config.Config) *gin.Engine { projectHandler.GetTeamProjects, ) protected.GET("/projects", projectHandler.GetProjects) - protected.GET("/projects/:projectId", + protected.GET("/projects/:projectId", rbac.RequireProjectAccess("view", "projectId", "project"), projectHandler.GetProject, ) - protected.PUT("/projects/:projectId", + protected.PUT("/projects/:projectId", rbac.RequireProjectAccess("manage", "projectId", "project"), projectHandler.UpdateProject, ) - protected.DELETE("/projects/:projectId", + protected.DELETE("/projects/:projectId", rbac.RequireProjectAccess("manage", "projectId", "project"), projectHandler.DeleteProject, ) // 看板相关 protected.GET("/boards", boardHandler.GetBoards) - protected.GET("/projects/:projectId/boards", + protected.GET("/projects/:projectId/boards", rbac.RequireProjectAccess("view", "projectId", "project"), boardHandler.GetBoardsByProject, ) - protected.POST("/projects/:projectId/boards", + protected.POST("/projects/:projectId/boards", rbac.RequireProjectAccess("manage", "projectId", "project"), boardHandler.CreateBoard, ) - protected.GET("/boards/:boardId", + protected.GET("/boards/:boardId", rbac.RequireProjectAccess("view", "boardId", "board"), boardHandler.GetBoard, ) - protected.PUT("/boards/:boardId", + protected.PUT("/boards/:boardId", rbac.RequireProjectAccess("manage", "boardId", "board"), boardHandler.UpdateBoard, ) - protected.DELETE("/boards/:boardId", + protected.DELETE("/boards/:boardId", rbac.RequireProjectAccess("manage", "boardId", "board"), boardHandler.DeleteBoard, ) @@ -136,7 +145,7 @@ func SetupRoutes(db *gorm.DB, cfg *config.Config) *gin.Engine { rbac.RequireProjectAccess("view", "boardId", "board"), columnHandler.GetColumns, ) - protected.POST("/boards/:boardId/columns", + protected.POST("/boards/:boardId/columns", // Only admins can create columns rbac.RequireProjectAccess("manage", "boardId", "board"), columnHandler.CreateColumn, @@ -163,7 +172,7 @@ func SetupRoutes(db *gorm.DB, cfg *config.Config) *gin.Engine { rbac.RequireProjectAccess("view", "columnId", "column"), taskHandler.CreateTask, ) - protected.GET("/tasks/:taskId", + protected.GET("/tasks/:taskId", rbac.RequireProjectAccess("view", "taskId", "task"), taskHandler.GetTask, ) @@ -171,7 +180,7 @@ func SetupRoutes(db *gorm.DB, cfg *config.Config) *gin.Engine { rbac.RequireProjectAccess("view", "taskId", "task"), taskHandler.UpdateTask, ) - protected.DELETE("/tasks/:taskId", + protected.DELETE("/tasks/:taskId", rbac.RequireProjectAccess("view", "taskId", "task"), taskHandler.DeleteTask, ) @@ -179,7 +188,7 @@ func SetupRoutes(db *gorm.DB, cfg *config.Config) *gin.Engine { rbac.RequireProjectAccess("view", "taskId", "task"), taskHandler.MoveTask, ) - + // 看板活动日志 protected.GET("/boards/:boardId/activities", boardActivitiesHandler.GetBoardActivities) diff --git a/backend/services/board_service.go b/backend/services/board_service.go index 24d151b..64e0780 100644 --- a/backend/services/board_service.go +++ b/backend/services/board_service.go @@ -76,12 +76,63 @@ func (s *BoardService) GetBoardsByProjectID(projectID uint) ([]models.Board, err return boards, nil } -// CreateBoard 创建看板 +// CreateBoard 创建看板(带默认列) func (s *BoardService) CreateBoard(board *models.Board) error { - if err := s.db.Create(board).Error; err != nil { - return fmt.Errorf("创建看板失败: %v", err) - } - return nil + return s.db.Transaction(func(tx *gorm.DB) error { + if err := tx.Create(board).Error; err != nil { + return fmt.Errorf("创建看板失败: %v", err) + } + + // 初始化默认列 + defaultColumns := []models.Column{ + { + Name: "Backlog", + Description: "待办事项", + Color: "#6B7280", // Gray + Position: 1000, + BoardID: board.ID, + Status: models.ColumnStatusActive, + }, + { + Name: "Ready", + Description: "准备就绪", + Color: "#3B82F6", // Blue + Position: 2000, + BoardID: board.ID, + Status: models.ColumnStatusActive, + }, + { + Name: "In processing", + Description: "进行中", + Color: "#F59E0B", // Yellow + Position: 3000, + BoardID: board.ID, + Status: models.ColumnStatusActive, + }, + { + Name: "In review", + Description: "审核中", + Color: "#8B5CF6", // Purple + Position: 4000, + BoardID: board.ID, + Status: models.ColumnStatusActive, + }, + { + Name: "Done", + Description: "已完成", + Color: "#10B981", // Green + Position: 5000, + BoardID: board.ID, + Status: models.ColumnStatusActive, + }, + } + + if err := tx.Create(&defaultColumns).Error; err != nil { + return fmt.Errorf("创建默认列失败: %v", err) + } + + return nil + }) } // UpdateBoard 更新看板 diff --git a/frontend/src/components/features/CreateColumnDialog.vue b/frontend/src/components/features/CreateColumnDialog.vue new file mode 100644 index 0000000..a590f22 --- /dev/null +++ b/frontend/src/components/features/CreateColumnDialog.vue @@ -0,0 +1,115 @@ + + + + diff --git a/frontend/src/components/features/CreateTaskDialog.vue b/frontend/src/components/features/CreateTaskDialog.vue new file mode 100644 index 0000000..75df558 --- /dev/null +++ b/frontend/src/components/features/CreateTaskDialog.vue @@ -0,0 +1,119 @@ + + + + diff --git a/frontend/src/components/features/KanbanColumn.vue b/frontend/src/components/features/KanbanColumn.vue index 63e6411..f9d96e8 100644 --- a/frontend/src/components/features/KanbanColumn.vue +++ b/frontend/src/components/features/KanbanColumn.vue @@ -1,39 +1,50 @@ @@ -51,7 +62,8 @@ const props = defineProps() const emit = defineEmits<{ 'select-task': [task: any] - 'delete-task': [taskId: string] + 'delete-task': [taskId: number] + 'add-task': [columnId: number] }>() const kanbanStore = useKanbanStore() @@ -60,35 +72,45 @@ const kanbanStore = useKanbanStore() const onDragEnd = async (event: any) => { const { item, to, from, newIndex, oldIndex } = event - // 获取被拖拽的任务ID - item 本身就是 Card 元素 - const taskId = item.getAttribute('data-task-id') - console.log('拖拽结束 - taskId:', taskId, 'event:', event) - if (!taskId) { + // 获取被拖拽的任务ID + const taskIdStr = item.getAttribute('data-task-id') + if (!taskIdStr) { console.error('未找到 taskId') return } + const taskId = parseInt(taskIdStr, 10) // 获取目标列ID const targetColumnElement = to.closest('[data-column-id]') - const targetColumnId = targetColumnElement?.getAttribute('data-column-id') - console.log('目标列 - targetColumnId:', targetColumnId) - if (!targetColumnId) { + const targetColumnIdStr = targetColumnElement?.getAttribute('data-column-id') + if (!targetColumnIdStr) { console.error('未找到 targetColumnId') return } + const targetColumnId = parseInt(targetColumnIdStr, 10) // 如果是同一列内的重新排序,或者跨列移动 if (from !== to || newIndex !== oldIndex) { - console.log('开始调用API - taskId:', taskId, 'targetColumnId:', targetColumnId, 'newIndex:', newIndex) + console.log('拖拽移动 - taskId:', taskId, 'to column:', targetColumnId, 'index:', newIndex) try { + // 在 store 中已经处理了 API 调用和乐观更新 + // 如果 store 中的同步逻辑出错(比如找不到列),这里会捕获到异常 + // 但异步的 API 错误会在 store 内部捕获并处理回滚,这里不需要 await await kanbanStore.moveTaskWithDrag(taskId, targetColumnId, newIndex) - console.log('API调用成功') } catch (error) { console.error('拖拽移动任务失败:', error) - // 这里可以添加错误提示 + + // 如果是同步逻辑出错,手动回滚 UI(VueDraggable 会自动修改 v-model,需要撤销) + // 注意:VueDraggable 的 v-model 双向绑定已经修改了数据 + // 如果 store 抛出错误,说明 store 的数据没有更新或者更新失败 + // 但因为 v-model 是直接绑定到 column.tasks 的,VueDraggable 可能已经修改了数组 + // 最简单的回滚方式是重新获取看板数据,或者利用 store 的回滚机制 + + // 由于 store.moveTaskWithDrag 内部已经有 try-catch 处理同步错误并回滚 + // 这里其实主要捕获的是 store 抛出的同步错误 + // 我们可以选择刷新看板数据来确保一致性 + // kanbanStore.fetchBoardDetail(kanbanStore.currentBoardId!) } - } else { - console.log('未触发移动 - 位置未改变') } } @@ -102,25 +124,12 @@ const onDragEnd = async (event: any) => { } :deep(.chosen-task) { - transform: rotate(5deg); + transform: rotate(2deg); box-shadow: 0 8px 16px rgba(0, 0, 0, 0.15); } :deep(.drag-task) { - transform: rotate(5deg); - opacity: 0.8; -} - -/* 拖拽区域样式 */ -.sortable-ghost { - opacity: 0.5; -} - -.sortable-chosen { - opacity: 0.8; -} - -.sortable-drag { - opacity: 0.6; + transform: rotate(2deg); + opacity: 0.9; } diff --git a/frontend/src/components/features/TaskCard.vue b/frontend/src/components/features/TaskCard.vue index e531e6e..30befa0 100644 --- a/frontend/src/components/features/TaskCard.vue +++ b/frontend/src/components/features/TaskCard.vue @@ -1,17 +1,17 @@