Flow
flow节点画布编排器 · 零依赖原生 Pointer Events + SVG 贝塞尔连线 · 拖节点/拖桩连线/平移缩放/适配视图 + 受控 nodes/edges(onNodesChange/onConnect/删点删线) + renderNode 自定义节点内容 · 几何抽纯函数带单测 · AI 工作流/流程编排旗舰
用法
基础节点画布
受控 nodes/edges;getHandles 声明每个节点的连接桩(左 target 入 / 右 source 出)。拖节点改位置、从输出桩拖到输入桩连线、滚轮平移、Ctrl+滚轮缩放。
提示词
赛博朋克城市夜景
生图模型
SDXL · 30 步
高清放大
×2 · 面部修复
输出
1 张图片
tsx
const [nodes, setNodes] = useState(initialNodes);
const [edges, setEdges] = useState(initialEdges);
<Flow
nodes={nodes}
edges={edges}
getHandles={(n) => [
...(n.data.io.includes("in") ? [{ id: "in", type: "target" as const }] : []),
...(n.data.io.includes("out") ? [{ id: "out", type: "source" as const }] : []),
]}
onNodesChange={setNodes}
onConnect={(c) => setEdges((e) => [...e, { id: uid(), ...c }])}
renderNode={(n) => <NodeCard data={n.data} />}
/>选中与删除
selectedId / onSelectNode 受控单选:点节点选中,点空白取消。选中后点 × 或按 Delete 删除节点(连带删除其连线)。
提示词
赛博朋克城市夜景
生图模型
SDXL · 30 步
高清放大
×2 · 面部修复
输出
1 张图片
tsx
<Flow
nodes={nodes}
edges={edges}
getHandles={getHandles}
selectedId={selected}
onSelectNode={setSelected}
onNodeDelete={(id) => {
setNodes((p) => p.filter((n) => n.id !== id));
setEdges((p) => p.filter((e) => e.source !== id && e.target !== id));
setSelected(null);
}}
onEdgesDelete={(ids) => setEdges((p) => p.filter((e) => !ids.includes(e.id)))}
renderNode={(n) => <NodeCard data={n.data} />}
/>运行中(连线流光)
isEdgeAnimated 返回 true 的连线走流光动画,常用于标记正在执行的链路。
提示词
赛博朋克城市夜景
生图模型
SDXL · 30 步
高清放大
×2 · 面部修复
输出
1 张图片
tsx
<Flow
nodes={nodes}
edges={edges}
getHandles={getHandles}
isEdgeAnimated={() => true}
renderNode={(n) => <NodeCard data={n.data} />}
/>隐藏缩放控制条
controls={false} 关闭右下角缩放 / 适配工具条,适合纯展示场景。
提示词
赛博朋克城市夜景
生图模型
SDXL · 30 步
高清放大
×2 · 面部修复
输出
1 张图片
tsx
<Flow
nodes={nodes}
edges={edges}
getHandles={getHandles}
controls={false}
renderNode={(n) => <NodeCard data={n.data} />}
/>何时用
节点画布式可视化编排(AI 工作流、流程/DAG 编辑、节点连线图),受控 nodes/edges + 拖节点/拖桩连线/平移缩放/适配视图。只需多列卡片流转用 Kanban;单列表排序用 Sortable。
导入
ts
import { Flow, bezierPath, clampZoom, fitViewport, handleOffsetRatio, handlePoint, nodesBounds, screenToCanvas, zoomAtPoint } from "@hulianui/ui"Props
FlowProps<T> 泛型。
| 名称 | 类型 | 默认 | 说明 |
|---|---|---|---|
| nodes * | FlowNode\<T\>[] | — | 受控节点数组({ id, position, data, width? }) |
| edges * | FlowEdge[] | — | 受控连线数组({ id, source, target, sourceHandle?, targetHandle? }) |
| getHandles * | (node: FlowNode\<T\>) => FlowHandleSpec[] | — | 声明每节点连接桩(左 target/右 source,按返回顺序在该侧均分纵向位置) |
| selectedId | string | null | — | 单选受控:当前选中节点 id |
| defaultNodeWidth | number | 240 | 默认节点宽度(px,画布坐标) |
| minZoom | number | 0.35 | 缩放下限 |
| maxZoom | number | 2 | 缩放上限 |
| controls | boolean | true | 是否显示右下角缩放/适配工具条 |
| isEdgeAnimated | (edge: FlowEdge) => boolean | — | 某条连线是否走流光动画(如运行中链路) |
| className | string | — | 画布外层类名(须有确定高度,组件填满) |
| apiRef | MutableRefObject\<FlowApi | null\> | — | 命令式句柄(fitView / zoomIn / zoomOut / reset / autoLayout) |
Events
| 事件 | 类型 | 说明 |
|---|---|---|
| onNodesChange | (nodes: FlowNode\<T\>[]) => void | 节点拖动后回吐整组新位置(组件不直接改 data) |
| onConnect | (connection: FlowConnection) => void | 从输出桩拖到合法输入桩成功 → 新连接(无 id,你补 id 并去重) |
| onEdgesDelete | (ids: string[]) => void | 删除连线(选中后点 × 或按 Delete) |
| onNodeDelete | (id: string) => void | 删除节点(选中后点 × 或按 Delete) |
| onSelectNode | (id: string | null) => void | 选中变化(点节点=id,点空白=null) |
Slots
| 插槽 | 类型 | 说明 |
|---|---|---|
| renderNode * | (node: FlowNode\<T\>, state: { selected: boolean }) => ReactNode | 渲染节点内容的渲染函数(外框/桩/选中态由组件负责) |
| background | ReactNode | false | 画布底纹(false 关闭);默认内置点阵 |
禁忌 / 坑
- 外层
className必须有确定高度(如h-[420px]或父级撑满),画布按外层尺寸填充——高度塌缩则画布不可见。 - 全受控:
onConnect回吐的连接无 id,你要自己补 id 并去重;onNodeDelete只给节点 id,关联连线要你自己一并清掉(否则留下悬空 edge)。 - 客户端组件(原生 Pointer Events),必须在 client 上下文用。
- 候选坑
base-ui-navigation-menu-content-must-stay-in-flow-for-popup-size-measure仅因「flow」字面匹配,讲的是 Base UI 导航菜单布局,与本组件无关,不引用。
相关
Table · Book3D · ProTable · PricingTable · JsonViewer · EditableTable
Playground
提示词
赛博朋克城市夜景
生图模型
SDXL · 30 步
高清放大
×2 · 面部修复
输出
1 张图片
<Flow
nodes={nodes}
edges={edges}
getHandles={(n) => [{ id: "in", type: "target" }, { id: "out", type: "source" }]}
onNodesChange={setNodes}
onConnect={(c) => setEdges((e) => [...e, { id: uid(), ...c }])}
renderNode={(n) => <NodeCard data={n.data} />}
/>