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
5 changes: 3 additions & 2 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,18 @@
"pinia": "^3.0.3",
"tailwind-merge": "^3.3.1",
"vue": "^3.5.21",
"vue-draggable-plus": "^0.6.0",
"vue-router": "^4.5.1"
},
"devDependencies": {
"@types/node": "^24.5.2",
"@vitejs/plugin-vue": "^6.0.1",
"@vitejs/plugin-vue": "^5.1.4",
"@vue/tsconfig": "^0.8.1",
"autoprefixer": "^10.4.21",
"postcss": "^8.5.6",
"tailwindcss": "^3.4.17",
"typescript": "~5.8.3",
"vite": "^7.1.7",
"vite": "^5.4.10",
"vue-tsc": "^3.0.7"
}
}
966 changes: 468 additions & 498 deletions frontend/pnpm-lock.yaml

Large diffs are not rendered by default.

93 changes: 87 additions & 6 deletions frontend/src/components/features/KanbanColumn.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<template>
<div class="bg-muted/50 rounded-lg p-4 min-h-[500px]">
<div class="bg-muted/50 rounded-lg p-4 min-h-[500px]" :data-column-id="column.id">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-semibold">
{{ column.title }}
Expand All @@ -9,7 +9,16 @@
</span>
</div>

<div class="space-y-3">
<VueDraggable
v-model="column.tasks"
:group="{ name: 'tasks', pull: true, put: true }"
:animation="200"
ghost-class="ghost-task"
chosen-class="chosen-task"
drag-class="drag-task"
class="space-y-3 min-h-[200px]"
@end="onDragEnd"
>
<TaskCard
v-for="task in column.tasks"
:key="task.id"
Expand All @@ -20,26 +29,98 @@

<div
v-if="column.tasks.length === 0"
class="text-center text-muted-foreground py-8"
class="text-center text-muted-foreground py-8 pointer-events-none"
>
暂无任务
</div>
</div>
</VueDraggable>
</div>
</template>

<script setup lang="ts">
import { VueDraggable } from 'vue-draggable-plus'
import type { Column } from '@/stores/kanban'
import { useKanbanStore } from '@/stores/kanban'
import TaskCard from '@/components/features/TaskCard.vue'

interface Props {
column: Column
}

defineProps<Props>()
const props = defineProps<Props>()

defineEmits<{
const emit = defineEmits<{
'select-task': [task: any]
'delete-task': [taskId: string]
}>()

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) {
console.error('未找到 taskId')
return
}

// 获取目标列ID
const targetColumnElement = to.closest('[data-column-id]')
const targetColumnId = targetColumnElement?.getAttribute('data-column-id')
console.log('目标列 - targetColumnId:', targetColumnId)
if (!targetColumnId) {
console.error('未找到 targetColumnId')
return
}

// 如果是同一列内的重新排序,或者跨列移动
if (from !== to || newIndex !== oldIndex) {
console.log('开始调用API - taskId:', taskId, 'targetColumnId:', targetColumnId, 'newIndex:', newIndex)
try {
await kanbanStore.moveTaskWithDrag(taskId, targetColumnId, newIndex)
console.log('API调用成功')
} catch (error) {
console.error('拖拽移动任务失败:', error)
// 这里可以添加错误提示
}
} else {
console.log('未触发移动 - 位置未改变')
}
}
</script>

<style scoped>
/* 拖拽样式 */
:deep(.ghost-task) {
opacity: 0.5;
background: #f0f0f0;
border: 2px dashed #ccc;
}

:deep(.chosen-task) {
transform: rotate(5deg);
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;
}
</style>
3 changes: 2 additions & 1 deletion frontend/src/components/features/TaskCard.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<template>
<Card
class="p-4 cursor-move hover:shadow-md transition-shadow"
class="p-4 cursor-pointer hover:shadow-md transition-all hover:scale-[1.02]"
:data-task-id="task.id"
@click="$emit('select', task)"
>
<div class="space-y-2">
Expand Down
10 changes: 10 additions & 0 deletions frontend/src/config/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
6 changes: 6 additions & 0 deletions frontend/src/router/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
144 changes: 121 additions & 23 deletions frontend/src/services/board-api.ts
Original file line number Diff line number Diff line change
@@ -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<T> {
msg?: string
data?: T
}

// 看板列表响应接口
export interface KanbanListResponse extends Array<Board> {}

// 用户信息接口
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<ApiResponse<KanbanListResponse>> {
return this.request<KanbanListResponse>(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<ApiResponse<Board>> {
return this.request<Board>(`${getEndpointUrl('KANBAN_DETAIL')}/${boardId}`, {
method: getEndpointMethod('KANBAN_DETAIL')
})
/**
* 获取单个看板详情(包含嵌套的列和任务)
* 需要认证: Bearer Token (JWT)
*
* @param boardId - 看板ID
* @returns 完整的看板数据,包括 columns 和 tasks
*/
async getKanbanDetail(boardId: string | number): Promise<BoardDetail | null> {
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<Board, 'id' | 'createdAt' | 'updatedAt'>): Promise<ApiResponse<Board>> {
return this.request<Board>(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<Board>): Promise<ApiResponse<Board>> {
return this.request<Board>(`${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<ApiResponse<void>> {
return this.request<void>(`${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
}
}
}
}

Expand Down
18 changes: 17 additions & 1 deletion frontend/src/services/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
// API 服务统一导出
export { BaseApiService, type ApiResponse } from './base-api'
export { BoardApiService, boardApiService, type KanbanListResponse } from './board-api'
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'
Loading