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

article/2025/6/16 5:33:55

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

效果演示

image-20250530222903130

image-20250530222932759

image-20250530223009693

页面结构

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>

http://www.hkcw.cn/article/bRCSmHumJu.shtml

相关文章

进程间通信II·命名管道

目录 原理 创建过程 特性 代码练习 客户端与服务端交互 小知识 原理 原理&#xff1a;两个进程各自的struct file 指向相同的inode和文件缓冲区&#xff08;这里的inode和文件缓冲区也应用了引用计数&#xff09;。 命名管道创建的是磁盘上的一种不刷新数据到缓冲区的常规…

Redis--缓存工具封装

经过前面的学习&#xff0c;发现缓存中的问题&#xff0c;无论是缓存穿透&#xff0c;缓存雪崩&#xff0c;还是缓存击穿&#xff0c;这些问题的解决方案业务代码逻辑都很复杂&#xff0c;我们也不应该每次都来重写这些逻辑&#xff0c;我们可以将其封装成工具。而在封装的时候…

ZC-OFDM雷达通信一体化减小PAPR——选择性映射法(SLM)

文章目录 前言一、SLM 技术1、简介2、原理 二、MATLAB 仿真1、核心代码2、仿真结果 三、资源自取 前言 在 OFDM 雷达通信一体化系统中&#xff0c;信号的传输由多个子载波协同完成&#xff0c;多个载波信号相互叠加形成最终的发射信号。此叠加过程可能导致信号峰值显著高于其均…

ESP32-idf学习(四)esp32C3驱动lcd

一、前言 屏幕是人机交互的重要媒介&#xff0c;而且现在我们产品升级的趋势越来越高大尚&#xff0c;不少产品都会用lcd来做界面&#xff0c;而esp32c3在一些项目上是可以替代主mcu&#xff0c;所以驱动lcd也是必须学会的啦 我新买的这块st7789&#xff0c;突然发现是带触摸…

Remote Sensing投稿记录(投稿邮箱写错、申请大修延期...)风雨波折投稿路

历时近一个半月&#xff0c;我中啦&#xff01; RS是中科院二区&#xff0c;2023-2024影响因子4.2&#xff0c;五年影响因子4.9。 投稿前特意查了下预警&#xff0c;发现近五年都不在预警名单中&#xff0c;甚至最新中科院SCI分区&#xff08;2025年3月&#xff09;在各小类上…

ZC-OFDM雷达通信一体化减小PAPR——部分传输序列法(PTS)

文章目录 前言一、PTS 技术1、简介2、原理 二、MATLAB 仿真1、核心代码2、仿真结果 三、资源自取 前言 在 OFDM 雷达通信一体化系统中&#xff0c;信号的传输由多个子载波协同完成&#xff0c;多个载波信号相互叠加形成最终的发射信号。此叠加过程可能导致信号峰值显著高于其均…

第6章 放大电路的反馈

本章基本要求 会判&#xff1a;判断电路中有无反馈及反馈的性质 会算&#xff1a;估算深度负反馈条件下的放大倍数 会引&#xff1a;根据需求引入合适的反馈 会判振消振&#xff1a;判断电路是否能稳定工作&#xff0c;会消除自激振荡。 6.1 反馈的概念及判断 一、反馈的…

知识管理五强对比:Baklib高效突围

Baklib核心技术优势 Baklib的底层技术架构以知识中台为核心&#xff0c;深度融合自然语言处理&#xff08;NLP&#xff09;与分布式存储技术&#xff0c;实现多源异构数据的统一纳管。其智能分类引擎通过语义理解自动关联碎片化文档&#xff0c;结合动态标签体系与多维度权限控…

电机驱动器辐射骚扰整改

定位低压DC部分的骚扰源&#xff08;排除法&#xff09;&#xff1a; 为确定是电源哪部分出现问题&#xff0c;可以采取如下步骤进行验证&#xff1a; a.将12V转5V的芯片去掉&#xff0c;仅剩12V器件工作&#xff0c;然后测试&#xff1b; b.将5V转3.3V和隔离5V的芯片去掉&am…

CTFHub-RCE 命令注入-过滤空格

观察源代码 代码里面可以发现过滤了空格 判断是Windows还是Linux 源代码中有 ping -c 4 说明是Linux 查看有哪些文件 127.0.0.1|ls 打开flag文件 我们尝试将空格转义打开这个文件 利用 ${IFS} 127.0.0.1|cat${IFS}flag_195671031713417.php 可是发现 文本内容显示不出来&…

