有些项目不是为了发布,而是为了练手;有些代码不是为了交付,而是为了成长。而我今天要讲的这个 Kanban 看板,就是这样一个陪伴我成长、让我抓头又让我骄傲的项目。
一、灵感来自生活的混乱
那天,我盯着浏览器里无数个 Tab,看着 Notion 上堆积如山的 TODO 清单,感觉自己的生活像是一个没打扫的桌面 —— 到处是文件夹,到处是“稍后再看”的标签。于是我灵机一动:为什么不自己写一个看板,把所有的事情视觉化地管理起来?
我想要的并不复杂:
- 有三个状态:待办、进行中、已完成
- 卡片可以自由拖动
- 状态变化能够保存下来,刷新页面也不会丢
- UI 要简洁、现代、有呼吸感(别像是 Excel 表格)
就这样,我打开了 VS Code,一行行敲下了这个看板的起点。
二、界面设计:现代简约风,看得见的灵感
我没有使用什么设计稿工具,直接在浏览器里调试着写 CSS。但这一次,我决定不再用混乱的全局 CSS,而是上了 CSS Modules —— 每个组件一套自己的样式,不互相干扰,用起来也舒服。
整个 UI 的布局大致如下:
每个列是一个组件,每张卡片也是一个组件。布局采用 Flex 实现响应式设计,在大屏和小屏之间切换都很自然。颜色方面我选用了低饱和度的蓝灰色调,配合一点点圆角和微妙的阴影,整体观感舒适不压抑。
三、技术选型与项目初始化
这是一个纯前端项目,为了让代码模块化、便于维护,我选用了:
- React 18 + Vite:构建快、开发体验丝滑
- CSS Modules:告别全局样式污染
- 原生 Drag & Drop API:避免引入沉重的拖拽库,轻量清爽
- localStorage:实现本地持久化
项目初始化非常简单,一条命令:
然后我清空了默认模板,开始创建组件。
四、实现拖拽逻辑:一步步走进浏览器的拖拽机制
拖拽的灵魂其实很简单,主要依赖浏览器内建的 dragstart
、dragover
、drop
等事件。但实现过程中,细节比想象中多得多。
我先定义了一个 Card
组件,它的核心代码看起来像这样:
关键在 draggable
和 onDragStart
。这时候我需要记住拖的是哪一张卡片,因此我在 dragStart 的回调中保存了当前卡片的 ID。
而每个列组件则需要接收拖拽进入的卡片,并决定是否接受“放下”:
拖拽中最关键的一点是 “要阻止默认行为”,否则浏览器可能会把你的卡片当成文件来处理……
实现完拖拽,我试着把一张卡片从“待办”拖到“进行中”,它果然“滑”过去了!
五、状态管理:用 useState 管住整个世界
为了管理所有卡片的状态,我在 App 中使用了 useState
,并将 cards
结构设计为:
我封装了一个 moveCard
函数,每当 drop 事件发生,就更新目标卡片的状态。
这个函数简单得不能再简单,但却是整个看板流转的核心。
六、持久化数据:localStorage,小而美的解决方案
页面刷新后,数据还在吗?
起初当然是不在。于是我加了 localStorage 的支持,用 useEffect
实现了加载和保存逻辑。
到这一步,我的卡片可以自由拖动,状态自动保存,UI 美观舒适。我坐在屏幕前,看着这些小卡片整齐地排列在各自的列中,心里充满了成就感。
七、多看板切换:刷新思路的小锦囊
用着用着,我发现自己同时想管理学习任务和工作任务,又或者是生活中偶尔蹦出的点子。这时候,给项目加上 多看板切换 功能,就显得格外有必要。于是,我给数据结构里再包一层:
想法很简单:在最上面放一个看板列表,点击名字就切换。当页面加载时,我先从 localStorage
中读入 boards
,再根据 activeBoardId
渲染当前看板。
切换看板只要更新 activeBoardId
即可:
在界面上,我在标题右侧设计了一个下拉菜单,点开后列出所有看板。流程大致长这样:
当我第一次实现这部分时,下拉菜单点开后,看板名称覆盖在卡片上。有趣的是,这个问题让我想起了 CSS 层级(z-index)的小坑。我迅速给下拉做了一个 z-index: 1000
,而看板列的 z-index
只要保持默认就好,瞬间解决。就是这些看似不起眼的小细节,让我越写越上瘾。
八、卡片详情弹窗:信息展示与交互的最佳实践
长按卡片或者点击卡片上的「…」按钮,我希望弹出一个详情弹窗,让用户能看到更多信息,比如描述、截止日期、标签,甚至评论。于是,我先在 CardType
里加上新字段:
弹窗组件的设计思路
我的 Modal 组件非常简单,不引入额外库,用纯 React 实现。大致结构是一个半透明遮罩层,里头是一个居中的内容容器。遮罩层点击时,会自动关闭弹窗。
这里的关键在于 stopPropagation()
:防止点击内容区域时触发遮罩层的 onClick
,导致弹窗莫名其妙地关闭掉!
在样式里,我用了 position: fixed
让弹窗始终保持在视口正中,用 display: flex; align-items: center; justify-content: center;
来垂直水平居中。弹窗背景是纯白色,四周留有足够的内边距,还加了圆角和轻微的阴影,让它“悬浮”在看板之上。
打开与关闭流程
梳理整个弹窗逻辑流程:
当我第一次测试时,发现点击弹窗内部的按钮依然会关闭,这就是没有加 stopPropagation()
的后果。修复后,一切正常。
九、标签筛选:让信息不过载
随着卡片增多,有时候想只看特定类型的任务,比如“前端” 或者 “设计”。标签功能就派上用场了。我给每张卡片都关联一个字符串数组 tags
,例如 ['frontend', 'urgent']
。界面上,我在看板顶部再加一个标签筛选条,展示所有已存在的标签,点击某个标签,就只保留带有该标签的卡片。
实现逻辑其实也很直观:把 cards
先根据 status
分类,然后再每个列里做一次 filter(card => card.tags.includes(activeTag))
。我把这个筛选操作封装成一个 Hook,命名为 useFilteredCards
:
这样,列组件接收的 cards
就是已经处理好、按需展示的内容。我还实现了“清除筛选”按钮,一键恢复全部卡片的视图。
在实现标签列表时,我注意到了 React 渲染 key 的选择:如果直接用标签名称当 key,多个看板里可能会重复。于是,我拼接了看板 ID 和标签名,保证 key 的唯一性。每次点击某个标签,就调用 setActiveTag(tag)
;再次点击时,如果该标签已经处于激活状态,就清空 activeTag
。
十、UI 细节打磨:让设计更有温度
在做完功能之后,我开始关注一些小细节。比如:
- 过渡动画:卡片拖动时,列背景从浅灰变为淡蓝,给用户一点视觉反馈;弹窗打开时,加一个缩放渐变。
- 响应式布局:在窄屏幕下,三列会变成可横向滚动的卡片列表;在超大屏上,列宽也不会无限拉长,用最大宽度限制在 400px。
- 暗黑模式:我加了一个主题切换按钮,
data-theme
属性切换后,所有颜色都基于 CSS 变量调整。白天晚上都能舒服地看板。
Snippet 小片段来感受一下动画处理: