Files
DevFlow/docs/04-功能迭代/父子任务支持设计-2026-08-04.md
T
lxy 28de5d6143 新增: 父子任务支持(数据→后端→前端全链路,会话前基线收尾)
- df-nodes task_advance_node(父聚合推进)+ task.rs 命令(create parent_id 支持/delete 级联软删子任务)+ task_graph 工具

- 前端 Tasks 树形列表(折叠箭头/子进度徽章/缩进)+ 新建弹窗父任务下拉 + TaskDetail 父面包屑/子任务面板

- 设计文档: 父子任务支持设计-2026-08-04
2026-08-05 22:15:01 +08:00

216 lines
11 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.
# 父子任务支持设计
> 日期:2026-08-04
> 目标:完成父任务/子任务的完整支持(数据→后端→前端),**UI/UX 重点设计**。
> 关联:知识图谱 Phase 1 V29tasks.parent_id 列 + 父聚合规则已落地数据层)、Phase 2 命令层已大部就绪。
---
## 1. 现状盘点(探索结论)
### 已就绪(复用,不重复造)
| 层 | 已有能力 | 位置 |
|---|---|---|
| 数据层 | `tasks.parent_id TEXT REFERENCES tasks(id)`V29 | migrations.rs:755-761 |
| 数据层 | `TaskRecord.parent_id` / `TaskQuery.parent_id` | models.rs:114 / task_repo.rs:87 |
| 数据层 | `get_children` / `count_children_by_status` / `set_status_for_aggregation` | task_repo.rs:500/532/574 |
| 命令层 | `create_task`/`update_task` 的 parent_id 1 级嵌套校验 | task.rs:257-278/388-408 |
| 命令层 | `advance_task` 子任务推进后触发 `recompute_parent_status` | task.rs:533-542 |
| 命令层 | `get_task_tree`(父 + 直接子) | task.rs:854-867 |
| 约束 | 1 级嵌套(无孙任务),由 IPC 校验不进 DB 约束 | models.rs:108-111 |
### 缺口(本次要补)
1. **前端完全空白**`TaskRecord/CreateTaskInput/TaskQuery` 无 parent_id/queue 字段;`Tasks.vue` 扁平列表无层级;`TaskDetail.vue` 无父子信息;新建弹窗无父任务选择。
2. **父聚合只在 IPC advance_task 触发**AI 工具 `ai/tools/task.rs:209` 与 df-mcp `tools.rs:577``advance_task` 都只调 `advance_task_atomic`,不重算父 status(与 IPC 不一致)。
3. **df-mcp create_task 不支持 parent_id**schema 无入参,构造时硬编码 `parent_id: None`tools.rs:484)。
4. **删除父任务后子任务悬挂**`delete_task` 仅软删单条,子任务 `parent_id` 仍指向已软删父任务。
---
## 2. 设计决策
| # | 决策 | 理由 |
|---|---|---|
| D1 | 保持 1 级嵌套(无孙任务) | 与现有注释/校验/数据模型一致,不引入递归复杂度 |
| D2 | 复用 V29 `parent_id` 列,**不新增迁移** | 数据层已完备,无需 DB 变更 |
| D3 | 父聚合逻辑下沉 df-nodes 共享层,三方(IPC/AI/MCP)统一调用 | 消除双轨不一致,单一真相源 |
| D4 | 删除父任务 = 级联软删子任务(带确认提示) | 容器语义,删父即删整个工作单元;前端树数据可准确提示子任务数 |
| D5 | 任务列表页改**一次性加载 + 前端组装树**(limit 放大到 500 钳制上限),移除真分页 | 个人工具数据量小;树形需要完整父子关系,分页会割裂父/子 |
| D6 | 前端父任务进度条/徽章数据从树数据**前端计算**,不加新后端 API | 全量已在前端,无需额外往返 |
---
## 3. 后端改动
### 3.1 df-nodes 共享父聚合(核心)
`crates/df-nodes/src/task_advance_node.rs` 新增两个公共函数(迁移自 task.rs 私有实现):
```rust
/// 父任务 status 重算(容器模型,不走状态机)。
/// 聚合规则(优先级从高到低):任一 blocked→blocked;任一 in_progress→in_progress
/// 全 done/cancelled→done;全 todo→todo;其他混合→in_progress。
/// 无子任务(悬空)→ 不重算,返回当前 status。
pub async fn recompute_parent_status(
repo: &TaskRepo,
parent_id: &str,
) -> df_types::error::Result<String>
/// 推进任务 + 若为子任务则触发父聚合(父聚合失败仅 warn 不阻断,宽容语义)。
pub async fn advance_task_with_parent(
repo: &TaskRepo,
id: &str,
target_status: &str,
) -> df_types::error::Result<TaskRecord>
```
- `recompute_parent_status` 错误用 `Error::NotFound` / `Error::Storage` 包装。
- 数据源 `repo.count_children_by_status`(一次 GROUP BY);写入 `repo.set_status_for_aggregation`
- 状态相同则不写(避免 updated_at 抖动)—— 逻辑原样迁移。
### 3.2 IPC `src-tauri/src/commands/task.rs`
- `advance_task`:改为调 `df_nodes::task_advance_node::advance_task_with_parent`,删除本地 `recompute_parent_status` 私有函数。
- `delete_task`:级联软删。新返回结构:
```rust
#[derive(Debug, Serialize)]
pub struct TaskDeleteResult {
pub ok: bool,
/// 级联软删的子任务数
pub cascaded: i32,
}
```
流程:`get_children(id)` → 逐个 `soft_delete(child)``soft_delete(id)` → emit `task_deleted`(父任务的事件)→ 返回 `{ok, cascaded}`
### 3.3 AI 工具 `src-tauri/src/commands/ai/tools/task.rs`
- `advance_task` handler:改调 `advance_task_with_parent`(与 IPC 同源,消除双轨)。
### 3.4 df-mcp `crates/df-mcp/src/tools.rs`
- `create_task`schema 增加 `parent_id`(可选 string);构造时透传;校验:parent 存在 + parent 自身无 parent_id(1 级嵌套),违反返回明确错误。
- `advance_task`:改调 `advance_task_with_parent`
### 3.5 契约(前后端共用)
- `TaskRecord``queue: string``parent_id?: string | null``content_json?: string`
- `delete_task` 返回 `TaskDeleteResult { ok: boolean; cascaded: number }`(破坏性变更,仅 store/视图两处调用点,内部可控)。
---
## 4. 前端改动
### 4.1 类型与 API
`src/api/types.ts`
- `TaskRecord` + `queue: string``parent_id?: string | null``content_json?: string`
- `CreateTaskInput` + `queue?: string``parent_id?: string | null`(空串→后端视为 None
- `TaskQuery` + `queue?: string | null``parent_id?: string | null`
- 新增 `TaskTreeNode { parent: TaskRecord; children: TaskRecord[] }`
- 新增 `TaskDeleteResult { ok: boolean; cascaded: number }`
`src/api/task.ts`
- `delete(id): Promise<TaskDeleteResult>`(适配新返回)
- 新增 `getTree(id): Promise<TaskTreeNode>``invoke('get_task_tree', { parentId: id })`
- `create` 透传 `input`(已含 parent_id/queue
`src/stores/project/tasks.ts`
- `deleteTask``state.tasks = state.tasks.filter(t => t.id !== id && t.parent_id !== id)`(父删连带子移除)
- `createTask` 入参类型 + `parent_id?: string | null`
### 4.2 Tasks.vue — 树形列表(UI/UX 重点)
**数据加载**`buildTaskQuery()``limit` 固定放大(如 500,钳制上限),offset 恒 0`totalTasks` 改用 `store.tasks.length`(一次加载即全部);**移除 `<Paginator>`**。
**树组装**computed `taskRows`):
```ts
interface TaskRow {
task: TaskRecord
children: TaskRecord[] // 父任务的直接子(仅父有)
progress?: { done: number; total: number } // 父任务子进度
isParent: boolean
}
```
- 顶层 = `store.tasks.filter(t => !t.parent_id)`,按现有排序/项目分组逻辑处理。
- 每个顶层任务的 children = `store.tasks.filter(t => t.parent_id === t.id)`1 级嵌套,无需递归)。
- 父任务 progress = children 中 `status === 'done' || 'cancelled'` 计数 / total。
**分组渲染改造**(每个项目组内):
```
├ 顶层任务AisParent=true) → 折叠箭头 + 标题 + 优先级 + 子进度徽章(2/5) + 迷你进度条 + 状态 + ⚙️
│ └ 子任务A1/A2... → 缩进 + 左侧竖线引导线 + 圆点连接符,常规行操作
├ 顶层任务BisParent=false)→ 普通行
```
**父任务行新增**
- 折叠箭头 `▸/▾` 按钮(点击仅切换展开,`@click.stop` 防跳详情)
- 标题前父任务图标(如 `📑`,与子任务区分)
- **子进度徽章** `n/m`(如 `2/5`+ **迷你进度条**`.mini-progress` 渐变填充,done 百分比)
- 快捷菜单新增「+ 添加子任务」(`@click.stop`,带 parent_id 预填打开新建弹窗)
- 展开/折叠状态:`expandedParents: reactive(Set<string>)` + localStorage 记忆(沿用折叠模式)
**子任务行**
- `padding-left` 缩进 + 左侧 `border-left` 引导线(延续父任务竖线)+ 行首圆点 `•`/连接符
- 常规快捷操作(状态/优先级/删除)与顶层一致
- 点击行跳 `/tasks/{child.id}`
**新建任务弹窗**新增「父任务」下拉:
- 选项 = 当前选中项目的**顶层任务**列表 + 首项「无(顶层任务)」
- 选择父任务时 `project_id` 锁定为该父任务所属项目(下拉只列该项目顶层任务)
- 提交时 `parent_id` 透传
**顶部「新建任务」**默认父任务=无(创建顶层任务)。
### 4.3 TaskDetail.vue — 父子面板
**父面包屑**:左栏「关联信息」面板顶部新增:
-`task.parent_id` 有值:`父任务: → [标题]`router-link 跳 `/tasks/{parent_id}`parent 标题由 `getTaskTree` 或从列表解析)
- 数据源:load 时若 `task.parent_id` 有值,额外 `taskApi.get(parent_id)` 取标题。
**子任务面板**:若当前任务是父任务(`children.length > 0`),左栏新增「子任务」面板:
```
┌ 子任务 (5) ─────────────┐
│ ▓▓▓▓░░░░░ 3/5 完成 │ ← 顶部进度条 + 计数
│ ├ [子任务1] [✅] │ ← 点击跳详情
│ ├ [子任务2] [🔨] ⚙️ │ ← 行快捷推进
│ └ [+ 添加子任务] │
└──────────────────────────┘
```
- 数据:load 时 `taskApi.list({ project_id, parent_id: task.id })`(或 `getTree`
- 子任务行:标题 + 状态徽章 + 优先级徽章;点击跳转;⚙️ 快捷菜单(复用列表页 quickStatuses/quickPriorities 模式,advance 后刷新子列表)
- 「+ 添加子任务」按钮:打开小弹窗(标题 + 优先级 + 描述),project_id/parent_id 继承当前任务
**子任务空态**:父任务无子任务时显示「暂无子任务」+ 添加入口(父任务详情可空树创建)。
### 4.4 i18n 新增 key
`zh-CN/tasks.ts` + `en/tasks.ts`
```ts
modal: { ..., parentTask: '父任务', parentPlaceholder: '无(顶层任务)' }
addSubtask: ' 添加子任务'
tree: { progress: '进度' }
confirmDeleteWithChildren: '确定删除「{title}」吗?将同时删除 {n} 个子任务。'
```
`zh-CN/taskDetail.ts` + `en/taskDetail.ts`
```ts
parentTask: '父任务'
childrenTitle: '子任务'
subtaskCount: '{n} 个子任务'
childEmpty: '暂无子任务'
addSubtask: ' 添加子任务'
progressTitle: '完成进度'
```
---
## 5. 边界与不做
- **不做**:孙任务(D1)、任务回收站前端 UIlist_deleted_tasks 无命令,超范围,登记待办)、queue 管理池看板视图(move_task_queue 前端 UI,超范围)。
- **回归风险**delete_task 返回结构变更影响 `store.deleteTask`/Tasks.vue 两处;Tasks.vue 移除分页器影响 `Paginator`/`totalTasks` 逻辑——核查时重点验证。
- **UI 设计原则**:树形沿用现有任务卡视觉(CSS token、状态徽章、快捷菜单),父/子层级用「缩进 + 竖线 + 折叠箭头 + 进度条」表达,不引入新 UI 库。