很多刚接触前端的朋友,可能都想过一个问题:能不能只用HTML就做出一个像Trello那样可以拖拽任务的看板? 答案是,HTML本身做不到。它负责的是网页的骨架和内容结构,就像房子的承重墙。而一个交互式看板的灵魂——流畅的拖拽、清晰的状态分组、数据的持久化保存——这些“活”必须交给CSS和Ja vaS
HTML看板任务面板区块实现教程
时间:2026-06-04
浏览:251
文章目录
很多刚接触前端的朋友,可能都想过一个问题:能不能只用HTML就做出一个像Trello那样可以拖拽任务的看板?
答案是,HTML本身做不到。它负责的是网页的骨架和内容结构,就像房子的承重墙。而一个交互式看板的灵魂——流畅的拖拽、清晰的状态分组、数据的持久化保存——这些“活”必须交给CSS和Ja vaScript来完成。纯HTML加CSS,最多只能画出一个静态的、不能动的“看板图片”。一旦涉及到“把任务拖到另一列”或者“刷新页面后任务还在原位”,dragstart、drop、localStorage这些API就绕不开了。
用 flexbox 实现三栏式看板容器布局
看板的经典布局通常是“待办”、“进行中”、“已完成”三列。要实现这个,display: flex是目前最稳妥、也最灵活的选择。相比老式的float或者inline-block,Flexbox在控制对齐和实现响应式布局上要顺手得多。
这里有个小建议:初期先别急着用CSS Grid。虽然Grid很强大,但在看板这种场景下,每一列的宽度可能会因为卡片数量多少而动态变化(比如“待办”列任务爆满)。这时候,Flexbox的flex-wrap属性和min-width配合起来,控制力反而更强,也更不容易出意外。
几个布局的关键点:
- 最外层的
container直接设置display: flex; gap: 16px。用gap来统一控制列间距,比手动给每一列加margin要干净利落。 - 每一列(
column)可以设置flex: 0 0 300px。这表示“不伸缩、不压缩、基础宽度300px”,能有效防止在窄屏下各列被挤成一团。 - 列内的卡片列表(
card-list)用flex-direction: column; gap: 8px来垂直排列卡片。同样,用gap替代margin-bottom来设置卡片间距,代码更简洁,维护起来也方便。
让卡片支持拖拽:必须监听 dragstart 和 drop
给卡片加个draggable="true"属性,只是万&里长征第一步。浏览器的默认行为是只允许拖拽图片和链接,对于普通的div卡片,你需要手动触发拖拽事件。更重要的是,drop(放置)事件的默认行为是被阻止的,如果你不在相关事件里调用event.preventDefault(),卡片根本“放”不进去。
实践中,下面这几个坑几乎每个人都会踩到:
- 在
dragover事件里忘了写event.preventDefault()。结果就是,拖着卡片在目标列上方移动时,光标会显示一个“禁止”图标,松开鼠标也没任何反应。 - 在
dragstart事件中,没有正确调用e.dataTransfer.setData('text/plain', id)来传递卡片ID。导致在drop事件里,你根本不知道拖过来的是哪张卡片。 - 错误地把
drop事件绑定在了卡片(.card)上,而不是列的容器(.column)上。这样做的后果是,只有当用户把卡片精确拖到另一张卡片上时才能触发放置,拖到列里的空白区域则会失败,体验非常糟糕。
一个最简化的代码片段大概是这样的:
card.addEventListener('dragstart', e => {
e.dataTransfer.setData('text/plain', card.dataset.id);
});
column.addEventListener('dragover', e => e.preventDefault());
column.addEventListener('drop', e => {
const id = e.dataTransfer.getData('text/plain');
column.appendChild(document.querySelector(`[data-id="${id}"]`));
});
用 localStorage 保存看板状态,但别存整个 DOM
实现刷新页面后任务状态不丢失,是看板体验的临门一脚。这里的核心思路是序列化(保存)每一列里卡片的顺序,而不是把整个列的HTML代码存进去。
把innerHTML整个扔进localStorage是个危险的做法:一来,HTML结构稍有变动就可能解析失败;二来,用这种方式恢复的DOM元素,上面绑定的事件监听器会全部丢失。
正确的做法是设计一个轻量的数据结构来保存状态:
- 用一个对象来对应各列,比如
{ todo: ['task-1', 'task-3'], doing: ['task-2'] }。每个值就是一个卡片ID的数组。 - 页面上每个卡片元素都用唯一的
data-id属性来标识。 - 每次拖放操作完成后,立即执行
localStorage.setItem('kanban-state', JSON.stringify(state)),把最新的状态存起来。 - 页面加载时,优先读取
localStorage.getItem('kanban-state'),然后按照保存的ID顺序去重建DOM。注意,这个操作应该在DOM元素创建后、插入页面之前进行,而不是等所有东西都渲染完了再去调整顺序。
话说回来,真正让开发者头疼的,往往不是基础的拖拽逻辑,而是更精细的“拖放位置判定”。举个例子:用户把卡片从A列拖到B列,松手的位置在B列最后一个卡片下方20像素的空白处。这时候,程序需要判断用户的意图是“放在这个卡片下面”还是“放在列表末尾”。要实现这种智能判断,就得靠getBoundingClientRect()来计算鼠标坐标和每个卡片区域的相对位置,这可比简单地监听一个drop事件要复杂得多,也是区分一个看板“能用”和“好用”的关键所在。
实用工具推荐
绿化、压缩、噪点和虚拟水印,自由调节做旧程度。
选择画质和输出格式,预览压缩前后的大小。