当前位置: 首页 > 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.zskr.cn/news/61783.html

相关文章:

  • iac工具-Terraform
  • netcore 项目健康检查(healthcheck)
  • 2025年质量好的四川轻集料混凝土热门厂家排行榜单
  • 2025年市面上四川净化板厂家最新权威推荐排行榜
  • 【LVGL】微调器部件
  • 2025年下半石材雕刻机/墓碑雕刻机/绳锯机品牌综合推荐指南TOP10
  • DC-DC转换器DIO8016驱动适配
  • 【前端从0到1实战】第7篇:构建“深色模式切换”与本地存储 (Dark Mode LocalStorage)
  • 【多所高校组织】第七届水利与土木建筑工程国际学术会议(HCCE 2025)
  • 2025年专业的信号发生器品牌TOP5推荐
  • 2025 年木塑地板厂家最新推荐,聚焦资质、案例、售后的五家品牌深度解读室外木塑地板厂家/共挤木塑地板厂家/wpc木塑地板厂家/防腐木塑地板厂家
  • Linux的进程控制 - 教程
  • 【webpack】Webpack 最常用的 20 道面试题总结 - 教程
  • 留学机构排行榜TOP10:2025申请季的黄金法则!
  • 2025年11月深圳装修设计公司最新推荐,办公室、酒店、展厅、写字楼、厂房、公寓、店铺工装装修多维度综合考量
  • 2025 最新制氧机经销商权威推荐:国际协会测评认证,聚焦品质与服务的靠谱选择云南制氧机/昆明制氧机/美之氧制氧机/映美制氧机/鱼跃制氧机公司推荐
  • 2025年效果好的助眠的口服液供货商权威推荐榜单:改善睡眠质量的产品‌/安神助眠的产品‌/效果好的助眠产品源头供货商精选
  • 【LVGL】消息框部件
  • 实用指南:Day49 | J.U.C集合-CopyOnWriteArrayList详解
  • 深度揭秘 HBase 协同机制:HMaster、RegionServer 与 ZooKeeper 的三角之恋
  • permutation
  • 2025英国留学机构十大口碑推荐榜!教育行业认证,唯寻国际教育摘冠
  • 中美电力对比
  • 推荐专业的北京离婚财产纠纷律师及相关法律服务参考
  • 北京离婚官司最厉害的律师有哪些?实务经验参考
  • 质量好的工业洗地机实用推荐指南
  • 2025堆垛机立体库厂家哪家好?最新堆垛机立体库制造商排名推荐与趋势综合分析
  • 推荐几家高温炉定制厂家,技术实力与服务解析
  • 2025年线性霍尔元件批发厂家权威推荐榜单:双极霍尔元件/微功耗霍尔元件/单极霍尔元件源头厂家精选
  • k8s event 单独存储一个etcd