# 功能更新:树状分类 + 连线右键菜单 ## 更新内容 ### 1. ✨ 节点面板多层级树状分类 #### 功能描述 - 支持最多 **3 级树状目录结构**(类似文件树) - 超出3级的分类会作为 `subCategory` 标签显示在节点下方 - 支持递归展开/折叠,自动统计子节点数量 - 搜索时自动展开所有匹配的分类 #### 实现细节 **文件**:[web/src/components/NodePalette.tsx](web/src/components/NodePalette.tsx) **核心类型定义**: ```typescript interface TreeNode { name: string items: Array<{ type: string; meta: PluginMeta; subCategory?: string }> // 叶子节点 children: Map // 子目录 expanded: boolean } const MAX_TREE_DEPTH = 3 // 最大树层级 ``` **分类示例**: ```python # 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](web/src/components/Workspace.tsx) **菜单类型扩展**: ```typescript const [contextMenu, setContextMenu] = useState<{ x: number y: number type: 'pane' | 'node' | 'edge' // 新增 'edge' 类型 nodeId?: string edgeId?: string // 新增边 ID } | null>(null) ``` **事件处理器**: ```typescript // 连线右键菜单 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`): ```python "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 的输入和输出连线都被移除 --- ## 技术要点 ### 树状分类算法 **关键函数**: ```typescript 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 事件绑定**: ```tsx ``` **删除同一接口所有连线**: ```typescript 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`,添加多级分类: ```python "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 只有一级,也能正常显示 - ✅ 数据安全:所有操作都有确认提示(清空画布等危险操作)