悬停:上一版靠「底色跳两档 + 四边边框一起提亮 + 外圈白光晕 + 白环」制造悬浮感, 五个变化同向叠加,结果是整张卡片均匀发亮、看着像换了张更浅的卡,光晕还把边缘糊住。 改用明暗方向表达厚度:新增受光层伪元素(顶部白 7% → 底部黑 14%,悬停淡入), 配一道锐利的接触阴影压实边界、一层远处柔化投影作高度暗示;底色与边框各只提一档。 过渡 150ms → 200ms ease-out。新增按下态:位移归零、阴影收紧、受光减弱,卡片「落回纸面」。 焦点:全局描边在深色下掺透明度柔化(sky-400 78%);卡片被 :focus-visible 时与悬停 一致地抬起并受光,焦点环退为最后一道提示,不再是一圈生硬方框。顺带删掉 :focus-visible 里的 border-radius: 4px —— 对带 rounded-* 的元素无效(layer 优先级), 对没圆角的元素反而会让聚焦瞬间跳一下。 动效:新增 pop-in(160ms) / sheet-in(260ms) / scrim-in(200ms) 三组关键帧, 下拉从锚点上方落入、抽屉自右侧滑入、遮罩淡入,组件上一律配 motion-reduce:animate-none。 只做入场不做退场:关闭是用户主动取消,瞬时消失更利落;做退场要引入 Vue Transition, 会牵扯 SideDrawer 的焦点与卸载时序,收益不抵风险。 实现要点: - Tailwind v4 会把 @theme 的阴影值内联进 --tw-shadow,覆盖 CSS 变量对已生成的工具类 无效,深色阴影因此直接重写 box-shadow,且规则不放进 @layer 以压过原子类; 选择器要按 hover: 变体转义后的类名书写(.hover\:shadow-card-hover),否则静默失效 - 焦点态位移不能只改 --tw-translate-y:translate 属性声明只存在于 :hover 那条原子类 规则里,焦点态不经过 :hover,必须直接写 translate - 受光层要画在内容之下,因此 TaskCard 的内容容器补了 relative(同为定位元素时按 DOM 顺序绘制,伪元素在前) 验证:vue-tsc -b 与 eslint src 干净,738 例全量单测通过;Playwright 读计算样式确认 动画时序(下拉 @30ms opacity .909、抽屉 translateX 214.77px、遮罩 opacity .419), 并用垂直亮度剖面确认卡片上亮下暗(内部亮度 33 → 20)。
看板 · 个人任务管理
一个轻量的个人看板工具:打开即用、数据存在本地、不依赖后端。
- 固定三列:待办 / 进行中 / 已完成
- 已完成列按完成时间倒序:最近完成的永远在最上面(列内不支持手动调序)
- 快速新增、拖拽排序、详情编辑、删除撤销;详情里可直接切换状态
- 批量添加:一次录入多行清单,整批共用目标列 / 优先级 / 标签 / 截止日期
- 批量编辑:多选后可统一移动列、改优先级、加/减标签、设截止日期、归档
- 回收站:删除是软删除,30 天内可随时恢复;也可永久删除或清空
- 搜索与筛选(关键词 / 优先级 / 标签 / 今日到期 / 逾期)
- 标签管理:全局重命名、合并、删除标签,不必逐个任务改
- 今日视图:按紧急度分组汇总(逾期 / 今天到期 / 进行中 / 待办),含本周完成等统计
- 键盘快捷键;任务卡片可用方向键换列与调序
- 深色模式:跟随系统 / 浅色 / 深色三态
- 导出导入 JSON(覆盖 / 合并两种模式)+ 启动自动备份;覆盖或合并前都会自动留档
- 已完成任务可批量归档,归档任务可在详情里随时恢复
- 任务时间:详情抽屉显示创建 / 完成 / 更新 / 归档时间;已完成卡片标注完成日期,悬停卡片可看精确时间
- 可安装为 PWA,支持离线使用
快速开始
npm install
npm run dev
打开终端提示的地址即可。首次进入是空看板,在任意列顶部的输入框里输入标题、回车即可创建任务。
批量添加、批量编辑与回收站
工具栏的「批量添加」可以一次录入多条任务:清单里一行一条,整段粘贴即可;行首的 -、*、1. 这类列表符号会自动去掉,空行会被忽略。
这一批共用同一个目标列、优先级、标签与截止日期 —— 先统一建出来,再单独调整个别任务。
不打开面板也行:在任意列的快速新增输入框里粘贴多行文本,会按行直接拆成多条任务(插入该列列首);单行粘贴仍是常规输入,不会被拦截。
要批量处理,点工具栏的「多选」:卡片变成可勾选的方框,点哪张选哪张,操作条上可以「全选 N 个」(只覆盖当前筛选后可见的任务)或「清空选择」。第二行是批量编辑器:
- 移动到:统一换到目标列,追加到该列末尾
- 优先级:低 / 中 / 高
- 标签:输入后「添加」或「移除」—— 是增删,不会覆盖每条任务原有的其它标签
- 截止日:可设置,也可清空
- 归档:批量归档,保留撤销入口
批量删除与批量归档仍提供 Toast 撤销;批量改属性不做撤销(与详情编辑一致),只提示「已更新 N 个任务」。
删除是软删除:任务进入顶部导航的「回收站」,保留 30 天后在下次打开时自动清理。回收站里可以逐条「恢复」或「永久删除」,也可以「清空回收站」(二次确认)。恢复会回到原列、原顺序,已归档的任务恢复后仍保持归档态。
多选期间拖拽会停用,避免顺手拖乱了顺序;筛选条件一变也会清空选择,保证「改掉的都是眼前看得见的」。
标签管理
标签下拉面板底部有「管理标签…」,打开后可以:
- 查看每个标签被多少任务使用(只统计未归档、未删除的任务)
- 重命名:所有任务上的该标签一次性替换,其它标签不受影响
- 合并:把两个标签并成一个,任务上的标签自动去重
- 删除:只从任务上移除标签,不删除任务
操作后会提示影响了多少个任务。回收站里的任务不参与这些改写。
脚本
| 命令 | 说明 |
|---|---|
npm run dev |
启动开发服务器 |
npm run build |
类型检查 + 生产构建,产物在 dist/ |
npm run preview |
本地预览生产构建 |
npm run typecheck |
只做类型检查 |
npm run test |
单元测试(Vitest) |
npm run test:coverage |
单元测试 + 覆盖率报告 |
npm run test:e2e |
端到端测试(Playwright,默认驱动系统已安装的 Chrome) |
node scripts/screenshot.mjs |
生成界面截图(需先起 dev server),产物在 .screenshots/ |
npm run lint / npm run format |
代码检查与格式化 |
快捷键
| 按键 | 作用 |
|---|---|
N |
聚焦「待办」列的快速新增输入框 |
/ |
聚焦搜索框 |
? |
打开快捷键帮助 |
Esc |
关闭当前面板 |
卡片获得焦点(Tab 或点击)后:
| 按键 | 作用 |
|---|---|
← → |
把卡片移到上一列 / 下一列 |
↑ ↓ |
在列内上移 / 下移 |
Enter |
打开任务详情 |
在输入框内输入时不会触发全局快捷键。方向键移动在筛选态下只允许跨列(列内排序需要看到完整列表才有意义);
「已完成」列的列内顺序由完成时间决定,↑ ↓ 在该列不生效(拖拽同理只能跨列)。
两个视图
顶部导航可在两个视图之间切换:
| 视图 | 路径 | 组织方式 |
|---|---|---|
| 看板 | / |
按状态:待办 / 进行中 / 已完成 |
| 今日 | /today |
按紧急度:已逾期 → 今天到期 → 进行中 → 待办 |
今日视图顶部有四张统计卡片(逾期 / 今天到期 / 进行中 / 本周完成),下方是可直接勾选完成的待办列表。
分组是互斥的:一张卡片只会出现在一组里,归属优先级为「逾期 > 今天到期 > 进行中 > 待办」,所以你能一眼数清今天到底有几件事。已完成和已归档的任务不会出现在今日视图。
只要任务未完成且未归档,就一定会出现在某个分组里 —— 包括没设截止日期的待办(归入「待办」组)。
安装与离线(PWA)
应用可以像原生应用一样安装到桌面或手机主屏,安装后支持完全离线使用 —— Service Worker 会预缓存页面与静态资源,断网后照常打开,数据本来就在本地。
- 桌面 Chrome / Edge:地址栏右侧会出现安装图标
- 手机:浏览器菜单 →「添加到主屏幕」
- 安装后长按图标有「今日」快捷方式,直达
/today
更新策略是 prompt:检测到新版本时会在底部弹出提示,由你决定何时刷新,不会打断正在进行的编辑。
图标由 scripts/generate-icons.py(Pillow)生成,需要改图标时运行:
python scripts/generate-icons.py
数据与备份(重要)
数据只保存在你这台设备的浏览器里(localStorage,键名 kanban.tasks)。没有服务端,也不会自动同步到其它设备。
这意味着以下操作会导致数据丢失:
- 清理浏览器数据 / 缓存
- 使用隐私模式
- 换浏览器或换设备
- Safari 的 7 天未访问存储清理策略
因此工具内置了三道保护:
- 启动自动备份:每次打开看板时留一份快照,最多保留最近 3 份,可在「备份」面板中一键恢复。
- 覆盖前自动留档:导入 JSON 或从快照恢复会整份替换数据,执行前会先把当前数据写一份快照,选错文件或选错备份也能退回去。
- 手动导出:「备份」面板 → 导出为 JSON,文件名形如
kanban-2026-10-18.json。
导入支持两种模式,选中文件后会先校验并显示预览,需要二次确认才会执行:
- 覆盖导入:替换当前全部数据,并显示将被覆盖的任务数量
- 合并导入:按
id合并,同一条任务以updatedAt较新的一方为准;文件里新增的任务追加到对应列末尾,本地独有的任务保留(合并不是同步,不会删本地数据)。预览会显示「新增 X 个 / 更新 Y 个 / 保持 Z 个」
两种模式在确认前都会自动把当前数据写一份快照;文件不合法时不会改动任何现有数据。导出默认包含回收站里的任务,这样换设备后回收站也能一并带走。
任务详情里的「状态」可以直接切换待办 / 进行中 / 已完成,不必回到看板拖拽。编辑后如果没保存就关闭,抽屉会先提醒,不会静默丢弃草稿。
抽屉底部的「时间」区块是只读的:创建 与 更新 一律显示;切到「已完成」并保存后写入 完成 时间(未完成时标注「未完成」);归档任务额外显示归档时间。看板上已完成的卡片会在右下角标出完成日期,鼠标悬停任意卡片可看到精确到分钟的时间;回收站条目则同时给出创建时间与删除时间。
如果需要在多设备间同步,需要自行接入后端;
TaskRepository接口就是为此预留的扩展点(见下)。
部署
产物是纯静态文件,把 dist/ 目录交给任意静态托管即可(Nginx、Vercel、Netlify、GitHub Pages 等)。
npm run build
# 将 dist/ 上传或指向静态服务器
部署到云服务器(当前实际架构)
服务器由 1Panel 管理,80/443 已被 Nginx Proxy Manager 占用,所以站点不装宿主机 Nginx,
而是跑一个 nginx:alpine 容器,由 NPM 反代:
浏览器 → NPM(80/443, 证书) → kanban 容器(:80) → /opt/kanban/dist
↑
CI runner 构建后 rsync 到这里(原地同步)
| 文件 | 用途 |
|---|---|
deploy/nginx.docker.conf |
容器内站点配置:SPA 回落、sw.js 不缓存、/assets 长缓存、gzip |
deploy/gitea-runner.config.yaml |
runner 配置:host 模式、ubuntu-latest 标签 |
deploy/gitea-runner.service |
runner 的 systemd 单元 |
deploy/deploy.sh |
不走 CI 时的手动发布脚本 |
deploy/nginx.conf + server-init.sh |
备选路径:80 端口空闲、直接用宿主机 Nginx 的机器 |
容器启动命令(已在服务器上运行,重建时照抄):
docker run -d --name kanban --restart unless-stopped \
--network 1panel-network \
-p 127.0.0.1:8081:80 \
-v /opt/kanban/dist:/usr/share/nginx/html:ro \
-v /opt/kanban/nginx.conf:/etc/nginx/conf.d/default.conf:ro \
nginx:alpine
反向代理:NPM 后台 → Hosts → Proxy Hosts → Add
| 字段 | 值 |
|---|---|
| Domain Names | kanban.shoulin.net |
| Scheme | http |
| Forward Hostname | 172.17.0.1 |
| Forward Port | 8081 |
其余站点都走 172.17.0.1:端口,这里保持一致(用容器名 kanban:80 实测也可通,但容器重建后 IP 可能变)。
SSL 页签挂证书并开启 Force SSL,域名解析到服务器 IP 即可。
手动发布(CI 不可用时):
SERVER=root@132.232.203.88 WEB_ROOT=/opt/kanban/dist ./deploy/deploy.sh
注意:
/opt/kanban/dist是容器挂载目录,不能整目录mv——bind mount 跟的是 inode, 换目录后容器仍指向旧内容。deploy.sh与 CI 都用 rsync 原地同步,就是为了避开这个坑。
HTTPS 不是可选项:Service Worker 只在 HTTPS 或 localhost 下注册成功。用 IP 走 HTTP 访问时, 应用能打开,但「安装到桌面 / 离线可用 / 版本更新提示」全部失效。证书在 NPM 里配置。
其它静态托管
Netlify / Vercel 等平台,在 dist/ 同级放一个 _redirects 或配置即可:
/* /index.html 200
注意:应用使用前端路由(
/today),因此必须配置 SPA 回落,否则直接访问或刷新/today会 404。 这不是可选项。若你的托管环境无法配置回落,可以改用 hash 路由(把src/router/index.ts里的createWebHistory()换成createWebHashHistory()),代价是 URL 变成/#/today。
Service Worker 的 navigateFallback 已设为 index.html,因此离线状态下深链 /today 也能正常打开,
无需服务器参与。
技术栈
Vue 3 · TypeScript · Vite · Pinia · Vue Router · Tailwind CSS v4 · vue-draggable-plus (SortableJS) · dayjs · vite-plugin-pwa (Workbox) · Vitest · Playwright
目录结构
src/
├── components/ # 看板列、卡片、抽屉、工具栏、导航、PWA 提示等 UI 组件
├── composables/ # 可复用的组合式逻辑(键盘快捷键)
├── data/ # 持久化:schema 与迁移、仓库实现、快照
├── router/ # 路由定义(看板 / 今日)
├── stores/ # Pinia:任务、筛选、提示
├── types/ # 领域类型与常量
├── utils/ # 纯函数:排序、筛选、日期、归档、拖拽解析、今日分组、统计、任务补丁、合并、标签
└── views/ # 页面级组件(BoardView / TodayView)
docs/
├── adr.md # 架构决策记录
├── tasks.md # 开发任务卡(含验收标准)
└── tasks.json # 可导入看板的种子数据
e2e/ # Playwright 端到端测试
scripts/ # 图标生成等辅助脚本
.gitea/workflows/ # CI(lint / typecheck / test / build)
持续集成与自动部署
仓库内置 .gitea/workflows/ci.yml,分两个 job:
| Job | 触发条件 | 做什么 |
|---|---|---|
verify |
push 与合并请求 | npm ci → lint → typecheck → test → build |
deploy |
push 到 master / main 且 verify 通过 |
重新构建 → rsync 到 /opt/kanban/dist → 线上自检 |
合并请求只跑校验、不部署。部署用 rsync -a --delete 原地同步,失败时旧文件仍在,不会把站点弄空;
同步完会自检根路径、/today 回落与 sw.js,任一异常就让 job 变红。
Runner(自托管,已部署)
服务器 132.232.203.88 上的 tencent-sh-1,host 模式,systemd 常驻:
systemctl status gitea-runner
journalctl -u gitea-runner -n 50 --no-pager
配置见 deploy/gitea-runner.config.yaml 与 deploy/gitea-runner.service,几个要点:
- 标签
ubuntu-latest:host,任务直接在宿主机执行,不再套一层容器 DEFAULT_ACTIONS_URL=https://gitea.com—— 这台机器访问 github.com 不通,action 必须换源- 宿主机 Node 22 在
/opt/node22,软链到/usr/local/bin;npm 源指向registry.npmmirror.com - runner 与站点同机,部署直接写本地目录,不需要配置任何 Secrets
为什么不用 SSH 部署
runner 与站点同一台机器,绕一圈 SSH 只是多一个故障点、多一份私钥要管。
真要跨机部署,用 deploy/deploy.sh 的 SERVER=... WEB_ROOT=... 形式即可。
E2E 需要真实浏览器,默认不放进 CI(要求 runner 预装 Chrome);请在本地或用带浏览器的 runner 执行 npm run test:e2e。
架构要点
- 单一数据源:Pinia store 是内存态的唯一来源,所有变更经由
TaskRepository落盘;拖拽也只消费事件,不接管数据。看板与今日视图共享同一份数据。 - 持久化端口:
TaskRepository采用「整份快照读写」(load/save),而非细粒度 CRUD。要换成 IndexedDB 或后端 API,只需实现同一接口。 - 显式迁移:落盘结构带
schemaVersion,升级规则登记在MIGRATIONS表中。当前版本为 3(v1 → v2 补archivedAt,v2 → v3 补deletedAt以支持回收站)。 - 软删除是数据状态,不是筛选维度:
deletedAt在activeTasks层统一剔除,列、计数、统计、今日视图、标签下拉都消费它;回收站单独由trashTasks提供。 - 损坏数据先备份再报错:读取失败时把原始内容另存到
kanban.tasks.corrupt.<时间戳>,不覆盖用户数据。 - 视图与数据解耦:今日视图只做「分组 + 排序」,全部逻辑在
src/utils/today.ts与stats.ts的纯函数里,组件不持有业务规则。
更多取舍与理由见 docs/adr.md。
测试
- 单元测试:738 个用例,覆盖
src/下的纯逻辑与组件行为。 - 端到端测试:28 个用例,分两组运行 —— 交互类跑 dev server,PWA 类跑生产构建(Service Worker 只在构建产物中生成)。覆盖「新增 → 拖拽 → 完成 → 刷新保持」「批量添加」「多选批量删除 + 撤销」「导出 → 清空 → 导入还原」「删除撤销」「搜索筛选」「键盘快捷键与方向键移动」「深色模式切换」「今日视图」「离线打开」等主链路,每个用例使用独立的浏览器上下文。
npm run test:coverage
npm run test:e2e
关于 E2E 使用的浏览器
默认驱动系统已安装的 Chrome(channel: 'chrome'),不下载 Playwright 自带的 Chromium —— 后者约 197MB,在网络受限的环境下容易超时失败。
如需改用其它通道:
PLAYWRIGHT_CHANNEL=msedge npm run test:e2e # 用系统 Edge
PLAYWRIGHT_CHANNEL=chromium npm run test:e2e # 用自带 Chromium(需先 npx playwright install chromium)
已知限制
- 单用户、单设备,无协作与同步能力(离线可用,但多设备之间不会同步)。
- 看板列固定为三列,不支持自定义。
- 「已完成」列固定按完成时间倒序,不支持手动调序(拖拽与方向键只能跨列)。
- 归档阈值固定为「已完成超过 7 天」,暂不可配置。
- 今日视图的分组规则固定(逾期 > 今天到期 > 进行中 > 待办),不可自定义。
- 统计只覆盖本周完成数等基础指标,没有趋势图或历史留存。
- 深色模式只跟随系统或手动三态切换,没有按时间段自动切换。
- 回收站固定保留 30 天,没有搜索 / 筛选,只有「按删除时间」一种顺序。
- 批量改属性不提供撤销(标签是增删语义,误改可以再改回来);导入冲突策略固定为「
updatedAt新者胜」,不可配置。 - 标签还没有颜色,重命名 / 合并会同步落盘但不会保留旧名映射。