- df-nodes task_advance_node(父聚合推进)+ task.rs 命令(create parent_id 支持/delete 级联软删子任务)+ task_graph 工具 - 前端 Tasks 树形列表(折叠箭头/子进度徽章/缩进)+ 新建弹窗父任务下拉 + TaskDetail 父面包屑/子任务面板 - 设计文档: 父子任务支持设计-2026-08-04
11 KiB
父子任务支持设计
日期: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 |
缺口(本次要补)
- 前端完全空白:
TaskRecord/CreateTaskInput/TaskQuery无 parent_id/queue 字段;Tasks.vue扁平列表无层级;TaskDetail.vue无父子信息;新建弹窗无父任务选择。 - 父聚合只在 IPC advance_task 触发:AI 工具
ai/tools/task.rs:209与 df-mcptools.rs:577的advance_task都只调advance_task_atomic,不重算父 status(与 IPC 不一致)。 - df-mcp create_task 不支持 parent_id:schema 无入参,构造时硬编码
parent_id: None(tools.rs:484)。 - 删除父任务后子任务悬挂:
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 私有实现):
/// 父任务 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:级联软删。新返回结构:
#[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_taskhandler:改调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?: stringCreateTaskInput+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):
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:
modal: { ..., parentTask: '父任务', parentPlaceholder: '无(顶层任务)' }
addSubtask: '+ 添加子任务'
tree: { progress: '进度' }
confirmDeleteWithChildren: '确定删除「{title}」吗?将同时删除 {n} 个子任务。'
zh-CN/taskDetail.ts + en/taskDetail.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 库。