# 父子任务支持设计

> 日期：2026-08-04
> 目标：完成父任务/子任务的完整支持（数据→后端→前端），**UI/UX 重点设计**。
> 关联：知识图谱 Phase 1 V29（tasks.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。

**分组渲染改造**（每个项目组内）：
```
├ 顶层任务A（isParent=true） → 折叠箭头 + 标题 + 优先级 + 子进度徽章(2/5) + 迷你进度条 + 状态 + ⚙️
│   └ 子任务A1/A2... → 缩进 + 左侧竖线引导线 + 圆点连接符，常规行操作
├ 顶层任务B（isParent=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）、任务回收站前端 UI（list_deleted_tasks 无命令，超范围，登记待办）、queue 管理池看板视图（move_task_queue 前端 UI，超范围）。
- **回归风险**：delete_task 返回结构变更影响 `store.deleteTask`/Tasks.vue 两处；Tasks.vue 移除分页器影响 `Paginator`/`totalTasks` 逻辑——核查时重点验证。
- **UI 设计原则**：树形沿用现有任务卡视觉（CSS token、状态徽章、快捷菜单），父/子层级用「缩进 + 竖线 + 折叠箭头 + 进度条」表达，不引入新 UI 库。