2022年 中国商务年鉴(excel电子表格版)

2022年 中国商务年鉴&#xff08;excel电子表格版&#xff09;.ziphttps://download.csdn.net/download/2401_84585615/89772883 https://download.csdn.net/download/2401_84585615/89772883 《中国商务年鉴2022》是由商务部国际贸易经济合作研究院主办的年度统计资料&#xf…

家长速查!3岁男童误吞“水精灵”危及生命

给孩子挑选放心的玩具是不少家长群讨论的热点。“小玩具”关乎“大安全”,如何帮助孩子远离“毒”“危”玩具?怎样合理选购、安全使用,让玩具成为孩子的益友?“六一”国际儿童节前夕,记者就此进行了走访。“毒”“危”玩具有何隐患?“本月我们又接诊了一名3岁男童误吞‘水…

划龙舟有多拼 鼓点一响全员开挂 岭南文化盛宴

广东龙舟不仅是一种仪式,更是一种文化符号。每一声鼓点都充满了热血与奋进,每一次冲刺都体现了拼搏与荣光。“下水!起桨!”有着20多年“龙舟龄”的东莞万江街道龙舟划手黄柱良,为了近日在东江江面举行的龙舟趁景活动,和伙伴们准备了1个多星期。活动当天上午,黄柱良和其他…

大巴黎如何拿到2025年欧冠的 战术转型与团队足球

2025年6月1日凌晨,2024-2025赛季欧冠决赛在慕尼黑安联球场举行,巴黎圣日耳曼以5-0大胜国际米兰,队史首次夺得欧冠奖杯。这场胜利不仅终结了巴黎多年来的“欧冠魔咒”,也标志着球队在姆巴佩离队后的战术转型取得巨大成功。比赛期间,大巴黎主帅恩里克延续了本赛季后半段的43…

thinkpad T-440p 2025.05.31

thinkpad T-440p 2025.05.31 老了退休了&#xff0c;说起来真的可恶现在笔记本的设计师&#xff0c;只有固态硬盘了

堆与堆排序及 Top-K 问题解析:从原理到实践

一、堆的本质与核心特性 堆是一种基于完全二叉树的数据结构&#xff0c;其核心特性为父节点与子节点的数值关系&#xff0c;分为大堆和小堆两类&#xff1a; 大堆&#xff1a;每个父节点的值均大于或等于其子节点的值&#xff0c;堆顶元素为最大值。如: 小堆&#xff1a;每个…

【题解-洛谷】P8094 [USACO22JAN] Cow Frisbee S

题目&#xff1a;P8094 [USACO22JAN] Cow Frisbee S 题目描述 Farmer John 的 N ( N ≤ 3 10 5 ) N\ (N\le 3\times 10^5) N (N≤3105) 头奶牛的高度为 1 , 2 , … , N 1, 2, \ldots, N 1,2,…,N。一天&#xff0c;奶牛以某个顺序排成一行玩飞盘&#xff1b;令 h 1 … h …

如何利用差分隐私技术在医疗领域守护患者隐私

在数字化医疗快速发展的当下&#xff0c;医疗数据已然成为一座蕴藏无限价值的宝库。一份完整的电子病历&#xff0c;不仅记录着患者的疾病诊断、治疗记录&#xff0c;还可能包含基因数据、生活习惯等敏感信息&#xff1b;而基因检测报告中携带的遗传密码&#xff0c;更是与个人…

Kanass入门教程- 事项管理

kanass是一款国产开源免费、简洁易用的项目管理工具&#xff0c;包含项目管理、项目集管理、事项管理、版本管理、迭代管理、计划管理等相关模块。工具功能完善&#xff0c;用户界面友好&#xff0c;操作流畅。本文主要介绍事项管理使用指南。 1、添加事项 事项有多种类型 分…

主人回应狗王“长毛”爆火 小狗成网红引来百万关注

近日,河北承德一只下司犬“长毛”的视频在外网爆火。视频中,“长毛”凭借威严的姿态让闹事的狗狗臣服。因此小狗被外国网友取名“查理国王”“狗王”等称号,连小狗的肖像都被印在T恤上作为周边售卖。火爆全网的狗王“长毛”。网络截图网友们纷纷表达了自己的惊叹与崇拜:“阿…