新增: 父子任务支持(数据→后端→前端全链路,会话前基线收尾)
- df-nodes task_advance_node(父聚合推进)+ task.rs 命令(create parent_id 支持/delete 级联软删子任务)+ task_graph 工具 - 前端 Tasks 树形列表(折叠箭头/子进度徽章/缩进)+ 新建弹窗父任务下拉 + TaskDetail 父面包屑/子任务面板 - 设计文档: 父子任务支持设计-2026-08-04
This commit is contained in:
@@ -0,0 +1,215 @@
|
||||
# 父子任务支持设计
|
||||
|
||||
> 日期: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 库。
|
||||
Reference in New Issue
Block a user