Files
iboard/docs/V0.2/任务模块/任务模块功能设计.md
T
jif2.zhang c5fc328f2b
Docker Build and Push / build-image (push) Successful in 1m27s
feat(任务/打卡): 已归档页面按原型重做; 打卡日历完成度改 4 档配色
一、已归档任务管理: 按评审通过的原型落地 (docs/V0.2/任务模块/已归档任务管理-原型.html)

- 去掉「任务类型」过滤: 类型是弱维度, 归档场景几乎不按它检索, 反而占掉一整行;
  类型标签仍在行内展示。
- 关键词输入框前加常驻标签「标题」, 框内占位改为「输入关键词」。
- 归档时间新增快捷项: 全部 / 本周 / 本月 / 本季度 / 自定义;
  选中后下方灰字回显实际区间 (本周=周一起算, 区间截至今天, 不跨未来);
  「自定义」才展开两个日期选择器。
- 补上「筛选」按钮 (带生效条件数徽标) 与「重置」: 条件先编辑, 点按钮统一提交。
  原实现改任一条件就即时重查, 连点会连发请求, 输关键词时列表会闪。
- 列表上方显示已应用条件标签 (可单独 ✕ 撤销) + 命中条数。
- 列表行重排: 标题独占一行, 元信息收成一行 (#编号 · 看板列 · 归档于日期),
  「恢复 / 删除」由右侧竖排改到底部右侧横排, 降低行高。
- 空状态区分「这个项目还没有归档任务」(提示去 ⋯ 菜单归档) 与
  「没有符合条件的归档任务」(建议放宽条件)。
- 修正日期边界: archivedList 的 from/to 原来直接拼 'T00:00:00Z',
  本地选的日期会偏移 8 小时, 当天凌晨的归档被漏掉; 改为按本地时区换算成 UTC 瞬时。

二、打卡日历完成度配色: 由 3 档改 4 档

    小半 (ratio < 0.5)  -> 红 #ff3141
    大半 (0.5 <= r < 1) -> 橘 #ff8f1f
    完成 (r = 1)        -> 蓝 #1677ff
    超额                -> 绿 #00b578

  注意服务端统计时把 actual 封顶在 expected (`Math.min(cnt, exp)`),
  所以 ratio 永远 <= 1,**超额只能靠 status === 'over' 判断**,不能看比例;
  DayRing 内新增 tierOf() 统一分级,图例同步改为四档。

验证 (真实 Windows 外壳 + CDP):
- 打卡页: 图例显示「小半/大半/完成/超额」, 计算样式分别为
  rgb(255,49,65) / rgb(255,143,31) / rgb(22,119,255) / rgb(0,181,120);
  当月实际渲染出 low=5 / high=1 / over=1。
- 已归档页: 筛选行标签为 标题/看板列/归档时间; 时间选项五项齐全;
  按钮为 重置/筛选; 点「本周」回显 2026-10-26 ~ 2026-10-28, 点「筛选」后
  徽标显示 1、条件标签可撤销、命中条数正确。
- 归档 → 已归档列表可见 → 恢复 全链路走通 (含上次修好的 TDZ 问题),
  服务端确认 archived 由 t 变回 f。
- 控制台无异常。
2026-10-28 10:19:17 +08:00

400 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# iBoard V0.2 任务模块 功能设计
| 项 | 内容 |
| --- | --- |
| 版本 | V0.2(对应分支 `dev-0.2`,应用版本 `0.2.0`) |
| 文档状态 | **设计基线**(待评审) |
| 上游文档 | `任务模块需求文档.md`(**149 条需求点**,编号 `T-*`) |
| 配套文档 | `任务模块设计文档-框架设计.md`、`任务模块数据库设计文档.md`、`任务模块界面设计.html` |
| 对应关系 | **本文与需求点一一对应**:每条需求点均给出「设计落点(功能/组件/接口/表字段)」与「实现要点」 |
> **阅读方式**
> 1. 本文按需求文档的模块码组织(`VW` / `COL` / `BD` / `FL` / `TK` / `AS` / `ST` / `PJ` / `PM` / `CP` / `UI` / `DT` / `NF` / `EX`)。
> 2. 每条需求点一行:**编号 → 设计落点 → 实现要点**。设计落点中的组件名、接口路径、表字段均与《任务模块数据库设计文档》《任务模块界面设计》一致。
> 3. 文末 §15 给出**覆盖自检表**,确保 149 条无遗漏。
---
## 0. 通用约定
### 0.1 契约命名
| 类别 | 约定 | 示例 |
| --- | --- | --- |
| 状态枚举 | `todo` / `doing` / `done` | 未开始 / 执行中 / 完成 |
| 列 | `BoardColumn` 实体,按 `sortOrder` 升序 | 默认 `Plan/Doing/Action/Done` |
| 保留选项 | `never` / `today` / `3d` / `week` / `month` / `forever` | 永不显示 / 当天 / 3 天 / 本周 / 本月 / 永久显示 |
| 视图 | `list` / `board` | 设备端记忆 |
| 设备端键 | `iboard_task_prefs` | `{ lastProjectId, viewMode: { [projectId]: 'list'\|'board' } }` |
### 0.2 接口总览
| 方法 | 路径 | 用途 |
| --- | --- | --- |
| GET | `/api/projects` | 我参与的项目(含个人默认项目、归档标记) |
| POST | `/api/projects` | 新建普通项目(同时初始化 4 个默认列) |
| GET | `/api/projects/:id` | 项目详情(含成员、列配置) |
| PUT | `/api/projects/:id` | 改名 / 归档 |
| DELETE | `/api/projects/:id` | 删除项目(个人项目 400) |
| POST | `/api/projects/:id/members` | 加成员(个人项目 400) |
| DELETE | `/api/projects/:id/members/:uid` | 移成员 / 自己退出 |
| GET/POST | `/api/projects/:id/columns` | 列列表 / 新增列 |
| PUT | `/api/columns/:id` | 改名 / 改绑定 / 改保留选项 |
| DELETE | `/api/columns/:id` | 删列(需 `moveToColumnId`,最后列 400) |
| PUT | `/api/projects/:id/columns/order` | 列排序 |
| GET | `/api/projects/:id/tasks` | 任务列表(含子任务与列信息) |
| POST | `/api/projects/:id/tasks` | 新建任务 / 子任务 |
| GET | `/api/tasks/:id` | 任务详情 |
| PUT | `/api/tasks/:id` | 改状态 / 换列 / 改字段(触发裁决) |
| DELETE | `/api/tasks/:id` | 删除任务(级联子任务) |
| POST | `/api/tasks/:id/copy` | 跨项目拷贝 |
---
## 1. 视图体系(T-VW)
| 编号 | 设计落点 | 实现要点 |
| --- | --- | --- |
| T-VW-01 | `ProjectView.jsx` 内 `viewMode` 分支渲染 `ListView` / `BoardView` | 两视图共用 `TaskStore` 的同一份任务数据,仅渲染方式不同 |
| T-VW-02 | `SwipeTabs`/分段控件切换 `viewMode` | 纯本地状态切换,不触发路由跳转、不重新拉取数据(满足"无整页刷新") |
| T-VW-03 | `prefs.js` → `viewMode[projectId]` | 按项目 id 分键存储,A/B 项目互不影响 |
| T-VW-04 | `ProjectView` 挂载时读 `prefs.getViewMode(projectId)` | 有值用之;无值走 T-VW-05 默认 |
| T-VW-05 | 默认值 `'list'` | 新项目首次进入为列表 |
| T-VW-06 | `localStorage.iboard_task_prefs` | **不落库、不同步**;换设备不继承(与 T-FL-15 的项目级配置区分) |
| T-VW-07 | `prefs.getLastProjectId()` / `setLastProjectId()` | 进入 `/task` 时读取;无记录或记录的项目已不可见 ⇒ 回落个人默认项目 |
| T-VW-08 | `TaskPage` 首屏上部 `RecentProjects` 条 | 取最近访问的 3 个项目(按设备端访问时间倒序),末尾「更多项目」入口 → `/task/list`(完整项目列表 `ProjectList`,从「更多」进入) |
| T-VW-09 | `TaskPage` 首屏下部直接内嵌 `ProjectView` | 当前项目 = 上次访问项目(无记录 ⇒ 个人默认项目);点首屏项目卡片切换当前项目,下部整体切换,不跳转页面 |
| T-VW-10 | `prefs.js` 增 `recentOrder[]`(设备端) | 长按卡片拖动排序并持久化;无手动记录时按最近访问时间自动排列;项目不可见时从记录剔除 |
> **首屏结构调整(T-VW-08/09)**:`TaskPage`(`/task`)不再以项目列表为首屏,改为「最近项目条 + 当前项目 `ProjectView`」;完整项目列表迁移至 `/task/list`。旧路由 `/task/:pid` 保留(打开指定项目),供深链使用。
> **实现细节**:`lastProjectId` 指向的项目若已被退出/删除,视为"无记录",静默回落到个人默认项目,不弹错。
---
## 2. 看板列管理(T-COL)
| 编号 | 设计落点 | 实现要点 |
| --- | --- | --- |
| T-COL-01 | `POST /api/projects` 内部初始化 4 列 | 事务内创建 `Plan(todo, sort 0)`、`Doing(doing,1)`、`Action(null,2)`、`Done(done,3)` |
| T-COL-02 | `ColumnSettings` → `POST /api/projects/:id/columns` | `sortOrder = max+1` 追加末尾;`boundStatus=null`、`retention='forever'`(B-18) |
| T-COL-03 | `PUT /api/columns/:id` | 改 `name`;前端 `TaskStore` 整体替换列列表后重渲染 |
| T-COL-04 | `DELETE /api/columns/:id?moveToColumnId=` | 二次确认弹窗;见 T-EX-16 |
| T-COL-05 | `PUT /api/projects/:id/columns/order` | 传完整 `[columnId…]` 数组,服务端按索引重写 `sortOrder` |
| T-COL-06 | 服务端校验总量 | `1 <= count <= 10`;达上限拒绝新增(T-EX-19)、仅剩 1 列拒绝删除(T-EX-17) |
| T-COL-07 | `BoardColumn.name` 校验 | trim 后非空、长度 ≤ 12;`@@unique([projectId,name])` 兜底重名 409 |
| T-COL-08 | 列删除即绑定消失 | 绑定存在列记录上,列删则解除,无需额外清理 |
| T-COL-09 | `requireProjectMember` | 项目级配置,任一成员可改;不影响其他项目 |
| T-COL-10 | `ListView` 不分组但仍显示列信息 | 任务行内以小标签显示所属列名(可关闭),归属始终正确 |
---
## 3. 看板与列-状态绑定(T-BD)
> 联动的**唯一实现**在 `server/task-domain/resolveStatus.js`,前端只做乐观更新。判定矩阵见《框架设计》§2.3。
| 编号 | 设计落点 | 实现要点 |
| --- | --- | --- |
| T-BD-01 | `BoardView` 按 `columns` 数组渲染列 | 列来自项目配置,可增删改排序 |
| T-BD-02 | `ColumnSettings` 每列一组「绑定开关 + 状态下拉」 | 写入 `BoardColumn.boundStatus`(`null`/`todo`/`doing`/`done`) |
| T-BD-03 | 无唯一约束限制多列绑定同状态 | 允许 `done` 与 `action` 同为 `done` |
| T-BD-04 | `onColumnChange()` | 目标列 `boundStatus != null` ⇒ `status = boundStatus` |
| T-BD-05 | `onStatusChange()` | 状态变更后若当前列已匹配则不动作,否则走归位 |
| T-BD-06 | 归位取列序首个匹配 | `columns` 按 `sortOrder` 排序后 `find()` 首个 |
| T-BD-07 | **只看目标列** | 目标列不绑定 ⇒ 保留原状态;**无"拖出回退"规则** |
| T-BD-08 | 不绑定列完全正交 | 拖入/拖出、改状态均不改该列以外语义 |
| T-BD-14 | `resolveColumn(task, newStatus)` | 当前列 `boundStatus !== newStatus` ⇒ 触发归位 |
| T-BD-15 | 按当前 `sortOrder` 取首个 | 用户调序后归位目标随之改变 |
| T-BD-16 | 无匹配返回 `null` | `null` ⇒ 不改 `columnId`,状态照常写库 |
| T-BD-09 | 初始化默认值 | `Plan=todo`、`Doing=doing`、`Done=done`、`Action=null` |
| T-BD-10 | 项目级配置 | 任一成员可改;不同项目独立 |
| T-BD-11 | 配置变更不回溯 | 保存配置时**不触发**任何任务更新语句 |
| T-BD-12 | 保存后返回最新列列表 | 前端整体替换 + 重算可见集合,无需刷新 |
| T-BD-13 | `resolveStatus()` 首行判 `parentId` | 子任务直接跳过全部联动 |
---
## 4. 已完成任务显示控制(T-FL)
| 编号 | 设计落点 | 实现要点 |
| --- | --- | --- |
| T-FL-01 | `BoardColumn.retention` | 每列独立字段 |
| T-FL-02 | `ColumnSettings` 下拉 6 项 | 固定枚举,无自由输入 |
| T-FL-03 | `retention='forever'` | 判定恒为可见 |
| T-FL-04 | `retention='never'` | 已完成任务恒不可见 |
| T-FL-05 | `retention='today'` | `doneAt >= 今天 00:00` |
| T-FL-06 | `retention='3d'` | `doneAt >= 今天-2天 00:00` |
| T-FL-07 | `retention='week'` | `doneAt >= 本周一 00:00` |
| T-FL-08 | `retention='month'` | `doneAt >= 本月1日 00:00` |
| T-FL-09 | `task-domain/retention.js` + 前端定时重算 | 超出窗口自动隐藏,无需操作 |
| T-FL-10 | 判定字段 `Task.doneAt` | 与当前列无关;取消完成时 `doneAt=null` |
| T-FL-11 | 前端可见性统一走同一函数 | 列表与看板共用 `isTaskVisible(task, column)`;列表不分组但归属不变 |
| T-FL-12 | 判定前置 `status !== 'done'` | 未完成任务恒可见 |
| T-FL-13 | 纯展示层过滤 | 不修改任何字段;接口返回全量,前端过滤(或服务端按需过滤但不改数据) |
| T-FL-14 | 列计数取过滤后集合长度 | 列头计数与卡片数同源,杜绝不一致 |
| T-FL-15 | 存在 `BoardColumn` 上 | 项目级共享,非个人偏好(对比 T-VW-06 设备端) |
> **实现细节**:保留判定在前后端各有一份实现(服务端用于分页/统计,前端用于即时过滤),但**必须共用同一口径常量**,跨度边界由 `date.js` 单测覆盖。
---
## 5. 任务字段体系(T-TK)
| 编号 | 设计落点 | 实现要点 |
| --- | --- | --- |
| T-TK-01 | `Task` 表 | 字段:`title`/`description`/`projectId`/`columnId`/`status`/`assigneeId`/`creatorId`/`doneAt`/`sortOrder` + 关联成员/标签/扩展 |
| T-TK-02 | `title` 非空校验 | 其余字段允许空;trim 后为空 ⇒ 400 |
| T-TK-03 | `status` 枚举三值 | DB 层用 `VARCHAR(8)` + 应用层校验(避免 ENUM 改值困难) |
| T-TK-04 | **已移除 UI**(2026-10-24):`ExtrasCard` 组件删除;信息记录由项目信息块(T-PJ-13)承载 |
| T-TK-05 | **随 T-TK-04 移除**;`extras` 字段在 DB 保留不再使用 |
| T-TK-06 | **随 T-TK-04 移除** |
| T-TK-07 | **随 T-TK-04 移除** |
| T-TK-08 | `Task.assigneeId` 单值 | 重复设置以最后一次为准 |
| T-TK-09 | 新建时服务端默认赋值 | 未显式传 `assigneeId` ⇒ 取 `req.user.id`(B-10) |
| T-TK-10 | `assigneeId` 允许 `null` | 显式传 `null` 即清空,UI 显示「未指派」 |
| T-TK-11 | `TaskMember` 关联表 | 多对多,`@@unique([taskId,userId])` 去重 |
| T-TK-12 | `TaskTag` 表 + 卡片标签渲染 | 多标签,展示于卡片/条目 |
| T-TK-13 | 服务端维护 | `creatorId`/`createdAt` 不可改;`updatedAt` 自动刷新 |
| T-TK-14 | 标题 30 字上限 + 快速添加截断 | 服务端 create/update 校验 `title ≤ 30`;前端 `quickAdd.js#splitTitle`:≤30 原样,超长在 30 字内(≥8 字)找最后一个分隔符截断,前段为标题、**完整原文**写入 `description`;快速添加输入框 `maxLength=200` |
| T-TK-15 | `Task.markDate DATETIME(3) NULL`(迁移 20261025120000) | create/update 接受 `YYYY-MM-DD`(空/null 清除),完整日期存储;详情页 DatePicker(precision=day)选择/清除;卡片展示 `MM-dd` 小标签(`date-tag`) |
| T-TK-16 | `quickAdd.js#parseTimePrefix` | 前缀词表:今天/明天/后天/本周/下周/本月/下月(须后跟分隔符或结束);今天=当日,明天/后天=+1/+2,本周=本周日、下周=下周日(周一为一周开始),本月/下月=当月/下月最后一天;命中后前缀移出标题并写入 `markDate` |
---
## 6. 负责人 & 成员(T-AS)
| 编号 | 设计落点 | 实现要点 |
| --- | --- | --- |
| T-AS-01 | `TaskCard` 负责人区 | 字号 13px/600 + 头像,位于标题下方主信息位 |
| T-AS-02 | `TaskCard` 成员区 | 字号 11px/400、色 `#999`,位于卡片底部 |
| T-AS-03 | `UserPicker` 数据源 = 项目成员 | 只列 `ProjectMember` 中的用户 |
| T-AS-04 | 同上(多选模式) | 不可选项目外用户 |
| T-AS-05 | `assigneeId === null` 时渲染「未指派」 | 占位样式弱化,可点击再次指派 |
| T-AS-06 | 渲染时 `LEFT JOIN ProjectMember` 判定 | 非成员 ⇒ 显示「已退出」灰标;见 T-EX-06 |
| T-AS-07 | 权限模型 | 无"负责人可编辑"概念,所有成员全读写(T-PM-02) |
---
## 7. 子任务体系(T-ST)
| 编号 | 设计落点 | 实现要点 |
| --- | --- | --- |
| T-ST-01 | `Task.parentId` 自关联 | 创建子任务时校验:父任务 `parentId IS NULL`,否则 400 |
| T-ST-02 | 一致性校验 | 子任务 `projectId` 必须等于父任务 `projectId`,否则 400(T-EX-12) |
| T-ST-03 | 状态独立 | 父/子状态字段各自独立,无级联更新 |
| T-ST-04 | `SubtaskRow` 缩进 | 左侧 28px 缩进 + 层级线 |
| T-ST-05 | 折叠/展开按钮 | 组件内存状态,折叠时不渲染子项 |
| T-ST-06 | `TaskCard` 内嵌 `SubtaskRow` 列表 | 简洁单行:复选框 + 标题,最多显示 3 条 + 「查看全部」 |
| T-ST-07 | 卡片内直接勾选 | 勾选调 `PUT /api/tasks/:id {status}`;完整编辑走 `TaskDetail` 弹窗 |
| T-ST-08 | 子任务跳过绑定 | 见 T-BD-13;父任务完成时子任务状态不变 |
| T-ST-09 | `SubtaskRow` 带 `StatusDot` | 三色小圆点:灰=未开始 / 蓝=执行中 / 绿=完成 |
| T-ST-10 | 看板只渲染顶层任务 | 看板查询 `WHERE parentId IS NULL` |
| T-ST-11 | `Task.parentId` 级联删除 | `onDelete: Cascade`(T-EX-07) |
| T-ST-12 | `useLongPressDrag` 落点为目标顶层任务 | 设置 `parentId` |
| T-ST-13 | 目标为子任务 ⇒ 拒绝 | Toast「仅支持一级子任务」 |
| T-ST-14 | 被拖任务有子任务 ⇒ 拒绝 | 避免二级嵌套 |
| T-ST-15 | 保持原列与状态 | 仅更新 `parentId`,不触发状态裁决 |
---
## 8. 项目体系(T-PJ)
| 编号 | 设计落点 | 实现要点 |
| --- | --- | --- |
| T-PJ-01 | 用户首次访问自动创建 | 按 `isPersonal=true` 且 `ownerId=userId` 幂等创建 |
| T-PJ-02 | `DELETE /api/projects/:id` 校验 `isPersonal` | 为真 ⇒ 400(T-EX-09) |
| T-PJ-03 | `POST .../members` 校验 `isPersonal` | 为真 ⇒ 400 |
| T-PJ-04 | `name` 初始 `${user.name}的任务` | 允许 `PUT` 改名,`isPersonal` 不变 |
| T-PJ-05 | `POST /api/projects` | 创建者自动写入 `ProjectMember` |
| T-PJ-06 | 成员接口 | 增/删成员,即时影响可见性 |
| T-PJ-07 | `DELETE .../members/:uid` 允许 `uid == ownerId` | 创建人退出后不再是成员 ⇒ 不可见 |
| T-PJ-08 | 删除最后成员不删项目 | 仅删 `ProjectMember`(B-11);见 T-EX-11 |
| T-PJ-09 | `Project.archived` | 列表分「进行中 / 已归档」 |
| T-PJ-10 | 列表查询按成员关系过滤 | `WHERE EXISTS ProjectMember(userId)` |
| T-PJ-11 | 新组件 `ProjectManage`(项目管理页) | 分区承载:成员(复用 `ProjectMembers` 逻辑)、列设置(复用 `ColumnSettings`)、项目信息;`ProjectView` 头部仅保留「项目管理」一个入口 |
| T-PJ-12 | `ProjectManage` 的「项目信息」分区 | 改名(`PUT /api/projects/:id {name}`)、归档/取消归档(`{archived}`)、删除(个人项目隐藏入口)、退出(仅创建人可见);删除与退出均有二次确认 |
| T-PJ-13 | `Project.notes` Json + `ProjectManage` 的「信息块」分区 | 存 `[{title, content}]`(≤50 块,标题 ≤50 字、内容 ≤20000 字);`PUT {notes}` 整体替换;底部弹层独立编辑(标题输入 + 多行 textarea + 删除),键盘弹出时复用 `useKeyboardInset` 抬升 |
---
## 9. 项目权限与入口(T-PM)
| 编号 | 设计落点 | 实现要点 |
| --- | --- | --- |
| T-PM-01 | `requireProjectMember` 中间件 | 非成员 403;列表查询天然过滤 |
| T-PM-02 | 无角色区分 | 所有成员对任务/列/成员均有全读写 |
| T-PM-03 | `ProjectMember.permission` 字段 | 本期恒为 `member`,预留分级扩展位 |
| T-PM-04 | 归档接口无权限限制 | 任意成员可归档(`PUT /api/projects/:id {archived}`) |
| T-PM-05 | 复用 `authRequired` | 未登录 401 |
| T-PM-06 | `BottomNav.TABS` 插入 `{ key:'/task', label:'任务' }` | 位于 `打卡` 之后;`HomePath` 仍为 `/checkin` |
| T-PM-07 | `prefs.lastProjectId` | 设备端;无记录 ⇒ 个人默认项目 |
---
## 10. 跨项目拷贝(T-CP)
| 编号 | 设计落点 | 实现要点 |
| --- | --- | --- |
| T-CP-01 | `POST /api/tasks/:id/copy` | 复制而非移动,源任务零改动 |
| T-CP-02 | `copyTask()` 字段映射 | 标题/描述/状态/列/扩展/标签/成员 全量复制 |
| T-CP-03 | 递归复制子任务 | 只复制一层(子任务无更深层级) |
| T-CP-04 | `creatorId = req.user.id` | 覆盖源创建人 |
| T-CP-05 | 无任何关联字段 | 拷贝后两条独立记录 |
| T-CP-06 | 校验目标项目成员身份 | 非成员 403 |
| T-CP-07 | 人员交集过滤 | 保留属于目标项目成员者,返回 `dropped` 名单(T-EX-08) |
| T-CP-08 | 子任务 `projectId` 同步改写 | 与父任务一致 |
| T-CP-09 | 状态与列沿用不纠正 | 见 T-EX-14 |
---
## 11. UI 交互规范(T-UI)
| 编号 | 设计落点 | 实现要点 |
| --- | --- | --- |
| T-UI-01 | 列表条目结构 / 快速新增 / 子任务折叠 | 参考 Todoist:底部输入框回车即建 |
| T-UI-02 | `TaskCard` 精简版 | 仅标题、状态点、负责人、子任务摘要;无边框装饰 |
| T-UI-03 | 单 `TaskStore` | 任一视图修改写入 store,另一视图读取同一数据 |
| T-UI-04 | 视图切换纯本地 | 不重新请求,不重载页面 |
| T-UI-05 | 复用 `.card` 与安全区变量 | 375~430px 无横向滚动;底部预留 TabBar 高度 |
| T-UI-06 | 勾选即时变色 | 乐观更新,先改 UI 再发请求 |
| T-UI-07 | `useLongPressDrag` | 长按 300ms 拖拽 + 「⋯」菜单「移动到」兜底 |
| T-UI-08 | `EmptyState` 组件 | 无项目 / 无任务 / 无子任务三档空态 + CTA |
| T-UI-09 | `LoadingState` 骨架屏 | 首屏与切换视图 |
| T-UI-10 | `ErrorState` 组件 | 展示错误 + 重试按钮 |
| T-UI-11 | 弹层容器统一 `padding-bottom: calc(env(safe-area-inset-bottom) + 20px)` | 底部按钮(删除等)与屏幕底边保持安全距离;小屏下弹层 `max-height` + 内部滚动,底部按钮始终完整可见 |
---
## 12. 数据需求(T-DT)
| 编号 | 设计落点 | 实现要点 |
| --- | --- | --- |
| T-DT-01 | `Project` / `ProjectMember` | 见《数据库设计文档》 |
| T-DT-02 | `BoardColumn.boundStatus` | 每列独立字段,允许多列同值 |
| T-DT-03 | `BoardColumn.retention` | 六档枚举字符串 |
| T-DT-04 | `Task` | 含 `parentId` 自关联、`assigneeId`、`status`、`columnId`、`extras` |
| T-DT-05 | `TaskMember` / `TaskTag` | 关联表;`extras` 为 JSON |
| T-DT-06 | 设备端 | 不落库 |
| T-DT-07 | 索引 | `(projectId, columnId)`、`(projectId, status)`、`(parentId)` |
| T-DT-08 | 迁移文件 | `prisma migrate deploy` 流程 |
| T-DT-09 | `Project.archived` / 任务无物理删 | 归档保留历史 |
---
## 13. 非功能需求(T-NF)
| 编号 | 设计落点 | 实现要点 |
| --- | --- | --- |
| T-NF-01 | 单请求聚合 + 乐观更新 | 见《框架设计》§4.2 |
| T-NF-02 | 不依赖桌面专有 API | 触控事件统一 `touch` / `pointer` 兼容 |
| T-NF-03 | `src/pages/task/*` + `server/routes/*.js` | 与既有模块结构一致 |
| T-NF-04 | 路由挂载于 `server/index.js` | 沿用既有组织方式 |
| T-NF-05 | 千级任务可用 | 索引 + 分页/虚拟滚动 |
---
## 14. 异常与边界(T-EX)
| 编号 | 设计落点 | 实现要点 |
| --- | --- | --- |
| T-EX-01 | 前端 500ms 防抖 + 服务端幂等判定 | 相同终态请求直接返回当前任务 |
| T-EX-02 | 乐观更新快照回滚 | 失败恢复快照 + Toast + 重试按钮 |
| T-EX-03 | 列配置改为不绑定 | 已有任务保持不动 |
| T-EX-04 | 状态与列不匹配 | 走 T-BD-14~16 归位 |
| T-EX-05 | 拖到不绑定列 | 状态保留,不回退 |
| T-EX-06 | 非成员渲染「已退出」 | 见 T-AS-06 |
| T-EX-07 | 级联删除子任务 | 确认弹窗提示条数 |
| T-EX-08 | 拷贝丢弃人员 | 返回 `dropped` 并提示 |
| T-EX-09 | 个人项目两个保护接口 | 400 + 明确文案 |
| T-EX-10 | 列空态 | 展示该列空态,不报错 |
| T-EX-11 | 无成员项目 | 静默不可见,无特判报错 |
| T-EX-12 | 子任务跨项目 | 服务端 400 |
| T-EX-13 | 拖拽降级 | 保持原列与状态 |
| T-EX-14 | 拷贝状态/列无法对应 | 沿用源值,不纠正 |
| T-EX-15 | 拖到自身或后代 | 拒绝 + Toast |
| T-EX-16 | 删除有任务的列 | 二次确认 + 迁移到指定列(默认相邻)+ 重新裁决 |
| T-EX-17 | 删除最后一列 | 400「至少保留一列」 |
| T-EX-18 | 跨周期当场消失 | 定时重算 + 退场动画 + 重排 |
| T-EX-19 | 达到列数上限 | 拒绝新增 + 提示 |
---
## 15. 任务归档(T-ARC)
> 归档与 T-FL 显示控制的分层:**隐藏 = 展示过滤(可逆、不落库)**;**归档 = 移出主工作集(落库、可恢复)**。
> 自动归档的倒计时起点是任务**被隐藏的时刻**(由 `doneAt + 列保留窗口` 推导,不新增 hiddenAt 字段)。
### 15.1 数据
| 落点 | 说明 |
| --- | --- |
| `Project.autoArchive VARCHAR(8) NOT NULL DEFAULT 'never'` | 自动归档档位:`3d` / `1w` / `1m` / `never`;项目级共享(同 T-FL-12 口径) |
| `Task.archived BOOLEAN NOT NULL DEFAULT false` | 归档标记;归档**不迁移表**,主查询统一追加 `archived = false` |
| `Task.archivedAt DATETIME(3) NULL` | 归档时刻(查询过滤与排序键) |
| `Task.archiveSource VARCHAR(8) NULL` | `'manual'` / `'auto'`,溯源用 |
| 索引 | `idx_task_project_archived (projectId, archived, archivedAt)`;`idx_task_project_status_archived (projectId, status, archived)` |
| 迁移(待实施) | `20261026000000_task_archive`:三列 + 两索引(MySQL DDL 见数据库设计文档 §3.1/§3.4) |
### 15.2 API
| 接口 | 说明 |
| --- | --- |
| `POST /api/tasks/:id/archive` | 手动归档:父任务连带全部后代置 `archived/archivedAt/archiveSource='manual'`;幂等(已归档直接返回当前态) |
| `POST /api/tasks/:id/unarchive` | 恢复:父任务连带后代清除归档三字段;原列因 `ON DELETE RESTRICT` 必存在,状态保持不变 |
| `GET /api/projects/:id/tasks/archived?keyword&kind&columnId&from&to&cursor&limit` | 归档查询:`keyword` 标题 LIKE、`kind`、`columnId`、`from/to`(归档日期,含边界);`limit` 默认 20、≤100;游标 = `archivedAt+id` 复合降序,返回 `{ items, nextCursor }`,`nextCursor` 为空即到底 |
| `PUT /api/projects/:id` | 增接受 `autoArchive`,枚举校验(非法 400) |
| 既有查询 | `GET /projects/:id/tasks`、统计、列计数等全部追加 `archived: false`;`taskCount`(projectSummary)同口径 |
### 15.3 自动归档执行
- **归档到期判定**:`due = 隐藏时刻 + autoArchive 档位`;隐藏时刻 = `doneAt + 列保留窗口`(复用 `retentionStart` 口径,含 `today/3d/week/month`;`forever` 列任务永不隐藏、不进入自动归档)。
- **执行时机(双保险)**:
1. **读时惰性**:加载项目任务时对该项目先执行一轮到期归档(保证用户看到的数据与判定一致,刚到期的任务当场消失);
2. **每小时 cron 兜底**:扫描全部 `autoArchive != 'never'` 项目的到期 done 任务批量归档(`archiveSource='auto'`)。
- **幂等**:`archived = false` 且 `status = 'done'` 且 `doneAt` 非空才进入判定;重复执行无副作用。
- **批量写**:cron 用 `updateMany`(按到期集合),读时惰性用单项目范围 `updateMany`。
### 15.4 前端
| 落点 | 说明 |
| --- | --- |
| 任务 `⋯` 菜单 | 新增「归档」(`Dialog.confirm`,含子任务时提示连带条数);成功后列表/看板乐观移除 |
| 项目管理 | 「归档设置」分区:`Selector` 四档(3 天后/一周后/一月后/永不),保存即生效 |
| 项目视图头部 | 项目栏新增「已归档」入口 → 全屏弹层:过滤条(**关键词 / 看板列 / 归档时段**;时段含「本周 / 本月 / 本季度」快捷项与自定义区间;条件编辑后点「筛选」统一提交,另有「重置」;**不提供类型过滤**)+ 已应用条件标签(可单独撤销)与命中条数 + 分页「加载更多」+ 行内「恢复 / 删除」 |
| 主视图 | 列表/看板无需感知归档(数据源已排除);恢复后经 `load()` 回到原位 |
### 15.5 边界
| 场景 | 处理 |
| --- | --- |
| 归档父任务时新增/恢复子任务 | 子任务随父判定:父 archived 则子不单独出现在主列表(查询按父排除) |
| 自动归档期间用户正在编辑该任务 | 详情弹层保存正常(归档不锁写);保存后任务仍在归档集 |
| 恢复后任务再次到期隐藏 | 重新进入自动归档倒计时(`doneAt` 未变则可能立即再次到期,属预期) |
| 「永不」档位 | 不做任何自动归档;手动归档不受影响 |
| 拷贝任务 | 拷贝件不继承归档态(`copyTask` 不复制 archived 三字段) |
---
## 16. 覆盖自检表
| 模块 | 编号范围 | 条数 | 本文对应章节 |
| --- | --- | --- | --- |
| 视图体系 | T-VW-01 ~ 10 | 10 | §1 |
| 看板列管理 | T-COL-01 ~ 10 | 10 | §2 |
| 列-状态绑定 | T-BD-01 ~ 16 | 16 | §3 |
| 已完成显示控制 | T-FL-01 ~ 15 | 15 | §4 |
| 任务字段体系 | T-TK-01 ~ 13 | 13 | §5 |
| 负责人 & 成员 | T-AS-01 ~ 07 | 7 | §6 |
| 子任务体系 | T-ST-01 ~ 15 | 15 | §7 |
| 项目体系 | T-PJ-01 ~ 13 | 13 | §8 |
| 项目权限与入口 | T-PM-01 ~ 07 | 7 | §9 |
| 跨项目拷贝 | T-CP-01 ~ 09 | 9 | §10 |
| UI 交互规范 | T-UI-01 ~ 11 | 11 | §11 |
| 数据需求 | T-DT-01 ~ 09 | 9 | §12 |
| 非功能需求 | T-NF-01 ~ 05 | 5 | §13 |
| 异常与边界 | T-EX-01 ~ 19 | 19 | §14 |
| 任务归档 | T-ARC-01 ~ 08 | 8 | §15 |
| **合计** | — | **157** | — |
> 需求文档共 **157 条**需求点,本文逐条给出设计落点,覆盖率 **157/157**。(其中 `T-BD-14~16`、`T-COL-*`、`T-EX-18~19`、`T-VW-08~10`、`T-PJ-11/12`、`T-UI-11`、`T-ARC-*` 为评审后新增,编号不连续,非笔误。)