mirror of
https://gitee.com/ByteDance/flowgram.ai.git
synced 2025-07-07 17:43:29 +08:00
373 lines
9.8 KiB
Plaintext
373 lines
9.8 KiB
Plaintext
|
||
# 创建固定布局画布
|
||
|
||
本案例可通过 `npx @flowgram.ai/create-app@latest fixed-layout-simple` 安装,完整代码及效果见:
|
||
|
||
<div className="rs-tip">
|
||
<a className="rs-link" href="/examples/fixed-layout/fixed-layout-simple.html">
|
||
固定布局基础用法
|
||
</a>
|
||
</div>
|
||
|
||
文件结构:
|
||
|
||
```
|
||
- hooks
|
||
- use-editor-props.ts # 画布配置
|
||
- components
|
||
- base-node.tsx # 节点渲染
|
||
- tools.tsx # 画布工具栏
|
||
- initial-data.ts # 初始化数据
|
||
- node-registries.ts # 节点配置
|
||
- app.tsx # 画布入口
|
||
```
|
||
|
||
### 1. 画布入口
|
||
|
||
- `FixedLayoutEditorProvider`: 画布配置器, 内部会生成 react-context 供子组件消费
|
||
- `EditorRenderer`: 为最终渲染的画布,可以包装在其他组件下边方便定制画布位置
|
||
|
||
```tsx pure title="app.tsx"
|
||
|
||
import {
|
||
FixedLayoutEditorProvider,
|
||
EditorRenderer,
|
||
} from '@flowgram.ai/fixed-layout-editor';
|
||
|
||
import '@flowgram.ai/fixed-layout-editor/index.css'; // 加载样式
|
||
|
||
import { useEditorProps } from './hooks/use-editor-props' // 画布详细的 props 配置
|
||
import { Tools } from './components/tools' // 画布工具
|
||
|
||
function App() {
|
||
const editorProps = useEditorProps()
|
||
return (
|
||
<FixedLayoutEditorProvider {...editorProps}>
|
||
<EditorRenderer className="demo-editor" />
|
||
<Tools />
|
||
</FixedLayoutEditorProvider>
|
||
);
|
||
}
|
||
```
|
||
|
||
### 2. 配置画布
|
||
|
||
画布配置采用声明式,提供 数据、渲染、事件、插件相关配置
|
||
|
||
```tsx pure title="hooks/use-editor-props.tsx"
|
||
import { useMemo } from 'react';
|
||
import { type FixedLayoutProps } from '@flowgram.ai/fixed-layout-editor';
|
||
import { defaultFixedSemiMaterials } from '@flowgram.ai/fixed-semi-materials';
|
||
import { createMinimapPlugin } from '@flowgram.ai/minimap-plugin';
|
||
|
||
import { initialData } from './initial-data' // 初始化数据
|
||
import { nodeRegistries } from './node-registries' // 节点声明配置
|
||
import { BaseNode } from './base-node' // 节点渲染
|
||
|
||
export function useEditorProps(
|
||
): FixedLayoutProps {
|
||
return useMemo<FixedLayoutProps>(
|
||
() => ({
|
||
/**
|
||
* 初始化数据
|
||
*/
|
||
initialData,
|
||
/**
|
||
* 画布节点定义
|
||
*/
|
||
nodeRegistries,
|
||
/**
|
||
* 可以通过 key 自定义 UI 组件, 比如添加按钮,这里提供了一套 semi 组件方便快速验证, 如果需要深度定制,参考:
|
||
* https://github.com/bytedance/flowgram.ai/blob/main/packages/materials/fixed-semi-materials/src/components/index.tsx
|
||
*/
|
||
materials: {
|
||
components: {
|
||
...defaultFixedSemiMaterials,
|
||
// [FlowRendererKey.ADDER]: NodeAdder,
|
||
// [FlowRendererKey.BRANCH_ADDER]: BranchAdder,
|
||
},
|
||
renderDefaultNode: BaseNode, // 节点渲染组件
|
||
},
|
||
/**
|
||
* 节点引擎, 用于渲染节点表单
|
||
*/
|
||
nodeEngine: {
|
||
enable: true,
|
||
},
|
||
/**
|
||
* 画布历史记录, 用于控制 redo/undo
|
||
*/
|
||
history: {
|
||
enable: true,
|
||
enableChangeNode: true, // 用于监听节点表单数据变化
|
||
},
|
||
/**
|
||
* 画布初始化回调
|
||
*/
|
||
onInit: ctx => {
|
||
// 如果要动态加载数据,可以通过如下方法异步执行
|
||
// ctx.docuemnt.fromJSON(initialData)
|
||
},
|
||
/**
|
||
* 画布第一次渲染完成回调
|
||
*/
|
||
onAllLayersRendered: (ctx) => {},
|
||
/**
|
||
* 画布销毁回调
|
||
*/
|
||
onDispose: () => { },
|
||
plugins: () => [
|
||
/**
|
||
* 缩略图插件
|
||
*/
|
||
createMinimapPlugin({}),
|
||
],
|
||
}),
|
||
[],
|
||
);
|
||
}
|
||
|
||
```
|
||
|
||
|
||
### 3. 配置数据
|
||
|
||
画布文档数据采用树形结构,支持嵌套
|
||
|
||
:::note 文档数据基本结构:
|
||
|
||
- nodes `array` 节点列表, 支持嵌套
|
||
|
||
:::
|
||
|
||
:::note 节点数据基本结构:
|
||
|
||
|
||
- id: `string` 节点唯一标识, 必须保证唯一
|
||
- meta: `object` 节点的 ui 配置信息,如自由布局的 `position` 信息放这里
|
||
- type: `string | number` 节点类型,会和 `nodeRegistries` 中的 `type` 对应
|
||
- data: `object` 节点表单数据
|
||
- blocks: `array` 节点的分支, 采用 `block` 更贴近 `Gramming`
|
||
|
||
:::
|
||
|
||
```tsx pure title="initial-data.tsx"
|
||
import { FlowDocumentJSON } from '@flowgram.ai/fixed-layout-editor';
|
||
|
||
/**
|
||
* 配置流程数据,数据为 blocks 嵌套的格式
|
||
*/
|
||
export const initialData: FlowDocumentJSON = {
|
||
nodes: [
|
||
// 开始节点
|
||
{
|
||
id: 'start_0',
|
||
type: 'start',
|
||
data: {
|
||
title: 'Start',
|
||
content: 'start content'
|
||
},
|
||
blocks: [],
|
||
},
|
||
// 分支节点
|
||
{
|
||
id: 'condition_0',
|
||
type: 'condition',
|
||
data: {
|
||
title: 'Condition'
|
||
},
|
||
blocks: [
|
||
{
|
||
id: 'branch_0',
|
||
type: 'block',
|
||
data: {
|
||
title: 'Branch 0',
|
||
content: 'branch 1 content'
|
||
},
|
||
blocks: [
|
||
{
|
||
id: 'custom_0',
|
||
type: 'custom',
|
||
data: {
|
||
title: 'Custom',
|
||
content: 'custrom content'
|
||
},
|
||
},
|
||
],
|
||
},
|
||
{
|
||
id: 'branch_1',
|
||
type: 'block',
|
||
data: {
|
||
title: 'Branch 1',
|
||
content: 'branch 1 content'
|
||
},
|
||
blocks: [],
|
||
},
|
||
],
|
||
},
|
||
// 结束节点
|
||
{
|
||
id: 'end_0',
|
||
type: 'end',
|
||
data: {
|
||
title: 'End',
|
||
content: 'end content'
|
||
},
|
||
},
|
||
],
|
||
};
|
||
|
||
```
|
||
|
||
### 4. 声明节点
|
||
|
||
声明节点可以用于确定节点的类型及渲染方式
|
||
|
||
```tsx pure title="node-registries.tsx"
|
||
import { FlowNodeRegistry, ValidateTrigger } from '@flowgram.ai/fixed-layout-editor';
|
||
|
||
/**
|
||
* 自定义节点注册
|
||
*/
|
||
export const nodeRegistries: FlowNodeRegistry[] = [
|
||
{
|
||
/**
|
||
* 自定义节点类型
|
||
*/
|
||
type: 'condition',
|
||
/**
|
||
* 自定义节点扩展:
|
||
* - loop: 扩展为循环节点
|
||
* - start: 扩展为开始节点
|
||
* - dynamicSplit: 扩展为分支节点
|
||
* - end: 扩展为结束节点
|
||
* - tryCatch: 扩展为 tryCatch 节点
|
||
* - default: 扩展为普通节点 (默认)
|
||
*/
|
||
extend: 'dynamicSplit',
|
||
/**
|
||
* 节点配置信息
|
||
*/
|
||
meta: {
|
||
// isStart: false, // 是否为开始节点
|
||
// isNodeEnd: false, // 是否为结束节点,结束节点后边无法再添加节点
|
||
// draggable: false, // 是否可拖拽,如开始节点和结束节点无法拖拽
|
||
// selectable: false, // 触发器等开始节点不能被框选
|
||
// deleteDisable: true, // 禁止删除
|
||
// copyDisable: true, // 禁止copy
|
||
// addDisable: true, // 禁止添加
|
||
},
|
||
/**
|
||
* 配置节点表单的校验及渲染,
|
||
* 注:validate 采用数据和渲染分离,保证节点即使不渲染也能对数据做校验
|
||
*/
|
||
formMeta: {
|
||
validateTrigger: ValidateTrigger.onChange,
|
||
validate: {
|
||
title: ({ value }) => (value ? undefined : 'Title is required'),
|
||
},
|
||
/**
|
||
* Render form
|
||
*/
|
||
render: () => (
|
||
<>
|
||
<Field name="title">
|
||
{({ field }) => <div className="demo-free-node-title">{field.value}</div>}
|
||
</Field>
|
||
<Field name="content">
|
||
{({ field }) => <input onChange={field.onChange} value={field.value}/>}
|
||
</Field>
|
||
</>
|
||
)
|
||
},
|
||
},
|
||
];
|
||
|
||
```
|
||
### 5. 渲染节点
|
||
|
||
渲染节点用于添加样式、事件及表单渲染的位置
|
||
|
||
```tsx pure title="components/base-node.tsx"
|
||
import { useNodeRender } from '@flowgram.ai/fixed-layout-editor';
|
||
|
||
export const BaseNode = () => {
|
||
/**
|
||
* 提供节点渲染相关的方法
|
||
*/
|
||
const nodeRender = useNodeRender();
|
||
/**
|
||
* 只有在节点引擎开启时候才能使用表单
|
||
*/
|
||
const form = nodeRender.form;
|
||
|
||
return (
|
||
<div
|
||
className="demo-fixed-node"
|
||
onMouseEnter={nodeRender.onMouseEnter}
|
||
onMouseLeave={nodeRender.onMouseLeave}
|
||
onMouseDown={e => {
|
||
// 触发拖拽
|
||
nodeRender.startDrag(e);
|
||
e.stopPropagation();
|
||
}}
|
||
style={{
|
||
// BlockOrderIcon 表示为分支的第一个节点,BlockIcon 则表示整个 condition 的头部节点
|
||
...(nodeRender.isBlockOrderIcon || nodeRender.isBlockIcon ? { width: 260 } : {}),
|
||
outline: form?.state.invalid ? '1px solid red' : 'none', // 表单校验错误让边框标红
|
||
}}
|
||
>
|
||
{
|
||
// 表单渲染通过 formMeta 生成
|
||
form?.render()
|
||
}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
```
|
||
|
||
|
||
### 6. 添加工具
|
||
|
||
工具主要用于控制画布缩放等操作, 工具汇总在 `usePlaygroundTools` 中, 而 `useClientContext` 用于获取画布的上下文, 里边包含画布的核心模块如 `history`
|
||
|
||
```tsx pure title="components/tools.tsx"
|
||
import { useEffect, useState } from 'react'
|
||
import { usePlaygroundTools, useClientContext } from '@flowgram.ai/fixed-layout-editor';
|
||
|
||
export function Tools() {
|
||
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: 16, 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.changeLayout()}>ChangeLayout</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>
|
||
}
|
||
|
||
```
|
||
### 7. 效果
|
||
|
||
import { FixedLayoutSimple } from '../../../../components';
|
||
|
||
<div style={{ position: 'relative', width: '100%', height: '600px'}}>
|
||
<FixedLayoutSimple />
|
||
</div>
|