Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion backend/middleware/rbac.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
37 changes: 23 additions & 14 deletions backend/routes/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -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)
Expand Down Expand Up @@ -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,
)
Expand All @@ -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,
Expand All @@ -163,23 +172,23 @@ 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,
)
protected.PUT("/tasks/:taskId",
rbac.RequireProjectAccess("view", "taskId", "task"),
taskHandler.UpdateTask,
)
protected.DELETE("/tasks/:taskId",
protected.DELETE("/tasks/:taskId",
rbac.RequireProjectAccess("view", "taskId", "task"),
taskHandler.DeleteTask,
)
protected.PATCH("/tasks/:taskId/move",
rbac.RequireProjectAccess("view", "taskId", "task"),
taskHandler.MoveTask,
)

// 看板活动日志
protected.GET("/boards/:boardId/activities", boardActivitiesHandler.GetBoardActivities)

Expand Down
61 changes: 56 additions & 5 deletions backend/services/board_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 更新看板
Expand Down
115 changes: 115 additions & 0 deletions frontend/src/components/features/CreateColumnDialog.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
<template>
<div v-if="isOpen" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div class="bg-background rounded-lg p-6 w-full max-w-sm mx-4">
<h2 class="text-xl font-semibold mb-4">创建新列</h2>
<form @submit.prevent="handleSubmit">
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-foreground mb-2">列名称</label>
<Input
v-model="form.name"
type="text"
required
placeholder="输入列名称"
class="w-full"
autoFocus
/>
</div>
<div>
<label class="block text-sm font-medium text-foreground mb-2">描述(可选)</label>
<textarea
v-model="form.description"
class="w-full px-3 py-2 border border-input rounded-md focus:outline-none focus:ring-2 focus:ring-ring bg-background"
rows="3"
placeholder="输入描述"
></textarea>
</div>
<div>
<label class="block text-sm font-medium text-foreground mb-2">颜色</label>
<div class="flex flex-wrap gap-2">
<button
v-for="color in presetColors"
:key="color"
type="button"
@click="form.color = color"
class="w-6 h-6 rounded-full border-2 transition-all hover:scale-110"
:class="form.color === color ? 'border-foreground shadow-md' : 'border-transparent hover:border-muted-foreground'"
:style="{ backgroundColor: color }"
:title="color"
></button>
</div>
</div>
</div>
<div class="flex justify-end space-x-2 mt-6">
<Button type="button" variant="outline" @click="handleCancel">
取消
</Button>
<Button type="submit" :disabled="loading">
{{ loading ? '创建中...' : '创建' }}
</Button>
</div>
</form>
</div>
</div>
</template>

<script setup lang="ts">
import { reactive, watch } from 'vue'
import Button from '@/components/ui/Button.vue'
import Input from '@/components/ui/Input.vue'

interface Props {
isOpen: boolean
loading?: boolean
}

interface Emits {
(e: 'submit', data: { name: string; description: string; color: string }): void
(e: 'cancel'): void
}

const props = withDefaults(defineProps<Props>(), {
loading: false
})

const emit = defineEmits<Emits>()

const form = reactive({
name: '',
description: '',
color: '#9CA3AF' // 默认灰色
})

const presetColors = [
'#9CA3AF', // 灰色
'#3B82F6', // 蓝色
'#10B981', // 绿色
'#EF4444', // 红色
'#F59E0B', // 黄色
'#8B5CF6', // 紫色
]

const handleSubmit = () => {
if (form.name.trim()) {
emit('submit', {
name: form.name.trim(),
description: form.description.trim(),
color: form.color
})
}
}

const handleCancel = () => {
emit('cancel')
}

// 当对话框关闭时重置表单
watch(() => props.isOpen, (isOpen) => {
if (!isOpen) {
form.name = ''
form.description = ''
form.color = '#9CA3AF'
}
})
</script>

Loading