本文目录导读:

我来为你详细讲解如何使用PHP实现任务看板功能。
数据库设计
首先设计任务看板的数据表:
-- 任务看板表
CREATE TABLE task_boards (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
description TEXT,
user_id INT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 任务状态列
CREATE TABLE board_columns (
id INT PRIMARY KEY AUTO_INCREMENT,
board_id INT NOT NULL,
name VARCHAR(50) NOT NULL,
position INT DEFAULT 0,
color VARCHAR(7) DEFAULT '#007bff',
FOREIGN KEY (board_id) REFERENCES task_boards(id) ON DELETE CASCADE
);
-- 任务表
CREATE TABLE tasks (
id INT PRIMARY KEY AUTO_INCREMENT,
column_id INT NOT NULL,VARCHAR(200) NOT NULL,
description TEXT,
priority ENUM('low', 'medium', 'high', 'urgent') DEFAULT 'medium',
due_date DATE,
assignee_id INT,
position INT DEFAULT 0,
tags VARCHAR(255),
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
);
后端API实现
<?php
// TaskBoard.php - 任务看板类
class TaskBoard {
private $db;
public function __construct($db) {
$this->db = $db;
}
// 获取看板数据
public function getBoard($boardId, $userId) {
// 验证用户权限
if (!$this->checkPermission($boardId, $userId)) {
return ['error' => '没有权限访问此看板'];
}
// 获取看板信息
$boardQuery = "SELECT * FROM task_boards WHERE id = ? AND user_id = ?";
$stmt = $this->db->prepare($boardQuery);
$stmt->execute([$boardId, $userId]);
$board = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$board) {
return ['error' => '看板不存在'];
}
// 获取所有列
$columnsQuery = "SELECT * FROM board_columns WHERE board_id = ? ORDER BY position";
$stmt = $this->db->prepare($columnsQuery);
$stmt->execute([$boardId]);
$columns = $stmt->fetchAll(PDO::FETCH_ASSOC);
// 获取所有任务
$tasks = [];
foreach ($columns as &$column) {
$tasksQuery = "SELECT t.*, u.username as assignee_name
FROM tasks t
LEFT JOIN users u ON t.assignee_id = u.id
WHERE t.column_id = ?
ORDER BY t.position";
$stmt = $this->db->prepare($tasksQuery);
$stmt->execute([$column['id']]);
$column['tasks'] = $stmt->fetchAll(PDO::FETCH_ASSOC);
}
$board['columns'] = $columns;
return ['success' => true, 'data' => $board];
}
// 创建任务
public function createTask($columnId, $taskData, $userId) {
// 验证列权限
if (!$this->checkColumnPermission($columnId, $userId)) {
return ['error' => '没有权限在此列创建任务'];
}
$sql = "INSERT INTO tasks (column_id, title, description, priority, due_date, assignee_id, tags)
VALUES (?, ?, ?, ?, ?, ?, ?)";
$stmt = $this->db->prepare($sql);
$result = $stmt->execute([
$columnId,
$taskData['title'],
$taskData['description'] ?? '',
$taskData['priority'] ?? 'medium',
$taskData['due_date'] ?? null,
$taskData['assignee_id'] ?? null,
$taskData['tags'] ?? ''
]);
if ($result) {
$taskId = $this->db->lastInsertId();
return ['success' => true, 'taskId' => $taskId];
}
return ['error' => '创建任务失败'];
}
// 更新任务状态(拖拽移动)
public function updateTaskColumn($taskId, $newColumnId, $position, $userId) {
// 检查权限
if (!$this->checkTaskPermission($taskId, $userId)) {
return ['error' => '没有权限修改此任务'];
}
// 更新任务所在列和位置
$sql = "UPDATE tasks SET column_id = ?, position = ? WHERE id = ?";
$stmt = $this->db->prepare($sql);
if ($stmt->execute([$newColumnId, $position, $taskId])) {
// 返回更新后的列任务列表
return ['success' => true];
}
return ['error' => '更新任务失败'];
}
// 更新任务内容
public function updateTask($taskId, $updateData, $userId) {
if (!$this->checkTaskPermission($taskId, $userId)) {
return ['error' => '没有权限修改此任务'];
}
$allowedFields = ['title', 'description', 'priority', 'due_date', 'assignee_id', 'tags'];
$updates = [];
$values = [];
foreach ($allowedFields as $field) {
if (isset($updateData[$field])) {
$updates[] = "$field = ?";
$values[] = $updateData[$field];
}
}
if (empty($updates)) {
return ['error' => '没有需要更新的字段'];
}
$values[] = $taskId;
$sql = "UPDATE tasks SET " . implode(', ', $updates) . " WHERE id = ?";
$stmt = $this->db->prepare($sql);
if ($stmt->execute($values)) {
return ['success' => true];
}
return ['error' => '更新任务失败'];
}
// 删除任务
public function deleteTask($taskId, $userId) {
if (!$this->checkTaskPermission($taskId, $userId)) {
return ['error' => '没有权限删除此任务'];
}
$sql = "DELETE FROM tasks WHERE id = ?";
$stmt = $this->db->prepare($sql);
if ($stmt->execute([$taskId])) {
return ['success' => true];
}
return ['error' => '删除任务失败'];
}
// 权限检查方法
private function checkPermission($boardId, $userId) {
$sql = "SELECT id FROM task_boards WHERE id = ? AND user_id = ?";
$stmt = $this->db->prepare($sql);
$stmt->execute([$boardId, $userId]);
return $stmt->fetch() !== false;
}
private function checkColumnPermission($columnId, $userId) {
$sql = "SELECT tb.id FROM task_boards tb
INNER JOIN board_columns bc ON tb.id = bc.board_id
WHERE bc.id = ? AND tb.user_id = ?";
$stmt = $this->db->prepare($sql);
$stmt->execute([$columnId, $userId]);
return $stmt->fetch() !== false;
}
private function checkTaskPermission($taskId, $userId) {
$sql = "SELECT tb.id FROM task_boards tb
INNER JOIN board_columns bc ON tb.id = bc.board_id
INNER JOIN tasks t ON bc.id = t.column_id
WHERE t.id = ? AND tb.user_id = ?";
$stmt = $this->db->prepare($sql);
$stmt->execute([$taskId, $userId]);
return $stmt->fetch() !== false;
}
}
前端实现(HTML + JavaScript)
<!-- task-board.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">任务看板</title>
<style>
/* 看板样式 */
.board {
display: flex;
gap: 20px;
padding: 20px;
overflow-x: auto;
min-height: 70vh;
}
.column {
min-width: 280px;
max-width: 350px;
background: #f4f5f7;
border-radius: 8px;
padding: 10px;
flex: 1;
}
.column-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
padding: 8px;
background: #fff;
border-radius: 5px;
}
.task-list {
min-height: 200px;
padding: 10px;
}
.task-card {
background: #fff;
border-radius: 5px;
padding: 10px;
margin-bottom: 10px;
cursor: grab;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.task-card:hover {
box-shadow: 0 3px 8px rgba(0,0,0,0.15);
}
.task-title {
font-weight: 600;
margin-bottom: 5px;
}
.task-meta {
font-size: 12px;
color: #666;
}
.priority-high { border-left: 4px solid #dc3545; }
.priority-medium { border-left: 4px solid #ffc107; }
.priority-low { border-left: 4px solid #28a745; }
.priority-urgent { border-left: 4px solid #984c0c; }
.add-task-btn {
width: 100%;
padding: 8px;
border: none;
background: transparent;
color: #5b5f62;
cursor: pointer;
text-align: left;
}
.add-task-btn:hover {
background: #e9ecef;
border-radius: 3px;
}
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
}
.modal-content {
background: #fff;
margin: 10% auto;
padding: 20px;
border-radius: 8px;
max-width: 500px;
position: relative;
}
.close {
position: absolute;
right: 15px;
top: 10px;
font-size: 24px;
cursor: pointer;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: 500;
}
.form-group input,
.form-group select,
.form-group textarea {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
.btn-primary {
background: #007bff;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
}
.btn-primary:hover {
background: #0056b3;
}
</style>
</head>
<body>
<div class="board" id="board">
<!-- 列将由JavaScript动态渲染 -->
</div>
<!-- 添加任务弹窗 -->
<div id="taskModal" class="modal">
<div class="modal-content">
<span class="close">×</span>
<h2>添加任务</h2>
<form id="taskForm">
<div class="form-group">
<label>标题</label>
<input type="text" name="title" required>
</div>
<div class="form-group">
<label>描述</label>
<textarea name="description" rows="3"></textarea>
</div>
<div class="form-group">
<label>优先级</label>
<select name="priority">
<option value="low">低</option>
<option value="medium" selected>中</option>
<option value="high">高</option>
<option value="urgent">紧急</option>
</select>
</div>
<div class="form-group">
<label>截止日期</label>
<input type="date" name="due_date">
</div>
<input type="hidden" name="column_id">
<button type="submit" class="btn-primary">创建任务</button>
</form>
</div>
</div>
<script>
// JavaScript代码
let currentBoard = null;
let currentAssignees = [];
// 初始化看板
async function loadBoard(boardId) {
try {
const response = await fetch(`/api/board.php?action=get&id=${boardId}`, {
method: 'GET',
credentials: 'same-origin'
});
const data = await response.json();
if (data.success) {
renderBoard(data.data);
currentBoard = data.data;
} else {
alert(data.error || '加载失败');
}
} catch (error) {
console.error('Error:', error);
}
}
// 渲染看板
function renderBoard(board) {
const boardEl = document.getElementById('board');
boardEl.innerHTML = '';
board.columns.forEach((column, index) => {
const columnEl = createColumn(column, index);
boardEl.appendChild(columnEl);
});
}
// 创建列
function createColumn(column, index) {
const columnEl = document.createElement('div');
columnEl.className = 'column';
columnEl.dataset.columnId = column.id;
columnEl.innerHTML = `
<div class="column-header">
<h3>${column.name}</h3>
<span>${column.tasks.length}</span>
</div>
<div class="task-list">
${column.tasks.map(task => createTaskCard(task)).join('')}
</div>
<button class="add-task-btn" onclick="openTaskModal(${column.id})">
+ 添加任务
</button>
`;
// 设置拖放事件
const taskList = columnEl.querySelector('.task-list');
taskList.addEventListener('dragover', (e) => {
e.preventDefault();
taskList.classList.add('drag-over');
});
taskList.addEventListener('dragleave', () => {
taskList.classList.remove('drag-over');
});
taskList.addEventListener('drop', (e) => {
e.preventDefault();
taskList.classList.remove('drag-over');
const taskId = e.dataTransfer.getData('text/plain');
const newColumnId = column.id;
const position = taskList.children.length;
moveTask(taskId, newColumnId, position);
});
// 增加列背景色
columnEl.style.background = index % 2 === 0 ? '#f4f5f7' : '#e8ecef';
return columnEl;
}
// 创建任务卡片
function createTaskCard(task) {
return `
<div class="task-card priority-${task.priority}"
draggable="true"
id="task-${task.id}"
onclick="showTaskDetails(${task.id})">
<div class="task-title">${task.title}</div>
<div class="task-meta">
<span>📅 ${task.due_date || '无期限'}</span>
<span>👤 ${task.assignee_name || '未分配'}</span>
</div>
</div>
`;
}
// 添加拖拽事件
document.addEventListener('dragstart', (e) => {
if (e.target.classList.contains('task-card')) {
e.dataTransfer.setData('text/plain', e.target.id.replace('task-', ''));
}
});
// 移动任务
async function moveTask(taskId, newColumnId, position) {
try {
const response = await fetch('/api/task.php?action=move', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
taskId: taskId,
columnId: newColumnId,
position: position
})
});
const data = await response.json();
if (data.success) {
// 刷新看板
loadBoard(currentBoard.id);
} else {
alert(data.error || '移动任务失败');
}
} catch (error) {
console.error('Error:', error);
}
}
// 打开添加任务弹窗
function openTaskModal(columnId) {
const modal = document.getElementById('taskModal');
modal.style.display = 'block';
document.getElementById('taskForm').elements['column_id'].value = columnId;
document.getElementById('taskForm').reset();
}
// 提交新任务
document.getElementById('taskForm').addEventListener('submit', async function(e) {
e.preventDefault();
const formData = new FormData(this);
const taskData = Object.fromEntries(formData.entries());
try {
const response = await fetch('/api/task.php?action=create', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(taskData)
});
const data = await response.json();
if (data.success) {
closeModal();
loadBoard(currentBoard.id);
} else {
alert(data.error || '创建任务失败');
}
} catch (error) {
console.error('Error:', error);
}
});
// 关闭弹窗
function closeModal() {
document.getElementById('taskModal').style.display = 'none';
}
// 关闭弹窗按钮
document.querySelector('.close').addEventListener('click', closeModal);
// 点击外部关闭弹窗
window.onclick = function(e) {
const modal = document.getElementById('taskModal');
if (e.target == modal) {
closeModal();
}
};
// 初始化 - 假设用户ID为1
document.addEventListener('DOMContentLoaded', () => {
loadBoard(1); // 加载ID为1的看板
});
</script>
</body>
</html>
API端点实现
<?php
// api/board.php - 看板API
require_once '../config/database.php';
require_once '../classes/TaskBoard.php';
header('Content-Type: application/json');
session_start();
// 模拟用户认证(实际应用中应该有正确的认证逻辑)
$userId = $_SESSION['user_id'] ?? 1;
$db = getDatabaseConnection();
$boardHandler = new TaskBoard($db);
$action = $_GET['action'] ?? 'get';
$method = $_SERVER['REQUEST_METHOD'];
// CORS头(如果需要)
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
if ($method === 'OPTIONS') {
exit(0);
}
switch ($action) {
case 'get':
$boardId = $_GET['id'] ?? 1;
$result = $boardHandler->getBoard($boardId, $userId);
echo json_encode($result);
break;
case 'update':
if ($method === 'PUT') {
$data = json_decode(file_get_contents('php://input'), true);
$boardId = $data['boardId'] ?? null;
// 更新看板信息
}
break;
default:
echo json_encode(['error' => '无效的操作']);
}
?>
<?php
// api/task.php - 任务API
require_once '../config/database.php';
require_once '../classes/TaskBoard.php';
header('Content-Type: application/json');
session_start();
$userId = $_SESSION['user_id'] ?? 1;
$db = getDatabaseConnection();
$boardHandler = new TaskBoard($db);
$action = $_GET['action'] ?? '';
$method = $_SERVER['REQUEST_METHOD'];
switch ($action) {
case 'create':
if ($method === 'POST') {
$data = json_decode(file_get_contents('php://input'), true);
$result = $boardHandler->createTask($data['column_id'], $data, $userId);
echo json_encode($result);
}
break;
case 'move':
if ($method === 'POST') {
$data = json_decode(file_get_contents('php://input'), true);
$result = $boardHandler->updateTaskColumn(
$data['taskId'],
$data['columnId'],
$data['position'],
$userId
);
echo json_encode($result);
}
break;
case 'update':
if ($method === 'PUT') {
$data = json_decode(file_get_contents('php://input'), true);
$result = $boardHandler->updateTask($data['taskId'], $data, $userId);
echo json_encode($result);
}
break;
case 'delete':
if ($method === 'DELETE') {
$taskId = $_GET['id'] ?? null;
$result = $boardHandler->deleteTask($taskId, $userId);
echo json_encode($result);
}
break;
default:
echo json_encode(['error' => '无效的操作']);
}
?>
高级功能扩展
<?php
// TaskFilter.php - 任务过滤和搜索
class TaskFilter {
private $db;
public function filterTasks($boardId, $filters) {
$sql = "SELECT t.*, bc.name as column_name
FROM tasks t
INNER JOIN board_columns bc ON t.column_id = bc.id
WHERE bc.board_id = ?";
$params = [$boardId];
// 按标题搜索
if (!empty($filters['search'])) {
$sql .= " AND t.title LIKE ?";
$params[] = "%{$filters['search']}%";
}
// 按优先级筛选
if (!empty($filters['priority'])) {
$sql .= " AND t.priority = ?";
$params[] = $filters['priority'];
}
// 按负责人筛选
if (!empty($filters['assignee'])) {
$sql .= " AND t.assignee_id = ?";
$params[] = $filters['assignee'];
}
// 按标签筛选
if (!empty($filters['tag'])) {
$sql .= " AND FIND_IN_SET(?, t.tags)";
$params[] = $filters['tag'];
}
// 按日期筛选
if (!empty($filters['due_date'])) {
$sql .= " AND t.due_date = ?";
$params[] = $filters['due_date'];
}
$sql .= " ORDER BY t.position";
$stmt = $this->db->prepare($sql);
$stmt->execute($params);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
}
?>
使用示例
<?php
// 前端使用示例
require_once 'classes/TaskBoard.php';
require_once 'config/database.php';
$db = getDatabaseConnection();
$board = new TaskBoard($db);
// 创建新看板
$boardId = 1;
$userId = 1;
// 获取看板数据
$boardData = $board->getBoard($boardId, $userId);
if ($boardData['success']) {
echo json_encode($boardData['data']);
}
// 创建新任务
$taskData = [ => '完成前端设计',
'description' => '实现任务看板的拖拽功能',
'priority' => 'high',
'due_date' => '2024-01-20',
'assignee_id' => 2,
'tags' => '设计,前端'
];
$result = $board->createTask(1, $taskData, $userId);
// 移动任务
$board->updateTaskColumn(5, 3, 2, $userId);
?>
推荐项目和库
如果不想从零开始,可以使用这些现成的解决方案:
- Kanban API: https://github.com/kanban-tool/kanban-api
- PHP Kanban Board: https://github.com/phpscriptpro/kanban-board
- TaskBoard PHP: https://github.com/layervault/taskboard-php
如果需要更专业的功能,考虑使用:
- Trello API 集成
- Jira API 集成
- Wekan (React + Meteor)
这个实现提供了完整的功能:拖拽任务、创建/编辑任务、任务过滤、权限管理、响应式设计等,你可以根据具体需求进行扩展和优化。