# 父子任务支持设计 > 日期: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 /// 推进任务 + 若为子任务则触发父聚合(父聚合失败仅 warn 不阻断,宽容语义)。 pub async fn advance_task_with_parent( repo: &TaskRepo, id: &str, target_status: &str, ) -> df_types::error::Result ``` - `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`(适配新返回) - 新增 `getTree(id): Promise` → `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`(一次加载即全部);**移除 ``**。 **树组装**(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)` + 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 库。