如果想开发一套稳定、好用的看板系统,其实直接使用原生 HTML + CSS + JS 就足够了,重点在于把浏览器默认行为和限制处理到位。页面结构上,建议用语义明确的 div 来组织,例如 board、column、card;在拖拽开始时通过 dragstart 传递唯一 ID,到了 dragover 和 drop 这两个关键阶段,preventDefault 一定不能省略;列内排序则应根据鼠标 Y 坐标判断卡片应该插入到哪个位置;至于状态管理,保存轻量级的 ID 序列即可,不要直接把 DOM 当作状态源来维护。

直接用 HTML + CSS + JavaScript,就可以开发出支持拖拽排序、跨列移动、状态持久化的看板任务管理系统。关键不在于“能不能实现”,而在于“如何绕开浏览器默认限制,并让整体交互逻辑足够稳定”。原生 drag/drop 只是基础,真正要补强的核心有三部分:位置判断、状态同步以及视觉反馈。
结构要清晰,别让 DOM 结构干扰业务逻辑
不一定非要用 section 或 article 包裹,直接使用语义清楚的 div 结构会更直观:
- 外层
作为整个看板系统的主容器 - 每一列使用
,并带上唯一标识,方便任务列识别 - 每张任务卡片使用
,并且必须设置draggable="true" - 列内建议通过
flex-direction: column; gap: 8px纵向堆叠卡片,避免 margin 折叠影响拖拽排序定位
拖拽逻辑必须固定这三步
少了任何一步,都可能出现“无法拖动”“松开鼠标没有反应”“卡片位置异常甚至消失”等问题:
- dragstart 里:调用
e.dataTransfer.setData('text/plain', card.dataset.id),只传递卡片 ID,不要直接传 HTML 或 JSON(Safari 对 MIME 类型较敏感) - dragover 里:给列容器绑定监听后,立即执行
e.preventDefault()——这是触发 drop 的前提条件,不是可选操作 - drop 里:先执行
e.preventDefault(),再通过e.target.closest('.column')定位目标列,最后移除原卡片并插入到新位置
列内排序依赖坐标判断,不是单纯靠 hover
同一列中的拖拽重排,不能只依赖“悬停到某张卡片上”,而应该实时计算鼠标 Y 坐标对应的落点:
- 在
dragover过程中获取当前列中的所有卡片元素(排除正在拖拽的自身),并遍历调用getBoundingClientRect() - 比较
e.clientY与每张卡片的上半区和下半区位置,判断应该插入到它前面还是后面 - 通过
list.insertBefore(card, target)实现精确插入,不要只用appendChild做简单追加
状态保存不要存 DOM,应该存轻量数据结构
页面刷新后任务还能保持原来的位置,依赖的是顺序序列化,而不是把 innerHTML 整段塞进 localStorage:
- 定义数据模型,例如
{ todo: ['task-1', 'task-3'], doing: ['task-2'] } - 每次 drop 结束后更新对应数组,然后执行
localStorage.setItem('kanban-state', JSON.stringify(state)) - 页面加载时读取状态并按顺序渲染任务卡片,不要依赖当前 DOM 顺序来还原数据
整体实现并不复杂,但这些细节在开发原生拖拽看板任务管理系统时非常容易被忽略。
