TraceStudio-dev/docs/web1.0/FEATURE_TREE_CONTEXTMENU.md

310 lines
8.3 KiB
Markdown
Raw Normal View History

2026-01-07 19:34:45 +08:00
# 功能更新:树状分类 + 连线右键菜单
## 更新内容
### 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 只有一级,也能正常显示
- ✅ 数据安全:所有操作都有确认提示(清空画布等危险操作)