Appearance
ADR-006: 使用临时 Vue Flow 节点作为多选 Group Handle 载体
Status
Accepted
Date
2026-03-26
Context
实现"节点多选批量连线"功能时,需要在多选选区上显示可拖拽的 Handle(source / target),让用户从 Handle 拖出连线到目标节点,一次性创建多条边。
Vue Flow 的 <Handle> 组件必须在自定义节点内部使用(内部通过 inject 获取父节点 ID),无法在选区框(NodesSelection)上直接放置。选区框也没有暴露 slot。
Options
A: 自定义 DOM 模拟 Handle
在 zoom-pane slot 或 body 层叠加绝对定位的 div,手动实现拖拽检测、连线预览、落点分发。
- 优点:不引入额外节点
- 缺点:无法复用 Vue Flow 原生连线交互(无预览线、需手动做
elementFromPoint检测、需自己画 SVG 连线)
B: 临时 Vue Flow 节点(Group Node)
创建一个真正的 Vue Flow 节点,覆盖选中节点的包围盒,内部放置原生 <Handle> 组件。节点设为 selectable: false, draggable: false, deletable: false,视觉上透明。syncToCanvas(注:已移除,flowStore 是 graph 唯一 mutable owner)过滤该节点,不持久化。
- 优点:Handle 交互完全原生(拖拽预览、
onConnect/onConnectEnd事件、isValidConnection校验) - 优点:可结合
connection-lineslot 渲染多条预览线 - 缺点:需要管理临时节点的生命周期
C: 隐藏占位节点(零尺寸)
类似 B,但使用零尺寸不可见节点。
- 缺点:Handle 位置无法覆盖选区范围,用户交互体验差
Decision
选择 方案 B。
临时 group node 的生命周期由 useGroupNode composable 管理,通过 Vue Flow 事件驱动(onNodesChange、onPaneClick、onNodeDrag、onSelectionDrag、onConnectStart、onConnectEnd)。
关键设计:
- 节点 ID 固定为
__stormflow-group-selection__,类型为group - flowStore 是 graph 唯一 mutable owner,group node 不发送到后端(
nodes.filter(n => n.type !== 'group')+edges.filter(e => source/target !== GROUP_NODE_ID)确保不污染持久化数据) - 连线拦截在
useEdgeConnection的onConnect/onConnectEnd中完成,检测到 group node 后展开为addBatchEdges批量调用 connection-lineslot 检测到 source 是 group node 时,渲染 N 条贝塞尔曲线(从各成员节点的 handle 位置到鼠标)- 连线拖拽开始时 group node 设为
visibility: hidden(隐藏 handle 但保留节点数据供 connection-line slot 读取),连线结束后恢复
实现中发现的陷阱:
deletable: false会阻止编程方式的removeNodes,移除前须先updateNode({ deletable: true })- Vue 响应式(
watch/watchEffect)对 Vue Flow 的getSelectedNodes追踪不可靠,须用 Vue Flow 原生事件(onNodesChange、onPaneClick)驱动生命周期 - 节点拖拽位置更新需用
onNodeDrag(持续触发),不能用onNodeDragStop(仅结束时触发)
Consequences
正面:
- 完全复用 Vue Flow 原生 Handle 交互,零自定义拖拽代码
- 连线预览、校验、事件分发全走已有路径,只需在关键节点做拦截和展开
- 模式可复用:未来如需在选区上添加其他交互(批量操作工具栏等),同样可在 group node 上扩展
负面:
- WS Command 发送时必须始终过滤 group node,遗漏会导致数据污染
isValidConnection需对 group node 特殊放行(返回true),实际验证推迟到addBatchEdgesdeletable: false阻止removeNodes,编程移除前需先翻转标志- 其他消费
nodes.value的代码(如shared/utils/upstream.ts中的上游收集函数)可能需要感知 group node 的存在并过滤