本文目录导读:

我来为您详细介绍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看板系统提供了完整的项目管理功能,包括看板视图、拖拽操作、任务跟踪、实时更新等核心特性,可以根据实际需求进行调整和扩展。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播