free-layout-simple.mdx 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701
  1. ---
  2. outline: true
  3. ---
  4. # 基础用法
  5. import { FreeLayoutSimplePreview } from '../../../../components';
  6. <FreeLayoutSimplePreview />
  7. ## 功能介绍
  8. Free Layout 是 Flowgram.ai 提供的自由布局编辑器组件,允许用户创建和编辑流程图、工作流和各种节点连接图表。核心功能包括:
  9. - 节点自由拖拽与定位
  10. - 节点连接与边缘管理
  11. - 可配置的节点注册与自定义渲染
  12. - 内置撤销/重做历史记录
  13. - 支持插件扩展(如缩略图、自动对齐等)
  14. ## 从零构建自由布局编辑器
  15. 本节将带你从零开始构建一个自由布局编辑器应用,完整演示如何使用 @flowgram.ai/free-layout-editor 包构建一个可交互的流程编辑器。
  16. ### 1. 环境准备
  17. 首先,我们需要创建一个新的项目:
  18. ```bash
  19. # 使用脚手架快速创建项目
  20. npx @flowgram.ai/create-app@latest free-layout-simple
  21. # 进入项目目录
  22. cd free-layout-simple
  23. # 安装依赖
  24. npm install
  25. ```
  26. ### 2. 项目结构
  27. 创建完成后,项目结构如下:
  28. ```
  29. free-layout-simple/
  30. ├── src/
  31. │ ├── components/ # 组件目录
  32. │ │ ├── node-add-panel.tsx # 节点添加面板
  33. │ │ ├── tools.tsx # 工具栏组件
  34. │ │ └── minimap.tsx # 缩略图组件
  35. │ ├── hooks/
  36. │ │ └── use-editor-props.tsx # 编辑器配置
  37. │ ├── initial-data.ts # 初始数据定义
  38. │ ├── node-registries.ts # 节点类型注册
  39. │ ├── editor.tsx # 编辑器主组件
  40. │ ├── app.tsx # 应用入口
  41. │ ├── index.tsx # 渲染入口
  42. │ └── index.css # 样式文件
  43. ├── package.json
  44. └── ...其他配置文件
  45. ```
  46. ### 3. 开发流程
  47. #### 步骤一:定义初始数据
  48. 首先,我们需要定义画布的初始数据结构,包括节点和连线:
  49. ```tsx
  50. // src/initial-data.ts
  51. import { WorkflowJSON } from '@flowgram.ai/free-layout-editor';
  52. export const initialData: WorkflowJSON = {
  53. nodes: [
  54. {
  55. id: 'start_0',
  56. type: 'start',
  57. meta: {
  58. position: { x: 0, y: 0 },
  59. },
  60. data: {
  61. title: '开始节点',
  62. content: '这是开始节点'
  63. },
  64. },
  65. {
  66. id: 'node_0',
  67. type: 'custom',
  68. meta: {
  69. position: { x: 400, y: 0 },
  70. },
  71. data: {
  72. title: '自定义节点',
  73. content: '这是自定义节点'
  74. },
  75. },
  76. {
  77. id: 'end_0',
  78. type: 'end',
  79. meta: {
  80. position: { x: 800, y: 0 },
  81. },
  82. data: {
  83. title: '结束节点',
  84. content: '这是结束节点'
  85. },
  86. },
  87. ],
  88. edges: [
  89. {
  90. sourceNodeID: 'start_0',
  91. targetNodeID: 'node_0',
  92. },
  93. {
  94. sourceNodeID: 'node_0',
  95. targetNodeID: 'end_0',
  96. },
  97. ],
  98. };
  99. ```
  100. #### 步骤二:注册节点类型
  101. 接下来,我们需要定义不同类型节点的行为和外观:
  102. ```tsx
  103. // src/node-registries.ts
  104. import { WorkflowNodeRegistry } from '@flowgram.ai/free-layout-editor';
  105. /**
  106. * 你可以自定义节点的注册器
  107. */
  108. export const nodeRegistries: WorkflowNodeRegistry[] = [
  109. {
  110. type: 'start',
  111. meta: {
  112. isStart: true, // 开始节点标记
  113. deleteDisable: true, // 开始节点不能被删除
  114. copyDisable: true, // 开始节点不能被 copy
  115. defaultPorts: [{ type: 'output' }], // 定义 input 和 output 端口,开始节点只有 output 端口
  116. },
  117. },
  118. {
  119. type: 'end',
  120. meta: {
  121. deleteDisable: true,
  122. copyDisable: true,
  123. defaultPorts: [{ type: 'input' }], // 结束节点只有 input 端口
  124. },
  125. },
  126. {
  127. type: 'custom',
  128. meta: {},
  129. defaultPorts: [{ type: 'output' }, { type: 'input' }], // 普通节点有两个端口
  130. },
  131. ];
  132. ```
  133. #### 步骤三:创建编辑器配置
  134. 使用 React hook 封装编辑器配置:
  135. ```tsx
  136. // src/hooks/use-editor-props.tsx
  137. import { useMemo } from 'react';
  138. import {
  139. FreeLayoutProps,
  140. WorkflowNodeProps,
  141. WorkflowNodeRenderer,
  142. Field,
  143. useNodeRender,
  144. } from '@flowgram.ai/free-layout-editor';
  145. import { createMinimapPlugin } from '@flowgram.ai/minimap-plugin';
  146. import { createFreeSnapPlugin } from '@flowgram.ai/free-snap-plugin';
  147. import { nodeRegistries } from '../node-registries';
  148. import { initialData } from '../initial-data';
  149. export const useEditorProps = () =>
  150. useMemo<FreeLayoutProps>(
  151. () => ({
  152. // 启用背景网格
  153. background: true,
  154. // 非只读模式
  155. readonly: false,
  156. // 初始数据
  157. initialData,
  158. // 节点类型注册
  159. nodeRegistries,
  160. // 默认节点注册
  161. getNodeDefaultRegistry(type) {
  162. return {
  163. type,
  164. meta: {
  165. defaultExpanded: true,
  166. },
  167. formMeta: {
  168. // 节点表单渲染
  169. render: () => (
  170. <>
  171. <Field<string> name="title">
  172. {({ field }) => <div className="demo-free-node-title">{field.value}</div>}
  173. </Field>
  174. <div className="demo-free-node-content">
  175. <Field<string> name="content">
  176. <input />
  177. </Field>
  178. </div>
  179. </>
  180. ),
  181. },
  182. };
  183. },
  184. // 节点渲染
  185. materials: {
  186. renderDefaultNode: (props: WorkflowNodeProps) => {
  187. const { form } = useNodeRender();
  188. return (
  189. <WorkflowNodeRenderer className="demo-free-node" node={props.node}>
  190. {form?.render()}
  191. </WorkflowNodeRenderer>
  192. );
  193. },
  194. },
  195. // 内容变更回调
  196. onContentChange(ctx, event) {
  197. console.log('数据变更: ', event, ctx.document.toJSON());
  198. },
  199. // 启用节点表单引擎
  200. nodeEngine: {
  201. enable: true,
  202. },
  203. // 启用历史记录
  204. history: {
  205. enable: true,
  206. enableChangeNode: true, // 监听节点引擎数据变化
  207. },
  208. // 初始化回调
  209. onInit: (ctx) => {},
  210. // 渲染完成回调
  211. onAllLayersRendered(ctx) {
  212. ctx.document.fitView(false); // 适应视图
  213. },
  214. // 销毁回调
  215. onDispose() {
  216. console.log('编辑器已销毁');
  217. },
  218. // 插件配置
  219. plugins: () => [
  220. // 缩略图插件
  221. createMinimapPlugin({
  222. disableLayer: true,
  223. canvasStyle: {
  224. canvasWidth: 182,
  225. canvasHeight: 102,
  226. canvasPadding: 50,
  227. canvasBackground: 'rgba(245, 245, 245, 1)',
  228. canvasBorderRadius: 10,
  229. viewportBackground: 'rgba(235, 235, 235, 1)',
  230. viewportBorderRadius: 4,
  231. viewportBorderColor: 'rgba(201, 201, 201, 1)',
  232. viewportBorderWidth: 1,
  233. viewportBorderDashLength: 2,
  234. nodeColor: 'rgba(255, 255, 255, 1)',
  235. nodeBorderRadius: 2,
  236. nodeBorderWidth: 0.145,
  237. nodeBorderColor: 'rgba(6, 7, 9, 0.10)',
  238. overlayColor: 'rgba(255, 255, 255, 0)',
  239. },
  240. }),
  241. // 自动对齐插件
  242. createFreeSnapPlugin({
  243. edgeColor: '#00B2B2',
  244. alignColor: '#00B2B2',
  245. edgeLineWidth: 1,
  246. alignLineWidth: 1,
  247. alignCrossWidth: 8,
  248. }),
  249. ],
  250. }),
  251. []
  252. );
  253. ```
  254. #### 步骤四:创建节点添加面板
  255. ```tsx
  256. // src/components/node-add-panel.tsx
  257. import React from 'react';
  258. import { WorkflowDragService, useService } from '@flowgram.ai/free-layout-editor';
  259. const nodeTypes = ['自定义节点1', '自定义节点2'];
  260. export const NodeAddPanel: React.FC = () => {
  261. const dragService = useService<WorkflowDragService>(WorkflowDragService);
  262. return (
  263. <div className="demo-free-sidebar">
  264. {nodeTypes.map(nodeType => (
  265. <div
  266. key={nodeType}
  267. className="demo-free-card"
  268. onMouseDown={e => dragService.startDragCard(nodeType, e, {
  269. data: {
  270. title: nodeType,
  271. content: '拖拽创建的节点'
  272. }
  273. })}
  274. >
  275. {nodeType}
  276. </div>
  277. ))}
  278. </div>
  279. );
  280. };
  281. ```
  282. #### 步骤五:创建工具栏和缩略图
  283. ```tsx
  284. // src/components/tools.tsx
  285. import React from 'react';
  286. import { useEffect, useState } from 'react';
  287. import { usePlaygroundTools, useClientContext } from '@flowgram.ai/free-layout-editor';
  288. export const Tools: React.FC = () => {
  289. const { history } = useClientContext();
  290. const tools = usePlaygroundTools();
  291. const [canUndo, setCanUndo] = useState(false);
  292. const [canRedo, setCanRedo] = useState(false);
  293. useEffect(() => {
  294. const disposable = history.undoRedoService.onChange(() => {
  295. setCanUndo(history.canUndo());
  296. setCanRedo(history.canRedo());
  297. });
  298. return () => disposable.dispose();
  299. }, [history]);
  300. return (
  301. <div
  302. style={{ position: 'absolute', zIndex: 10, bottom: 16, left: 226, display: 'flex', gap: 8 }}
  303. >
  304. <button onClick={() => tools.zoomin()}>ZoomIn</button>
  305. <button onClick={() => tools.zoomout()}>ZoomOut</button>
  306. <button onClick={() => tools.fitView()}>Fitview</button>
  307. <button onClick={() => tools.autoLayout()}>AutoLayout</button>
  308. <button onClick={() => history.undo()} disabled={!canUndo}>
  309. Undo
  310. </button>
  311. <button onClick={() => history.redo()} disabled={!canRedo}>
  312. Redo
  313. </button>
  314. <span>{Math.floor(tools.zoom * 100)}%</span>
  315. </div>
  316. );
  317. };
  318. // src/components/minimap.tsx
  319. import { MinimapRender } from '@flowgram.ai/minimap-plugin';
  320. export const Minimap = () => {
  321. return (
  322. <div
  323. style={{
  324. position: 'absolute',
  325. left: 226,
  326. bottom: 51,
  327. zIndex: 100,
  328. width: 198,
  329. }}
  330. >
  331. <MinimapRender
  332. containerStyles={{
  333. pointerEvents: 'auto',
  334. position: 'relative',
  335. top: 'unset',
  336. right: 'unset',
  337. bottom: 'unset',
  338. left: 'unset',
  339. }}
  340. inactiveStyle={{
  341. opacity: 1,
  342. scale: 1,
  343. translateX: 0,
  344. translateY: 0,
  345. }}
  346. />
  347. </div>
  348. );
  349. };
  350. ```
  351. #### 步骤六:组装编辑器主组件
  352. ```tsx
  353. // src/editor.tsx
  354. import { EditorRenderer, FreeLayoutEditorProvider } from '@flowgram.ai/free-layout-editor';
  355. import { useEditorProps } from './hooks/use-editor-props';
  356. import { Tools } from './components/tools';
  357. import { NodeAddPanel } from './components/node-add-panel';
  358. import { Minimap } from './components/minimap';
  359. import '@flowgram.ai/free-layout-editor/index.css';
  360. import './index.css';
  361. export const Editor = () => {
  362. const editorProps = useEditorProps();
  363. return (
  364. <FreeLayoutEditorProvider {...editorProps}>
  365. <div className="demo-free-container">
  366. <div className="demo-free-layout">
  367. <NodeAddPanel />
  368. <EditorRenderer className="demo-free-editor" />
  369. </div>
  370. <Tools />
  371. <Minimap />
  372. </div>
  373. </FreeLayoutEditorProvider>
  374. );
  375. };
  376. ```
  377. #### 步骤七:创建应用入口
  378. ```tsx
  379. // src/app.tsx
  380. import React from 'react';
  381. import ReactDOM from 'react-dom';
  382. import { Editor } from './editor';
  383. ReactDOM.render(<Editor />, document.getElementById('root'))
  384. ```
  385. #### 步骤八:添加样式
  386. ```css
  387. /* src/index.css */
  388. .demo-free-node {
  389. display: flex;
  390. min-width: 300px;
  391. min-height: 100px;
  392. flex-direction: column;
  393. align-items: flex-start;
  394. box-sizing: border-box;
  395. border-radius: 8px;
  396. border: 1px solid var(--light-usage-border-color-border, rgba(28, 31, 35, 0.08));
  397. background: #fff;
  398. box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.1);
  399. }
  400. .demo-free-node-title {
  401. background-color: #93bfe2;
  402. width: 100%;
  403. border-radius: 8px 8px 0 0;
  404. padding: 4px 12px;
  405. }
  406. .demo-free-node-content {
  407. padding: 4px 12px;
  408. flex-grow: 1;
  409. width: 100%;
  410. }
  411. .demo-free-node::before {
  412. content: '';
  413. position: absolute;
  414. top: 0;
  415. right: 0;
  416. bottom: 0;
  417. left: 0;
  418. z-index: -1;
  419. background-color: white;
  420. border-radius: 7px;
  421. }
  422. .demo-free-node:hover:before {
  423. -webkit-filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.3)) drop-shadow(0 4px 14px rgba(0, 0, 0, 0.1));
  424. filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.3)) drop-shadow(0 4px 14px rgba(0, 0, 0, 0.1));
  425. }
  426. .demo-free-node.activated:before,
  427. .demo-free-node.selected:before {
  428. outline: 2px solid var(--light-usage-primary-color-primary, #4d53e8);
  429. -webkit-filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.3)) drop-shadow(0 4px 14px rgba(0, 0, 0, 0.1));
  430. filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.3)) drop-shadow(0 4px 14px rgba(0, 0, 0, 0.1));
  431. }
  432. .demo-free-sidebar {
  433. height: 100%;
  434. overflow-y: auto;
  435. padding: 12px 16px 0;
  436. box-sizing: border-box;
  437. background: #f7f7fa;
  438. border-right: 1px solid rgba(29, 28, 35, 0.08);
  439. }
  440. .demo-free-right-top-panel {
  441. position: fixed;
  442. right: 10px;
  443. top: 70px;
  444. width: 300px;
  445. z-index: 999;
  446. }
  447. .demo-free-card {
  448. width: 140px;
  449. height: 60px;
  450. display: flex;
  451. align-items: center;
  452. justify-content: center;
  453. font-size: 20px;
  454. background: #fff;
  455. border-radius: 8px;
  456. box-shadow: 0 6px 8px 0 rgba(28, 31, 35, 0.03);
  457. cursor: -webkit-grab;
  458. cursor: grab;
  459. line-height: 16px;
  460. margin-bottom: 12px;
  461. overflow: hidden;
  462. padding: 16px;
  463. position: relative;
  464. color: black;
  465. }
  466. .demo-free-layout {
  467. display: flex;
  468. flex-direction: row;
  469. flex-grow: 1;
  470. }
  471. .demo-free-editor {
  472. flex-grow: 1;
  473. position: relative;
  474. height: 100%;
  475. }
  476. .demo-free-container {
  477. position: absolute;
  478. left: 0;
  479. top: 0;
  480. display: flex;
  481. width: 100%;
  482. height: 100%;
  483. flex-direction: column;
  484. }
  485. ```
  486. ### 4. 运行项目
  487. 完成上述步骤后,你可以运行项目查看效果:
  488. ```bash
  489. npm run dev
  490. ```
  491. 项目将在本地启动,通常访问 http://localhost:3000 即可看到效果。
  492. ## 核心概念
  493. ### 1. 数据结构
  494. Free Layout 使用标准化的数据结构来描述节点和连接:
  495. ```tsx
  496. // 工作流数据结构
  497. const initialData: WorkflowJSON = {
  498. // 节点定义
  499. nodes: [
  500. {
  501. id: 'start_0', // 节点唯一ID
  502. type: 'start', // 节点类型(对应 nodeRegistries 中的注册)
  503. meta: {
  504. position: { x: 0, y: 0 }, // 节点位置
  505. },
  506. data: {
  507. title: 'Start', // 节点数据(可自定义)
  508. content: 'Start content'
  509. },
  510. },
  511. // 更多节点...
  512. ],
  513. // 连线定义
  514. edges: [
  515. {
  516. sourceNodeID: 'start_0', // 源节点ID
  517. targetNodeID: 'node_0', // 目标节点ID
  518. },
  519. // 更多连线...
  520. ],
  521. };
  522. ```
  523. ### 2. 节点注册
  524. 使用 `nodeRegistries` 定义不同类型节点的行为和外观:
  525. ```tsx
  526. // 节点注册
  527. import { WorkflowNodeRegistry } from '@flowgram.ai/free-layout-editor';
  528. export const nodeRegistries: WorkflowNodeRegistry[] = [
  529. // 开始节点定义
  530. {
  531. type: 'start',
  532. meta: {
  533. isStart: true, // Mark as start
  534. deleteDisable: true, // The start node cannot be deleted
  535. copyDisable: true, // The start node cannot be copied
  536. defaultPorts: [{ type: 'output' }], // Used to define the input and output ports, the start node only has the output port
  537. },
  538. },
  539. // 更多节点类型...
  540. ];
  541. ```
  542. ### 3. 编辑器组件
  543. ```tsx
  544. // 核心编辑器容器与渲染器
  545. import {
  546. FreeLayoutEditorProvider,
  547. EditorRenderer
  548. } from '@flowgram.ai/free-layout-editor';
  549. // 编辑器配置示例
  550. const editorProps = {
  551. background: true, // 启用背景网格
  552. readonly: false, // 非只读模式,允许编辑
  553. initialData: {...}, // 初始化数据:节点和边的定义
  554. nodeRegistries: [...], // 节点类型注册
  555. nodeEngine: {
  556. enable: true, // 启用节点表单引擎
  557. },
  558. history: {
  559. enable: true, // 启用历史记录
  560. enableChangeNode: true, // 监听节点数据变化
  561. }
  562. };
  563. // 完整编辑器渲染
  564. <FreeLayoutEditorProvider {...editorProps}>
  565. <div className="container">
  566. <NodeAddPanel /> {/* 节点添加面板 */}
  567. <EditorRenderer /> {/* 核心编辑器渲染区域 */}
  568. <Tools /> {/* 工具栏 */}
  569. <Minimap /> {/* 缩略图 */}
  570. </div>
  571. </FreeLayoutEditorProvider>
  572. ```
  573. ### 4. 核心钩子函数
  574. 在组件中可以使用多种钩子函数获取和操作编辑器:
  575. ```tsx
  576. // 获取拖拽服务
  577. const dragService = useService<WorkflowDragService>(WorkflowDragService);
  578. // 开始拖拽节点
  579. dragService.startDragCard('nodeType', event, { data: {...} });
  580. // 获取编辑器上下文
  581. const { document, playground } = useClientContext();
  582. // 操作画布
  583. document.fitView(); // 适应视图
  584. playground.config.zoomin(); // 缩放画布
  585. document.fromJSON(newData); // 更新数据
  586. ```
  587. ### 5. 插件扩展
  588. Free Layout 支持通过插件机制扩展功能:
  589. ```tsx
  590. plugins: () => [
  591. // 缩略图插件
  592. createMinimapPlugin({
  593. canvasStyle: {
  594. canvasWidth: 180,
  595. canvasHeight: 100,
  596. canvasBackground: 'rgba(245, 245, 245, 1)',
  597. }
  598. }),
  599. // 自动对齐插件
  600. createFreeSnapPlugin({
  601. edgeColor: '#00B2B2', // 对齐线颜色
  602. alignColor: '#00B2B2', // 辅助线颜色
  603. edgeLineWidth: 1, // 线宽
  604. }),
  605. ],
  606. ```
  607. ## 安装
  608. ```bash
  609. npx @flowgram.ai/create-app@latest free-layout-simple
  610. ```
  611. ## 源码
  612. https://github.com/bytedance/flowgram.ai/tree/main/apps/demo-free-layout-simple