# 看板 · 个人任务管理 一个轻量的个人看板工具:打开即用、数据存在本地、不依赖后端。 - 固定三列:**待办 / 进行中 / 已完成** - **已完成列按完成时间倒序**:最近完成的永远在最上面(列内不支持手动调序) - 快速新增、拖拽排序、详情编辑、删除撤销;详情里可直接切换状态 - **全局撤销 / 重做**:`Ctrl+Z` 回退最近 50 步操作,`Ctrl+Shift+Z` 重做;批量改属性、批量归档、标签重命名都能回退 - **批量添加**:一次录入多行清单,整批共用目标列 / 优先级 / 标签 / 截止日期 - **批量编辑**:多选后可统一移动列、改优先级、加/减标签、设截止日期、归档 - **回收站**:删除是软删除,30 天内可随时恢复;也可永久删除或清空 - 搜索与筛选(关键词 / 优先级 / 标签 / 今日到期 / 逾期) - **标签管理**:全局重命名、合并、删除标签,不必逐个任务改 - **标签颜色**:8 色色板,设过颜色的标签在卡片与筛选下拉里带一个圆点;重命名 / 合并时颜色自动跟随 - **今日视图**:按紧急度分组汇总(逾期 / 今天到期 / 进行中 / 待办),含本周完成等统计 - **统计视图**:近一年完成热力图(按当天完成数分五档着色)、连续完成天数、本月与累计完成,附带标签分布与完成周期 - 键盘快捷键;任务卡片可用方向键换列与调序 - **深色模式**:跟随系统 / 浅色 / 深色三态 - 导出导入 JSON(**覆盖 / 合并**两种模式)+ 启动自动备份;覆盖或合并前都会自动留档 - 已完成任务可批量归档,归档任务可在详情里随时恢复 - **任务时间**:详情抽屉显示创建 / 完成 / 更新 / 归档时间;已完成卡片标注完成日期,悬停卡片可看精确时间 - **可安装为 PWA,支持离线使用** --- ## 快速开始 ```bash npm install npm run dev ``` 打开终端提示的地址即可。首次进入是空看板,在任意列顶部的输入框里输入标题、回车即可创建任务。 ## 批量添加、批量编辑与回收站 工具栏的「批量添加」可以一次录入多条任务:清单里**一行一条**,整段粘贴即可;行首的 `-`、`*`、`1.` 这类列表符号会自动去掉,空行会被忽略。 这一批共用同一个目标列、优先级、标签与截止日期 —— 先统一建出来,再单独调整个别任务。 不打开面板也行:在任意列的快速新增输入框里**粘贴多行文本**,会按行直接拆成多条任务(插入该列列首);单行粘贴仍是常规输入,不会被拦截。 要批量处理,点工具栏的「多选」:卡片变成可勾选的方框,点哪张选哪张,操作条上可以「全选 N 个」(只覆盖**当前筛选后可见**的任务)或「清空选择」。第二行是批量编辑器: - **移动到**:统一换到目标列,追加到该列末尾 - **优先级**:低 / 中 / 高 - **标签**:输入后「添加」或「移除」—— 是增删,不会覆盖每条任务原有的其它标签 - **截止日**:可设置,也可清空 - **归档**:批量归档,保留撤销入口 批量删除与批量归档仍提供 Toast 撤销;批量改属性不做撤销(与详情编辑一致),只提示「已更新 N 个任务」。 删除是**软删除**:任务进入顶部导航的「回收站」,保留 30 天后在下次打开时自动清理。回收站里可以逐条「恢复」或「永久删除」,也可以「清空回收站」(二次确认)。恢复会回到原列、原顺序,已归档的任务恢复后仍保持归档态。 多选期间拖拽会停用,避免顺手拖乱了顺序;筛选条件一变也会清空选择,保证「改掉的都是眼前看得见的」。 ## 标签管理 标签下拉面板底部有「管理标签…」,打开后可以: - 查看每个标签被多少任务使用(只统计未归档、未删除的任务) - **重命名**:所有任务上的该标签一次性替换,其它标签不受影响 - **合并**:把两个标签并成一个,任务上的标签自动去重 - **删除**:只从任务上移除标签,不删除任务 操作后会提示影响了多少个任务。回收站里的任务不参与这些改写。 ### 标签颜色 标签默认**不带颜色** —— 没设过就不画圆点,避免每个标签都顶着一个灰点。 在标签管理面板里给标签点一个色块即可(8 色,另有虚线圆点表示「清除颜色」)。颜色会同步出现在看板卡片和标签筛选下拉里。重命名或合并标签时颜色自动跟着走,不会出现「改名之后突然掉色」。 颜色是**本机显示偏好**,存在 `kanban.tagColors`,不进入任务数据:导出 JSON 与快照恢复都不会带上它,换设备后需要重新设置。这样「怎么显示」不会污染「有哪些任务」,导入别人的数据时也不会把颜色映射一起覆盖掉。 ## 脚本 | 命令 | 说明 | | --------------------------------- | --------------------------------------------------------- | | `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` | 关闭当前面板 | | `Ctrl+Z` | 撤销上一步操作(macOS 用 `⌘Z`) | | `Ctrl+Shift+Z` | 重做被撤销的操作(`Ctrl+Y` 同样可用) | 撤销历史只保留在内存里:刷新页面或换一个标签页即清空。在输入框内按 `Ctrl+Z` 撤销的是**文字输入**,不是任务操作。 卡片获得焦点(`Tab` 或点击)后: | 按键 | 作用 | | ------- | ------------------------- | | `←` `→` | 把卡片移到上一列 / 下一列 | | `↑` `↓` | 在列内上移 / 下移 | | `Enter` | 打开任务详情 | 在输入框内输入时不会触发全局快捷键。方向键移动在筛选态下只允许跨列(列内排序需要看到完整列表才有意义); 「已完成」列的列内顺序由完成时间决定,`↑` `↓` 在该列不生效(拖拽同理只能跨列)。 --- ## 三个视图 顶部导航可在三个视图之间切换: | 视图 | 路径 | 组织方式 | | ---- | -------- | ----------------------------------------------- | | 看板 | `/` | 按**状态**:待办 / 进行中 / 已完成 | | 今日 | `/today` | 按**紧急度**:已逾期 → 今天到期 → 进行中 → 待办 | | 统计 | `/stats` | 按**时间**:近一年的每日完成量 | 今日视图顶部有四张统计卡片(逾期 / 今天到期 / 进行中 / 本周完成),下方是可直接勾选完成的待办列表。 分组是**互斥**的:一张卡片只会出现在一组里,归属优先级为「逾期 > 今天到期 > 进行中 > 待办」,所以你能一眼数清今天到底有几件事。已完成和已归档的任务不会出现在今日视图。 只要任务未完成且未归档,就一定会出现在某个分组里 —— 包括没设截止日期的待办(归入「待办」组)。 ### 统计视图 今日视图往前看(今天该做什么),统计视图往回看(这些天做过什么)。 - **顶部四张卡片**:今天完成 / 连续完成天数 / 本月完成 / 累计完成 - **完成热力图**:近一年(53 周),一列一周、一行一天,颜色分五档(0 / 1 / 2 / 3 / 4 个及以上)。悬停任意格子可看当天日期与完成数;今天那格带一圈描边 - **最忙的一天**:完成数最多的一天(并列时取最近的) - **时间花在哪**:已完成任务按标签分布(前 6 项,条形长度按最大项归一)。一个任务带多个标签时每个标签各计一次,没有标签的归入「未分类」 - **一些细节**:平均完成周期(中位数)/ 最长连续 / 有记录天数 / 平均每天 「连续完成」允许今天还没开始:只要昨天有记录就继续算,不会在零点一过就把成绩清零。 平均完成周期取**中位数**而不是平均值:一条躺了半年的任务就能把平均值拉偏到没法看,中位数描述的是「通常要多久」。不足一天显示「当天」。 「平均每天」按**有记录的天数**算,不是按自然日 —— 否则周末不干活会被算成效率下降,而这只是没上班。 热力图只根据任务上的 `completedAt` 推算,**不额外维护计数表**。代价是永久删除或清空回收站会连同那天的记录一起消失,所以这里的数字通常比实际做过的偏少 —— 换来的是「数字永远和任务对得上」,不会出现恢复一条任务后计数却对不上的情况。 --- ## 安装与离线(PWA) 应用可以像原生应用一样安装到桌面或手机主屏,安装后支持**完全离线**使用 —— Service Worker 会预缓存页面与静态资源,断网后照常打开,数据本来就在本地。 - 桌面 Chrome / Edge:地址栏右侧会出现安装图标 - 手机:浏览器菜单 →「添加到主屏幕」 - 安装后长按图标有「今日」快捷方式,直达 `/today` 更新策略是 **prompt**:检测到新版本时会在底部弹出提示,由你决定何时刷新,不会打断正在进行的编辑。 图标由 `scripts/generate-icons.py`(Pillow)生成,需要改图标时运行: ```bash python scripts/generate-icons.py ``` --- ## 数据与备份(重要) **数据只保存在你这台设备的浏览器里**(localStorage,键名 `kanban.tasks`)。没有服务端,也不会自动同步到其它设备。 这意味着以下操作会导致数据丢失: - 清理浏览器数据 / 缓存 - 使用隐私模式 - 换浏览器或换设备 - Safari 的 7 天未访问存储清理策略 因此工具内置了三道保护: 1. **启动自动备份**:每次打开看板时留一份快照,最多保留最近 3 份,可在「备份」面板中一键恢复。 2. **覆盖前自动留档**:导入 JSON 或从快照恢复会整份替换数据,执行前会先把当前数据写一份快照,选错文件或选错备份也能退回去。 3. **手动导出**:「备份」面板 → 导出为 JSON,文件名形如 `kanban-2026-10-18.json`。 导入支持两种模式,选中文件后会先校验并显示预览,需要二次确认才会执行: - **覆盖导入**:替换当前全部数据,并显示将被覆盖的任务数量 - **合并导入**:按 `id` 合并,同一条任务以 `updatedAt` 较新的一方为准;文件里新增的任务追加到对应列末尾,本地独有的任务保留(合并不是同步,不会删本地数据)。预览会显示「新增 X 个 / 更新 Y 个 / 保持 Z 个」 两种模式在确认前都会自动把当前数据写一份快照;文件不合法时不会改动任何现有数据。导出默认包含回收站里的任务,这样换设备后回收站也能一并带走。 任务详情里的「状态」可以直接切换待办 / 进行中 / 已完成,不必回到看板拖拽。编辑后如果没保存就关闭,抽屉会先提醒,不会静默丢弃草稿。 抽屉底部的「时间」区块是只读的:**创建** 与 **更新** 一律显示;切到「已完成」并保存后写入 **完成** 时间(未完成时标注「未完成」);归档任务额外显示归档时间。看板上已完成的卡片会在右下角标出完成日期,鼠标悬停任意卡片可看到精确到分钟的时间;回收站条目则同时给出创建时间与删除时间。 > 如果需要在多设备间同步,需要自行接入后端;`TaskRepository` 接口就是为此预留的扩展点(见下)。 --- ## 部署 产物是纯静态文件,把 `dist/` 目录交给任意静态托管即可(Nginx、Vercel、Netlify、GitHub Pages 等)。 ```bash 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 的机器 | **容器启动命令**(已在服务器上运行,重建时照抄): ```bash 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 不可用时): ```bash 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 常驻: ```bash 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](docs/adr.md)。 ## 测试 - 单元测试:738 个用例,覆盖 `src/` 下的纯逻辑与组件行为。 - 端到端测试:28 个用例,分两组运行 —— 交互类跑 dev server,PWA 类跑生产构建(Service Worker 只在构建产物中生成)。覆盖「新增 → 拖拽 → 完成 → 刷新保持」「批量添加」「多选批量删除 + 撤销」「导出 → 清空 → 导入还原」「删除撤销」「搜索筛选」「键盘快捷键与方向键移动」「深色模式切换」「今日视图」「离线打开」等主链路,每个用例使用独立的浏览器上下文。 ```bash npm run test:coverage npm run test:e2e ``` ### 关于 E2E 使用的浏览器 默认驱动**系统已安装的 Chrome**(`channel: 'chrome'`),不下载 Playwright 自带的 Chromium —— 后者约 197MB,在网络受限的环境下容易超时失败。 如需改用其它通道: ```bash PLAYWRIGHT_CHANNEL=msedge npm run test:e2e # 用系统 Edge PLAYWRIGHT_CHANNEL=chromium npm run test:e2e # 用自带 Chromium(需先 npx playwright install chromium) ``` ## 已知限制 - 单用户、单设备,无协作与同步能力(离线可用,但多设备之间不会同步)。 - 看板列固定为三列,不支持自定义。 - 「已完成」列固定按完成时间倒序,不支持手动调序(拖拽与方向键只能跨列)。 - 归档阈值固定为「已完成超过 7 天」,暂不可配置。 - 今日视图的分组规则固定(逾期 > 今天到期 > 进行中 > 待办),不可自定义。 - 统计只根据任务上的完成时间推算,永久删除或清空回收站会带走相应记录,因此数字比实际偏少。 - 统计没有按优先级拆分,也不支持自定义时间范围。 - 统计的标签分布按「每个标签各计一次」汇总,因此各项之和会大于任务数(多标签任务对多个方向都算贡献)。 - 深色模式只跟随系统或手动三态切换,没有按时间段自动切换。 - 回收站固定保留 30 天,没有搜索 / 筛选,只有「按删除时间」一种顺序。 - 撤销栈只保留最近 50 步,且仅存在于当前会话与当前标签页:刷新或换标签页即清空,不跨设备同步;另一标签页改动数据后,本页的历史会作废。 - 导入冲突策略固定为「`updatedAt` 新者胜」,不可配置。 - 标签颜色是本机显示偏好,不随导出 JSON 或快照恢复携带,换设备后需要重新设置。