flowgram.ai/apps/docs/src/zh/guide/getting-started/create-free-layout-simple.mdx
dragooncjw d7bdf8a078 feat: init flowgram.ai
Co-Authored-By: xiamidaxia <xiamidaxia@icloud.com>
2025-02-21 16:26:12 +08:00

334 lines
8.5 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 创建自由布局画布
本案例可通过 `npx @flowgram.ai/create-app@latest free-layout-simple` 安装,完整代码及效果见:
<div className="rs-tip">
<a className="rs-link" href="/flowgram.ai/examples/free-layout/free-layout-simple.html">
自由布局基础用法
</a>
</div>
### 1. 画布入口
- `FreeLayoutEditorProvider`: 画布配置器, 内部会生成 react-context 供子组件消费
- `EditorRenderer`: 为最终渲染的画布,可以包装在其他组件下边方便定制画布位置
```tsx pure title="app.tsx"
import {
FreeLayoutEditorProvider,
EditorRenderer,
} from '@flowgram.ai/free-layout-editor';
import { useEditorProps } from './use-editor-props' // 画布详细的 props 配置
import { Tools } from './tools' // 画布工具
function App() {
const editorProps = useEditorProps()
return (
<FixedLayoutEditorProvider {...editorProps}>
<EditorRenderer className="demo-editor" />
<Tools />
</FixedLayoutEditorProvider>
);
}
```
### 2. 配置画布
画布配置采用声明式,提供 数据、渲染、事件、插件相关配置
```tsx pure title="use-editor-props.tsx"
import { useMemo } from 'react';
import { type FixedLayoutProps } from '@flowgram.ai/free-layout-editor';
import { createMinimapPlugin } from '@flowgram.ai/minimap-plugin';
import { intialData } from './initial-data' // 初始化数据
import { nodeRegistries } from './node-registries' // 节点声明配置
import { BaseNode } from './base-node' // 节点渲染
export function useEditorProps(
): FixedLayoutProps {
return useMemo<FixedLayoutProps>(
() => ({
/**
* 初始化数据
*/
initialData,
/**
* 画布节点定义
*/
nodeRegistries,
/**
* 物料
*/
materials: {
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` 节点列表, 支持嵌套
- edges `array` 边列表
:::
:::note 节点数据基本结构:
- id: `string` 节点唯一标识, 必须保证唯一
- meta: `object` 节点的 ui 配置信息,如自由布局的 `position` 信息放这里
- type: `string | number` 节点类型,会和 `nodeRegistries` 中的 `type` 对应
- data: `object` 节点表单数据, 业务可自定义
- blocks: `array` 节点的分支, 采用 `block` 更贴近 `Gramming`
:::
:::note 边数据基本结构:
- sourceNodeID: `string` 开始节点 id
- targetNodeID: `string` 目标节点 id
- sourcePortID?: `string | number` 开始端口 id, 缺省则采用开始节点的默认端口
- targetPortID?: `string | number` 目标端口 id, 缺省则采用目标节点的默认端口
:::
```tsx pure title="initial-data.tsx"
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: 'Start',
content: 'Start content'
},
},
{
id: 'node_0',
type: 'custom',
meta: {
position: { x: 400, y: 0 },
},
data: {
title: 'Custom',
content: 'Custom node content'
},
},
{
id: 'end_0',
type: 'end',
meta: {
position: { x: 800, y: 0 },
},
data: {
title: 'End',
content: 'End content'
},
},
],
edges: [
{
sourceNodeID: 'start_0',
targetNodeID: 'node_0',
},
{
sourceNodeID: 'node_0',
targetNodeID: 'end_0',
},
],
};
```
### 4. 声明节点
声明节点可以用于确定节点的类型及渲染方式
```tsx pure title="node-registries.tsx"
import { WorkflowNodeRegistry, ValidateTrigger } from '@flowgram.ai/free-layout-editor';
/**
* You can customize your own node registry
* 你可以自定义节点的注册器
*/
export const nodeRegistries: WorkflowNodeRegistry[] = [
{
type: 'start',
meta: {
isStart: true, // 标记为开始节点
deleteDisable: true, // 开始节点不能删除
copyDisable: true, // 开始节点不能复制
defaultPorts: [{ type: 'output' }], // 用于定义节点的输入和输出端口, 开始节点只有输出端口
// dynamicPort: true, // 用于动态端口,会寻找 data-port-id 和 data-port-type 属性的 dom 作为端口
},
/**
* 配置节点表单的校验及渲染,
* 注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>
</>
)
},
},
{
type: 'end',
meta: {
deleteDisable: true,
copyDisable: true,
defaultPorts: [{ type: 'input' }],
},
formMeta: {
// ...
}
},
{
type: 'custom',
meta: {
},
formMeta: {
// ...
},
defaultPorts: [{ type: 'output' }, { type: 'input' }], // 普通节点有两个端口
},
];
```
### 5. 渲染节点
渲染节点用于添加样式、事件及表单渲染的位置
```tsx pure title="base-node.tsx"
import { useNodeRender, WorkflowNodeRenderer } from '@flowgram.ai/free-layout-editor';
export const BaseNode = () => {
/**
* 提供节点渲染相关的方法
*/
const { form } = useNodeRender()
/**
* WorkflowNodeRenderer 会添加节点拖拽事件及 端口渲染,如果要深度定制,可以看该组件源代码:
* https://github.com/coze-dev/flowgram.ai/blob/main/packages/client/free-layout-editor/src/components/workflow-node-renderer.tsx
*/
return (
<WorkflowNodeRenderer className="demo-free-node" node={props.node}>
{
// 表单渲染通过 formMeta 生成
form?.render()
}
</WorkflowNodeRenderer>
)
};
```
### 6. 添加工具
工具主要用于控制画布缩放等操作, 工具汇总在 `usePlaygroundTools` 中, 而 `useClientContext` 用于获取画布的上下文, 里边包含画布的核心模块如 `history`
```tsx pure title="tools.tsx"
import { useEffect, useState } from 'react'
import { usePlaygroundTools, useClientContext } from '@flowgram.ai/free-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: 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>
}
```
### 7. 效果
import { FreeLayoutSimple } from '../../../../components';
<div style={{ position: 'relative', width: '100%', height: '600px'}}>
<FreeLayoutSimple />
</div>