使用 HTML + JavaScript 实现可拖拽的任务看板系统

本文将介绍如何使用 HTML、CSS 和 JavaScript 创建一个交互式任务看板系统。该系统支持拖拽任务、添加新任务以及动态创建列,适用于任务管理和团队协作场景。

效果演示

页面结构

HTML 部分主要包含三个默认的任务列(待办、进行中、已完成)和一个用于添加新列的按钮。

html 复制代码
<div class="board" id="board">
    <div class="column" id="todo-column">
        <div class="column-title">待办</div>
        <div class="task-list" id="todo-list">
            <div class="task" draggable="true">设计登录页面UI</div>
            <div class="task" draggable="true">编写API接口文档</div>
            <div class="task" draggable="true">项目需求评审会议</div>
        </div>
        <div class="add-task" onclick="showAddTaskForm('todo-list')">添加任务</div>
    </div>
     <!-- 其他两个列 -->
    <div class="add-column" onclick="addNewColumn()">
        <div class="add-column-icon">+</div>
        <div>添加新列</div>
    </div>
</div>

核心功能实现

拖拽功能实现

完整的拖拽逻辑包括拖拽开始、结束、移动和放置操作。

首先,获取拖拽容器的元素用来绑定拖拽时间,定义一个变量用于保存当前正在被拖动的任务项

js 复制代码
const board = document.getElementById('board');
let draggedTask = null;

拖拽开始

js 复制代码
board.addEventListener('dragstart', function(e) {
    if (e.target.classList.contains('task')) {
        draggedTask = e.target;
        setTimeout(() => {
            e.target.classList.add('dragging');
        }, 0);
    }
});

拖拽过程

js 复制代码
board.addEventListener('dragover', function(e) {
    e.preventDefault();
    const afterElement = getDragAfterElement(e.target.closest('.task-list'), e.clientY);
    const draggingTask = document.querySelector('.dragging');

    if (draggingTask && e.target.closest('.task-list')) {
        const list = e.target.closest('.task-list');
        if (afterElement) {
            list.insertBefore(draggingTask, afterElement);
        } else {
            list.appendChild(draggingTask);
        }
    }
});

拖拽结束

js 复制代码
board.addEventListener('dragend', function(e) {
    if (e.target.classList.contains('task')) {
        e.target.classList.remove('dragging');
    }
});

获取拖拽后应该放置的位置

js 复制代码
function getDragAfterElement(container, y) {
    const draggableElements = [...container.querySelectorAll('.task:not(.dragging)')];

    return draggableElements.reduce((closest, child) => {
        const box = child.getBoundingClientRect();
        const offset = y - box.top - box.height / 2;

        if (offset < 0 && offset > closest.offset) {
            return { offset: offset, element: child };
        } else {
            return closest;
        }
    }, { offset: Number.NEGATIVE_INFINITY }).element;
}

添加任务功能

当用户点击"添加任务"按钮时,会动态创建一个任务输入表单,替换原来的按钮,供用户输入新任务内容。

js 复制代码
function showAddTaskForm(listId) {
    const list = document.getElementById(listId);
    const addButton = list.nextElementSibling;

    // 检查是否已存在表单
    if (list.querySelector('.task-form')) return;

    // 创建表单
    const form = document.createElement('div');
    form.className = 'task-form';
    form.innerHTML = `<input type="text" class="task-input" placeholder="输入任务内容..." autofocus>
        <div class="btn-group">
        <button class="btn btn-primary" onclick="addTask('${listId}')">
        <span>添加任务</span>
        </button>
        <button class="btn btn-outline" onclick="cancelAddTask('${listId}')">
        <span>取消</span>
        </button>
        </div>`;

    // 替换添加按钮为表单
    addButton.style.display = 'none';
    list.appendChild(form);

    // 按Enter键添加任务
    form.querySelector('.task-input').addEventListener('keypress', function(e) {
        if (e.key === 'Enter') {
            addTask(listId);
        }
    });
}
js 复制代码
function addTask(listId) {
    const list = document.getElementById(listId);
    const input = list.querySelector('.task-input');
    const taskText = input.value.trim();
    if (taskText) {
        const task = document.createElement('div');
        task.className = 'task';
        task.draggable = true;
        task.textContent = taskText;
        list.insertBefore(task, list.querySelector('.task-form') || list.firstChild);
        input.value = '';
    }
    cancelAddTask(listId);
}
js 复制代码
function cancelAddTask(listId) {
    const list = document.getElementById(listId);
    const form = list.querySelector('.task-form');
    const addButton = list.nextElementSibling;

    if (form) {
        list.removeChild(form);
    }

    addButton.style.display = 'flex';
}

