PHP项目看板与任务跟踪

wen PHP项目 1

本文目录导读:

PHP项目看板与任务跟踪

  1. 系统架构设计
  2. 核心功能实现
  3. 前端实现
  4. API 接口设计
  5. 高级功能

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

抱歉,评论功能暂时关闭!