当前位置: 首页 > news >正文

【前端从0到1实战】第8篇:构建“拖拽看板” (Drag Drop Kanban)

欢迎来到本系列的第8篇。在前几篇中,我们处理了点击、滑动和异步加载。今天,我们要解放鼠标,实现“拖拽 (Drag and Drop)”交互。

看板(Kanban)是任务管理的经典视图。一个标准的看板包含多个“列”(如待办、进行中、已完成),用户可以通过拖拽将“任务卡片”在这些列之间移动,或者在同一列中重新排序。

本篇我们将手写一个原生看板,不依赖任何第三方库(如 React-DnD 或 Sortable.js),直接驾驭浏览器底层 API。

第一部分:HTML 结构搭建 (列与卡片)

HTML 结构反映了看板的逻辑层级:

看板容器 (.board):横向排列所有列。

列 (.column):包含标题和任务列表容器。

任务列表 (.task-list):这是卡片的“放置区 (Drop Zone)”。

卡片 (.card):这是可被拖拽的元素,必须设置 draggable="true"。

<body>
<div class="app-container"><h1>项目任务看板</h1><p>试着把卡片拖动到不同的列,或者调整它们的顺序。</p><!-- 1. 看板主容器 --><div id="kanban-board" class="board"><!-- 列 1: 待办事项 --><div class="column"><div class="column-header"><span class="column-title">待办 (Todo)</span><span class="column-count">3</span></div><!-- 放置区 --><div class="task-list" id="todo-list"><!-- 卡片 A --><div class="card" draggable="true" id="card-1"><div class="card-tag tag-design">设计</div><p class="card-text">设计新的登录页面 UI</p></div><!-- 卡片 B --><div class="card" draggable="true" id="card-2"><div class="card-tag tag-dev">开发</div><p class="card-text">修复 API 接口 500 错误</p></div><!-- 卡片 C --><div class="card" draggable="true" id="card-3"><div class="card-tag tag-test">测试</div><p class="card-text">编写自动化测试用例</p></div></div></div><!-- 列 2: 进行中 --><div class="column"><div class="column-header"><span class="column-title">进行中 (In Progress)</span><span class="column-count">1</span></div><div class="task-list" id="progress-list"><!-- 卡片 D --><div class="card" draggable="true" id="card-4"><div class="card-tag tag-dev">开发</div><p class="card-text">重构用户验证模块</p></div></div></div><!-- 列 3: 已完成 --><div class="column"><div class="column-header"><span class="column-title">已完成 (Done)</span><span class="column-count">1</span></div><div class="task-list" id="done-list"><!-- 卡片 E --><div class="card" draggable="true" id="card-5"><div class="card-tag tag-ops">运维</div><p class="card-text">服务器安全补丁更新</p></div></div></div></div>
</div>
</body>

第二部分:CSS 样式 (布局与拖拽反馈)

CSS 的关键在于:

使用 Flexbox 横向排列“列”。

为拖拽中的元素 (.dragging) 设置样式,以便用户知道哪个元素正在被移动。

确保 .task-list 有最小高度,否则当它为空时,用户无法将卡片拖进去。

/* --- 基础布局 --- */
body {
background-color: #f4f7f6;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
color: #333;
display: flex;
justify-content: center;
min-height: 100vh;
}

.app-container {
width: 100%;
max-width: 1200px;
padding: 40px 20px;
}

h1 { text-align: center; margin-bottom: 10px; }
p { text-align: center; color: #666; margin-bottom: 40px; }

/* --- 看板容器 --- /
.board {
display: flex;
gap: 20px;
align-items: flex-start; /
让列的高度根据内容自适应,不要强制拉伸 /
overflow-x: auto; /
移动端支持横向滚动 */
padding-bottom: 20px;
}

/* --- 列样式 --- /
.column {
background-color: #e2e4e6;
border-radius: 8px;
width: 300px; /
固定列宽 /
flex-shrink: 0; /
防止列被压缩 /
display: flex;
flex-direction: column;
max-height: 80vh; /
防止列太高 */
}

.column-header {
padding: 15px;
font-weight: bold;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid rgba(0,0,0,0.05);
}
.column-count {
background-color: rgba(0,0,0,0.1);
border-radius: 12px;
padding: 2px 8px;
font-size: 0.8em;
}

/* --- 任务列表 (Drop Zone) --- /
.task-list {
padding: 10px;
flex-grow: 1;
min-height: 100px; /
核心:保证空列表也能接收拖拽 */
overflow-y: auto;
}

/* --- 卡片样式 --- /
.card {
background-color: #fff;
border-radius: 6px;
padding: 15px;
margin-bottom: 10px;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
cursor: grab; /
鼠标手势:抓取 */
transition: transform 0.2s, box-shadow 0.2s;
}

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

.card:active {
cursor: grabbing; /* 鼠标手势:正在抓取 */
}