添加新列

当用户点击"添加新列"按钮时,会弹出一个输入框让用户输入列名。确认后,会在看板中新增一列。

js 复制代码
function addNewColumn() {
    const columnName = prompt("请输入新列的名称:");
    if (columnName) {
        const board = document.getElementById('board');
        const newColumnId = `column-${Date.now()}`;

        // 创建新列容器
        const column = document.createElement('div');
        column.className = 'column';
        column.id = newColumnId;

        // 新列的内容(标题 + 任务列表 + 添加任务按钮)
        column.innerHTML = `<div class="column-title" style="background-color: #9b59b6;">${columnName}</div>
            <div class="task-list" id="${newColumnId}-list"></div>
            <div class="add-task" onclick="showAddTaskForm('${newColumnId}-list')">添加任务</div`;

        // 插入到"添加新列"按钮之前
        const addColumnButton = document.querySelector('.add-column');
        board.insertBefore(column, addColumnButton);
    }
}

扩展建议

  • 任务修改和删除功能
  • 任务详情功能
  • 添加新看板,多任务看板切换
  • 任务优先级和标签系统
  • 接入后端 API,数据持久化

完整代码

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>任务看板</title>
    <style>
        * {
            box-sizing: border-box;
            margin: 0;
            padding: 0;
        }

        body {
            background-color: #f8fafc;
            color: #334155;
            line-height: 1.6;
            padding: 20px;
            min-height: 100vh;
        }

        h1 {
            color: #1e293b;
            margin-bottom: 24px;
            font-weight: 600;
            text-align: center;
            font-size: 2.2rem;
        }

        .board {
            display: flex;
            gap: 24px;
            overflow-x: auto;
            padding: 16px;
            min-height: calc(100vh - 120px);
        }

        .column {
            background-color: #f1f5f9;
            border-radius: 12px;
            width: 320px;
            min-width: 320px;
            padding: 16px;
            box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
            transition: all 0.2s ease-in-out;
            height: fit-content;
            max-height: 90vh;
            display: flex;
            flex-direction: column;
        }

        .column:hover {
            box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
        }

        .column-title {
            font-weight: 600;
            padding: 12px 16px;
            margin-bottom: 16px;
            border-radius: 8px;
            text-align: center;
            text-transform: uppercase;
            letter-spacing: 0.5px;
            font-size: 0.9rem;
            color: white;
            position: relative;
            overflow: hidden;
            min-height: 40px;
            display: flex;
            align-items: center;
            justify-content: center;
        }

        .column-title::after {
            content: '';
            position: absolute;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background: linear-gradient(135deg, rgba(255,255,255,0.2) 0%, rgba(255,255,255,0) 100%);
        }

        #todo-column .column-title {
            background-color: #fb6340;
        }

        #progress-column .column-title {
            background-color: #5e72e4;
        }

        #done-column .column-title {
            background-color: #2dce89;
        }

        .task-list {
            min-height: 100px;
            flex-grow: 1;
            overflow-y: auto;
            padding: 4px;
            margin-bottom: 16px;
            scrollbar-width: thin;
            scrollbar-color: #adb5bd transparent;
        }

        .task-list::-webkit-scrollbar {
            width: 6px;
        }

        .task-list::-webkit-scrollbar-track {
            background: transparent;
        }

        .task-list::-webkit-scrollbar-thumb {
            background-color: #adb5bd;
            border-radius: 3px;
        }

        .task {
            background-color: #ffffff;
            border-radius: 8px;
            padding: 16px;
            margin-bottom: 12px;
            box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
            cursor: grab;
            user-select: none;
            transition: all 0.2s ease-in-out;
            border-left: 4px solid transparent;
        }

        .task:hover {
            transform: translateY(-2px);
            box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
        }

        .task:active {
            cursor: grabbing;
        }

        .task.dragging {
            opacity: 0.5;
            background-color: #e6f7ff;
            border: 1px dashed #11cdef;
            transform: rotate(2deg);
        }

        #todo-list .task {
            border-left-color: #fb6340;
        }

        #progress-list .task {
            border-left-color: #5e72e4;
        }

        #done-list .task {
            border-left-color: #2dce89;
        }

        .add-task {
            color: #adb5bd;
            padding: 12px;
            border-radius: 8px;
            cursor: pointer;
            display: flex;
            align-items: center;
            transition: all 0.2s ease-in-out;
            font-weight: 500;
        }

        .add-task:hover {
            background-color: #e2e8f0;
            color: #212529;
        }

        .add-task::before {
            content: '+';
            display: inline-block;
            margin-right: 8px;
            font-size: 1.2rem;
        }

        .task-form {
            margin-top: 8px;
            animation: fadeIn 0.2s ease-out;
        }

        @keyframes fadeIn {
            from { opacity: 0; transform: translateY(-10px); }
            to { opacity: 1; transform: translateY(0); }
        }

        .task-input {
            width: 100%;
            padding: 12px;
            border: 1px solid #e2e8f0;
            border-radius: 8px;
            margin-bottom: 12px;
            font-family: inherit;
            font-size: 0.9rem;
            transition: all 0.2s ease-in-out;
            box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
        }

        .task-input:focus {
            outline: none;
            border-color: #5e72e4;
            box-shadow: 0 0 0 3px rgba(94, 114, 228, 0.2);
        }

        .btn-group {
            display: flex;
            gap: 8px;
        }

        .btn {
            padding: 8px 16px;
            border-radius: 6px;
            font-weight: 500;
            font-size: 0.85rem;
            cursor: pointer;
            transition: all 0.2s ease-in-out;
            border: none;
            flex-grow: 1;
            display: flex;
            align-items: center;
            justify-content: center;
        }

        .btn-primary {
            background-color: #5e72e4;
            color: white;
        }

        .btn-primary:hover {
            background-color: #4a5acf;
            transform: translateY(-1px);
        }

        .btn-outline {
            background-color: transparent;
            color: #adb5bd;
            border: 1px solid #adb5bd;
        }

        .btn-outline:hover {
            color: #212529;
            border-color: #212529;
        }

        .empty-state {
            text-align: center;
            padding: 20px;
            color: #adb5bd;
            font-size: 0.9rem;
        }

        .add-column {
            background-color: #fff;
            border-radius: 8px;
            box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
            width: 320px;
            min-width: 320px;
            height: 200px;
            padding: 15px;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            cursor: pointer;
            transition: all 0.3s ease;
        }

        .add-column:hover {
            background-color: #f1f2f6;
        }

        .add-column-icon {
            font-size: 24px;
            color: #7f8c8d;
            margin-bottom: 10px;
        }
    </style>
