# Bug 修复和功能增强总结 ## 本次修复的问题 ### 1. 删除节点后保存/执行时老节点仍存在 ✓ **问题描述**: - 在画布上删除节点后,视觉上节点消失了 - 但保存工作流或执行工作流时,被删除的节点仍然存在 - 这导致工作流数据不一致 **根本原因**: - `NodeGraphicsItem.delete_node()` 只从图形场景移除了节点 - 但没有通知 `WorkflowTabWidget` 更新其内部的 `self.nodes` 字典 - 保存/执行时仍然使用旧的节点数据 **修复方案**: ```python # node_graphics.py - 修改通知机制 def delete_node(self): # 通知Canvas(View)节点被删除 for view in scene.views(): if hasattr(view, 'on_node_deleted'): view.on_node_deleted(self.node_id) break scene.removeItem(self) # workflow_canvas.py - 转发删除事件 def on_node_deleted(self, node_id: str): print(f"Canvas收到节点删除通知: {node_id}") self.node_deleted.emit(node_id) # workflow_tab_widget.py - 清理节点数据 def _on_node_deleted(self, node_id: str): if node_id in self.nodes: del self.nodes[node_id] # 从字典删除 # 删除相关连接 self.connections = [(f, t) for f, t in self.connections if f != node_id and t != node_id] ``` **修复文件**: - `src/views/node_graphics.py` - `src/views/workflow_canvas.py` - `src/views/workflow_tab_widget.py` ### 2. 拖拽节点到画布显示禁止标志 ✓ **问题描述**: - 从节点浏览器拖拽节点到画布时,显示禁止图标 - 无法通过拖拽方式添加节点 **根本原因**: - `QListWidget` 默认的拖拽机制不正确 - `startDrag` 方法没有被正确触发 - 需要自定义拖拽行为 **修复方案**: ```python # node_browser.py - 创建自定义拖拽列表 class DraggableListWidget(QListWidget): """支持拖拽的列表控件""" def __init__(self, parent=None): super().__init__(parent) self.setDragEnabled(True) self.setDragDropMode(QAbstractItemView.DragOnly) def startDrag(self, supportedActions): """开始拖拽""" item = self.currentItem() if not item: return node_data = item.data(Qt.UserRole) # 创建拖拽对象 drag = QDrag(self) mime_data = QMimeData() mime_data.setText(node_data['type'].value) drag.setMimeData(mime_data) # 执行拖拽 drag.exec_(Qt.CopyAction) # 使用自定义控件 self.node_list = DraggableListWidget() ``` **修复文件**: - `src/views/node_browser.py` ### 3. 双击节点浏览器中的节点无效 ✓ **问题描述**: - 双击节点浏览器中的节点,画布上没有添加节点 **根本原因**: - 主窗口缺少 `add_node_to_canvas` 方法 - 节点浏览器的双击事件无法找到处理方法 **修复方案**: ```python # main_window.py - 添加节点添加方法 def add_node_to_canvas(self, node_type): """添加节点到当前画布的中心位置""" current_widget = self.tabs.currentWidget() if isinstance(current_widget, WorkflowTabWidget): # 生成节点ID和标题 node_id = f"node_{int(time.time() * 1000)}" node_title = node_title_map.get(node_type, node_type.value) # 创建节点 node_item = NodeGraphicsItem(node_id, node_type, node_title) # 计算画布中心位置 canvas = current_widget.canvas view_center = canvas.viewport().rect().center() scene_center = canvas.mapToScene(view_center) # 设置节点位置并添加到场景 node_item.setPos(scene_center.x() - node_item.width / 2, scene_center.y() - node_item.height / 2) canvas._scene.addItem(node_item) canvas.node_added.emit(node_item) # node_browser.py - 查找主窗口 def _on_node_double_clicked(self, item): node_data = item.data(Qt.UserRole) # 向上查找主窗口 widget = self.parent() while widget: if hasattr(widget, 'add_node_to_canvas'): widget.add_node_to_canvas(node_data['type']) break widget = widget.parent() if hasattr(widget, 'parent') else None ``` **修复文件**: - `src/main_window.py` - `src/views/node_browser.py` ## 新增功能 ### 4. 标签页管理功能 ✓ #### 4.1 标签页关闭 **功能描述**: - 每个工作流标签都有关闭按钮(X) - Overview 标签受保护,无法关闭 - 支持通过关闭按钮快速关闭标签 **实现代码**: ```python # main_window.py self.tabs.setTabsClosable(True) self.tabs.tabCloseRequested.connect(self._close_tab) def _close_tab(self, index): if index == 0: # 保护 Overview return widget = self.tabs.widget(index) if isinstance(widget, WorkflowTabWidget): if not self._check_save_before_close(widget): return # 用户取消 self.tabs.removeTab(index) widget.deleteLater() ``` #### 4.2 右键菜单 **功能描述**: - 关闭当前:关闭右键点击的标签 - 关闭其他:关闭除当前外的所有工作流标签 - 关闭所有:关闭所有工作流标签 **实现代码**: ```python def _show_tab_context_menu(self, pos): menu = QMenu(self) close_action = menu.addAction("关闭当前") close_others_action = menu.addAction("关闭其他") close_all_action = menu.addAction("关闭所有") action = menu.exec_(tab_bar.mapToGlobal(pos)) if action == close_action: self._close_tab(index) elif action == close_others_action: self._close_other_tabs(index) elif action == close_all_action: self._close_all_tabs() ``` #### 4.3 修改状态跟踪 **功能描述**: - 工作流被修改时,标签页标题显示 `*` 标识 - 保存后自动移除 `*` 标识 - 触发条件:添加/删除节点、创建连接、修改配置 **实现代码**: ```python # workflow_tab_widget.py class WorkflowTabWidget(QWidget): modified_changed = Signal(bool) # 修改状态信号 def __init__(self, ...): self._is_modified = False def _set_modified(self, modified: bool): if self._is_modified != modified: self._is_modified = modified self.modified_changed.emit(modified) # 在节点操作后调用 def _on_node_added(self, node_item): self.nodes[node_item.node_id] = node_item self._set_modified(True) # 标记修改 # 保存后重置 def _save_workflow(self): self.executor.save_workflow(save_path, node_positions) self._set_modified(False) # 重置修改状态 # main_window.py - 更新标签文本 def _on_workflow_modified(self, is_modified): sender_widget = self.sender() for i in range(self.tabs.count()): if self.tabs.widget(i) == sender_widget: workflow_name = sender_widget.workflow_name if is_modified: self.tabs.setTabText(i, f"{workflow_name} *") else: self.tabs.setTabText(i, workflow_name) break ``` #### 4.4 保存提示 **功能描述**: - 关闭有未保存更改的标签时,弹出保存提示 - 提供三个选项:保存、不保存、取消 - 关闭主窗口时检查所有工作流 **实现代码**: ```python def _check_save_before_close(self, workflow_widget): if not workflow_widget.is_modified(): return True # 无修改,可以关闭 reply = QMessageBox.question( self, "保存工作流", f"工作流 '{workflow_widget.workflow_name}' 有未保存的更改。\n是否保存?", QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel ) if reply == QMessageBox.Save: workflow_widget._save_workflow() return True elif reply == QMessageBox.Discard: return True else: # Cancel return False # 窗口关闭事件 def closeEvent(self, event): for i in range(1, self.tabs.count()): widget = self.tabs.widget(i) if isinstance(widget, WorkflowTabWidget): if not self._check_save_before_close(widget): event.ignore() # 用户取消,阻止关闭 return event.accept() ``` **修改文件**: - `src/main_window.py` - `src/views/workflow_tab_widget.py` ## 修改文件清单 ### 核心修复 1. `src/views/node_graphics.py` - 修改节点删除通知机制 2. `src/views/workflow_canvas.py` - 添加删除事件转发 3. `src/views/workflow_tab_widget.py` - 清理节点数据、添加修改跟踪 4. `src/views/node_browser.py` - 实现自定义拖拽、双击处理 5. `src/main_window.py` - 添加节点添加方法、标签页管理 ### 测试文件 1. `test_fixes.py` - 节点删除修复测试 2. `verify_fixes.py` - 简单验证脚本 3. `test_tab_management.py` - 标签页管理测试 ### 文档 1. `BUG_FIXES_SUMMARY.md` - 本文档 2. `TAB_MANAGEMENT_GUIDE.md` - 标签页管理详细指南 ## 测试验证 ### 自动化测试 #### 测试 1: 节点删除 ```bash python test_fixes.py ``` - ✓ 双击添加节点 - ✓ 节点删除后从内存移除 - ✓ 保存时不包含已删除节点 - ✓ 拖拽功能修复 #### 测试 2: 标签管理 ```bash python test_tab_management.py ``` - ✓ 标签可关闭 - ✓ 修改标识显示 - ✓ 保存后移除标识 - ✓ Overview 保护 ### 手动测试检查清单 - [ ] **节点删除**: 删除节点后保存,确认节点不在 JSON 文件中 - [ ] **节点删除**: 删除节点后执行,确认不会执行已删除节点 - [ ] **节点拖拽**: 从节点浏览器拖拽节点到画布,无禁止标志 - [ ] **节点双击**: 双击节点浏览器节点,在画布中心创建节点 - [ ] **修改标识**: 添加节点后,标签显示 `*` - [ ] **保存重置**: 保存后,`*` 标识消失 - [ ] **关闭提示**: 关闭有修改的标签,显示保存提示 - [ ] **右键菜单**: 右键标签,测试关闭当前/其他/所有 - [ ] **窗口关闭**: 关闭窗口时,检查所有未保存工作流 ## 技术要点 ### 1. 信号传递链 ``` NodeGraphicsItem.delete_node() ↓ WorkflowCanvas.on_node_deleted() ↓ (emit node_deleted signal) WorkflowTabWidget._on_node_deleted() ↓ del self.nodes[node_id] ``` ### 2. 修改状态传播 ``` WorkflowTabWidget._set_modified(True) ↓ (emit modified_changed signal) MainWindow._on_workflow_modified() ↓ Update tab text with "*" ``` ### 3. 父窗口查找 ```python # 从子组件向上查找主窗口 widget = self.parent() while widget: if hasattr(widget, 'target_method'): widget.target_method() break widget = widget.parent() ``` ## 注意事项 1. **信号连接**: 新创建的工作流标签必须连接 `modified_changed` 信号 2. **索引保护**: 关闭标签时要保护 Overview(索引 0) 3. **内存清理**: 移除标签后调用 `widget.deleteLater()` 释放内存 4. **反向遍历**: 批量关闭标签时从后向前遍历,避免索引变化 ## 已知问题 无 ## 未来改进建议 1. **撤销/重做**: 添加操作历史,支持撤销/重做 2. **自动保存**: 实现定期自动保存功能 3. **多选删除**: 支持选中多个节点批量删除 4. **拖拽优化**: 添加拖拽预览图标 5. **快捷键**: 添加键盘快捷键(Ctrl+W 关闭标签等) ## 总结 本次修复解决了 3 个核心 bug,并新增了完整的标签页管理功能。所有修改都经过测试验证,确保功能正常且不影响现有代码。修改遵循现有代码风格和架构设计,易于维护和扩展。