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 @@ + + + + 创建新列 + + + + 列名称 + + + + 描述(可选) + + + + 颜色 + + + + + + + + 取消 + + + {{ loading ? '创建中...' : '创建' }} + + + + + + + + + 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 @@ + + + + 创建新任务 + + + + 任务标题 + + + + + 描述 + + + + + + 优先级 + + 低 + 中 + 高 + 紧急 + + + + + + + + + + 取消 + + + {{ loading ? '创建中...' : '创建' }} + + + + + + + + + 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 @@ - - - - {{ column.title }} + + + + {{ column.name || column.title }} - - {{ column.tasks.length }} + + {{ column.tasks?.length || 0 }} - - - - + - 暂无任务 - - + + + + 暂无任务 + + + + + + + + 添加任务 + @@ -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 @@ - {{ task.title }} + {{ task.title }} × @@ -21,22 +21,18 @@ {{ task.description }} - + {{ getPriorityText(task.priority) }} - - {{ formatDate(task.updatedAt) }} + + {{ formatDate(task.updated_at || task.created_at) }} @@ -56,23 +52,35 @@ defineProps() defineEmits<{ select: [task: Task] - delete: [taskId: string] + delete: [taskId: number] }>() -const getPriorityText = (priority: string) => { - const priorityMap = { - high: '高', - medium: '中', - low: '低' +const getPriorityText = (priority: number) => { + const priorityMap: Record = { + 1: '低', + 2: '中', + 3: '高', + 4: '紧急' } - return priorityMap[priority as keyof typeof priorityMap] || priority + return priorityMap[priority] || '未知' } -const formatDate = (date: Date) => { +const getPriorityClass = (priority: number) => { + const map: Record = { + 1: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200', + 2: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200', + 3: 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200', + 4: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200' + } + return map[priority] || 'bg-gray-100 text-gray-800' +} + +const formatDate = (dateStr: string) => { + if (!dateStr) return '' return new Intl.DateTimeFormat('zh-CN', { month: 'short', day: 'numeric' - }).format(new Date(date)) + }).format(new Date(dateStr)) } diff --git a/frontend/src/services/column-api.ts b/frontend/src/services/column-api.ts new file mode 100644 index 0000000..7d43e72 --- /dev/null +++ b/frontend/src/services/column-api.ts @@ -0,0 +1,54 @@ +import api from '@/lib/api' +import type { ApiResponse } from './board-api' + +export interface Column { + id: number + name: string + description: string + color: string + position: number + board_id: number + status: number + created_at: string + tasks?: any[] +} + +export interface CreateColumnRequest { + name: string + description?: string + color?: string + position?: number +} + +export class ColumnApiService { + // 获取看板的所有列 + async getColumns(boardId: number): Promise> { + try { + const response = await api.get(`/boards/${boardId}/columns`) + return { + data: response.data.data // 后端返回格式通常是 { data: [...] } + } + } catch (error: any) { + return { + msg: error.response?.data?.msg || error.message || '获取列列表失败', + data: [] + } + } + } + + // 创建列 + async createColumn(boardId: number, data: CreateColumnRequest): Promise> { + try { + const response = await api.post(`/boards/${boardId}/columns`, data) + return response.data + } catch (error: any) { + return { + msg: error.response?.data?.msg || error.message || '创建列失败', + data: undefined + } + } + } +} + +export const columnApiService = new ColumnApiService() + diff --git a/frontend/src/services/index.ts b/frontend/src/services/index.ts index d2c6d1c..3d61145 100644 --- a/frontend/src/services/index.ts +++ b/frontend/src/services/index.ts @@ -17,4 +17,5 @@ export { type MoveTaskRequest, type MoveTaskResponse } from './task-api' +export * from './column-api' export * from './team-api' diff --git a/frontend/src/services/task-api.ts b/frontend/src/services/task-api.ts index b6da4c7..df26b32 100644 --- a/frontend/src/services/task-api.ts +++ b/frontend/src/services/task-api.ts @@ -20,6 +20,16 @@ export interface MoveTaskResponse { message: string } +export interface CreateTaskRequest { + title: string + description: string + priority: number + column_id: number + project_id: number + status: number + position: number +} + // API 响应格式 export interface ApiResponse { msg: string @@ -35,7 +45,10 @@ export class TaskApiService { async getTaskDetail(taskId: string): Promise> { try { const response = await api.get(`/tasks/${taskId}`) - return response.data + return { + msg: 'success', + data: response.data // 直接返回 task 对象 + } } catch (error: any) { return { msg: error.response?.data?.msg || error.message || '获取任务详情失败', @@ -43,8 +56,24 @@ export class TaskApiService { success: false, data: {} as Task, message: error.response?.data?.msg || error.message - } + } as unknown as TaskDetailResponse + } + } + } + + /** + * 创建任务 + */ + async createTask(columnId: string, request: CreateTaskRequest): Promise> { + try { + const response = await api.post(`/columns/${columnId}/tasks`, request) + return { + msg: 'success', + data: response.data } + } catch (error: any) { + console.error('创建任务失败:', error) + throw new Error(error.response?.data?.error || '创建任务失败') } } diff --git a/frontend/src/stores/kanban.ts b/frontend/src/stores/kanban.ts index e0f8e09..efd093c 100644 --- a/frontend/src/stores/kanban.ts +++ b/frontend/src/stores/kanban.ts @@ -1,155 +1,132 @@ import { defineStore } from 'pinia' import { ref } from 'vue' -import { taskApiService } from '@/services' - -export interface Task { - id: string - title: string - description?: string - status: 'todo' | 'in-progress' | 'done' - priority: 'low' | 'medium' | 'high' - createdAt: Date - updatedAt: Date +import { taskApiService, boardApiService, columnApiService } from '@/services' +import type { Column as ApiColumn, Task as ApiTask } from '@/services/board-api' +import type { CreateColumnRequest } from '@/services/column-api' +import type { MoveTaskRequest, MoveTaskResponse, CreateTaskRequest } from '@/services/task-api' + +// 前端使用的 Task 类型 (基于 API 类型扩展或适配) +export interface Task extends ApiTask { + // 可以添加前端特有的字段 } -export interface Column { - id: string - title: string - status: Task['status'] +// 前端使用的 Column 类型 +export interface Column extends ApiColumn { tasks: Task[] } -// API 响应类型 +// 任务详情响应 export interface TaskDetailResponse { success: boolean data: Task message?: string } -export interface MoveTaskRequest { - newColumnId: number - newOrder: number -} +export const useKanbanStore = defineStore('kanban', () => { + const columns = ref([]) + const isLoading = ref(false) + const error = ref(null) + const currentBoardId = ref(null) + const currentProjectId = ref(null) + + // 获取看板详情(包含列和任务) + const fetchBoardDetail = async (boardId: string | number) => { + isLoading.value = true + error.value = null + currentBoardId.value = boardId + + try { + const boardDetail = await boardApiService.getKanbanDetail(boardId) + if (boardDetail) { + currentProjectId.value = boardDetail.project_id // 保存项目ID,创建任务时需要 + // 确保 tasks 数组存在 + columns.value = (boardDetail.columns || []).map(col => ({ + ...col, + tasks: col.tasks || [] + })) + } else { + columns.value = [] + currentProjectId.value = null + } + } catch (err: any) { + console.error('Fetch board detail failed:', err) + error.value = err.message || '获取看板详情失败' + columns.value = [] + currentProjectId.value = null + } finally { + isLoading.value = false + } + } -export interface MoveTaskResponse { - message: string -} + // 创建新列 + const createColumn = async (data: CreateColumnRequest) => { + if (!currentBoardId.value) return -export const useKanbanStore = defineStore('kanban', () => { - const columns = ref([ - { - id: 'todo', - title: '待办', - status: 'todo', - tasks: [ - { - id: '1', - title: '设计用户界面', - description: '创建看板界面的设计稿', - status: 'todo', - priority: 'high', - createdAt: new Date(), - updatedAt: new Date() - }, - { - id: '2', - title: '设置项目环境', - description: '配置开发环境和依赖', - status: 'todo', - priority: 'medium', - createdAt: new Date(), - updatedAt: new Date() - } - ] - }, - { - id: 'in-progress', - title: '进行中', - status: 'in-progress', - tasks: [ - { - id: '3', - title: '实现拖拽功能', - description: '添加任务拖拽排序功能', - status: 'in-progress', - priority: 'high', - createdAt: new Date(), - updatedAt: new Date() - } - ] - }, - { - id: 'done', - title: '已完成', - status: 'done', - tasks: [ - { - id: '4', - title: '项目初始化', - description: '创建Vue项目并配置基础依赖', - status: 'done', - priority: 'low', - createdAt: new Date(), - updatedAt: new Date() + try { + const response = await columnApiService.createColumn(Number(currentBoardId.value), data) + if (response) { + // 重新获取看板数据以保持同步(或者手动添加到 columns) + // 简单起见,如果返回了新列数据,手动添加 + const newColumn: Column = { + ...response as unknown as ApiColumn, // 类型转换 + tasks: [] } - ] + columns.value.push(newColumn) + } + } catch (err: any) { + console.error('Create column failed:', err) + throw err } - ]) + } - const addTask = (task: Omit) => { - const newTask: Task = { - ...task, - id: Date.now().toString(), - createdAt: new Date(), - updatedAt: new Date() - } - - const column = columns.value.find(col => col.status === task.status) + const addTask = (task: Task) => { + const column = columns.value.find(col => col.id === task.column_id) if (column) { - column.tasks.push(newTask) + column.tasks.push(task) } } - const updateTask = (taskId: string, updates: Partial) => { - for (const column of columns.value) { - const task = column.tasks.find(t => t.id === taskId) - if (task) { - Object.assign(task, updates, { updatedAt: new Date() }) - break - } + // 创建任务 + const createTask = async (columnId: number, data: { title: string; description: string; priority: number }) => { + if (!currentProjectId.value) { + throw new Error('无法创建任务:缺少项目ID') } - } - const moveTask = (taskId: string, newStatus: Task['status']) => { - let taskToMove: Task | null = null - let sourceColumn: Column | null = null + const request: CreateTaskRequest = { + title: data.title, + description: data.description, + priority: data.priority, + column_id: columnId, + project_id: currentProjectId.value, + status: 1, // 默认待办 + position: 0 // 默认位置 + } - // 找到要移动的任务 - for (const column of columns.value) { - const taskIndex = column.tasks.findIndex(t => t.id === taskId) - if (taskIndex !== -1) { - taskToMove = column.tasks[taskIndex] as Task - sourceColumn = column - column.tasks.splice(taskIndex, 1) - break + try { + const response = await taskApiService.createTask(columnId.toString(), request) + if (response.data) { + // 添加到本地状态 + const newTask = response.data as unknown as Task + addTask(newTask) } + } catch (err: any) { + console.error('Create task failed:', err) + throw err } + } - if (taskToMove && sourceColumn) { - // 更新任务状态 - taskToMove.status = newStatus - taskToMove.updatedAt = new Date() - - // 添加到目标列 - const targetColumn = columns.value.find(col => col.status === newStatus) - if (targetColumn) { - targetColumn.tasks.push(taskToMove) + const updateTask = (taskId: number, updates: Partial) => { + for (const column of columns.value) { + const task = column.tasks.find(t => t.id === taskId) + if (task) { + Object.assign(task, updates, { updated_at: new Date().toISOString() }) + break } } } // 拖拽移动任务(乐观更新) - const moveTaskWithDrag = async (taskId: string, newColumnId: string, newOrder: number) => { + const moveTaskWithDrag = async (taskId: number, newColumnId: number, newOrder: number) => { // 保存原始状态用于回滚 const originalColumns = JSON.parse(JSON.stringify(columns.value)) @@ -183,9 +160,9 @@ export const useKanbanStore = defineStore('kanban', () => { throw new Error('目标列不存在') } - // 更新任务状态 - taskToMove.status = targetColumn.status - taskToMove.updatedAt = new Date() + // 更新任务列ID + taskToMove.column_id = newColumnId + // taskToMove.updated_at = new Date().toISOString() // 插入到目标列的指定位置 if (newOrder >= targetColumn.tasks.length) { @@ -195,29 +172,29 @@ export const useKanbanStore = defineStore('kanban', () => { } // 2. 调用API - // 将 columnId 转换为 number 类型(后端需要) - const columnIdNumber = parseInt(newColumnId, 10) - if (isNaN(columnIdNumber)) { - throw new Error('无效的列ID') - } - - await moveTaskAPI(taskId, { newColumnId: columnIdNumber, newOrder }) + // 注意:这里不使用 await,让 API 在后台异步执行,从而避免 UI 阻塞 + moveTaskAPI(taskId.toString(), { newColumnId: newColumnId, newOrder }) + .catch(error => { + // 只有在 API 失败时才回滚 + columns.value = originalColumns + console.error('Move task failed:', error) + // 可以添加一个全局提示,告诉用户同步失败 + }) } catch (error) { - // 3. 如果API调用失败,回滚到原始状态 + // 如果是同步逻辑出错(比如找不到列),立即回滚 columns.value = originalColumns - console.error('Move task failed:', error) + console.error('Move task local update failed:', error) throw error } } // 调用真实API移动任务 const moveTaskAPI = async (taskId: string, request: MoveTaskRequest): Promise => { - // 直接调用 API,如果失败会抛出异常 return await taskApiService.moveTask(taskId, request) } - const deleteTask = (taskId: string) => { + const deleteTask = (taskId: number) => { for (const column of columns.value) { const taskIndex = column.tasks.findIndex(t => t.id === taskId) if (taskIndex !== -1) { @@ -227,17 +204,17 @@ export const useKanbanStore = defineStore('kanban', () => { } } - // 调用真实API获取任务详情 const fetchTaskDetail = async (taskId: string): Promise => { const response = await taskApiService.getTaskDetail(taskId) - // 适配 API 响应格式 if (response.data) { - return response.data + return { + success: true, + data: response.data as unknown as Task + } } - // 如果API调用失败,返回失败响应 return { success: false, data: {} as Task, @@ -247,9 +224,14 @@ export const useKanbanStore = defineStore('kanban', () => { return { columns, + isLoading, + error, + currentBoardId, + fetchBoardDetail, + createColumn, + createTask, addTask, updateTask, - moveTask, moveTaskWithDrag, deleteTask, fetchTaskDetail diff --git a/frontend/src/views/dashboard/KanbanView.vue b/frontend/src/views/dashboard/KanbanView.vue index 746605e..ee8485d 100644 --- a/frontend/src/views/dashboard/KanbanView.vue +++ b/frontend/src/views/dashboard/KanbanView.vue @@ -1,45 +1,173 @@ - - - - 项目看板 - - 返回首页 - + + + + + {{ boardName }} + {{ error }} + + + + {{ isLoading ? '加载中...' : '刷新' }} + + + 返回 + + + - + + + + + + + + + 添加新列 + + + + + + + +
{{ error }}