# 基础用法 import { FreeLayoutSimplePreview } from '../../../../components'; ## 功能介绍 Free Layout 是 Flowgram.ai 提供的自由布局编辑器组件,允许用户创建和编辑流程图、工作流和各种节点连接图表。核心功能包括: * 节点自由拖拽与定位 * 节点连接与边缘管理 * 可配置的节点注册与自定义渲染 * 内置撤销/重做历史记录 * 支持插件扩展(如缩略图、自动对齐等) ## 从零构建自由布局编辑器 本节将带你从零开始构建一个自由布局编辑器应用,完整演示如何使用 @flowgram.ai/free-layout-editor 包构建一个可交互的流程编辑器。 ### 1. 环境准备 首先,我们需要创建一个新的项目: ```bash # 使用脚手架快速创建项目 npx @flowgram.ai/create-app@latest free-layout-simple # 进入项目目录 cd free-layout-simple # 安装依赖 npm install ``` ### 2. 项目结构 创建完成后,项目结构如下: ``` free-layout-simple/ ├── src/ │ ├── components/ # 组件目录 │ │ ├── node-add-panel.tsx # 节点添加面板 │ │ ├── tools.tsx # 工具栏组件 │ │ └── minimap.tsx # 缩略图组件 │ ├── hooks/ │ │ └── use-editor-props.tsx # 编辑器配置 │ ├── initial-data.ts # 初始数据定义 │ ├── node-registries.ts # 节点类型注册 │ ├── editor.tsx # 编辑器主组件 │ ├── app.tsx # 应用入口 │ ├── index.tsx # 渲染入口 │ └── index.css # 样式文件 ├── package.json └── ...其他配置文件 ``` ### 3. 开发流程 #### 步骤一:定义初始数据 首先,我们需要定义画布的初始数据结构,包括节点和连线: ```tsx // src/initial-data.ts import { WorkflowJSON } from '@flowgram.ai/free-layout-editor'; export const initialData: WorkflowJSON = { nodes: [ { id: 'start_0', type: 'start', meta: { position: { x: 0, y: 0 }, }, data: { title: '开始节点', content: '这是开始节点' }, }, { id: 'node_0', type: 'custom', meta: { position: { x: 400, y: 0 }, }, data: { title: '自定义节点', content: '这是自定义节点' }, }, { id: 'end_0', type: 'end', meta: { position: { x: 800, y: 0 }, }, data: { title: '结束节点', content: '这是结束节点' }, }, ], edges: [ { sourceNodeID: 'start_0', targetNodeID: 'node_0', }, { sourceNodeID: 'node_0', targetNodeID: 'end_0', }, ], }; ``` #### 步骤二:注册节点类型 接下来,我们需要定义不同类型节点的行为和外观: ```tsx // src/node-registries.ts import { WorkflowNodeRegistry } from '@flowgram.ai/free-layout-editor'; /** * 你可以自定义节点的注册器 */ export const nodeRegistries: WorkflowNodeRegistry[] = [ { type: 'start', meta: { isStart: true, // 开始节点标记 deleteDisable: true, // 开始节点不能被删除 copyDisable: true, // 开始节点不能被 copy defaultPorts: [{ type: 'output' }], // 定义 input 和 output 端口,开始节点只有 output 端口 }, }, { type: 'end', meta: { deleteDisable: true, copyDisable: true, defaultPorts: [{ type: 'input' }], // 结束节点只有 input 端口 }, }, { type: 'custom', meta: {}, defaultPorts: [{ type: 'output' }, { type: 'input' }], // 普通节点有两个端口 }, ]; ``` #### 步骤三:创建编辑器配置 使用 React hook 封装编辑器配置: ```tsx // src/hooks/use-editor-props.tsx import { useMemo } from 'react'; import { FreeLayoutProps, WorkflowNodeProps, WorkflowNodeRenderer, Field, useNodeRender, } from '@flowgram.ai/free-layout-editor'; import { createMinimapPlugin } from '@flowgram.ai/minimap-plugin'; import { createFreeSnapPlugin } from '@flowgram.ai/free-snap-plugin'; import { nodeRegistries } from '../node-registries'; import { initialData } from '../initial-data'; export const useEditorProps = () => useMemo( () => ({ // 启用背景网格 background: true, // 非只读模式 readonly: false, // 初始数据 initialData, // 节点类型注册 nodeRegistries, // 默认节点注册 getNodeDefaultRegistry(type) { return { type, meta: { defaultExpanded: true, }, formMeta: { // 节点表单渲染 render: () => ( <> name="title"> {({ field }) =>
{field.value}
}
name="content">
), }, }; }, // 节点渲染 materials: { renderDefaultNode: (props: WorkflowNodeProps) => { const { form } = useNodeRender(); return ( {form?.render()} ); }, }, // 内容变更回调 onContentChange(ctx, event) { console.log('数据变更: ', event, ctx.document.toJSON()); }, // 启用节点表单引擎 nodeEngine: { enable: true, }, // 启用历史记录 history: { enable: true, enableChangeNode: true, // 监听节点引擎数据变化 }, // 初始化回调 onInit: (ctx) => {}, // 渲染完成回调 onAllLayersRendered(ctx) { ctx.document.fitView(false); // 适应视图 }, // 销毁回调 onDispose() { console.log('编辑器已销毁'); }, // 插件配置 plugins: () => [ // 缩略图插件 createMinimapPlugin({ disableLayer: true, canvasStyle: { canvasWidth: 182, canvasHeight: 102, canvasPadding: 50, canvasBackground: 'rgba(245, 245, 245, 1)', canvasBorderRadius: 10, viewportBackground: 'rgba(235, 235, 235, 1)', viewportBorderRadius: 4, viewportBorderColor: 'rgba(201, 201, 201, 1)', viewportBorderWidth: 1, viewportBorderDashLength: 2, nodeColor: 'rgba(255, 255, 255, 1)', nodeBorderRadius: 2, nodeBorderWidth: 0.145, nodeBorderColor: 'rgba(6, 7, 9, 0.10)', overlayColor: 'rgba(255, 255, 255, 0)', }, inactiveDebounceTime: 1, }), // 自动对齐插件 createFreeSnapPlugin({ edgeColor: '#00B2B2', alignColor: '#00B2B2', edgeLineWidth: 1, alignLineWidth: 1, alignCrossWidth: 8, }), ], }), [] ); ``` #### 步骤四:创建节点添加面板 ```tsx // src/components/node-add-panel.tsx import React from 'react'; import { WorkflowDragService, useService } from '@flowgram.ai/free-layout-editor'; const nodeTypes = ['自定义节点1', '自定义节点2']; export const NodeAddPanel: React.FC = () => { const dragService = useService(WorkflowDragService); return (
{nodeTypes.map(nodeType => (
dragService.startDragCard(nodeType, e, { data: { title: nodeType, content: '拖拽创建的节点' } })} > {nodeType}
))}
); }; ``` #### 步骤五:创建工具栏和缩略图 ```tsx // src/components/tools.tsx import React from 'react'; import { useEffect, useState } from 'react'; import { usePlaygroundTools, useClientContext } from '@flowgram.ai/free-layout-editor'; export const Tools: React.FC = () => { const { history } = useClientContext(); const tools = usePlaygroundTools(); const [canUndo, setCanUndo] = useState(false); const [canRedo, setCanRedo] = useState(false); useEffect(() => { const disposable = history.undoRedoService.onChange(() => { setCanUndo(history.canUndo()); setCanRedo(history.canRedo()); }); return () => disposable.dispose(); }, [history]); return (
{Math.floor(tools.zoom * 100)}%
); }; // src/components/minimap.tsx import { FlowMinimapService, MinimapRender } from '@flowgram.ai/minimap-plugin'; import { useService } from '@flowgram.ai/free-layout-editor'; export const Minimap = () => { const minimapService = useService(FlowMinimapService); return (
); }; ``` #### 步骤六:组装编辑器主组件 ```tsx // src/editor.tsx import { EditorRenderer, FreeLayoutEditorProvider } from '@flowgram.ai/free-layout-editor'; import { useEditorProps } from './hooks/use-editor-props'; import { Tools } from './components/tools'; import { NodeAddPanel } from './components/node-add-panel'; import { Minimap } from './components/minimap'; import '@flowgram.ai/free-layout-editor/index.css'; import './index.css'; export const Editor = () => { const editorProps = useEditorProps(); return (
); }; ``` #### 步骤七:创建应用入口 ```tsx // src/app.tsx import React from 'react'; import ReactDOM from 'react-dom'; import { Editor } from './editor'; ReactDOM.render(, document.getElementById('root')) ``` #### 步骤八:添加样式 ```css /* src/index.css */ .demo-free-node { display: flex; min-width: 300px; min-height: 100px; flex-direction: column; align-items: flex-start; box-sizing: border-box; border-radius: 8px; border: 1px solid var(--light-usage-border-color-border, rgba(28, 31, 35, 0.08)); background: #fff; box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.1); } .demo-free-node-title { background-color: #93bfe2; width: 100%; border-radius: 8px 8px 0 0; padding: 4px 12px; } .demo-free-node-content { padding: 4px 12px; flex-grow: 1; width: 100%; } .demo-free-node::before { content: ''; position: absolute; top: 0; right: 0; bottom: 0; left: 0; z-index: -1; background-color: white; border-radius: 7px; } .demo-free-node:hover:before { -webkit-filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.3)) drop-shadow(0 4px 14px rgba(0, 0, 0, 0.1)); filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.3)) drop-shadow(0 4px 14px rgba(0, 0, 0, 0.1)); } .demo-free-node.activated:before, .demo-free-node.selected:before { outline: 2px solid var(--light-usage-primary-color-primary, #4d53e8); -webkit-filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.3)) drop-shadow(0 4px 14px rgba(0, 0, 0, 0.1)); filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.3)) drop-shadow(0 4px 14px rgba(0, 0, 0, 0.1)); } .demo-free-sidebar { height: 100%; overflow-y: auto; padding: 12px 16px 0; box-sizing: border-box; background: #f7f7fa; border-right: 1px solid rgba(29, 28, 35, 0.08); } .demo-free-right-top-panel { position: fixed; right: 10px; top: 70px; width: 300px; z-index: 999; } .demo-free-card { width: 140px; height: 60px; display: flex; align-items: center; justify-content: center; font-size: 20px; background: #fff; border-radius: 8px; box-shadow: 0 6px 8px 0 rgba(28, 31, 35, 0.03); cursor: -webkit-grab; cursor: grab; line-height: 16px; margin-bottom: 12px; overflow: hidden; padding: 16px; position: relative; color: black; } .demo-free-layout { display: flex; flex-direction: row; flex-grow: 1; } .demo-free-editor { flex-grow: 1; position: relative; height: 100%; } .demo-free-container { position: absolute; left: 0; top: 0; display: flex; width: 100%; height: 100%; flex-direction: column; } ``` ### 4. 运行项目 完成上述步骤后,你可以运行项目查看效果: ```bash npm run dev ``` 项目将在本地启动,通常访问 http://localhost:3000 即可看到效果。 ## 核心概念 ### 1. 数据结构 Free Layout 使用标准化的数据结构来描述节点和连接: ```tsx // 工作流数据结构 const initialData: WorkflowJSON = { // 节点定义 nodes: [ { id: 'start_0', // 节点唯一ID type: 'start', // 节点类型(对应 nodeRegistries 中的注册) meta: { position: { x: 0, y: 0 }, // 节点位置 }, data: { title: 'Start', // 节点数据(可自定义) content: 'Start content' }, }, // 更多节点... ], // 连线定义 edges: [ { sourceNodeID: 'start_0', // 源节点ID targetNodeID: 'node_0', // 目标节点ID }, // 更多连线... ], }; ``` ### 2. 节点注册 使用 `nodeRegistries` 定义不同类型节点的行为和外观: ```tsx // 节点注册 import { WorkflowNodeRegistry } from '@flowgram.ai/free-layout-editor'; export const nodeRegistries: WorkflowNodeRegistry[] = [ // 开始节点定义 { type: 'start', meta: { isStart: true, // Mark as start deleteDisable: true, // The start node cannot be deleted copyDisable: true, // The start node cannot be copied defaultPorts: [{ type: 'output' }], // Used to define the input and output ports, the start node only has the output port }, }, // 更多节点类型... ]; ``` ### 3. 编辑器组件 ```tsx // 核心编辑器容器与渲染器 import { FreeLayoutEditorProvider, EditorRenderer } from '@flowgram.ai/free-layout-editor'; // 编辑器配置示例 const editorProps = { background: true, // 启用背景网格 readonly: false, // 非只读模式,允许编辑 initialData: {...}, // 初始化数据:节点和边的定义 nodeRegistries: [...], // 节点类型注册 nodeEngine: { enable: true, // 启用节点表单引擎 }, history: { enable: true, // 启用历史记录 enableChangeNode: true, // 监听节点数据变化 } }; // 完整编辑器渲染
{/* 节点添加面板 */} {/* 核心编辑器渲染区域 */} {/* 工具栏 */} {/* 缩略图 */}
``` ### 4. 核心钩子函数 在组件中可以使用多种钩子函数获取和操作编辑器: ```tsx // 获取拖拽服务 const dragService = useService(WorkflowDragService); // 开始拖拽节点 dragService.startDragCard('nodeType', event, { data: {...} }); // 获取编辑器上下文 const { document, playground } = useClientContext(); // 操作画布 document.fitView(); // 适应视图 playground.config.zoomin(); // 缩放画布 document.fromJSON(newData); // 更新数据 ``` ### 5. 插件扩展 Free Layout 支持通过插件机制扩展功能: ```tsx plugins: () => [ // 缩略图插件 createMinimapPlugin({ canvasStyle: { canvasWidth: 180, canvasHeight: 100, canvasBackground: 'rgba(245, 245, 245, 1)', } }), // 自动对齐插件 createFreeSnapPlugin({ edgeColor: '#00B2B2', // 对齐线颜色 alignColor: '#00B2B2', // 辅助线颜色 edgeLineWidth: 1, // 线宽 }), ], ``` ## 安装 ```bash npx @flowgram.ai/create-app@latest free-layout-simple ``` ## 源码 https://github.com/bytedance/flowgram.ai/tree/main/apps/demo-free-layout-simple