diff --git a/frontend/src/config/api.ts b/frontend/src/config/api.ts
index fb3146d..b024383 100644
--- a/frontend/src/config/api.ts
+++ b/frontend/src/config/api.ts
@@ -29,6 +29,16 @@ export const API_CONFIG = {
path: '/dashboard/kanban',
method: 'DELETE'
},
+
+ // 任务相关
+ TASK_DETAIL: {
+ path: '/tasks',
+ method: 'GET'
+ },
+ TASK_MOVE: {
+ path: '/tasks',
+ method: 'POST'
+ },
}
} as const
diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts
index 707b662..c9a31f2 100644
--- a/frontend/src/router/index.ts
+++ b/frontend/src/router/index.ts
@@ -31,6 +31,12 @@ const routes: RouteRecordRaw[] = [
component: () => import('@/views/dashboard/KanbanView.vue'),
meta: { requiresAuth: true }
},
+ {
+ path: '/tasks/:taskId',
+ name: 'TaskDetail',
+ component: () => import('@/views/dashboard/TaskDetailView.vue'),
+ props: true
+ },
{
path: '/profile',
name: 'Profile',
diff --git a/frontend/src/services/board-api.ts b/frontend/src/services/board-api.ts
index a86cd50..6a677ed 100644
--- a/frontend/src/services/board-api.ts
+++ b/frontend/src/services/board-api.ts
@@ -1,47 +1,145 @@
-import { BaseApiService, type ApiResponse } from './base-api'
-import { getEndpointUrl, getEndpointMethod } from '@/config/api'
+import api from '@/lib/api'
import type { Board } from '@/stores/board'
-// 看板 API 响应接口
+// API 响应格式
+export interface ApiResponse
{
+ msg?: string
+ data?: T
+}
+
+// 看板列表响应接口
export interface KanbanListResponse extends Array {}
+// 用户信息接口
+export interface User {
+ id: number
+ username: string
+ email: string
+}
+
+// 任务接口(后端返回格式)
+export interface Task {
+ id: number
+ title: string
+ description: string
+ priority: number
+ status: number
+ position: number
+ column_id: number
+ creator_id: number
+ assignee_id: number
+ project_id: number
+ created_at: string
+ assignee?: User
+ creator?: User
+}
+
+// 列接口(后端返回格式)
+export interface Column {
+ id: number
+ name: string
+ description: string
+ color: string
+ position: number
+ board_id: number
+ status: number
+ created_at: string
+ tasks: Task[]
+}
+
+// 看板详情接口(后端返回格式 - 包含嵌套的列和任务)
+export interface BoardDetail {
+ id: number
+ name: string
+ description: string
+ color: string
+ status: number
+ project_id: number
+ owner_id: number
+ position: number
+ created_at: string
+ updated_at: string
+ owner: User
+ columns: Column[]
+}
+
// 看板 API 服务
-export class BoardApiService extends BaseApiService {
+export class BoardApiService {
// 获取看板列表
async getKanbanList(): Promise> {
- return this.request(getEndpointUrl('KANBAN_LIST'), {
- method: getEndpointMethod('KANBAN_LIST')
- })
+ try {
+ const response = await api.get('/boards')
+ return response.data
+ } catch (error: any) {
+ return {
+ msg: error.response?.data?.msg || error.message || '获取看板列表失败',
+ data: []
+ }
+ }
}
- // 获取单个看板详情
- async getKanbanDetail(boardId: string): Promise> {
- return this.request(`${getEndpointUrl('KANBAN_DETAIL')}/${boardId}`, {
- method: getEndpointMethod('KANBAN_DETAIL')
- })
+ /**
+ * 获取单个看板详情(包含嵌套的列和任务)
+ * 需要认证: Bearer Token (JWT)
+ *
+ * @param boardId - 看板ID
+ * @returns 完整的看板数据,包括 columns 和 tasks
+ */
+ async getKanbanDetail(boardId: string | number): Promise {
+ try {
+ const response = await api.get(`/boards/${boardId}`)
+ return response.data
+ } catch (error: any) {
+ console.error('获取看板详情失败:', error.response?.data || error.message)
+
+ // 处理不同的错误状态
+ if (error.response?.status === 400) {
+ throw new Error('无效的看板ID')
+ } else if (error.response?.status === 404) {
+ throw new Error('看板不存在')
+ } else {
+ throw new Error(error.response?.data?.msg || error.message || '获取看板详情失败')
+ }
+ }
}
// 创建看板
async createKanban(board: Omit): Promise> {
- return this.request(getEndpointUrl('KANBAN_CREATE'), {
- method: getEndpointMethod('KANBAN_CREATE'),
- body: JSON.stringify(board),
- })
+ try {
+ const response = await api.post('/boards', board)
+ return response.data
+ } catch (error: any) {
+ return {
+ msg: error.response?.data?.msg || error.message || '创建看板失败',
+ data: undefined
+ }
+ }
}
// 更新看板
async updateKanban(boardId: string, updates: Partial): Promise> {
- return this.request(`${getEndpointUrl('KANBAN_UPDATE')}/${boardId}`, {
- method: getEndpointMethod('KANBAN_UPDATE'),
- body: JSON.stringify(updates),
- })
+ try {
+ const response = await api.put(`/boards/${boardId}`, updates)
+ return response.data
+ } catch (error: any) {
+ return {
+ msg: error.response?.data?.msg || error.message || '更新看板失败',
+ data: undefined
+ }
+ }
}
// 删除看板
async deleteKanban(boardId: string): Promise> {
- return this.request(`${getEndpointUrl('KANBAN_DELETE')}/${boardId}`, {
- method: getEndpointMethod('KANBAN_DELETE'),
- })
+ try {
+ const response = await api.delete(`/boards/${boardId}`)
+ return response.data
+ } catch (error: any) {
+ return {
+ msg: error.response?.data?.msg || error.message || '删除看板失败',
+ data: undefined
+ }
+ }
}
}
diff --git a/frontend/src/services/index.ts b/frontend/src/services/index.ts
index b3e53d7..d0285cd 100644
--- a/frontend/src/services/index.ts
+++ b/frontend/src/services/index.ts
@@ -1,3 +1,19 @@
// API 服务统一导出
export { BaseApiService, type ApiResponse } from './base-api'
-export { BoardApiService, boardApiService, type KanbanListResponse } from './board-api'
\ No newline at end of file
+export {
+ BoardApiService,
+ boardApiService,
+ type KanbanListResponse,
+ type BoardDetail,
+ type Column,
+ type Task as BoardTask,
+ type User
+} from './board-api'
+export {
+ TaskApiService,
+ taskApiService,
+ type TaskDetailRequest,
+ type TaskDetailResponse,
+ type MoveTaskRequest,
+ type MoveTaskResponse
+} from './task-api'
\ No newline at end of file
diff --git a/frontend/src/services/task-api.ts b/frontend/src/services/task-api.ts
new file mode 100644
index 0000000..b6da4c7
--- /dev/null
+++ b/frontend/src/services/task-api.ts
@@ -0,0 +1,82 @@
+import api from '@/lib/api'
+import type { Task } from '@/stores/kanban'
+
+export interface TaskDetailRequest {
+ taskId: string
+}
+
+export interface TaskDetailResponse {
+ success: boolean
+ data: Task
+ message?: string
+}
+
+export interface MoveTaskRequest {
+ newColumnId: number
+ newOrder: number
+}
+
+export interface MoveTaskResponse {
+ message: string
+}
+
+// API 响应格式
+export interface ApiResponse {
+ msg: string
+ data: T
+}
+
+export class TaskApiService {
+
+ /**
+ * 获取任务详情
+ * 需要认证:Bearer Token (JWT)
+ */
+ async getTaskDetail(taskId: string): Promise> {
+ try {
+ const response = await api.get(`/tasks/${taskId}`)
+ return response.data
+ } catch (error: any) {
+ return {
+ msg: error.response?.data?.msg || error.message || '获取任务详情失败',
+ data: {
+ success: false,
+ data: {} as Task,
+ message: error.response?.data?.msg || error.message
+ }
+ }
+ }
+ }
+
+ /**
+ * 移动任务(拖拽排序)
+ * 需要认证:Bearer Token (JWT)
+ *
+ * 支持:
+ * - 跨列移动:将任务从一个列移动到另一个列
+ * - 同列内移动:在同一列内调整任务顺序
+ *
+ * @param taskId - 任务ID
+ * @param request - 包含 newColumnId 和 newOrder
+ * @returns 移动成功的消息
+ */
+ async moveTask(taskId: string | number, request: MoveTaskRequest): Promise {
+ try {
+ const response = await api.patch(`/tasks/${taskId}/move`, request)
+ return response.data
+ } catch (error: any) {
+ console.error('移动任务失败:', error.response?.data || error.message)
+
+ // 处理不同的错误状态
+ if (error.response?.status === 400) {
+ throw new Error('请求参数错误(缺少 newColumnId 或 newOrder)')
+ } else if (error.response?.status === 404) {
+ throw new Error('任务不存在')
+ } else {
+ throw new Error(error.response?.data?.message || error.message || '移动任务失败')
+ }
+ }
+ }
+}
+
+export const taskApiService = new TaskApiService()
diff --git a/frontend/src/stores/kanban.ts b/frontend/src/stores/kanban.ts
index a1e7f7a..e0f8e09 100644
--- a/frontend/src/stores/kanban.ts
+++ b/frontend/src/stores/kanban.ts
@@ -1,5 +1,6 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
+import { taskApiService } from '@/services'
export interface Task {
id: string
@@ -18,6 +19,22 @@ export interface Column {
tasks: Task[]
}
+// API 响应类型
+export interface TaskDetailResponse {
+ success: boolean
+ data: Task
+ message?: string
+}
+
+export interface MoveTaskRequest {
+ newColumnId: number
+ newOrder: number
+}
+
+export interface MoveTaskResponse {
+ message: string
+}
+
export const useKanbanStore = defineStore('kanban', () => {
const columns = ref([
{
@@ -131,6 +148,75 @@ export const useKanbanStore = defineStore('kanban', () => {
}
}
+ // 拖拽移动任务(乐观更新)
+ const moveTaskWithDrag = async (taskId: string, newColumnId: string, newOrder: number) => {
+ // 保存原始状态用于回滚
+ const originalColumns = JSON.parse(JSON.stringify(columns.value))
+
+ try {
+ // 1. 乐观更新:立即更新本地状态
+ let taskToMove: Task | null = null
+ let sourceColumnIndex = -1
+ let sourceTaskIndex = -1
+
+ // 找到要移动的任务
+ for (let i = 0; i < columns.value.length; i++) {
+ const taskIndex = columns.value[i].tasks.findIndex(t => t.id === taskId)
+ if (taskIndex !== -1) {
+ taskToMove = columns.value[i].tasks[taskIndex]
+ sourceColumnIndex = i
+ sourceTaskIndex = taskIndex
+ break
+ }
+ }
+
+ if (!taskToMove) {
+ throw new Error('任务不存在')
+ }
+
+ // 从源列移除任务
+ columns.value[sourceColumnIndex].tasks.splice(sourceTaskIndex, 1)
+
+ // 找到目标列
+ const targetColumn = columns.value.find(col => col.id === newColumnId)
+ if (!targetColumn) {
+ throw new Error('目标列不存在')
+ }
+
+ // 更新任务状态
+ taskToMove.status = targetColumn.status
+ taskToMove.updatedAt = new Date()
+
+ // 插入到目标列的指定位置
+ if (newOrder >= targetColumn.tasks.length) {
+ targetColumn.tasks.push(taskToMove)
+ } else {
+ targetColumn.tasks.splice(newOrder, 0, taskToMove)
+ }
+
+ // 2. 调用API
+ // 将 columnId 转换为 number 类型(后端需要)
+ const columnIdNumber = parseInt(newColumnId, 10)
+ if (isNaN(columnIdNumber)) {
+ throw new Error('无效的列ID')
+ }
+
+ await moveTaskAPI(taskId, { newColumnId: columnIdNumber, newOrder })
+
+ } catch (error) {
+ // 3. 如果API调用失败,回滚到原始状态
+ columns.value = originalColumns
+ console.error('Move task failed:', error)
+ throw error
+ }
+ }
+
+ // 调用真实API移动任务
+ const moveTaskAPI = async (taskId: string, request: MoveTaskRequest): Promise => {
+ // 直接调用 API,如果失败会抛出异常
+ return await taskApiService.moveTask(taskId, request)
+ }
+
const deleteTask = (taskId: string) => {
for (const column of columns.value) {
const taskIndex = column.tasks.findIndex(t => t.id === taskId)
@@ -141,11 +227,31 @@ 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
+ }
+
+ // 如果API调用失败,返回失败响应
+ return {
+ success: false,
+ data: {} as Task,
+ message: response.msg || '获取任务详情失败'
+ }
+ }
+
return {
columns,
addTask,
updateTask,
moveTask,
- deleteTask
+ moveTaskWithDrag,
+ deleteTask,
+ fetchTaskDetail
}
})
diff --git a/frontend/src/views/dashboard/KanbanView.vue b/frontend/src/views/dashboard/KanbanView.vue
index 9ff2196..746605e 100644
--- a/frontend/src/views/dashboard/KanbanView.vue
+++ b/frontend/src/views/dashboard/KanbanView.vue
@@ -35,8 +35,8 @@ const goHome = () => {
}
const selectTask = (task: any) => {
- // 这里可以添加任务详情编辑功能
- console.log('Selected task:', task)
+ // 点击任务卡片跳转到任务详情页
+ router.push(`/tasks/${task.id}`)
}
const deleteTask = (taskId: string) => {
diff --git a/frontend/src/views/dashboard/TaskDetailView.vue b/frontend/src/views/dashboard/TaskDetailView.vue
new file mode 100644
index 0000000..fcf23ad
--- /dev/null
+++ b/frontend/src/views/dashboard/TaskDetailView.vue
@@ -0,0 +1,240 @@
+
+
+
+
+
+
+
+
+
{{ error }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ task.title }}
+
+
+ {{ getStatusText(task.status) }}
+
+
+ {{ getPriorityText(task.priority) }}
+
+
+
+
+
+
+
+
任务描述
+
+
+ {{ task.description }}
+
+
+ 暂无描述
+
+
+
+
+
+
+
+
基本信息
+
+
+ 任务ID:
+ {{ task.id }}
+
+
+ 状态:
+
+ {{ getStatusText(task.status) }}
+
+
+
+ 优先级:
+
+ {{ getPriorityText(task.priority) }}
+
+
+
+
+
+
+
时间信息
+
+
+ 创建时间:
+ {{ formatDateTime(task.createdAt) }}
+
+
+ 最后更新:
+ {{ formatDateTime(task.updatedAt) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+