| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701 |
- ---
- outline: true
- ---
- # 基础用法
- import { FreeLayoutSimplePreview } from '../../../../components';
- <FreeLayoutSimplePreview />
- ## 功能介绍
- 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<FreeLayoutProps>(
- () => ({
- // 启用背景网格
- background: true,
- // 非只读模式
- readonly: false,
- // 初始数据
- initialData,
- // 节点类型注册
- nodeRegistries,
- // 默认节点注册
- getNodeDefaultRegistry(type) {
- return {
- type,
- meta: {
- defaultExpanded: true,
- },
- formMeta: {
- // 节点表单渲染
- render: () => (
- <>
- <Field<string> name="title">
- {({ field }) => <div className="demo-free-node-title">{field.value}</div>}
- </Field>
- <div className="demo-free-node-content">
- <Field<string> name="content">
- <input />
- </Field>
- </div>
- </>
- ),
- },
- };
- },
- // 节点渲染
- materials: {
- renderDefaultNode: (props: WorkflowNodeProps) => {
- const { form } = useNodeRender();
- return (
- <WorkflowNodeRenderer className="demo-free-node" node={props.node}>
- {form?.render()}
- </WorkflowNodeRenderer>
- );
- },
- },
- // 内容变更回调
- 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)',
- },
- }),
- // 自动对齐插件
- 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>(WorkflowDragService);
- return (
- <div className="demo-free-sidebar">
- {nodeTypes.map(nodeType => (
- <div
- key={nodeType}
- className="demo-free-card"
- onMouseDown={e => dragService.startDragCard(nodeType, e, {
- data: {
- title: nodeType,
- content: '拖拽创建的节点'
- }
- })}
- >
- {nodeType}
- </div>
- ))}
- </div>
- );
- };
- ```
- #### 步骤五:创建工具栏和缩略图
- ```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 (
- <div
- style={{ position: 'absolute', zIndex: 10, bottom: 16, left: 226, display: 'flex', gap: 8 }}
- >
- <button onClick={() => tools.zoomin()}>ZoomIn</button>
- <button onClick={() => tools.zoomout()}>ZoomOut</button>
- <button onClick={() => tools.fitView()}>Fitview</button>
- <button onClick={() => tools.autoLayout()}>AutoLayout</button>
- <button onClick={() => history.undo()} disabled={!canUndo}>
- Undo
- </button>
- <button onClick={() => history.redo()} disabled={!canRedo}>
- Redo
- </button>
- <span>{Math.floor(tools.zoom * 100)}%</span>
- </div>
- );
- };
- // src/components/minimap.tsx
- import { MinimapRender } from '@flowgram.ai/minimap-plugin';
- export const Minimap = () => {
- return (
- <div
- style={{
- position: 'absolute',
- left: 226,
- bottom: 51,
- zIndex: 100,
- width: 198,
- }}
- >
- <MinimapRender
- containerStyles={{
- pointerEvents: 'auto',
- position: 'relative',
- top: 'unset',
- right: 'unset',
- bottom: 'unset',
- left: 'unset',
- }}
- inactiveStyle={{
- opacity: 1,
- scale: 1,
- translateX: 0,
- translateY: 0,
- }}
- />
- </div>
- );
- };
- ```
- #### 步骤六:组装编辑器主组件
- ```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 (
- <FreeLayoutEditorProvider {...editorProps}>
- <div className="demo-free-container">
- <div className="demo-free-layout">
- <NodeAddPanel />
- <EditorRenderer className="demo-free-editor" />
- </div>
- <Tools />
- <Minimap />
- </div>
- </FreeLayoutEditorProvider>
- );
- };
- ```
- #### 步骤七:创建应用入口
- ```tsx
- // src/app.tsx
- import React from 'react';
- import ReactDOM from 'react-dom';
- import { Editor } from './editor';
- ReactDOM.render(<Editor />, 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, // 监听节点数据变化
- }
- };
- // 完整编辑器渲染
- <FreeLayoutEditorProvider {...editorProps}>
- <div className="container">
- <NodeAddPanel /> {/* 节点添加面板 */}
- <EditorRenderer /> {/* 核心编辑器渲染区域 */}
- <Tools /> {/* 工具栏 */}
- <Minimap /> {/* 缩略图 */}
- </div>
- </FreeLayoutEditorProvider>
- ```
- ### 4. 核心钩子函数
- 在组件中可以使用多种钩子函数获取和操作编辑器:
- ```tsx
- // 获取拖拽服务
- const dragService = useService<WorkflowDragService>(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
|