310 lines
8.3 KiB
Markdown
310 lines
8.3 KiB
Markdown
|
|
# 功能更新:树状分类 + 连线右键菜单
|
|||
|
|
|
|||
|
|
## 更新内容
|
|||
|
|
|
|||
|
|
### 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<string, TreeNode> // 子目录
|
|||
|
|
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
|
|||
|
|
<ReactFlow
|
|||
|
|
onEdgeContextMenu={onEdgeContextMenu} // 新增
|
|||
|
|
onNodeContextMenu={onNodeContextMenu}
|
|||
|
|
onPaneContextMenu={onPaneContextMenu}
|
|||
|
|
// ...其他配置
|
|||
|
|
/>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**删除同一接口所有连线**:
|
|||
|
|
```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 只有一级,也能正常显示
|
|||
|
|
- ✅ 数据安全:所有操作都有确认提示(清空画布等危险操作)
|