/* 标签样式 */
.card-tag {
display: inline-block;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
margin-bottom: 8px;
color: white;
}
.tag-design { background-color: #ff9f43; }
.tag-dev { background-color: #54a0ff; }
.tag-test { background-color: #ee5253; }
.tag-ops { background-color: #10ac84; }

.card-text {
margin: 0;
line-height: 1.5;
}

/* --- 拖拽状态样式 (由 JS 添加) --- /
.card.dragging {
opacity: 0.5; /
让原元素变半透明 */
border: 2px dashed #999;
box-shadow: none;
}

第三部分:JS 交互逻辑 (Drag & Drop API)

这是最神奇的部分。HTML5 拖拽 API 默认并不支持“自动排序”或“插入到指定位置”,它只告诉我们“拖了谁”和“在哪松手”。

我们需要编写一个算法来计算鼠标位置,从而决定将卡片插入到列表的哪个位置。

document.addEventListener('DOMContentLoaded', () => {

// 1. 获取所有可拖拽的卡片和容器
const draggables = document.querySelectorAll('.card');
const containers = document.querySelectorAll('.task-list');// --- 2. 处理卡片的拖拽事件 ---
draggables.forEach(draggable => {// 开始拖拽draggable.addEventListener('dragstart', () => {// 添加类名,改变样式draggable.classList.add('dragging');});// 结束拖拽draggable.addEventListener('dragend', () => {// 移除类名draggable.classList.remove('dragging');});
});// --- 3. 处理容器的放置事件 ---
containers.forEach(container => {// 当有元素在容器上方拖动时触发container.addEventListener('dragover', (e) => {// 核心:默认情况下,浏览器禁止将元素 Drop 到另一个元素内// 我们必须阻止默认行为,才能允许 Drope.preventDefault();// 获取当前正在被拖拽的元素const afterElement = getDragAfterElement(container, e.clientY);const draggable = document.querySelector('.dragging');// 核心逻辑:决定插入位置// 如果 afterElement 为 null,说明我们在列表最下方,直接 append// 否则,插在 afterElement 之前if (afterElement == null) {container.appendChild(draggable);} else {container.insertBefore(draggable, afterElement);}});
});// --- 4. 辅助算法:计算插入位置 ---
// 这个函数的作用是:根据鼠标的 Y 坐标,找到鼠标下方的那个元素
// 这样我们就知道该把拖拽元素插到哪里了
function getDragAfterElement(container, y) {// 获取容器内所有 *没有* 被拖拽的卡片// 这里的 :not(.dragging) 很重要,排除掉自己const draggableElements = [...container.querySelectorAll('.card:not(.dragging)')];// 使用 reduce 循环,找出距离鼠标最近的那个元素return draggableElements.reduce((closest, child) => {const box = child.getBoundingClientRect();// 计算鼠标 Y 坐标与元素中心点的距离// offset < 0 表示鼠标在元素中心点上方const offset = y - box.top - box.height / 2;// 我们只关心鼠标在元素上方的情况 (offset < 0)// 并且要找那个距离最近的 (offset 最大,最接近 0)if (offset < 0 && offset > closest.offset) {return { offset: offset, element: child };} else {return closest;}}, { offset: Number.NEGATIVE_INFINITY }).element;
}

});

总结

恭喜!您刚刚手写了一个功能完备的看板系统。

我们学到了:

HTML5 Drag and Drop API:dragstart 用于标记源元素,dragover 用于实时计算位置,appendChild 用于移动 DOM 元素(是的,移动 DOM 不需要删除再新建,直接 append 就会自动移动)。

位置计算算法:如何利用 getBoundingClientRect 和鼠标坐标 clientY 来精确定位插入点。这是所有排序类 UI 库的核心原理。

掌握了这个,您就可以轻松实现列表排序、文件上传拖拽等高级交互功能。

http://www.jsqmd.com/news/51858/

相关文章:

  • iac工具-Terraform
  • week2--RE--刷题记录
  • API自动化与单元测试
  • 2025年11月婚纱摄影哪家好推荐:成都西安北京天津太原宁波婚纱摄影推荐拍摄核心优势!
  • netcore 项目健康检查(healthcheck)
  • 2025年市场上四川住人集装箱厂家最新用户好评榜
  • 2025年11月干燥机厂家推荐榜:闪蒸喷雾桨叶流化床干燥机厂家行业适配能力!
  • 2025年质量好的四川轻集料混凝土热门厂家排行榜单
  • 2025年市面上四川净化板厂家最新权威推荐排行榜
  • 2025年市面上成都房屋拆除建渣清运最新权威推荐排行榜
  • RAG 分块策略:从原理到实战优化,喂饭级教程不允许你踩坑
  • 2025 年试验箱厂家终极推荐!六大特色厂商破解采购难题,技术与服务双优
  • 发现大量关键漏洞的秘诀 - Alex Chapman访谈
  • 【LVGL】微调器部件
  • 2025年下半石材雕刻机/墓碑雕刻机/绳锯机品牌综合推荐指南TOP10
  • DC-DC转换器DIO8016驱动适配
  • 用“分区”来面对超大数据集和超大吞吐量
  • 2025年下半年石材雕刻机、墓碑雕刻机、绳锯机厂家综合推荐指南:十大优质厂商深度解析
  • 2025年11月溶剂油墨、玻璃油墨、水性油墨、UV油墨、溶剂耗材厂家综合推荐指南:十大优质供应商盘点
  • 【多所高校组织】第七届水利与土木建筑工程国际学术会议(HCCE 2025)
  • 【前端从0到1实战】第7篇:构建“深色模式切换”与本地存储 (Dark Mode LocalStorage)
  • 价值原语化与LLM协同驱动:法律智能的构建路径与范式探析
  • 2025年专业的信号发生器品牌TOP5推荐
  • 2025 年木塑地板厂家最新推荐,聚焦资质、案例、售后的五家品牌深度解读室外木塑地板厂家/共挤木塑地板厂家/wpc木塑地板厂家/防腐木塑地板厂家
  • 2025年下半年溶剂油墨/玻璃油墨/水性油墨/UV油墨/溶剂耗材推荐前十指南:专业选购与口碑解析
  • 实验四截图
  • 光谱传感芯片技术让日常设备具备超级视觉
  • Linux的进程控制 - 教程
  • 第九届能源、环境与材料科学国际学术会议(EEMS 2025)
  • 2025年卧室简约吊灯生产厂家推荐:助你提升家居品质