Files
DevFlow/docs/04-功能迭代/父子任务支持设计-2026-08-04.md
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

11 KiB
Raw Permalink Blame History

父子任务支持设计

日期: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:577advance_task 都只调 advance_task_atomic,不重算父 status(与 IPC 不一致)。
  3. df-mcp create_task 不支持 parent_idschema 无入参,构造时硬编码 parent_id: Nonetools.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 私有实现):

/// 父任务 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_task handler:改调 advance_task_with_parent(与 IPC 同源,消除双轨)。

3.4 df-mcp crates/df-mcp/src/tools.rs

  • create_taskschema 增加 parent_id(可选 string);构造时透传;校验:parent 存在 + parent 自身无 parent_id(1 级嵌套),违反返回明确错误。
  • advance_task:改调 advance_task_with_parent

3.5 契约(前后端共用)

  • TaskRecordqueue: stringparent_id?: string | nullcontent_json?: string
  • delete_task 返回 TaskDeleteResult { ok: boolean; cascaded: number }(破坏性变更,仅 store/视图两处调用点,内部可控)。

4. 前端改动

4.1 类型与 API

src/api/types.ts

  • TaskRecord + queue: stringparent_id?: string | nullcontent_json?: string
  • CreateTaskInput + queue?: stringparent_id?: string | null(空串→后端视为 None
  • TaskQuery + queue?: string | nullparent_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

  • deleteTaskstate.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 恒 0totalTasks 改用 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。

分组渲染改造(每个项目组内):

├ 顶层任务AisParent=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)、任务回收站前端 UIlist_deleted_tasks 无命令,超范围,登记待办)、queue 管理池看板视图(move_task_queue 前端 UI,超范围)。
  • 回归风险delete_task 返回结构变更影响 store.deleteTask/Tasks.vue 两处;Tasks.vue 移除分页器影响 Paginator/totalTasks 逻辑——核查时重点验证。
  • UI 设计原则:树形沿用现有任务卡视觉(CSS token、状态徽章、快捷菜单),父/子层级用「缩进 + 竖线 + 折叠箭头 + 进度条」表达,不引入新 UI 库。