Skip to content

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-line slot 渲染多条预览线
  • 缺点:需要管理临时节点的生命周期

C: 隐藏占位节点(零尺寸)

类似 B,但使用零尺寸不可见节点。

  • 缺点:Handle 位置无法覆盖选区范围,用户交互体验差

Decision

选择 方案 B

临时 group node 的生命周期由 useGroupNode composable 管理,通过 Vue Flow 事件驱动(onNodesChangeonPaneClickonNodeDragonSelectionDragonConnectStartonConnectEnd)。

关键设计:

  • 节点 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) 确保不污染持久化数据)
  • 连线拦截在 useEdgeConnectiononConnect / onConnectEnd 中完成,检测到 group node 后展开为 addBatchEdges 批量调用
  • connection-line slot 检测到 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 原生事件(onNodesChangeonPaneClick)驱动生命周期
  • 节点拖拽位置更新需用 onNodeDrag(持续触发),不能用 onNodeDragStop(仅结束时触发)

Consequences

正面:

  • 完全复用 Vue Flow 原生 Handle 交互,零自定义拖拽代码
  • 连线预览、校验、事件分发全走已有路径,只需在关键节点做拦截和展开
  • 模式可复用:未来如需在选区上添加其他交互(批量操作工具栏等),同样可在 group node 上扩展

负面:

  • WS Command 发送时必须始终过滤 group node,遗漏会导致数据污染
  • isValidConnection 需对 group node 特殊放行(返回 true),实际验证推迟到 addBatchEdges
  • deletable: false 阻止 removeNodes,编程移除前需先翻转标志
  • 其他消费 nodes.value 的代码(如 shared/utils/upstream.ts 中的上游收集函数)可能需要感知 group node 的存在并过滤