本文将介绍如何使用 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>