</head>
<body>
<h1>任务看板</h1>

<div class="board" id="board">
    <div class="column" id="todo-column">
        <div class="column-title">待办</div>
        <div class="task-list" id="todo-list">
            <div class="task" draggable="true">设计登录页面UI</div>
            <div class="task" draggable="true">编写API接口文档</div>
            <div class="task" draggable="true">项目需求评审会议</div>
        </div>
        <div class="add-task" onclick="showAddTaskForm('todo-list')">添加任务</div>
    </div>

    <div class="column" id="progress-column">
        <div class="column-title">进行中</div>
        <div class="task-list" id="progress-list">
            <div class="task" draggable="true">开发用户注册功能</div>
            <div class="task" draggable="true">数据库设计优化</div>
        </div>
        <div class="add-task" onclick="showAddTaskForm('progress-list')">添加任务</div>
    </div>

    <div class="column" id="done-column">
        <div class="column-title">已完成</div>
        <div class="task-list" id="done-list">
            <div class="task" draggable="true">项目初始化搭建</div>
            <div class="task" draggable="true">需求分析文档</div>
        </div>
        <div class="add-task" onclick="showAddTaskForm('done-list')">添加任务</div>
    </div>
    <div class="add-column" onclick="addNewColumn()">
        <div class="add-column-icon">+</div>
        <div>添加新列</div>
    </div>
</div>

