本文目录导读:

我来为您详细介绍PHP项目看板与任务跟踪系统的设计与实现。
系统架构设计
1 核心功能模块
// 项目看板核心结构
interface KanbanBoard {
// 看板列
public function getColumns($projectId);
public function createColumn($projectId, $data);
public function updateColumn($columnId, $data);
public function deleteColumn($columnId);
// 任务卡片
public function getCards($columnId);
public function createCard($columnId, $data);
public function updateCard($cardId, $data);
public function moveCard($cardId, $targetColumnId, $position);
public function deleteCard($cardId);
// 任务跟踪
public function getTaskTimeline($taskId);
public function logTaskActivity($taskId, $action, $data);
}
2 数据库表结构
-- 项目表
CREATE TABLE projects (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
description TEXT,
owner_id INT NOT NULL,
status ENUM('active', 'archived', 'completed') DEFAULT 'active',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (owner_id) REFERENCES users(id)
);
-- 看板列表
CREATE TABLE board_columns (
id INT PRIMARY KEY AUTO_INCREMENT,
project_id INT NOT NULL,
name VARCHAR(100) NOT NULL,
position INT DEFAULT 0,
color VARCHAR(7) DEFAULT '#3498db',
max_tasks INT DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
);
-- 任务卡片表
CREATE TABLE task_cards (
id INT PRIMARY KEY AUTO_INCREMENT,
column_id INT NOT NULL,VARCHAR(255) NOT NULL,
description TEXT,
priority ENUM('low', 'medium', 'high', 'urgent') DEFAULT 'medium',
due_date DATE,
assignee_id INT,
position INT DEFAULT 0,
labels JSON,
checklist JSON,
created_by INT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (column_id) REFERENCES board_columns(id) ON DELETE CASCADE,
FOREIGN KEY (assignee_id) REFERENCES users(id),
FOREIGN KEY (created_by) REFERENCES users(id)
);
-- 任务活动日志表
CREATE TABLE task_activities (
id INT PRIMARY KEY AUTO_INCREMENT,
task_id INT NOT NULL,
user_id INT NOT NULL,
action_type ENUM('created', 'updated', 'moved', 'commented', 'assigned', 'status_changed'),
description TEXT,
old_value TEXT,
new_value TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (task_id) REFERENCES task_cards(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id)
);
核心功能实现
1 看板管理类
<?php
class KanbanManager {
private $db;
private $cache;
public function __construct(PDO $db, Cache $cache) {
$this->db = $db;
$this->cache = $cache;
}
/**
* 获取项目看板数据
*/
public function getBoardData($projectId) {
$cacheKey = "board:{$projectId}";
// 尝试从缓存获取
if ($data = $this->cache->get($cacheKey)) {
return json_decode($data, true);
}
// 获取所有列
$columns = $this->getColumns($projectId);
// 获取每个列的任务
foreach ($columns as &$column) {
$column['cards'] = $this->getCardsWithDetails($column['id']);
}
// 缓存数据
$this->cache->set($cacheKey, json_encode([
'columns' => $columns
]), 300); // 5分钟缓存
return ['columns' => $columns];
}
/**
* 移动任务到新位置
*/
public function moveTask($taskId, $targetColumnId, $position, $userId) {
$this->db->beginTransaction();
try {
// 获取原位置
$task = $this->getTask($taskId);
$oldColumnId = $task['column_id'];
$oldPosition = $task['position'];
// 更新任务位置
$stmt = $this->db->prepare(
"UPDATE task_cards SET column_id = ?, position = ? WHERE id = ?"
);
$stmt->execute([$targetColumnId, $position, $taskId]);
// 重新排序
$this->reorderCards($targetColumnId, $position, $taskId);
// 记录活动
$this->logActivity($taskId, $userId, 'moved', [
'from' => $oldColumnId,
'to' => $targetColumnId,
'position' => $position
]);
$this->db->commit();
// 清除缓存
$this->clearBoardCache($task['project_id']);
return true;
} catch (Exception $e) {
$this->db->rollBack();
throw $e;
}
}
/**
* 创建新任务
*/
public function createTask($data, $userId) {
$this->db->beginTransaction();
try {
// 获取下一个位置
$position = $this->getNextPosition($data['column_id']);
$stmt = $this->db->prepare(
"INSERT INTO task_cards (column_id, title, description, priority,
due_date, assignee_id, position, created_by)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
);
$stmt->execute([
$data['column_id'],
$data['title'],
$data['description'] ?? '',
$data['priority'] ?? 'medium',
$data['due_date'] ?? null,
$data['assignee_id'] ?? null,
$position,
$userId
]);
$taskId = $this->db->lastInsertId();
// 记录活动
$this->logActivity($taskId, $userId, 'created', [
'title' => $data['title']
]);
$this->db->commit();
return $taskId;
} catch (Exception $e) {
$this->db->rollBack();
throw $e;
}
}
private function getNextPosition($columnId) {
$stmt = $this->db->prepare(
"SELECT COALESCE(MAX(position), -1) + 1 FROM task_cards WHERE column_id = ?"
);
$stmt->execute([$columnId]);
return (int)$stmt->fetchColumn();
}
private function reorderCards($columnId, $startPosition, $excludeId) {
$stmt = $this->db->prepare(
"UPDATE task_cards SET position = position + 1
WHERE column_id = ? AND position >= ? AND id != ?
ORDER BY position DESC"
);
$stmt->execute([$columnId, $startPosition, $excludeId]);
}
private function logActivity($taskId, $userId, $action, $details) {
$stmt = $this->db->prepare(
"INSERT INTO task_activities (task_id, user_id, action_type, description)
VALUES (?, ?, ?, ?)"
);
$stmt->execute([
$taskId,
$userId,
$action,
json_encode($details)
]);
}
private function clearBoardCache($projectId) {
$this->cache->delete("board:{$projectId}");
}
}
2 任务跟踪功能
<?php
class TaskTracker {
private $db;
/**
* 获取任务时间线
*/
public function getTaskTimeline($taskId) {
$stmt = $this->db->prepare(
"SELECT a.*, u.username, u.avatar
FROM task_activities a
LEFT JOIN users u ON a.user_id = u.id
WHERE a.task_id = ?
ORDER BY a.created_at DESC
LIMIT 50"
);
$stmt->execute([$taskId]);
$activities = $stmt->fetchAll(PDO::FETCH_ASSOC);
// 格式化活动数据
return array_map(function($activity) {
$activity['details'] = json_decode($activity['description'], true);
$activity['time_ago'] = $this->timeAgo($activity['created_at']);
return $activity;
}, $activities);
}
/**
* 添加任务评论
*/
public function addComment($taskId, $userId, $comment) {
$stmt = $this->db->prepare(
"INSERT INTO task_activities (task_id, user_id, action_type, description)
VALUES (?, ?, 'commented', ?)"
);
return $stmt->execute([$taskId, $userId, $comment]);
}
/**
* 获取任务统计
*/
public function getTaskStats($projectId) {
$stats = [];
// 各状态任务数量
$stmt = $this->db->prepare(
"SELECT bc.name, bc.color, COUNT(tc.id) as count
FROM board_columns bc
LEFT JOIN task_cards tc ON bc.id = tc.column_id
WHERE bc.project_id = ?
GROUP BY bc.id, bc.name, bc.color, bc.position
ORDER BY bc.position"
);
$stmt->execute([$projectId]);
$stats['column_distribution'] = $stmt->fetchAll(PDO::FETCH_ASSOC);
// 任务优先级分布
$stmt = $this->db->prepare(
"SELECT priority, COUNT(*) as count
FROM task_cards tc
JOIN board_columns bc ON tc.column_id = bc.id
WHERE bc.project_id = ?
GROUP BY priority"
);
$stmt->execute([$projectId]);
$stats['priority_distribution'] = $stmt->fetchAll(PDO::FETCH_ASSOC);
// 逾期任务
$stmt = $this->db->prepare(
"SELECT COUNT(*) as overdue
FROM task_cards tc
JOIN board_columns bc ON tc.column_id = bc.id
WHERE bc.project_id = ?
AND tc.due_date < CURDATE()
AND bc.name NOT IN ('已完成', '已关闭')"
);
$stmt->execute([$projectId]);
$stats['overdue_tasks'] = $stmt->fetchColumn();
return $stats;
}
private function timeAgo($timestamp) {
$time = strtotime($timestamp);
$diff = time() - $time;
if ($diff < 60) return '刚刚';
if ($diff < 3600) return floor($diff / 60) . '分钟前';
if ($diff < 86400) return floor($diff / 3600) . '小时前';
if ($diff < 2592000) return floor($diff / 86400) . '天前';
return date('Y-m-d', $time);
}
}
前端实现
1 Vue.js 看板组件
<template>
<div class="kanban-board">
<div class="board-header">
<h2>{{ project.name }}</h2>
<div class="board-actions">
<button @click="showSettings = true">设置</button>
<button @click="addColumn">添加列</button>
</div>
</div>
<!-- 看板列 -->
<div class="board-columns">
<div
v-for="column in columns"
:key="column.id"
class="board-column"
@drop="onDrop($event, column)"
@dragover.prevent
>
<div class="column-header">
<h3>{{ column.name }}</h3>
<span class="card-count">{{ column.cards.length }}</span>
<button @click="showAddCard(column.id)" class="add-card-btn">
+ 添加任务
</button>
</div>
<!-- 任务卡片 -->
<draggable
v-model="column.cards"
group="tasks"
@start="drag = true"
@end="onDragEnd"
item-key="id"
>
<template #item="{ element: card }">
<div class="task-card" @click="openCard(card)">
<div class="card-priority" :class="card.priority"></div>
<h4>{{ card.title }}</h4>
<div class="card-meta">
<span v-if="card.due_date" class="due-date">
{{ formatDate(card.due_date) }}
</span>
<span v-if="card.assignee" class="assignee">
<img :src="card.assignee.avatar" :alt="card.assignee.name">
</span>
</div>
</div>
</template>
</draggable>
</div>
</div>
</div>
</template>
<script>
import draggable from 'vuedraggable'
import { ref, onMounted } from 'vue'
import api from '@/api'
export default {
components: {
draggable
},
props: {
projectId: Number
},
setup(props) {
const columns = ref([])
const project = ref({})
// 加载看板数据
const loadBoard = async () => {
const response = await api.getBoard(props.projectId)
columns.value = response.columns
project.value = response.project
}
// 拖拽结束处理
const onDragEnd = async (event) => {
const cardId = event.item.id
const targetColumnId = columns.value[event.to.columnIndex].id
const position = event.newIndex
await api.moveTask(cardId, targetColumnId, position)
}
// 打开任务详情
const openCard = (card) => {
// 打开模态框显示任务详情
}
onMounted(() => {
loadBoard()
})
return {
columns,
project,
onDragEnd,
openCard
}
}
}
</script>
<style scoped>
.kanban-board {
display: flex;
flex-direction: column;
height: 100vh;
background: #f4f5f7;
}
.board-header {
padding: 20px;
background: white;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.board-columns {
display: flex;
gap: 20px;
padding: 20px;
overflow-x: auto;
flex: 1;
}
.board-column {
min-width: 300px;
max-width: 400px;
background: #ebecf0;
border-radius: 8px;
padding: 12px;
}
.column-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 15px;
}
.task-card {
background: white;
border-radius: 6px;
padding: 12px;
margin-bottom: 8px;
cursor: pointer;
transition: box-shadow 0.2s;
}
.task-card:hover {
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
}
</style>
API 接口设计
// routes/api.php
// 看板接口
Route::prefix('kanban')->group(function () {
Route::get('/boards/{projectId}', 'KanbanController@getBoard');
Route::post('/columns', 'KanbanController@createColumn');
Route::put('/columns/{columnId}', 'KanbanController@updateColumn');
Route::delete('/columns/{columnId}', 'KanbanController@deleteColumn');
Route::post('/cards', 'KanbanController@createCard');
Route::put('/cards/{cardId}', 'KanbanController@updateCard');
Route::post('/cards/{cardId}/move', 'KanbanController@moveCard');
Route::delete('/cards/{cardId}', 'KanbanController@deleteCard');
Route::get('/statistics/{projectId}', 'KanbanController@getStatistics');
});
// 任务跟踪接口
Route::prefix('tasks')->group(function () {
Route::get('/{taskId}/timeline', 'TaskController@getTimeline');
Route::post('/{taskId}/comments', 'TaskController@addComment');
Route::get('/{taskId}/activities', 'TaskController@getActivities');
});
// KanbanController 示例
class KanbanController extends Controller {
public function getBoard($projectId) {
$kanban = new KanbanManager(DB::connection(), Cache::store());
$data = $kanban->getBoardData($projectId);
return response()->json($data);
}
public function moveCard(Request $request, $cardId) {
$request->validate([
'target_column_id' => 'required|exists:board_columns,id',
'position' => 'required|integer|min:0'
]);
$kanban = new KanbanManager(DB::connection(), Cache::store());
$result = $kanban->moveTask(
$cardId,
$request->target_column_id,
$request->position,
Auth::id()
);
return response()->json(['success' => $result]);
}
}
高级功能
1 WebSocket 实时更新
// 使用 Laravel WebSocket
class KanbanBroadcast {
public function broadcastCardMove($projectId, $cardId, $fromColumn, $toColumn) {
broadcast(new CardMoved($projectId, $cardId, $fromColumn, $toColumn));
}
public function broadcastCardUpdate($projectId, $cardId, $changes) {
broadcast(new CardUpdated($projectId, $cardId, $changes));
}
}
// 前端监听
Echo.channel(`project.${projectId}`)
.listen('CardMoved', (e) => {
// 更新看板
updateBoard(e.cardId, e.toColumn);
});
2 通知系统
class NotificationManager {
public function sendTaskAssignment($taskId, $assigneeId, $assignedBy) {
$notification = new Notification();
$notification->user_id = $assigneeId;
$notification->type = 'task_assigned';
$notification->data = [
'task_id' => $taskId,
'assigned_by' => $assignedBy
];
$notification->save();
// 发送邮件
Mail::to($assignee->email)->send(new TaskAssigned($task));
// 推送通知
PushNotification::send($assignee, "您被分配了新任务: {$task->title}");
}
}
这套PHP看板系统提供了完整的项目管理功能,包括看板视图、拖拽操作、任务跟踪、实时更新等核心特性,可以根据实际需求进行调整和扩展。