8.3 KiB
8.3 KiB
功能更新:树状分类 + 连线右键菜单
更新内容
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:删除单条连线
- 右键点击连线
- 选择 "✂️ 删除此连线"
- 该连线被移除
场景2:清理多输入节点
CSVLoader1 ──┐
├──> Aggregator (input-0)
CSVLoader2 ──┘
- 右键点击任意一条连线
- 选择 "🗑️ 删除此接口所有连线"
- 两条连线都被移除(因为连接到同一 target handle)
场景3:断开节点所有连线
FilterRows ──> SelectColumns ──> Aggregator
- 右键点击 SelectColumns 节点
- 选择 "✂️ 断开所有连线"
- 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
}
注意事项
-
树层级限制:
- 当前
MAX_TREE_DEPTH = 3 - 超出部分会作为
subCategory标签显示 - 可以在代码中修改此常量来调整限制
- 当前
-
搜索行为:
- 搜索时自动展开所有包含匹配节点的目录
- 空目录会被过滤掉
- 节点总数统计包含所有子节点
-
连线删除:
- "删除此连线" 只删除当前选中的连线
- "删除此接口所有连线" 删除同一 target + targetHandle 的所有连线
- "断开所有连线" 删除节点的所有输入和输出连线
-
性能优化:
- 树构建使用 Map 数据结构,查找效率高
- 递归渲染时会跳过空节点
- 状态更新使用 React Flow 的原生 hooks
测试建议
测试1:树状分类
- 修改
server/main.py,添加多级分类:"TestNode": { "category": "A/B/C/D/E" # 5级,超出限制 } - 启动前后端,观察节点面板显示
- 应该看到:
A > B > C > TestNode+ 标签📁 D/E
测试2:连线右键菜单
- 创建 2 个 CSVLoader + 1 个 Aggregator
- 连接两个 Loader 到 Aggregator(多输入)
- 右键点击任意连线
- 选择 "删除此接口所有连线"
- 应该看到两条连线都被删除
测试3:节点断开连线
- 创建 FilterRows -> SelectColumns -> Aggregator 链
- 右键点击中间的 SelectColumns 节点
- 选择 "断开所有连线"
- 应该看到该节点的输入和输出连线都被移除
文件变更清单
| 文件 | 变更类型 | 描述 |
|---|---|---|
web/src/components/NodePalette.tsx |
重构 | 支持多层级树状分类 |
web/src/components/Workspace.tsx |
增强 | 添加连线右键菜单 |
兼容性说明
- ✅ 向后兼容:旧的二级分类(如
Loader/CSV)仍然正常工作 - ✅ 渐进增强:如果 category 只有一级,也能正常显示
- ✅ 数据安全:所有操作都有确认提示(清空画布等危险操作)