<script>
    // 拖拽功能实现
    document.addEventListener('DOMContentLoaded', function() {
        const board = document.getElementById('board');
        let draggedTask = null;

        board.addEventListener('dragstart', function(e) {
            if (e.target.classList.contains('task')) {
                draggedTask = e.target;
                setTimeout(() => {
                    e.target.classList.add('dragging');
                }, 0);
            }
        });

        board.addEventListener('dragend', function(e) {
            if (e.target.classList.contains('task')) {
                e.target.classList.remove('dragging');
            }
        });

        board.addEventListener('dragover', function(e) {
            e.preventDefault();
            const afterElement = getDragAfterElement(e.target.closest('.task-list'), e.clientY);
            const draggingTask = document.querySelector('.dragging');

            if (draggingTask && e.target.closest('.task-list')) {
                const list = e.target.closest('.task-list');
                if (afterElement) {
                    list.insertBefore(draggingTask, afterElement);
                } else {
                    list.appendChild(draggingTask);
                }
            }
        });

        // 获取拖拽后应该放置的位置
        function getDragAfterElement(container, y) {
            const draggableElements = [...container.querySelectorAll('.task:not(.dragging)')];

            return draggableElements.reduce((closest, child) => {
                const box = child.getBoundingClientRect();
                const offset = y - box.top - box.height / 2;

                if (offset < 0 && offset > closest.offset) {
                    return { offset: offset, element: child };
                } else {
                    return closest;
                }
            }, { offset: Number.NEGATIVE_INFINITY }).element;
        }
    });

    // 添加任务功能
    function showAddTaskForm(listId) {
        const list = document.getElementById(listId);
        const addButton = list.nextElementSibling;

        // 检查是否已存在表单
        if (list.querySelector('.task-form')) return;

        // 创建表单
        const form = document.createElement('div');
        form.className = 'task-form';
        form.innerHTML = `<input type="text" class="task-input" placeholder="输入任务内容..." autofocus>
                <div class="btn-group">
                    <button class="btn btn-primary" onclick="addTask('${listId}')">
                        <span>添加任务</span>
                    </button>
                    <button class="btn btn-outline" onclick="cancelAddTask('${listId}')">
                        <span>取消</span>
                    </button>
                </div>`;

        // 替换添加按钮为表单
        addButton.style.display = 'none';
        list.appendChild(form);

        // 按Enter键添加任务
        form.querySelector('.task-input').addEventListener('keypress', function(e) {
            if (e.key === 'Enter') {
                addTask(listId);
            }
        });
    }

    function addTask(listId) {
        const list = document.getElementById(listId);
        const input = list.querySelector('.task-input');
        const taskText = input.value.trim();

        if (taskText) {
            const task = document.createElement('div');
            task.className = 'task';
            task.draggable = true;
            task.textContent = taskText;

            // 添加到列表顶部
            list.insertBefore(task, list.querySelector('.task-form') || list.firstChild);

            // 清除输入框
            input.value = '';
        }

        cancelAddTask(listId);
    }

    function cancelAddTask(listId) {
        const list = document.getElementById(listId);
        const form = list.querySelector('.task-form');
        const addButton = list.nextElementSibling;

        if (form) {
            list.removeChild(form);
        }

        addButton.style.display = 'flex';
    }
    function addNewColumn() {
        const columnName = prompt("请输入新列的名称:");
        if (columnName) {
            const board = document.getElementById('board');
            const newColumnId = `column-${Date.now()}`;

            // 创建新列容器
            const column = document.createElement('div');
            column.className = 'column';
            column.id = newColumnId;

            // 新列的内容(标题 + 任务列表 + 添加任务按钮)
            column.innerHTML = `<div class="column-title" style="background-color: #9b59b6;">${columnName}</div>
            <div class="task-list" id="${newColumnId}-list"></div>
            <div class="add-task" onclick="showAddTaskForm('${newColumnId}-list')">添加任务</div`;

            // 插入到"添加新列"按钮之前
            const addColumnButton = document.querySelector('.add-column');
            board.insertBefore(column, addColumnButton);
        }
    }
</script>
</body>
</html>
相关推荐
集成显卡几秒前
图片压缩工具 | Electron+Vue3+Rsbuild开发桌面应用
前端·javascript·electron·vue
赵庆明老师5 分钟前
webpack打包基本配置
前端·webpack·node.js
偷光20 分钟前
现代 CSS 高阶技巧:实现平滑内凹圆角的工程化实践
前端·css·小程序
Blossom.11838 分钟前
人工智能在智能供应链中的创新应用与未来趋势
前端·人工智能·深度学习·安全·机器学习
无限大61 小时前
《计算机“十万个为什么”》之前端与后端
前端·后端·程序员
JuneXcy1 小时前
Vue 核心技术与实战day07
前端·javascript·vue.js
shibin1 小时前
基于axios 二次封装:构建强大的 HTTP 请求层
前端·typescript
xianshenglu1 小时前
我的 Angular 总结:创建一个通用测试模块,简化单元测试
前端·javascript·angular.js
前端工作日常1 小时前
资源加载错误捕获的深层解析:为什么只能用 addEventListener('error')?
javascript
粥里有勺糖1 小时前
视野修炼-技术周刊第121期 | Rolldown-Vite
前端·javascript·github