Files
iboard/docs/V0.2/任务模块/任务模块功能设计.md
T
jif2.zhang e05ed6b491
Docker Build and Push / build-image (push) Successful in 1m42s
fix(任务): 归档以整棵树为单位并移除隐藏中间态
- 自动归档改为 doneAt + autoArchive 到期,只判定顶层任务,到期整树归档
- 手动归档/恢复提升到根父任务,子任务完全跟随父任务
- 新增 normalizeArchiveTree 修复历史父子归档不一致
- 移除按列保留窗口的显示过滤(前后端实现删除,T-FL-01~15 废止)
- 列 retention 字段仅兼容旧数据保留,不再参与判定
- 已归档查询返回归档子任务,删除提示可正确显示连带条数
2026-11-10 09:20:40 +08:00

392 lines
25 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` |
| 自动归档档位 | `3d` / `1w` / `1m` / `never` | 完成后 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-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,已移除)
> **2026-11-10 变更**:取消「隐藏」这一中间状态。任务只有两种状态:
> **显示**(在主列表/看板中)或**归档**(在已归档列表中)。
>
> - 不再按看板列的保留窗口隐藏已完成任务;
> - `BoardColumn.retention` 字段仅为兼容旧数据保留,**不再参与任何显示或归档判定**;
> - 已完成任务在手动归档或项目自动归档到期前,始终留在主列表/看板;
> - 原 T-FL-01 ~ 15 全部废止,不再有前后端保留窗口过滤实现。
>
> 归档规则见 §15。子任务完全跟随父任务:要么一起显示,要么一起归档。
---
## 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)
> 任务只有「显示」与「归档」两种状态,不再存在「隐藏」中间态。
> 自动归档倒计时起点是任务的**完成时刻** `doneAt`,到期后整棵任务树一起归档。
### 15.1 数据
| 落点 | 说明 |
| --- | --- |
| `Project.autoArchive VARCHAR(8) NOT NULL DEFAULT 'never'` | 自动归档档位:`3d` / `1w` / `1m` / `never`;项目级共享 |
| `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'`;传子任务 id 也提升到根父任务;幂等 |
| `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 = doneAt + autoArchive 档位`(`3d` = 72 小时、`1w` = 7 天、`1m` = 上海日历月 +1;`never` = 不自动归档)。
- **判定范围**:只看**顶层任务**(`parentId IS NULL`)且 `status = 'done'` / `doneAt` 非空 / 未归档。子任务不单独判定。
- **整树归档**:顶层任务到期后,该任务与其全部子任务一起置 `archived/archivedAt/archiveSource='auto'`,不会出现父任务归档而子任务仍显示的情况。
- **执行时机(双保险)**:
1. **读时惰性**:加载项目任务时对该项目先执行一轮到期归档;
2. **每小时 cron 兜底**:扫描全部项目的到期顶层任务批量归档,并修复历史父子归档状态不一致。
- **幂等**:重复执行无副作用;批量写使用 `updateMany`。
### 15.4 前端
| 落点 | 说明 |
| --- | --- |
| 任务 `⋯` 菜单 | 新增「归档」(`Dialog.confirm`,含子任务时提示连带条数);子任务上点归档会提升到根父任务,整棵树一起处理 |
| 项目管理 | 「归档设置」分区:完成后自动归档四档(3 天后/一周后/一月后/永不),保存即生效 |
| 项目视图头部 | 项目栏新增「已归档」入口 → 全屏弹层:过滤条(**关键词 / 看板列 / 归档时段**;时段含「本周 / 本月 / 本季度」快捷项与自定义区间;条件编辑后点「筛选」统一提交,另有「重置」;**不提供类型过滤**)+ 已应用条件标签(可单独撤销)与命中条数 + 分页「加载更多」+ 行内「恢复 / 删除」 |
| 主视图 | 列表/看板无需感知归档(数据源已排除);恢复后经 `load()` 回到原位 |
### 15.5 边界
| 场景 | 处理 |
| --- | --- |
| 子任务单独归档/恢复 | 不允许单独变化:接口与前端都会提升到根父任务,整棵树一起归档或恢复 |
| 历史数据父子不一致 | 服务端归档扫描时执行 `normalizeArchiveTree`,子任务归档态强制跟随父任务 |
| 自动归档期间用户正在编辑该任务 | 详情弹层保存正常(归档不锁写);保存后任务仍在归档集 |
| 恢复后任务再次到期 | 重新进入自动归档倒计时(`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 条**需求点(其中 `T-FL-01~15` 已于 2026-11-10 废止,不再有隐藏中间态),本文逐条给出设计落点。(其中 `T-BD-14~16`、`T-COL-*`、`T-EX-18~19`、`T-VW-08~10`、`T-PJ-11/12`、`T-UI-11`、`T-ARC-*` 为评审后新增,编号不连续,非笔误。)