From 51235a59a2fb0bc03bf73f4d7c8561600be1e8d4 Mon Sep 17 00:00:00 2001 From: chenshoulin Date: Thu, 22 Oct 2026 13:15:25 +0800 Subject: [PATCH] =?UTF-8?q?feat(board):=20=E5=B7=B2=E5=AE=8C=E6=88=90?= =?UTF-8?q?=E5=88=97=E6=8C=89=E5=AE=8C=E6=88=90=E6=97=B6=E9=97=B4=E5=80=92?= =?UTF-8?q?=E5=BA=8F=E6=8E=92=E5=88=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 已完成列的展示顺序改由 completedAt 倒序决定(新增 sortCompletedDesc 纯函数), 最近完成的永远在最上面;缺 completedAt 的旧数据排在有完成时间的任务之后, 彼此退回 sortOrder 倒序 - 该列停用列内排序:拖拽与 ↑↓ 只能跨列,列头标注「按完成时间倒序」说明口径 - 数据层不动:不迁移 sortOrder,导入导出、快照结构与 schemaVersion 均不变, 倒序只在 BoardView.visibleColumns 单点发生 - 新增 ADR-009,并在 ADR-008 的否决项里标注被部分推翻 - 单测 705 → 738(sortOrder +5、BoardView +5、BoardColumn +1),README 同步 --- README.md | 7 +++- docs/adr.md | 40 +++++++++++++++++++ src/components/BoardColumn.spec.ts | 18 +++++++++ src/components/BoardColumn.vue | 8 ++++ src/utils/sortOrder.spec.ts | 53 +++++++++++++++++++++++++ src/utils/sortOrder.ts | 23 +++++++++++ src/views/BoardView.spec.ts | 63 ++++++++++++++++++++++++++++++ src/views/BoardView.vue | 21 ++++++++-- 8 files changed, 228 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index eafae70..3f24eb7 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ 一个轻量的个人看板工具:打开即用、数据存在本地、不依赖后端。 - 固定三列:**待办 / 进行中 / 已完成** +- **已完成列按完成时间倒序**:最近完成的永远在最上面(列内不支持手动调序) - 快速新增、拖拽排序、详情编辑、删除撤销;详情里可直接切换状态 - **批量添加**:一次录入多行清单,整批共用目标列 / 优先级 / 标签 / 截止日期 - **批量编辑**:多选后可统一移动列、改优先级、加/减标签、设截止日期、归档 @@ -92,7 +93,8 @@ npm run dev | `↑` `↓` | 在列内上移 / 下移 | | `Enter` | 打开任务详情 | -在输入框内输入时不会触发全局快捷键。方向键移动在筛选态下只允许跨列(列内排序需要看到完整列表才有意义)。 +在输入框内输入时不会触发全局快捷键。方向键移动在筛选态下只允许跨列(列内排序需要看到完整列表才有意义); +「已完成」列的列内顺序由完成时间决定,`↑` `↓` 在该列不生效(拖拽同理只能跨列)。 --- @@ -316,7 +318,7 @@ E2E 需要真实浏览器,默认不放进 CI(要求 runner 预装 Chrome) ## 测试 -- 单元测试:705 个用例,覆盖 `src/` 下的纯逻辑与组件行为。 +- 单元测试:738 个用例,覆盖 `src/` 下的纯逻辑与组件行为。 - 端到端测试:28 个用例,分两组运行 —— 交互类跑 dev server,PWA 类跑生产构建(Service Worker 只在构建产物中生成)。覆盖「新增 → 拖拽 → 完成 → 刷新保持」「批量添加」「多选批量删除 + 撤销」「导出 → 清空 → 导入还原」「删除撤销」「搜索筛选」「键盘快捷键与方向键移动」「深色模式切换」「今日视图」「离线打开」等主链路,每个用例使用独立的浏览器上下文。 ```bash @@ -339,6 +341,7 @@ PLAYWRIGHT_CHANNEL=chromium npm run test:e2e # 用自带 Chromium(需先 npx - 单用户、单设备,无协作与同步能力(离线可用,但多设备之间不会同步)。 - 看板列固定为三列,不支持自定义。 +- 「已完成」列固定按完成时间倒序,不支持手动调序(拖拽与方向键只能跨列)。 - 归档阈值固定为「已完成超过 7 天」,暂不可配置。 - 今日视图的分组规则固定(逾期 > 今天到期 > 进行中 > 待办),不可自定义。 - 统计只覆盖本周完成数等基础指标,没有趋势图或历史留存。 diff --git a/docs/adr.md b/docs/adr.md index 2cc78d2..a7036e2 100644 --- a/docs/adr.md +++ b/docs/adr.md @@ -232,6 +232,7 @@ MVP 有「启动快照 + 手动导出」两道保护,但两个整份覆盖的 创建时间对「先做哪件」几乎没有影响,放进 `title` 与详情抽屉足够。 - _增加「按创建 / 完成时间排序」开关_:完成列的顺序由手动 `sortOrder` 表达,再引入第二种排序会让 「看到的顺序」与「拖拽写回的顺序」产生两个真相;当前也没有对应使用场景。 + (其中「完成列按完成时间排序」后来被 [ADR-009](#adr-009--已完成列改按完成时间倒序展示) 部分推翻。) - _用相对时间(「3 天前」)_:文案依赖渲染时刻,页面开一整天就会和实际完成日对不上; 复盘场景下具体日期更有用。 - _提升 `schemaVersion` 做迁移_:字段早已存在,且 `coerceTask` 已把它们收敛为字符串或 `null`,无需迁移。 @@ -242,3 +243,42 @@ MVP 有「启动快照 + 手动导出」两道保护,但两个整份覆盖的 拖拽换列走 `moveTask`,两者规则一致(进入「已完成」写时间、离开即清空),展示值与状态不会互相矛盾。 - 新增两个纯函数与对应单测;`TrashPanel` 少了一个私有格式化函数。 - 卡片 `title` 是纯提示信息,不参与可访问性命名(`aria-label` 仅在多选态给出),读屏语义不变。 + +--- + +## ADR-009 · 已完成列改按完成时间倒序展示 + +**背景** + +ADR-008 否掉了「完成列按时间排序」,理由是维持单一顺序来源。但实际使用中这个取舍站不住: +完成任务时 `sortOrder` 是「追加到列尾」,于是**刚勾完的任务沉到列表最底下** —— +打开看板想问「我刚才做完什么」,得滚到列底;做完几十条之后,已完成列变成一条只有 +「按完成先后升序」的考古记录。对已完成的任务来说,用户唯一关心的顺序就是完成时间。 + +**结论** + +1. 已完成列的展示顺序由 `completedAt` 倒序决定,不再消费 `sortOrder`。 + 排序键收敛为一个纯函数 `sortCompletedDesc`(`src/utils/sortOrder.ts`), + 转换只在 `BoardView` 的 `visibleColumns` 单点发生,store 与数据层语义不变。 +2. 该列停用列内排序:拖拽与 `↑` `↓` 只允许跨列(`sortDisabled` 对 `done` 恒为 true), + 避免写出「永远看不见结果」的 `sortOrder`。 +3. 列头标注「按完成时间倒序」,把口径写在界面上,用户不会误以为拖不动是故障。 +4. 不做数据迁移:`sortOrder`、导入导出、快照结构与 `schemaVersion` 全都不动。 + `completedAt` 缺失的旧数据(手工导入、更早的备份)排在有完成时间的任务之后, + 彼此之间退回 `sortOrder` 倒序,至少保持「新完成的在上面」。 + +**被否决的方案** + +- _一次性按完成时间重排 `sortOrder`,新完成插列首_:需要迁移存量数据,且用户只要拖过一次, + 顺序就再次和时间脱钩 —— 等于保留两个真相,还多了写操作。 +- _只改插入位置(新完成插列首)_:改动最小,但存量列表仍是旧的升序,首屏出现 + 「新任务在顶、旧任务按升序续在后面」的断裂顺序。 +- _加排序开关_:一个开关多一份状态、多一组需要测试的组合,而「已完成」只有一种合理顺序。 + +**影响** + +- 落入已完成列的落点不再由拖拽位置决定(`completedAt` 刚写入即最新),这是预期行为。 +- `store.byColumn.done` 仍是 `sortOrder` 语义,只有视图层按时间倒序;两者差异集中在 + `BoardView.visibleColumns`,测试覆盖了「顺序与 `sortOrder` 无关」这一条。 +- 已完成列成为只读顺序:拖拽、`↑` `↓`、批量移动(只能移出该列)都不会改变其内部顺序。 + diff --git a/src/components/BoardColumn.spec.ts b/src/components/BoardColumn.spec.ts index c1d8506..5907aeb 100644 --- a/src/components/BoardColumn.spec.ts +++ b/src/components/BoardColumn.spec.ts @@ -77,6 +77,24 @@ describe('BoardColumn 渲染', () => { const { wrapper } = setup([]) expect(wrapper.find('input[aria-label="添加任务"]').exists()).toBe(true) }) + + it('已完成列标注排序口径,其它列不标注', () => { + const pinia = createPinia() + setActivePinia(pinia) + const store = useTaskStore() + store.init({ repository: createMemoryTaskRepository(), crossTab: false }) + + const mountWith = (column: ColumnDef) => + mount(BoardColumn, { + props: { column, tasks: [], sortDisabled: column.id === 'done' }, + global: { plugins: [pinia] }, + }) + + expect(mountWith({ id: 'done', title: '已完成' }).find('[data-role="column-sort-hint"]').text()).toBe( + '按完成时间倒序', + ) + expect(mountWith(TODO_COLUMN).find('[data-role="column-sort-hint"]').exists()).toBe(false) + }) }) describe('BoardColumn 拖拽契约', () => { diff --git a/src/components/BoardColumn.vue b/src/components/BoardColumn.vue index d8f392e..fbaedcd 100644 --- a/src/components/BoardColumn.vue +++ b/src/components/BoardColumn.vue @@ -72,6 +72,14 @@ useDraggable(listRef, {