Files
udmin/frontend/flow-fixed-layout-demo.md
ayou b0963e5e37 feat(flows): 新增流程编辑器基础功能与相关组件
feat(backend): 添加流程模型与服务支持
feat(frontend): 实现流程编辑器UI与交互
feat(assets): 添加流程节点图标资源
feat(plugins): 实现上下文菜单和运行时插件
feat(components): 新增基础节点和侧边栏组件
feat(routes): 添加流程相关路由配置
feat(models): 创建流程和运行日志数据模型
feat(services): 实现流程服务层逻辑
feat(migration): 添加流程相关数据库迁移
feat(config): 更新前端配置支持流程编辑器
feat(utils): 增强axios错误处理和工具函数
2025-09-15 00:27:13 +08:00

9.0 KiB
Raw Blame History

创建自由布局画布

本案例可通过 npx @flowgram.ai/create-app@latest free-layout-simple 安装,完整代码及效果见:

文件结构:

- hooks
  - use-editor-props.ts # 画布配置
- components
  - base-node.tsx # 节点渲染
  - tools.tsx # 画布工具栏
- initial-data.ts # 初始化数据
- node-registries.ts # 节点配置
- app.tsx # 画布入口

1. 画布入口

  • FreeLayoutEditorProvider: 画布配置器, 内部会生成 react-context 供子组件消费
  • EditorRenderer: 为最终渲染的画布,可以包装在其他组件下边方便定制画布位置

import {
  FreeLayoutEditorProvider,
  EditorRenderer,
} from '@flowgram.ai/free-layout-editor';

import '@flowgram.ai/free-layout-editor/index.css'; // 加载样式

import { useEditorProps } from './use-editor-props' // 画布详细的 props 配置
import { Tools } from './components/tools' // 画布工具

function App() {
  const editorProps = useEditorProps()
  return (
    <FreeLayoutEditorProvider {...editorProps}>
      <EditorRenderer className="demo-editor" />
      <Tools />
    </FreeLayoutEditorProvider>
  );
}

2. 配置画布

画布配置采用声明式,提供 数据、渲染、事件、插件相关配置

import { useMemo } from 'react';
import { type FreeLayoutProps } from '@flowgram.ai/free-layout-editor';
import { createMinimapPlugin } from '@flowgram.ai/minimap-plugin';

import { initialData } from './initial-data' // 初始化数据
import { nodeRegistries } from './node-registries' // 节点声明配置
import { BaseNode } from './components/base-node' // 节点渲染

export function useEditorProps(
): FreeLayoutProps {
  return useMemo<FreeLayoutProps>(
    () => ({
      /**
       * 初始化数据
       */
      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, 目前会存子画布的节点
  • edges: array 子画布的边数据

:::

:::note 边数据基本结构:

  • sourceNodeID: string 开始节点 id
  • targetNodeID: string 目标节点 id
  • sourcePortID?: string | number 开始端口 id, 缺省则采用开始节点的默认端口
  • targetPortID?: string | number 目标端口 id, 缺省则采用目标节点的默认端口

:::

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. 声明节点

声明节点可以用于确定节点的类型及渲染方式

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' }], // 用于定义节点的输入和输出端口, 开始节点只有输出端口
      // useDynamicPort: 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. 渲染节点

渲染节点用于添加样式、事件及表单渲染的位置

import { useNodeRender, WorkflowNodeRenderer } from '@flowgram.ai/free-layout-editor';

export const BaseNode = () => {
  /**
   * 提供节点渲染相关的方法
   */
  const { form } = useNodeRender()
  /**
   * WorkflowNodeRenderer 会添加节点拖拽事件及 端口渲染,如果要深度定制,可以看该组件源代码:
   * https://github.com/bytedance/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

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';