本文将介绍如何使用 HTML、CSS 和 JavaScript 创建一个交互式任务看板系统。该系统支持拖拽任务、添加新任务以及动态创建列,适用于任务管理和团队协作场景。
效果演示
页面结构
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>
核心功能实现
拖拽功能实现
完整的拖拽逻辑包括拖拽开始、结束、移动和放置操作。
首先,获取拖拽容器的元素用来绑定拖拽时间,定义一个变量用于保存当前正在被拖动的任务项
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('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);}}
});
拖拽结束
board.addEventListener('dragend', function(e) {if (e.target.classList.contains('task')) {e.target.classList.remove('dragging');}
});
获取拖拽后应该放置的位置
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);}
}
扩展建议
- 任务修改和删除功能
- 任务详情功能
- 添加新看板,多任务看板切换
- 任务优先级和标签系统
- 接入后端 API,数据持久化
完整代码
<!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>