TraceStudio-dev/docs/web1.0/FEATURE_TREE_CONTEXTMENU.md
2026-01-07 19:34:45 +08:00

8.3 KiB
Raw Blame History

功能更新:树状分类 + 连线右键菜单

更新内容

1. 节点面板多层级树状分类

功能描述

  • 支持最多 3 级树状目录结构(类似文件树)
  • 超出3级的分类会作为 subCategory 标签显示在节点下方
  • 支持递归展开/折叠,自动统计子节点数量
  • 搜索时自动展开所有匹配的分类

实现细节

文件web/src/components/NodePalette.tsx

核心类型定义

interface TreeNode {
  name: string
  items: Array<{ type: string; meta: PluginMeta; subCategory?: string }>  // 叶子节点
  children: Map<string, TreeNode>  // 子目录
  expanded: boolean
}

const MAX_TREE_DEPTH = 3  // 最大树层级

分类示例

# server/main.py
"CSVLoader": {
    "category": "Loader/CSV/hello/world",  # 4级分类
    # 实际显示:
    # Loader (目录)
    #   └─ CSV (目录)
    #      └─ hello (目录)
    #         └─ CSV 数据加载器 (节点)
    #            📁 world (subCategory标签超出3级部分)
}

功能特性

  • 递归构建树结构(buildTree()
  • 递归渲染树节点(renderTreeNode()
  • 自动计算节点总数(包含所有子节点)
  • 搜索时保持树结构,高亮匹配节点
  • 保留目录展开/折叠状态

2. 🖱️ 连线右键菜单

功能描述

  • 右键点击连线可显示操作菜单
  • 支持删除单条连线或同一接口的所有连线
  • 右键点击节点可断开该节点的所有连线

实现细节

文件web/src/components/Workspace.tsx

菜单类型扩展

const [contextMenu, setContextMenu] = useState<{
  x: number
  y: number
  type: 'pane' | 'node' | 'edge'  // 新增 'edge' 类型
  nodeId?: string
  edgeId?: string  // 新增边 ID
} | null>(null)

事件处理器

// 连线右键菜单
const onEdgeContextMenu = useCallback((event: React.MouseEvent, edge: any) => {
  event.preventDefault()
  setContextMenu({
    x: event.clientX,
    y: event.clientY,
    type: 'edge',
    edgeId: edge.id
  })
}, [])

菜单操作

右键对象 菜单项 功能描述
连线 ✂️ 删除此连线 删除当前选中的连线
连线 🗑️ 删除此接口所有连线 删除同一 target handle 的所有连线(用于清理多输入节点)
节点 ✂️ 断开所有连线 删除该节点的所有输入和输出连线
节点 📋 复制节点 复制节点到偏移位置
节点 🗑️ 删除节点 删除节点及其所有连线
画布 💾 保存工作流 保存到 localStorage
画布 📥 导入工作流 从 JSON 文件导入
画布 📤 导出工作流 导出为 JSON 文件
画布 🗑️ 清空画布 清空所有节点和连线

使用示例

示例1多级树状分类

后端配置 (server/main.py):

"FilterRows": {
    "function": "Transform",
    "category": "Transform/Filter/Advanced",  # 3级分类
}

"CSVLoader": {
    "function": "Loader",
    "category": "Loader/CSV/Local/File",  # 4级分类
    # 显示效果:
    # Loader
    #   └─ CSV
    #      └─ Local
    #         └─ CSV 数据加载器
    #            📁 File (subCategory标签)
}

前端显示

📥 Loader (2)
   ▸ CSV (1)
      ▸ Local (1)
         📥 CSV 数据加载器
            📁 File
   ▸ Trace (1)
      📥 UTrace 文件加载器

⚙️ Transform (4)
   ▸ Filter (2)
      ▸ Advanced (1)
         ⚙️ 行过滤器
      ⚙️ 时间范围过滤
   ▸ Select (1)
      ⚙️ 列选择器
   ▸ Aggregate (1)
      ⚙️ 数据聚合

示例2连线管理

场景1删除单条连线

  1. 右键点击连线
  2. 选择 "✂️ 删除此连线"
  3. 该连线被移除

场景2清理多输入节点

CSVLoader1 ──┐
              ├──> Aggregator (input-0)
CSVLoader2 ──┘
  1. 右键点击任意一条连线
  2. 选择 "🗑️ 删除此接口所有连线"
  3. 两条连线都被移除(因为连接到同一 target handle

场景3断开节点所有连线

FilterRows ──> SelectColumns ──> Aggregator
  1. 右键点击 SelectColumns 节点
  2. 选择 "✂️ 断开所有连线"
  3. SelectColumns 的输入和输出连线都被移除

技术要点

树状分类算法

关键函数

function buildTree(): TreeNode {
  const root: TreeNode = { name: 'root', items: [], children: new Map(), expanded: true }
  
  for (const [nodeType, meta] of Object.entries(plugins)) {
    const categoryPath = meta.category || 'Other'
    const parts = categoryPath.split('/')
    
    // 限制树的深度,超出部分作为 subCategory
    const treeParts = parts.slice(0, MAX_TREE_DEPTH)
    const subCategory = parts.length > MAX_TREE_DEPTH 
      ? parts.slice(MAX_TREE_DEPTH).join('/') 
      : undefined
    
    // 逐级构建树结构
    let currentNode = root
    for (let i = 0; i < treeParts.length; i++) {
      const part = treeParts[i]
      
      if (i === treeParts.length - 1) {
        // 最后一级,添加到 items
        currentNode.items.push({ type: nodeType, meta, subCategory })
      } else {
        // 中间层级,创建或获取子节点
        if (!currentNode.children.has(part)) {
          currentNode.children.set(part, {
            name: part,
            items: [],
            children: new Map(),
            expanded: expandedPaths.has(buildPath(treeParts.slice(0, i + 1)))
          })
        }
        currentNode = currentNode.children.get(part)!
      }
    }
  }
  
  return root
}

连线右键菜单

React Flow 事件绑定

<ReactFlow
  onEdgeContextMenu={onEdgeContextMenu}  // 新增
  onNodeContextMenu={onNodeContextMenu}
  onPaneContextMenu={onPaneContextMenu}
  // ...其他配置
/>

删除同一接口所有连线

case 'deleteAll': {
  const edge = edges.find(e => e.id === contextMenu.edgeId)
  if (edge) {
    // 删除同一 target handle 的所有连线
    setEdges((eds) => eds.filter((e) => 
      !(e.target === edge.target && e.targetHandle === edge.targetHandle)
    ))
  }
  break
}

注意事项

  1. 树层级限制

    • 当前 MAX_TREE_DEPTH = 3
    • 超出部分会作为 subCategory 标签显示
    • 可以在代码中修改此常量来调整限制
  2. 搜索行为

    • 搜索时自动展开所有包含匹配节点的目录
    • 空目录会被过滤掉
    • 节点总数统计包含所有子节点
  3. 连线删除

    • "删除此连线" 只删除当前选中的连线
    • "删除此接口所有连线" 删除同一 target + targetHandle 的所有连线
    • "断开所有连线" 删除节点的所有输入和输出连线
  4. 性能优化

    • 树构建使用 Map 数据结构,查找效率高
    • 递归渲染时会跳过空节点
    • 状态更新使用 React Flow 的原生 hooks

测试建议

测试1树状分类

  1. 修改 server/main.py,添加多级分类:
    "TestNode": {
        "category": "A/B/C/D/E"  # 5级超出限制
    }
    
  2. 启动前后端,观察节点面板显示
  3. 应该看到:A > B > C > TestNode + 标签 📁 D/E

测试2连线右键菜单

  1. 创建 2 个 CSVLoader + 1 个 Aggregator
  2. 连接两个 Loader 到 Aggregator多输入
  3. 右键点击任意连线
  4. 选择 "删除此接口所有连线"
  5. 应该看到两条连线都被删除

测试3节点断开连线

  1. 创建 FilterRows -> SelectColumns -> Aggregator 链
  2. 右键点击中间的 SelectColumns 节点
  3. 选择 "断开所有连线"
  4. 应该看到该节点的输入和输出连线都被移除

文件变更清单

文件 变更类型 描述
web/src/components/NodePalette.tsx 重构 支持多层级树状分类
web/src/components/Workspace.tsx 增强 添加连线右键菜单

兼容性说明

  • 向后兼容:旧的二级分类(如 Loader/CSV)仍然正常工作
  • 渐进增强:如果 category 只有一级,也能正常显示
  • 数据安全:所有操作都有确认提示(清空画布等危险操作)