refactor: 重构文档结构和文件位置
docs: 添加Redis集成测试文档 docs: 添加ID生成器分析报告 docs: 添加自由布局和固定布局示例文档 test: 添加ID生成器单元测试 fix: 删除重复的前端文档文件
This commit is contained in:
151
docs/ID_GENERATION_ANALYSIS.md
Normal file
151
docs/ID_GENERATION_ANALYSIS.md
Normal file
@ -0,0 +1,151 @@
|
||||
# ID生成器时间顺序分析报告
|
||||
|
||||
## 概述
|
||||
|
||||
本报告分析了 `udmin` 项目中基于 Snowflake 算法的ID生成器,验证其是否能够保证**后生成的ID永远比前面生成的ID大**。
|
||||
|
||||
## ID生成器架构
|
||||
|
||||
### 1. 基础实现
|
||||
- **算法**: 基于 Snowflake 分布式ID生成算法
|
||||
- **库**: 使用 `rs-snowflake` crate
|
||||
- **线程安全**: 通过 `Mutex<SnowflakeIdGenerator>` 保证并发安全
|
||||
- **全局单例**: 使用 `once_cell::sync::Lazy` 实现按需初始化
|
||||
|
||||
### 2. ID结构设计
|
||||
|
||||
```
|
||||
|-- 16 bits --|-- 8 bits --|------ 39 bits ------|
|
||||
| main_id | sub_id | snowflake_bits |
|
||||
| 业务主类型 | 业务子类型 | 时间戳+序列号 |
|
||||
```
|
||||
|
||||
- **总长度**: 63位 (最高位为0,保证为正数)
|
||||
- **main_id**: 16位业务主类型标识
|
||||
- **sub_id**: 8位业务子类型标识
|
||||
- **snowflake_bits**: 39位,包含时间戳和序列号
|
||||
|
||||
### 3. 业务ID类型
|
||||
|
||||
| 业务类型 | main_id | sub_id | 用途 |
|
||||
|---------|---------|--------|------|
|
||||
| 通用ID | 1 | 1 | 流程、任务等通用场景 |
|
||||
| 流程运行日志 | 2 | 1 | 流程执行日志记录 |
|
||||
| 请求日志 | 3 | 1 | HTTP请求日志记录 |
|
||||
|
||||
## 测试验证结果
|
||||
|
||||
### 测试1: 连续生成ID递增性 ✅
|
||||
|
||||
```
|
||||
ID 1: 141817072979969
|
||||
ID 2: 141817072979970
|
||||
ID 3: 141817072979971
|
||||
...
|
||||
ID 10: 141817072979978
|
||||
```
|
||||
|
||||
**结论**: 连续生成的ID严格递增,每次递增1。
|
||||
|
||||
### 测试2: 时间间隔ID递增性 ✅
|
||||
|
||||
```
|
||||
时间间隔ID 1: 141817072979979
|
||||
时间间隔ID 2: 141817509187584 (+436,207,605)
|
||||
时间间隔ID 3: 141817949589504 (+440,401,920)
|
||||
时间间隔ID 4: 141818389991424 (+440,401,920)
|
||||
时间间隔ID 5: 141818822004736 (+432,013,312)
|
||||
```
|
||||
|
||||
**结论**: 间隔100ms生成的ID显著递增,体现了时间戳的影响。
|
||||
|
||||
### 测试3: 不同业务类型ID递增性 ✅
|
||||
|
||||
```
|
||||
Flow ID 1: 141819258212352 (main_id=1, sub_id=1)
|
||||
Flow ID 2: 141819262406656 (main_id=1, sub_id=1)
|
||||
Log ID 1: 282556754956288 (main_id=2, sub_id=1)
|
||||
Log ID 2: 282556763344896 (main_id=2, sub_id=1)
|
||||
```
|
||||
|
||||
**结论**:
|
||||
- 同类型业务ID严格递增
|
||||
- 不同业务类型的ID由于高位不同,数值差异显著
|
||||
|
||||
### 测试4: 多线程并发唯一性 ✅
|
||||
|
||||
- **线程数**: 5个并发线程
|
||||
- **每线程生成**: 10个ID
|
||||
- **总ID数**: 50个
|
||||
- **唯一性**: 100% (无重复ID)
|
||||
|
||||
**结论**: 并发环境下所有ID都是唯一的,证明线程安全机制有效。
|
||||
|
||||
### 测试5: 时间戳部分验证 ✅
|
||||
|
||||
```
|
||||
ID1: 141819325321216, 时间戳部分: 532081152000
|
||||
ID2: 141819379847168, 时间戳部分: 532135677952
|
||||
```
|
||||
|
||||
**结论**: 后生成ID的时间戳部分大于前生成ID,体现了时间递增特性。
|
||||
|
||||
## 时间顺序保证机制
|
||||
|
||||
### 1. Snowflake算法保证
|
||||
|
||||
- **时间戳**: 毫秒级时间戳占主要位数,确保不同时间生成的ID递增
|
||||
- **序列号**: 同一毫秒内的序列号递增,确保同时间内ID递增
|
||||
- **机器ID**: 不同机器生成的ID通过机器ID区分,避免冲突
|
||||
|
||||
### 2. 业务层保证
|
||||
|
||||
- **业务前缀**: 高位业务标识确保不同业务类型ID有序分布
|
||||
- **时间戳保留**: 保留39位给Snowflake算法,确保时间精度
|
||||
- **全局锁**: Mutex确保生成过程原子性
|
||||
|
||||
### 3. 数学证明
|
||||
|
||||
设两个ID生成时间为 t1 < t2,则:
|
||||
|
||||
1. **不同毫秒**: timestamp(t2) > timestamp(t1) → ID2 > ID1
|
||||
2. **相同毫秒**: sequence(t2) > sequence(t1) → ID2 > ID1
|
||||
3. **业务前缀相同**: 低39位Snowflake部分决定大小关系
|
||||
4. **业务前缀不同**: 高位业务标识决定大小关系
|
||||
|
||||
## 性能特征
|
||||
|
||||
### 1. 生成速度
|
||||
- **理论QPS**: 每毫秒最多4096个ID (12位序列号)
|
||||
- **实际测试**: 并发生成50个ID无延迟
|
||||
- **锁竞争**: Mutex保护下的原子操作,性能良好
|
||||
|
||||
### 2. 存储效率
|
||||
- **位长度**: 63位,适合i64存储
|
||||
- **字符串长度**: 约19位十进制数字
|
||||
- **索引友好**: 数值类型,数据库索引效率高
|
||||
|
||||
## 结论
|
||||
|
||||
✅ **验证通过**: ID生成器完全满足"后生成的ID永远比前面生成的ID大"的要求
|
||||
|
||||
### 核心保证机制:
|
||||
|
||||
1. **时间递增**: Snowflake算法的时间戳机制
|
||||
2. **序列递增**: 同毫秒内序列号递增
|
||||
3. **业务隔离**: 不同业务类型通过高位区分
|
||||
4. **并发安全**: Mutex保证原子性操作
|
||||
5. **分布式支持**: 机器ID和节点ID避免多实例冲突
|
||||
|
||||
### 适用场景:
|
||||
|
||||
- ✅ 数据库主键 (保证唯一性和递增性)
|
||||
- ✅ 分布式系统ID (支持多节点部署)
|
||||
- ✅ 日志追踪ID (时间有序,便于查询)
|
||||
- ✅ 业务流水号 (业务类型区分,全局唯一)
|
||||
|
||||
### 注意事项:
|
||||
|
||||
- 依赖系统时钟,时钟回拨可能影响递增性
|
||||
- 单机QPS限制在4096/ms,超出需要优化
|
||||
- 业务类型规划需要提前设计,避免冲突
|
||||
78
docs/REDIS_INTEGRATION.md
Normal file
78
docs/REDIS_INTEGRATION.md
Normal file
@ -0,0 +1,78 @@
|
||||
# Redis集成测试
|
||||
|
||||
这个文件包含了Redis集成的测试说明。
|
||||
|
||||
## 环境配置
|
||||
|
||||
1. 复制环境配置文件:
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
2. 根据需要修改 `.env` 文件中的Redis配置:
|
||||
```bash
|
||||
REDIS_URL=redis://:123456@127.0.0.1:6379/9
|
||||
```
|
||||
|
||||
## 启动Redis服务
|
||||
|
||||
确保Redis服务器正在运行:
|
||||
```bash
|
||||
# 使用Docker运行Redis
|
||||
docker run -d -p 6379:6379 --name redis-udmin redis:7-alpine --requirepass 123456
|
||||
|
||||
# 或者使用本地Redis服务
|
||||
redis-server --requirepass 123456
|
||||
```
|
||||
|
||||
## 测试Redis连接
|
||||
|
||||
启动服务器:
|
||||
```bash
|
||||
cargo run
|
||||
```
|
||||
|
||||
查看日志中是否有Redis连接成功的信息:
|
||||
```
|
||||
[INFO] Connecting to Redis at: redis://:***@127.0.0.1:6379/9
|
||||
[INFO] Redis connection established successfully
|
||||
```
|
||||
|
||||
## 测试Token存储
|
||||
|
||||
1. 登录获取token:
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/api/auth/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"username":"admin","password":"Admin@123"}'
|
||||
```
|
||||
|
||||
2. 检查Redis中的token:
|
||||
```bash
|
||||
# 连接到Redis CLI
|
||||
redis-cli -a 123456 -n 9
|
||||
|
||||
# 查看所有token键
|
||||
KEYS token:*
|
||||
|
||||
# 查看特定用户的token
|
||||
GET token:access:user:1
|
||||
GET token:refresh:user:1
|
||||
```
|
||||
|
||||
## 功能验证
|
||||
|
||||
Redis集成后的新功能:
|
||||
|
||||
1. **Token存储**:所有JWT token都存储在Redis中
|
||||
2. **Token验证**:每次API调用都会验证Redis中的token
|
||||
3. **Token撤销**:logout时会从Redis中删除token
|
||||
4. **性能提升**:减少数据库查询,提升认证性能
|
||||
5. **单点登录**:防止多重登录(可配置)
|
||||
|
||||
## 环境变量说明
|
||||
|
||||
- `REDIS_URL`: Redis连接字符串
|
||||
- `REDIS_TOKEN_VALIDATION`: 是否启用Redis token验证(默认true)
|
||||
- `JWT_ACCESS_EXP_SECS`: 访问token过期时间(秒)
|
||||
- `JWT_REFRESH_EXP_SECS`: 刷新token过期时间(秒)
|
||||
346
docs/flow-fixed-layout-demo.md
Normal file
346
docs/flow-fixed-layout-demo.md
Normal file
@ -0,0 +1,346 @@
|
||||
# 创建自由布局画布
|
||||
|
||||
本案例可通过 `npx @flowgram.ai/create-app@latest free-layout-simple` 安装,完整代码及效果见:
|
||||
|
||||
<div className="rs-tip">
|
||||
<a className="rs-link" href="/examples/free-layout/free-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. 画布入口
|
||||
|
||||
* `FreeLayoutEditorProvider`: 画布配置器, 内部会生成 react-context 供子组件消费
|
||||
* `EditorRenderer`: 为最终渲染的画布,可以包装在其他组件下边方便定制画布位置
|
||||
|
||||
```tsx pure title="app.tsx"
|
||||
|
||||
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. 配置画布
|
||||
|
||||
画布配置采用声明式,提供 数据、渲染、事件、插件相关配置
|
||||
|
||||
```tsx pure title="hooks/use-editor-props.tsx"
|
||||
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, 缺省则采用目标节点的默认端口
|
||||
|
||||
:::
|
||||
|
||||
```tsx pure title="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: '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' }], // 用于定义节点的输入和输出端口, 开始节点只有输出端口
|
||||
// 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. 渲染节点
|
||||
|
||||
渲染节点用于添加样式、事件及表单渲染的位置
|
||||
|
||||
```tsx pure title="components/base-node.tsx"
|
||||
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`
|
||||
|
||||
```tsx pure title="components/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>
|
||||
701
docs/flow-free-layout-base-demo.md
Normal file
701
docs/flow-free-layout-base-demo.md
Normal file
@ -0,0 +1,701 @@
|
||||
# 基础用法
|
||||
|
||||
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)',
|
||||
},
|
||||
inactiveDebounceTime: 1,
|
||||
}),
|
||||
// 自动对齐插件
|
||||
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 { FlowMinimapService, MinimapRender } from '@flowgram.ai/minimap-plugin';
|
||||
import { useService } from '@flowgram.ai/free-layout-editor';
|
||||
|
||||
export const Minimap = () => {
|
||||
const minimapService = useService(FlowMinimapService);
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: 226,
|
||||
bottom: 51,
|
||||
zIndex: 100,
|
||||
width: 198,
|
||||
}}
|
||||
>
|
||||
<MinimapRender
|
||||
service={minimapService}
|
||||
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
|
||||
346
docs/flow-free-layout-demo.md
Normal file
346
docs/flow-free-layout-demo.md
Normal file
@ -0,0 +1,346 @@
|
||||
# 创建自由布局画布
|
||||
|
||||
本案例可通过 `npx @flowgram.ai/create-app@latest free-layout-simple` 安装,完整代码及效果见:
|
||||
|
||||
<div className="rs-tip">
|
||||
<a className="rs-link" href="/examples/free-layout/free-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. 画布入口
|
||||
|
||||
* `FreeLayoutEditorProvider`: 画布配置器, 内部会生成 react-context 供子组件消费
|
||||
* `EditorRenderer`: 为最终渲染的画布,可以包装在其他组件下边方便定制画布位置
|
||||
|
||||
```tsx pure title="app.tsx"
|
||||
|
||||
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. 配置画布
|
||||
|
||||
画布配置采用声明式,提供 数据、渲染、事件、插件相关配置
|
||||
|
||||
```tsx pure title="hooks/use-editor-props.tsx"
|
||||
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, 缺省则采用目标节点的默认端口
|
||||
|
||||
:::
|
||||
|
||||
```tsx pure title="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: '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' }], // 用于定义节点的输入和输出端口, 开始节点只有输出端口
|
||||
// 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. 渲染节点
|
||||
|
||||
渲染节点用于添加样式、事件及表单渲染的位置
|
||||
|
||||
```tsx pure title="components/base-node.tsx"
|
||||
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`
|
||||
|
||||
```tsx pure title="components/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>
|
||||
635
docs/flow-free-layout-json.md
Normal file
635
docs/flow-free-layout-json.md
Normal file
@ -0,0 +1,635 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "start_0",
|
||||
"type": "start",
|
||||
"meta": {
|
||||
"position": {
|
||||
"x": 180,
|
||||
"y": 573.7
|
||||
}
|
||||
},
|
||||
"data": {
|
||||
"title": "Start",
|
||||
"outputs": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"default": "Hello Flow."
|
||||
},
|
||||
"enable": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"array_obj": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"int": {
|
||||
"type": "number"
|
||||
},
|
||||
"str": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "condition_0",
|
||||
"type": "condition",
|
||||
"meta": {
|
||||
"position": {
|
||||
"x": 1100,
|
||||
"y": 510.20000000000005
|
||||
}
|
||||
},
|
||||
"data": {
|
||||
"title": "Condition",
|
||||
"conditions": [
|
||||
{
|
||||
"key": "if_0",
|
||||
"value": {
|
||||
"left": {
|
||||
"type": "ref",
|
||||
"content": [
|
||||
"start_0",
|
||||
"query"
|
||||
]
|
||||
},
|
||||
"operator": "contains",
|
||||
"right": {
|
||||
"type": "constant",
|
||||
"content": "Hello Flow."
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "if_f0rOAt",
|
||||
"value": {
|
||||
"left": {
|
||||
"type": "ref",
|
||||
"content": [
|
||||
"start_0",
|
||||
"enable"
|
||||
]
|
||||
},
|
||||
"operator": "is_true"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "end_0",
|
||||
"type": "end",
|
||||
"meta": {
|
||||
"position": {
|
||||
"x": 3008,
|
||||
"y": 573.7
|
||||
}
|
||||
},
|
||||
"data": {
|
||||
"title": "End",
|
||||
"inputsValues": {
|
||||
"success": {
|
||||
"type": "constant",
|
||||
"content": true,
|
||||
"schema": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"query": {
|
||||
"type": "ref",
|
||||
"content": [
|
||||
"start_0",
|
||||
"query"
|
||||
]
|
||||
}
|
||||
},
|
||||
"inputs": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"query": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "159623",
|
||||
"type": "comment",
|
||||
"meta": {
|
||||
"position": {
|
||||
"x": 180,
|
||||
"y": 756.7
|
||||
}
|
||||
},
|
||||
"data": {
|
||||
"size": {
|
||||
"width": 240,
|
||||
"height": 150
|
||||
},
|
||||
"note": "hi ~\n\nthis is a comment node\n\n- flowgram.ai"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "http_rDGIH",
|
||||
"type": "http",
|
||||
"meta": {
|
||||
"position": {
|
||||
"x": 640,
|
||||
"y": 447.35
|
||||
}
|
||||
},
|
||||
"data": {
|
||||
"title": "HTTP_1",
|
||||
"outputs": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"body": {
|
||||
"type": "string"
|
||||
},
|
||||
"headers": {
|
||||
"type": "object"
|
||||
},
|
||||
"statusCode": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"api": {
|
||||
"method": "GET",
|
||||
"url": {
|
||||
"type": "template",
|
||||
"content": ""
|
||||
}
|
||||
},
|
||||
"body": {
|
||||
"bodyType": "JSON"
|
||||
},
|
||||
"timeout": {
|
||||
"timeout": 10000,
|
||||
"retryTimes": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "loop_Ycnsk",
|
||||
"type": "loop",
|
||||
"meta": {
|
||||
"position": {
|
||||
"x": 1480,
|
||||
"y": 90.00000000000003
|
||||
}
|
||||
},
|
||||
"data": {
|
||||
"title": "Loop_1",
|
||||
"loopFor": {
|
||||
"type": "ref",
|
||||
"content": [
|
||||
"start_0",
|
||||
"array_obj"
|
||||
]
|
||||
},
|
||||
"loopOutputs": {
|
||||
"acm": {
|
||||
"type": "ref",
|
||||
"content": [
|
||||
"llm_6aSyo",
|
||||
"result"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"blocks": [
|
||||
{
|
||||
"id": "llm_6aSyo",
|
||||
"type": "llm",
|
||||
"meta": {
|
||||
"position": {
|
||||
"x": 344,
|
||||
"y": 0
|
||||
}
|
||||
},
|
||||
"data": {
|
||||
"title": "LLM_3",
|
||||
"inputsValues": {
|
||||
"modelName": {
|
||||
"type": "constant",
|
||||
"content": "gpt-3.5-turbo"
|
||||
},
|
||||
"apiKey": {
|
||||
"type": "constant",
|
||||
"content": "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
},
|
||||
"apiHost": {
|
||||
"type": "constant",
|
||||
"content": "https://mock-ai-url/api/v3"
|
||||
},
|
||||
"temperature": {
|
||||
"type": "constant",
|
||||
"content": 0.5
|
||||
},
|
||||
"systemPrompt": {
|
||||
"type": "template",
|
||||
"content": "# Role\nYou are an AI assistant.\n"
|
||||
},
|
||||
"prompt": {
|
||||
"type": "template",
|
||||
"content": ""
|
||||
}
|
||||
},
|
||||
"inputs": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"modelName",
|
||||
"apiKey",
|
||||
"apiHost",
|
||||
"temperature",
|
||||
"prompt"
|
||||
],
|
||||
"properties": {
|
||||
"modelName": {
|
||||
"type": "string"
|
||||
},
|
||||
"apiKey": {
|
||||
"type": "string"
|
||||
},
|
||||
"apiHost": {
|
||||
"type": "string"
|
||||
},
|
||||
"temperature": {
|
||||
"type": "number"
|
||||
},
|
||||
"systemPrompt": {
|
||||
"type": "string",
|
||||
"extra": {
|
||||
"formComponent": "prompt-editor"
|
||||
}
|
||||
},
|
||||
"prompt": {
|
||||
"type": "string",
|
||||
"extra": {
|
||||
"formComponent": "prompt-editor"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"result": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "llm_ZqKlP",
|
||||
"type": "llm",
|
||||
"meta": {
|
||||
"position": {
|
||||
"x": 804,
|
||||
"y": 0
|
||||
}
|
||||
},
|
||||
"data": {
|
||||
"title": "LLM_4",
|
||||
"inputsValues": {
|
||||
"modelName": {
|
||||
"type": "constant",
|
||||
"content": "gpt-3.5-turbo"
|
||||
},
|
||||
"apiKey": {
|
||||
"type": "constant",
|
||||
"content": "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
},
|
||||
"apiHost": {
|
||||
"type": "constant",
|
||||
"content": "https://mock-ai-url/api/v3"
|
||||
},
|
||||
"temperature": {
|
||||
"type": "constant",
|
||||
"content": 0.5
|
||||
},
|
||||
"systemPrompt": {
|
||||
"type": "template",
|
||||
"content": "# Role\nYou are an AI assistant.\n"
|
||||
},
|
||||
"prompt": {
|
||||
"type": "template",
|
||||
"content": ""
|
||||
}
|
||||
},
|
||||
"inputs": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"modelName",
|
||||
"apiKey",
|
||||
"apiHost",
|
||||
"temperature",
|
||||
"prompt"
|
||||
],
|
||||
"properties": {
|
||||
"modelName": {
|
||||
"type": "string"
|
||||
},
|
||||
"apiKey": {
|
||||
"type": "string"
|
||||
},
|
||||
"apiHost": {
|
||||
"type": "string"
|
||||
},
|
||||
"temperature": {
|
||||
"type": "number"
|
||||
},
|
||||
"systemPrompt": {
|
||||
"type": "string",
|
||||
"extra": {
|
||||
"formComponent": "prompt-editor"
|
||||
}
|
||||
},
|
||||
"prompt": {
|
||||
"type": "string",
|
||||
"extra": {
|
||||
"formComponent": "prompt-editor"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"result": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "block_start_PUDtS",
|
||||
"type": "block-start",
|
||||
"meta": {
|
||||
"position": {
|
||||
"x": 32,
|
||||
"y": 163.1
|
||||
}
|
||||
},
|
||||
"data": {}
|
||||
},
|
||||
{
|
||||
"id": "block_end_leBbs",
|
||||
"type": "block-end",
|
||||
"meta": {
|
||||
"position": {
|
||||
"x": 1116,
|
||||
"y": 163.1
|
||||
}
|
||||
},
|
||||
"data": {}
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"sourceNodeID": "block_start_PUDtS",
|
||||
"targetNodeID": "llm_6aSyo"
|
||||
},
|
||||
{
|
||||
"sourceNodeID": "llm_6aSyo",
|
||||
"targetNodeID": "llm_ZqKlP"
|
||||
},
|
||||
{
|
||||
"sourceNodeID": "llm_ZqKlP",
|
||||
"targetNodeID": "block_end_leBbs"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "group_nYl6D",
|
||||
"type": "group",
|
||||
"meta": {
|
||||
"position": {
|
||||
"x": 1644,
|
||||
"y": 730.2
|
||||
}
|
||||
},
|
||||
"data": {
|
||||
"parentID": "root",
|
||||
"blockIDs": [
|
||||
"llm_8--A3",
|
||||
"llm_vTyMa"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "llm_8--A3",
|
||||
"type": "llm",
|
||||
"meta": {
|
||||
"position": {
|
||||
"x": 180,
|
||||
"y": 0
|
||||
}
|
||||
},
|
||||
"data": {
|
||||
"title": "LLM_1",
|
||||
"inputsValues": {
|
||||
"modelName": {
|
||||
"type": "constant",
|
||||
"content": "gpt-3.5-turbo"
|
||||
},
|
||||
"apiKey": {
|
||||
"type": "constant",
|
||||
"content": "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
},
|
||||
"apiHost": {
|
||||
"type": "constant",
|
||||
"content": "https://mock-ai-url/api/v3"
|
||||
},
|
||||
"temperature": {
|
||||
"type": "constant",
|
||||
"content": 0.5
|
||||
},
|
||||
"systemPrompt": {
|
||||
"type": "template",
|
||||
"content": "# Role\nYou are an AI assistant.\n"
|
||||
},
|
||||
"prompt": {
|
||||
"type": "template",
|
||||
"content": "# User Input\nquery:{{start_0.query}}\nenable:{{start_0.enable}}"
|
||||
}
|
||||
},
|
||||
"inputs": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"modelName",
|
||||
"apiKey",
|
||||
"apiHost",
|
||||
"temperature",
|
||||
"prompt"
|
||||
],
|
||||
"properties": {
|
||||
"modelName": {
|
||||
"type": "string"
|
||||
},
|
||||
"apiKey": {
|
||||
"type": "string"
|
||||
},
|
||||
"apiHost": {
|
||||
"type": "string"
|
||||
},
|
||||
"temperature": {
|
||||
"type": "number"
|
||||
},
|
||||
"systemPrompt": {
|
||||
"type": "string",
|
||||
"extra": {
|
||||
"formComponent": "prompt-editor"
|
||||
}
|
||||
},
|
||||
"prompt": {
|
||||
"type": "string",
|
||||
"extra": {
|
||||
"formComponent": "prompt-editor"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"result": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "llm_vTyMa",
|
||||
"type": "llm",
|
||||
"meta": {
|
||||
"position": {
|
||||
"x": 640,
|
||||
"y": 10
|
||||
}
|
||||
},
|
||||
"data": {
|
||||
"title": "LLM_2",
|
||||
"inputsValues": {
|
||||
"modelName": {
|
||||
"type": "constant",
|
||||
"content": "gpt-3.5-turbo"
|
||||
},
|
||||
"apiKey": {
|
||||
"type": "constant",
|
||||
"content": "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
},
|
||||
"apiHost": {
|
||||
"type": "constant",
|
||||
"content": "https://mock-ai-url/api/v3"
|
||||
},
|
||||
"temperature": {
|
||||
"type": "constant",
|
||||
"content": 0.5
|
||||
},
|
||||
"systemPrompt": {
|
||||
"type": "template",
|
||||
"content": "# Role\nYou are an AI assistant.\n"
|
||||
},
|
||||
"prompt": {
|
||||
"type": "template",
|
||||
"content": "# LLM Input\nresult:{{llm_8--A3.result}}"
|
||||
}
|
||||
},
|
||||
"inputs": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"modelName",
|
||||
"apiKey",
|
||||
"apiHost",
|
||||
"temperature",
|
||||
"prompt"
|
||||
],
|
||||
"properties": {
|
||||
"modelName": {
|
||||
"type": "string"
|
||||
},
|
||||
"apiKey": {
|
||||
"type": "string"
|
||||
},
|
||||
"apiHost": {
|
||||
"type": "string"
|
||||
},
|
||||
"temperature": {
|
||||
"type": "number"
|
||||
},
|
||||
"systemPrompt": {
|
||||
"type": "string",
|
||||
"extra": {
|
||||
"formComponent": "prompt-editor"
|
||||
}
|
||||
},
|
||||
"prompt": {
|
||||
"type": "string",
|
||||
"extra": {
|
||||
"formComponent": "prompt-editor"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"result": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"sourceNodeID": "start_0",
|
||||
"targetNodeID": "http_rDGIH"
|
||||
},
|
||||
{
|
||||
"sourceNodeID": "http_rDGIH",
|
||||
"targetNodeID": "condition_0"
|
||||
},
|
||||
{
|
||||
"sourceNodeID": "condition_0",
|
||||
"targetNodeID": "llm_8--A3",
|
||||
"sourcePortID": "if_f0rOAt"
|
||||
},
|
||||
{
|
||||
"sourceNodeID": "condition_0",
|
||||
"targetNodeID": "loop_Ycnsk",
|
||||
"sourcePortID": "if_0"
|
||||
},
|
||||
{
|
||||
"sourceNodeID": "llm_vTyMa",
|
||||
"targetNodeID": "end_0"
|
||||
},
|
||||
{
|
||||
"sourceNodeID": "loop_Ycnsk",
|
||||
"targetNodeID": "end_0"
|
||||
},
|
||||
{
|
||||
"sourceNodeID": "llm_8--A3",
|
||||
"targetNodeID": "llm_vTyMa"
|
||||
}
|
||||
]
|
||||
}
|
||||
412
docs/flow-free-layout-simple-demo.md
Normal file
412
docs/flow-free-layout-simple-demo.md
Normal file
@ -0,0 +1,412 @@
|
||||
# 最佳实践
|
||||
|
||||
import { FreeFeatureOverview } from '../../../../components';
|
||||
|
||||
<FreeFeatureOverview />
|
||||
|
||||
## 安装
|
||||
|
||||
```shell
|
||||
npx @flowgram.ai/create-app@latest free-layout
|
||||
```
|
||||
|
||||
## 源码
|
||||
|
||||
https://github.com/bytedance/flowgram.ai/tree/main/apps/demo-free-layout
|
||||
|
||||
## 项目概览
|
||||
|
||||
### 核心技术栈
|
||||
|
||||
* **前端框架**: React 18 + TypeScript
|
||||
* **构建工具**: Rsbuild (基于 Rspack 的现代构建工具)
|
||||
* **样式方案**: Less + Styled Components + CSS Variables
|
||||
* **UI 组件库**: Semi Design (@douyinfe/semi-ui)
|
||||
* **状态管理**: 基于 Flowgram 自研的编辑器框架
|
||||
* **依赖注入**: Inversify
|
||||
|
||||
### 核心依赖包
|
||||
|
||||
* **@flowgram.ai/free-layout-editor**: 自由布局编辑器核心依赖
|
||||
* **@flowgram.ai/free-snap-plugin**: 自动对齐及辅助线插件
|
||||
* **@flowgram.ai/free-lines-plugin**: 连线渲染插件
|
||||
* **@flowgram.ai/free-node-panel-plugin**: 节点添加面板渲染插件
|
||||
* **@flowgram.ai/minimap-plugin**: 缩略图插件
|
||||
* **@flowgram.ai/free-container-plugin**: 子画布插件
|
||||
* **@flowgram.ai/free-group-plugin**: 分组插件
|
||||
* **@flowgram.ai/form-materials**: 表单物料
|
||||
* **@flowgram.ai/runtime-interface**: 运行时接口
|
||||
* **@flowgram.ai/runtime-js**: js 运行时模块
|
||||
|
||||
## 代码说明
|
||||
|
||||
### 目录结构
|
||||
|
||||
```
|
||||
src/
|
||||
├── app.tsx # 应用入口文件
|
||||
├── editor.tsx # 编辑器主组件
|
||||
├── initial-data.ts # 初始化数据配置
|
||||
├── assets/ # 静态资源
|
||||
├── components/ # 组件库
|
||||
│ ├── index.ts
|
||||
│ ├── add-node/ # 添加节点组件
|
||||
│ ├── base-node/ # 基础节点组件
|
||||
│ ├── comment/ # 注释组件
|
||||
│ ├── group/ # 分组组件
|
||||
│ ├── line-add-button/ # 连线添加按钮
|
||||
│ ├── node-menu/ # 节点菜单
|
||||
│ ├── node-panel/ # 节点添加面板
|
||||
│ ├── selector-box-popover/ # 选择框弹窗
|
||||
│ ├── sidebar/ # 侧边栏
|
||||
│ ├── testrun/ # 测试运行组件
|
||||
│ │ ├── hooks/ # 测试运行钩子
|
||||
│ │ ├── node-status-bar/ # 节点状态栏
|
||||
│ │ ├── testrun-button/ # 测试运行按钮
|
||||
│ │ ├── testrun-form/ # 测试运行表单
|
||||
│ │ ├── testrun-json-input/ # JSON输入组件
|
||||
│ │ └── testrun-panel/ # 测试运行面板
|
||||
│ └── tools/ # 工具组件
|
||||
├── context/ # React Context
|
||||
│ ├── node-render-context.ts # 当前渲染节点 Context
|
||||
│ ├── sidebar-context # 侧边栏 Context
|
||||
├── form-components/ # 表单组件库
|
||||
│ ├── form-content/ # 表单内容
|
||||
│ ├── form-header/ # 表单头部
|
||||
│ ├── form-inputs/ # 表单输入
|
||||
│ └── form-item/ # 表单项
|
||||
│ └── feedback.tsx # 表单校验错误渲染
|
||||
├── hooks/
|
||||
│ ├── index.ts
|
||||
│ ├── use-editor-props.tsx # 编辑器属性钩子
|
||||
│ ├── use-is-sidebar.ts # 侧边栏状态钩子
|
||||
│ ├── use-node-render-context.ts # 节点渲染上下文钩子
|
||||
│ └── use-port-click.ts # 端口点击钩子
|
||||
├── nodes/ # 节点定义
|
||||
│ ├── index.ts
|
||||
│ ├── constants.ts # 节点常量定义
|
||||
│ ├── default-form-meta.ts # 默认表单元数据
|
||||
│ ├── block-end/ # 块结束节点
|
||||
│ ├── block-start/ # 块开始节点
|
||||
│ ├── break/ # 中断节点
|
||||
│ ├── code/ # 代码节点
|
||||
│ ├── comment/ # 注释节点
|
||||
│ ├── condition/ # 条件节点
|
||||
│ ├── continue/ # 继续节点
|
||||
│ ├── end/ # 结束节点
|
||||
│ ├── group/ # 分组节点
|
||||
│ ├── http/ # HTTP节点
|
||||
│ ├── llm/ # LLM节点
|
||||
│ ├── loop/ # 循环节点
|
||||
│ ├── start/ # 开始节点
|
||||
│ └── variable/ # 变量节点
|
||||
├── plugins/ # 插件系统
|
||||
│ ├── index.ts
|
||||
│ ├── context-menu-plugin/ # 右键菜单插件
|
||||
│ ├── runtime-plugin/ # 运行时插件
|
||||
│ │ ├── client/ # 客户端
|
||||
│ │ │ ├── browser-client/ # 浏览器客户端
|
||||
│ │ │ └── server-client/ # 服务器客户端
|
||||
│ │ └── runtime-service/ # 运行时服务
|
||||
│ └── variable-panel-plugin/ # 变量面板插件
|
||||
│ └── components/ # 变量面板组件
|
||||
├── services/ # 服务层
|
||||
│ ├── index.ts
|
||||
│ └── custom-service.ts # 自定义服务
|
||||
├── shortcuts/ # 快捷键系统
|
||||
│ ├── index.ts
|
||||
│ ├── constants.ts # 快捷键常量
|
||||
│ ├── shortcuts.ts # 快捷键定义
|
||||
│ ├── type.ts # 类型定义
|
||||
│ ├── collapse/ # 折叠快捷键
|
||||
│ ├── copy/ # 复制快捷键
|
||||
│ ├── delete/ # 删除快捷键
|
||||
│ ├── expand/ # 展开快捷键
|
||||
│ ├── paste/ # 粘贴快捷键
|
||||
│ ├── select-all/ # 全选快捷键
|
||||
│ ├── zoom-in/ # 放大快捷键
|
||||
│ └── zoom-out/ # 缩小快捷键
|
||||
├── styles/ # 样式文件
|
||||
├── typings/ # 类型定义
|
||||
│ ├── index.ts
|
||||
│ ├── json-schema.ts # JSON Schema类型
|
||||
│ └── node.ts # 节点类型定义
|
||||
└── utils/ # 工具函数
|
||||
├── index.ts
|
||||
└── on-drag-line-end.ts # 拖拽连线结束处理
|
||||
```
|
||||
|
||||
### 关键目录功能说明
|
||||
|
||||
#### 1. `/components` - 组件库
|
||||
|
||||
* **base-node**: 所有节点的基础渲染组件
|
||||
* **testrun**: 完整的测试运行功能模块,包含状态栏、表单、面板等
|
||||
* **sidebar**: 侧边栏组件,提供工具和属性面板
|
||||
* **node-panel**: 节点添加面板,支持拖拽添加新节点
|
||||
|
||||
#### 2. `/nodes` - 节点系统
|
||||
|
||||
每个节点类型都有独立的目录,包含:
|
||||
|
||||
* 节点注册信息 (`index.ts`)
|
||||
* 表单元数据定义 (`form-meta.ts`)
|
||||
* 节点特定的组件和逻辑
|
||||
|
||||
#### 3. `/plugins` - 插件系统
|
||||
|
||||
* **runtime-plugin**: 支持浏览器和服务器两种运行模式
|
||||
* **context-menu-plugin**: 右键菜单功能
|
||||
* **variable-panel-plugin**: 变量管理面板
|
||||
|
||||
#### 4. `/shortcuts` - 快捷键系统
|
||||
|
||||
完整的快捷键支持,包括:
|
||||
|
||||
* 基础操作:复制、粘贴、删除、全选
|
||||
* 视图操作:放大、缩小、折叠、展开
|
||||
* 每个快捷键都有独立的实现模块
|
||||
|
||||
## 应用架构设计
|
||||
|
||||
### 核心设计模式
|
||||
|
||||
#### 1. 插件化架构 (Plugin Architecture)
|
||||
|
||||
应用采用高度模块化的插件系统,每个功能都作为独立插件存在:
|
||||
|
||||
```typescript
|
||||
plugins: () => [
|
||||
createFreeLinesPlugin({ renderInsideLine: LineAddButton }),
|
||||
createMinimapPlugin({ /* 配置 */ }),
|
||||
createFreeSnapPlugin({ /* 对齐配置 */ }),
|
||||
createFreeNodePanelPlugin({ renderer: NodePanel }),
|
||||
createContainerNodePlugin({}),
|
||||
createFreeGroupPlugin({ groupNodeRender: GroupNodeRender }),
|
||||
createContextMenuPlugin({}),
|
||||
createRuntimePlugin({ mode: 'browser' }),
|
||||
createVariablePanelPlugin({})
|
||||
]
|
||||
```
|
||||
|
||||
#### 2. 节点注册系统 (Node Registry Pattern)
|
||||
|
||||
通过注册表模式管理不同类型的工作流节点:
|
||||
|
||||
```typescript
|
||||
export const nodeRegistries: FlowNodeRegistry[] = [
|
||||
ConditionNodeRegistry, // 条件节点
|
||||
StartNodeRegistry, // 开始节点
|
||||
EndNodeRegistry, // 结束节点
|
||||
LLMNodeRegistry, // LLM节点
|
||||
LoopNodeRegistry, // 循环节点
|
||||
CommentNodeRegistry, // 注释节点
|
||||
HTTPNodeRegistry, // HTTP节点
|
||||
CodeNodeRegistry, // 代码节点
|
||||
// ... 更多节点类型
|
||||
];
|
||||
```
|
||||
|
||||
#### 3. 依赖注入模式 (Dependency Injection)
|
||||
|
||||
使用 Inversify 框架实现服务的依赖注入:
|
||||
|
||||
```typescript
|
||||
onBind: ({ bind }) => {
|
||||
bind(CustomService).toSelf().inSingletonScope();
|
||||
}
|
||||
```
|
||||
|
||||
## 核心功能分析
|
||||
|
||||
### 1. 编辑器配置系统
|
||||
|
||||
`useEditorProps` 是整个编辑器的配置中心,包含:
|
||||
|
||||
```typescript
|
||||
export function useEditorProps(
|
||||
initialData: FlowDocumentJSON,
|
||||
nodeRegistries: FlowNodeRegistry[]
|
||||
): FreeLayoutProps {
|
||||
return useMemo<FreeLayoutProps>(() => ({
|
||||
background: true, // 背景网格
|
||||
readonly: false, // 是否只读
|
||||
initialData, // 初始数据
|
||||
nodeRegistries, // 节点注册表
|
||||
|
||||
// 核心功能配置
|
||||
playground: { preventGlobalGesture: true /* 阻止 mac 浏览器手势翻页 */ },
|
||||
nodeEngine: { enable: true },
|
||||
variableEngine: { enable: true },
|
||||
history: { enable: true, enableChangeNode: true },
|
||||
|
||||
// 业务逻辑配置
|
||||
canAddLine: (ctx, fromPort, toPort) => { /* 连线规则 */ },
|
||||
canDeleteLine: (ctx, line) => { /* 删除连线规则 */ },
|
||||
canDeleteNode: (ctx, node) => { /* 删除节点规则 */ },
|
||||
canDropToNode: (ctx, params) => { /* 拖拽规则 */ },
|
||||
|
||||
// 插件配置
|
||||
plugins: () => [/* 插件列表 */],
|
||||
|
||||
// 事件处理
|
||||
onContentChange: debounce((ctx, event) => { /* 自动保存 */ }, 1000),
|
||||
onInit: (ctx) => { /* 初始化 */ },
|
||||
onAllLayersRendered: (ctx) => { /* 渲染完成 */ }
|
||||
}), []);
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 节点类型系统
|
||||
|
||||
应用支持多种工作流节点类型:
|
||||
|
||||
```typescript
|
||||
export enum WorkflowNodeType {
|
||||
Start = 'start', // 开始节点
|
||||
End = 'end', // 结束节点
|
||||
LLM = 'llm', // 大语言模型节点
|
||||
HTTP = 'http', // HTTP请求节点
|
||||
Code = 'code', // 代码执行节点
|
||||
Variable = 'variable', // 变量节点
|
||||
Condition = 'condition', // 条件判断节点
|
||||
Loop = 'loop', // 循环节点
|
||||
BlockStart = 'block-start', // 子画布开始节点
|
||||
BlockEnd = 'block-end', // 子画布结束节点
|
||||
Comment = 'comment', // 注释节点
|
||||
Continue = 'continue', // 继续节点
|
||||
Break = 'break', // 中断节点
|
||||
}
|
||||
```
|
||||
|
||||
每个节点都遵循统一的注册模式:
|
||||
|
||||
```typescript
|
||||
export const StartNodeRegistry: FlowNodeRegistry = {
|
||||
type: WorkflowNodeType.Start,
|
||||
meta: {
|
||||
isStart: true,
|
||||
deleteDisable: true, // 不可删除
|
||||
copyDisable: true, // 不可复制
|
||||
nodePanelVisible: false, // 不在节点面板显示
|
||||
defaultPorts: [{ type: 'output' }],
|
||||
size: { width: 360, height: 211 }
|
||||
},
|
||||
info: {
|
||||
icon: iconStart,
|
||||
description: '工作流的起始节点,用于设置启动工作流所需的信息。'
|
||||
},
|
||||
formMeta, // 表单配置
|
||||
canAdd() { return false; } // 不允许添加多个开始节点
|
||||
};
|
||||
```
|
||||
|
||||
### 3. 插件化架构
|
||||
|
||||
应用的功能通过插件系统实现模块化:
|
||||
|
||||
#### 核心插件列表
|
||||
|
||||
1. **FreeLinesPlugin** - 连线渲染和交互
|
||||
2. **MinimapPlugin** - 缩略图导航
|
||||
3. **FreeSnapPlugin** - 自动对齐和辅助线
|
||||
4. **FreeNodePanelPlugin** - 节点添加面板
|
||||
5. **ContainerNodePlugin** - 容器节点(如循环节点)
|
||||
6. **FreeGroupPlugin** - 节点分组功能
|
||||
7. **ContextMenuPlugin** - 右键菜单
|
||||
8. **RuntimePlugin** - 工作流运行时
|
||||
9. **VariablePanelPlugin** - 变量管理面板
|
||||
|
||||
### 4. 运行时系统
|
||||
|
||||
应用支持两种运行模式:
|
||||
|
||||
```typescript
|
||||
createRuntimePlugin({
|
||||
mode: 'browser', // 浏览器模式
|
||||
// mode: 'server', // 服务器模式
|
||||
// serverConfig: {
|
||||
// domain: 'localhost',
|
||||
// port: 4000,
|
||||
// protocol: 'http',
|
||||
// },
|
||||
})
|
||||
```
|
||||
|
||||
## 设计理念与架构优势
|
||||
|
||||
### 1. 高度模块化
|
||||
|
||||
* **插件化架构**: 每个功能都是独立插件,易于扩展和维护
|
||||
* **节点注册系统**: 新节点类型可以轻松添加,无需修改核心代码
|
||||
* **组件化设计**: UI组件高度复用,职责清晰
|
||||
|
||||
### 2. 类型安全
|
||||
|
||||
* **完整的TypeScript支持**: 从配置到运行时的全链路类型保护
|
||||
* **JSON Schema集成**: 节点数据结构通过Schema验证
|
||||
* **强类型的插件接口**: 插件开发有明确的类型约束
|
||||
|
||||
### 3. 用户体验优化
|
||||
|
||||
* **实时预览**: 支持工作流的实时运行和调试
|
||||
* **丰富的交互**: 拖拽、缩放、对齐、快捷键等完整的编辑体验
|
||||
* **可视化反馈**: 缩略图、状态指示、连线动画等视觉反馈
|
||||
|
||||
### 4. 扩展性设计
|
||||
|
||||
* **开放的插件系统**: 第三方可以轻松开发自定义插件
|
||||
* **灵活的节点系统**: 支持自定义节点类型和表单配置
|
||||
* **多运行时支持**: 浏览器和服务器双模式运行
|
||||
|
||||
### 5. 性能优化
|
||||
|
||||
* **按需加载**: 组件和插件支持按需加载
|
||||
* **防抖处理**: 自动保存等高频操作的性能优化
|
||||
|
||||
## 技术亮点
|
||||
|
||||
### 1. 自研编辑器框架
|
||||
|
||||
基于 `@flowgram.ai/free-layout-editor` 自研框架,提供:
|
||||
|
||||
* 自由布局的画布系统
|
||||
* 完整的撤销/重做功能
|
||||
* 节点和连线的生命周期管理
|
||||
* 变量引擎和表达式系统
|
||||
|
||||
### 2. 先进的构建配置
|
||||
|
||||
使用 Rsbuild 作为构建工具:
|
||||
|
||||
```typescript
|
||||
export default defineConfig({
|
||||
plugins: [pluginReact(), pluginLess()],
|
||||
source: {
|
||||
entry: { index: './src/app.tsx' },
|
||||
decorators: { version: 'legacy' } // 支持装饰器
|
||||
},
|
||||
tools: {
|
||||
rspack: {
|
||||
ignoreWarnings: [/Critical dependency/] // 忽略特定警告
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 3. 国际化支持
|
||||
|
||||
内置多语言支持:
|
||||
|
||||
```typescript
|
||||
i18n: {
|
||||
locale: navigator.language,
|
||||
languages: {
|
||||
'zh-CN': {
|
||||
'Never Remind': '不再提示',
|
||||
'Hold {{key}} to drag node out': '按住 {{key}} 可以将节点拖出',
|
||||
},
|
||||
'en-US': {},
|
||||
}
|
||||
}
|
||||
```
|
||||
412
docs/flow-free-layout-sj-demo.md
Normal file
412
docs/flow-free-layout-sj-demo.md
Normal file
@ -0,0 +1,412 @@
|
||||
# 最佳实践
|
||||
|
||||
import { FreeFeatureOverview } from '../../../../components';
|
||||
|
||||
<FreeFeatureOverview />
|
||||
|
||||
## 安装
|
||||
|
||||
```shell
|
||||
npx @flowgram.ai/create-app@latest free-layout
|
||||
```
|
||||
|
||||
## 源码
|
||||
|
||||
https://github.com/bytedance/flowgram.ai/tree/main/apps/demo-free-layout
|
||||
|
||||
## 项目概览
|
||||
|
||||
### 核心技术栈
|
||||
|
||||
* **前端框架**: React 18 + TypeScript
|
||||
* **构建工具**: Rsbuild (基于 Rspack 的现代构建工具)
|
||||
* **样式方案**: Less + Styled Components + CSS Variables
|
||||
* **UI 组件库**: Semi Design (@douyinfe/semi-ui)
|
||||
* **状态管理**: 基于 Flowgram 自研的编辑器框架
|
||||
* **依赖注入**: Inversify
|
||||
|
||||
### 核心依赖包
|
||||
|
||||
* **@flowgram.ai/free-layout-editor**: 自由布局编辑器核心依赖
|
||||
* **@flowgram.ai/free-snap-plugin**: 自动对齐及辅助线插件
|
||||
* **@flowgram.ai/free-lines-plugin**: 连线渲染插件
|
||||
* **@flowgram.ai/free-node-panel-plugin**: 节点添加面板渲染插件
|
||||
* **@flowgram.ai/minimap-plugin**: 缩略图插件
|
||||
* **@flowgram.ai/free-container-plugin**: 子画布插件
|
||||
* **@flowgram.ai/free-group-plugin**: 分组插件
|
||||
* **@flowgram.ai/form-materials**: 表单物料
|
||||
* **@flowgram.ai/runtime-interface**: 运行时接口
|
||||
* **@flowgram.ai/runtime-js**: js 运行时模块
|
||||
|
||||
## 代码说明
|
||||
|
||||
### 目录结构
|
||||
|
||||
```
|
||||
src/
|
||||
├── app.tsx # 应用入口文件
|
||||
├── editor.tsx # 编辑器主组件
|
||||
├── initial-data.ts # 初始化数据配置
|
||||
├── assets/ # 静态资源
|
||||
├── components/ # 组件库
|
||||
│ ├── index.ts
|
||||
│ ├── add-node/ # 添加节点组件
|
||||
│ ├── base-node/ # 基础节点组件
|
||||
│ ├── comment/ # 注释组件
|
||||
│ ├── group/ # 分组组件
|
||||
│ ├── line-add-button/ # 连线添加按钮
|
||||
│ ├── node-menu/ # 节点菜单
|
||||
│ ├── node-panel/ # 节点添加面板
|
||||
│ ├── selector-box-popover/ # 选择框弹窗
|
||||
│ ├── sidebar/ # 侧边栏
|
||||
│ ├── testrun/ # 测试运行组件
|
||||
│ │ ├── hooks/ # 测试运行钩子
|
||||
│ │ ├── node-status-bar/ # 节点状态栏
|
||||
│ │ ├── testrun-button/ # 测试运行按钮
|
||||
│ │ ├── testrun-form/ # 测试运行表单
|
||||
│ │ ├── testrun-json-input/ # JSON输入组件
|
||||
│ │ └── testrun-panel/ # 测试运行面板
|
||||
│ └── tools/ # 工具组件
|
||||
├── context/ # React Context
|
||||
│ ├── node-render-context.ts # 当前渲染节点 Context
|
||||
│ ├── sidebar-context # 侧边栏 Context
|
||||
├── form-components/ # 表单组件库
|
||||
│ ├── form-content/ # 表单内容
|
||||
│ ├── form-header/ # 表单头部
|
||||
│ ├── form-inputs/ # 表单输入
|
||||
│ └── form-item/ # 表单项
|
||||
│ └── feedback.tsx # 表单校验错误渲染
|
||||
├── hooks/
|
||||
│ ├── index.ts
|
||||
│ ├── use-editor-props.tsx # 编辑器属性钩子
|
||||
│ ├── use-is-sidebar.ts # 侧边栏状态钩子
|
||||
│ ├── use-node-render-context.ts # 节点渲染上下文钩子
|
||||
│ └── use-port-click.ts # 端口点击钩子
|
||||
├── nodes/ # 节点定义
|
||||
│ ├── index.ts
|
||||
│ ├── constants.ts # 节点常量定义
|
||||
│ ├── default-form-meta.ts # 默认表单元数据
|
||||
│ ├── block-end/ # 块结束节点
|
||||
│ ├── block-start/ # 块开始节点
|
||||
│ ├── break/ # 中断节点
|
||||
│ ├── code/ # 代码节点
|
||||
│ ├── comment/ # 注释节点
|
||||
│ ├── condition/ # 条件节点
|
||||
│ ├── continue/ # 继续节点
|
||||
│ ├── end/ # 结束节点
|
||||
│ ├── group/ # 分组节点
|
||||
│ ├── http/ # HTTP节点
|
||||
│ ├── llm/ # LLM节点
|
||||
│ ├── loop/ # 循环节点
|
||||
│ ├── start/ # 开始节点
|
||||
│ └── variable/ # 变量节点
|
||||
├── plugins/ # 插件系统
|
||||
│ ├── index.ts
|
||||
│ ├── context-menu-plugin/ # 右键菜单插件
|
||||
│ ├── runtime-plugin/ # 运行时插件
|
||||
│ │ ├── client/ # 客户端
|
||||
│ │ │ ├── browser-client/ # 浏览器客户端
|
||||
│ │ │ └── server-client/ # 服务器客户端
|
||||
│ │ └── runtime-service/ # 运行时服务
|
||||
│ └── variable-panel-plugin/ # 变量面板插件
|
||||
│ └── components/ # 变量面板组件
|
||||
├── services/ # 服务层
|
||||
│ ├── index.ts
|
||||
│ └── custom-service.ts # 自定义服务
|
||||
├── shortcuts/ # 快捷键系统
|
||||
│ ├── index.ts
|
||||
│ ├── constants.ts # 快捷键常量
|
||||
│ ├── shortcuts.ts # 快捷键定义
|
||||
│ ├── type.ts # 类型定义
|
||||
│ ├── collapse/ # 折叠快捷键
|
||||
│ ├── copy/ # 复制快捷键
|
||||
│ ├── delete/ # 删除快捷键
|
||||
│ ├── expand/ # 展开快捷键
|
||||
│ ├── paste/ # 粘贴快捷键
|
||||
│ ├── select-all/ # 全选快捷键
|
||||
│ ├── zoom-in/ # 放大快捷键
|
||||
│ └── zoom-out/ # 缩小快捷键
|
||||
├── styles/ # 样式文件
|
||||
├── typings/ # 类型定义
|
||||
│ ├── index.ts
|
||||
│ ├── json-schema.ts # JSON Schema类型
|
||||
│ └── node.ts # 节点类型定义
|
||||
└── utils/ # 工具函数
|
||||
├── index.ts
|
||||
└── on-drag-line-end.ts # 拖拽连线结束处理
|
||||
```
|
||||
|
||||
### 关键目录功能说明
|
||||
|
||||
#### 1. `/components` - 组件库
|
||||
|
||||
* **base-node**: 所有节点的基础渲染组件
|
||||
* **testrun**: 完整的测试运行功能模块,包含状态栏、表单、面板等
|
||||
* **sidebar**: 侧边栏组件,提供工具和属性面板
|
||||
* **node-panel**: 节点添加面板,支持拖拽添加新节点
|
||||
|
||||
#### 2. `/nodes` - 节点系统
|
||||
|
||||
每个节点类型都有独立的目录,包含:
|
||||
|
||||
* 节点注册信息 (`index.ts`)
|
||||
* 表单元数据定义 (`form-meta.ts`)
|
||||
* 节点特定的组件和逻辑
|
||||
|
||||
#### 3. `/plugins` - 插件系统
|
||||
|
||||
* **runtime-plugin**: 支持浏览器和服务器两种运行模式
|
||||
* **context-menu-plugin**: 右键菜单功能
|
||||
* **variable-panel-plugin**: 变量管理面板
|
||||
|
||||
#### 4. `/shortcuts` - 快捷键系统
|
||||
|
||||
完整的快捷键支持,包括:
|
||||
|
||||
* 基础操作:复制、粘贴、删除、全选
|
||||
* 视图操作:放大、缩小、折叠、展开
|
||||
* 每个快捷键都有独立的实现模块
|
||||
|
||||
## 应用架构设计
|
||||
|
||||
### 核心设计模式
|
||||
|
||||
#### 1. 插件化架构 (Plugin Architecture)
|
||||
|
||||
应用采用高度模块化的插件系统,每个功能都作为独立插件存在:
|
||||
|
||||
```typescript
|
||||
plugins: () => [
|
||||
createFreeLinesPlugin({ renderInsideLine: LineAddButton }),
|
||||
createMinimapPlugin({ /* 配置 */ }),
|
||||
createFreeSnapPlugin({ /* 对齐配置 */ }),
|
||||
createFreeNodePanelPlugin({ renderer: NodePanel }),
|
||||
createContainerNodePlugin({}),
|
||||
createFreeGroupPlugin({ groupNodeRender: GroupNodeRender }),
|
||||
createContextMenuPlugin({}),
|
||||
createRuntimePlugin({ mode: 'browser' }),
|
||||
createVariablePanelPlugin({})
|
||||
]
|
||||
```
|
||||
|
||||
#### 2. 节点注册系统 (Node Registry Pattern)
|
||||
|
||||
通过注册表模式管理不同类型的工作流节点:
|
||||
|
||||
```typescript
|
||||
export const nodeRegistries: FlowNodeRegistry[] = [
|
||||
ConditionNodeRegistry, // 条件节点
|
||||
StartNodeRegistry, // 开始节点
|
||||
EndNodeRegistry, // 结束节点
|
||||
LLMNodeRegistry, // LLM节点
|
||||
LoopNodeRegistry, // 循环节点
|
||||
CommentNodeRegistry, // 注释节点
|
||||
HTTPNodeRegistry, // HTTP节点
|
||||
CodeNodeRegistry, // 代码节点
|
||||
// ... 更多节点类型
|
||||
];
|
||||
```
|
||||
|
||||
#### 3. 依赖注入模式 (Dependency Injection)
|
||||
|
||||
使用 Inversify 框架实现服务的依赖注入:
|
||||
|
||||
```typescript
|
||||
onBind: ({ bind }) => {
|
||||
bind(CustomService).toSelf().inSingletonScope();
|
||||
}
|
||||
```
|
||||
|
||||
## 核心功能分析
|
||||
|
||||
### 1. 编辑器配置系统
|
||||
|
||||
`useEditorProps` 是整个编辑器的配置中心,包含:
|
||||
|
||||
```typescript
|
||||
export function useEditorProps(
|
||||
initialData: FlowDocumentJSON,
|
||||
nodeRegistries: FlowNodeRegistry[]
|
||||
): FreeLayoutProps {
|
||||
return useMemo<FreeLayoutProps>(() => ({
|
||||
background: true, // 背景网格
|
||||
readonly: false, // 是否只读
|
||||
initialData, // 初始数据
|
||||
nodeRegistries, // 节点注册表
|
||||
|
||||
// 核心功能配置
|
||||
playground: { preventGlobalGesture: true /* 阻止 mac 浏览器手势翻页 */ },
|
||||
nodeEngine: { enable: true },
|
||||
variableEngine: { enable: true },
|
||||
history: { enable: true, enableChangeNode: true },
|
||||
|
||||
// 业务逻辑配置
|
||||
canAddLine: (ctx, fromPort, toPort) => { /* 连线规则 */ },
|
||||
canDeleteLine: (ctx, line) => { /* 删除连线规则 */ },
|
||||
canDeleteNode: (ctx, node) => { /* 删除节点规则 */ },
|
||||
canDropToNode: (ctx, params) => { /* 拖拽规则 */ },
|
||||
|
||||
// 插件配置
|
||||
plugins: () => [/* 插件列表 */],
|
||||
|
||||
// 事件处理
|
||||
onContentChange: debounce((ctx, event) => { /* 自动保存 */ }, 1000),
|
||||
onInit: (ctx) => { /* 初始化 */ },
|
||||
onAllLayersRendered: (ctx) => { /* 渲染完成 */ }
|
||||
}), []);
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 节点类型系统
|
||||
|
||||
应用支持多种工作流节点类型:
|
||||
|
||||
```typescript
|
||||
export enum WorkflowNodeType {
|
||||
Start = 'start', // 开始节点
|
||||
End = 'end', // 结束节点
|
||||
LLM = 'llm', // 大语言模型节点
|
||||
HTTP = 'http', // HTTP请求节点
|
||||
Code = 'code', // 代码执行节点
|
||||
Variable = 'variable', // 变量节点
|
||||
Condition = 'condition', // 条件判断节点
|
||||
Loop = 'loop', // 循环节点
|
||||
BlockStart = 'block-start', // 子画布开始节点
|
||||
BlockEnd = 'block-end', // 子画布结束节点
|
||||
Comment = 'comment', // 注释节点
|
||||
Continue = 'continue', // 继续节点
|
||||
Break = 'break', // 中断节点
|
||||
}
|
||||
```
|
||||
|
||||
每个节点都遵循统一的注册模式:
|
||||
|
||||
```typescript
|
||||
export const StartNodeRegistry: FlowNodeRegistry = {
|
||||
type: WorkflowNodeType.Start,
|
||||
meta: {
|
||||
isStart: true,
|
||||
deleteDisable: true, // 不可删除
|
||||
copyDisable: true, // 不可复制
|
||||
nodePanelVisible: false, // 不在节点面板显示
|
||||
defaultPorts: [{ type: 'output' }],
|
||||
size: { width: 360, height: 211 }
|
||||
},
|
||||
info: {
|
||||
icon: iconStart,
|
||||
description: '工作流的起始节点,用于设置启动工作流所需的信息。'
|
||||
},
|
||||
formMeta, // 表单配置
|
||||
canAdd() { return false; } // 不允许添加多个开始节点
|
||||
};
|
||||
```
|
||||
|
||||
### 3. 插件化架构
|
||||
|
||||
应用的功能通过插件系统实现模块化:
|
||||
|
||||
#### 核心插件列表
|
||||
|
||||
1. **FreeLinesPlugin** - 连线渲染和交互
|
||||
2. **MinimapPlugin** - 缩略图导航
|
||||
3. **FreeSnapPlugin** - 自动对齐和辅助线
|
||||
4. **FreeNodePanelPlugin** - 节点添加面板
|
||||
5. **ContainerNodePlugin** - 容器节点(如循环节点)
|
||||
6. **FreeGroupPlugin** - 节点分组功能
|
||||
7. **ContextMenuPlugin** - 右键菜单
|
||||
8. **RuntimePlugin** - 工作流运行时
|
||||
9. **VariablePanelPlugin** - 变量管理面板
|
||||
|
||||
### 4. 运行时系统
|
||||
|
||||
应用支持两种运行模式:
|
||||
|
||||
```typescript
|
||||
createRuntimePlugin({
|
||||
mode: 'browser', // 浏览器模式
|
||||
// mode: 'server', // 服务器模式
|
||||
// serverConfig: {
|
||||
// domain: 'localhost',
|
||||
// port: 4000,
|
||||
// protocol: 'http',
|
||||
// },
|
||||
})
|
||||
```
|
||||
|
||||
## 设计理念与架构优势
|
||||
|
||||
### 1. 高度模块化
|
||||
|
||||
* **插件化架构**: 每个功能都是独立插件,易于扩展和维护
|
||||
* **节点注册系统**: 新节点类型可以轻松添加,无需修改核心代码
|
||||
* **组件化设计**: UI组件高度复用,职责清晰
|
||||
|
||||
### 2. 类型安全
|
||||
|
||||
* **完整的TypeScript支持**: 从配置到运行时的全链路类型保护
|
||||
* **JSON Schema集成**: 节点数据结构通过Schema验证
|
||||
* **强类型的插件接口**: 插件开发有明确的类型约束
|
||||
|
||||
### 3. 用户体验优化
|
||||
|
||||
* **实时预览**: 支持工作流的实时运行和调试
|
||||
* **丰富的交互**: 拖拽、缩放、对齐、快捷键等完整的编辑体验
|
||||
* **可视化反馈**: 缩略图、状态指示、连线动画等视觉反馈
|
||||
|
||||
### 4. 扩展性设计
|
||||
|
||||
* **开放的插件系统**: 第三方可以轻松开发自定义插件
|
||||
* **灵活的节点系统**: 支持自定义节点类型和表单配置
|
||||
* **多运行时支持**: 浏览器和服务器双模式运行
|
||||
|
||||
### 5. 性能优化
|
||||
|
||||
* **按需加载**: 组件和插件支持按需加载
|
||||
* **防抖处理**: 自动保存等高频操作的性能优化
|
||||
|
||||
## 技术亮点
|
||||
|
||||
### 1. 自研编辑器框架
|
||||
|
||||
基于 `@flowgram.ai/free-layout-editor` 自研框架,提供:
|
||||
|
||||
* 自由布局的画布系统
|
||||
* 完整的撤销/重做功能
|
||||
* 节点和连线的生命周期管理
|
||||
* 变量引擎和表达式系统
|
||||
|
||||
### 2. 先进的构建配置
|
||||
|
||||
使用 Rsbuild 作为构建工具:
|
||||
|
||||
```typescript
|
||||
export default defineConfig({
|
||||
plugins: [pluginReact(), pluginLess()],
|
||||
source: {
|
||||
entry: { index: './src/app.tsx' },
|
||||
decorators: { version: 'legacy' } // 支持装饰器
|
||||
},
|
||||
tools: {
|
||||
rspack: {
|
||||
ignoreWarnings: [/Critical dependency/] // 忽略特定警告
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 3. 国际化支持
|
||||
|
||||
内置多语言支持:
|
||||
|
||||
```typescript
|
||||
i18n: {
|
||||
locale: navigator.language,
|
||||
languages: {
|
||||
'zh-CN': {
|
||||
'Never Remind': '不再提示',
|
||||
'Hold {{key}} to drag node out': '按住 {{key}} 可以将节点拖出',
|
||||
},
|
||||
'en-US': {},
|
||||
}
|
||||
}
|
||||
```
|
||||
1
docs/test/DEMO:
Normal file
1
docs/test/DEMO:
Normal file
@ -0,0 +1 @@
|
||||
CHECK = SYS: FLOWLOG:
|
||||
1
docs/test/body_login.json
Normal file
1
docs/test/body_login.json
Normal file
@ -0,0 +1 @@
|
||||
{"code":0,"message":"ok","data":{"access_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsInVpZCI6MSwiaXNzIjoidWRtaW4iLCJleHAiOjE3NTU3OTAwMjUsInR5cCI6ImFjY2VzcyJ9.Jp9I3Q6eCUHWe7TM4Xwzy6iLd-Y_It-izRgLNPRC7XE","user":{"id":1,"username":"admin","nickname":"Administrator","status":1}}}
|
||||
19
docs/test/branch-async-create.json
Normal file
19
docs/test/branch-async-create.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "Branch A->COND->B/C (async)",
|
||||
"code": "branch_async",
|
||||
"design_json": {
|
||||
"name": "Branch A->COND->B/C (async)",
|
||||
"execution_mode": "async",
|
||||
"nodes": [
|
||||
{ "id": "A", "kind": "http", "name": "http A", "task": "http", "config": { "url": "https://httpbin.org/get", "method": "GET" } },
|
||||
{ "id": "COND", "kind": "condition", "name": "cond", "task": "condition", "config": { "expression": { "left": { "type": "constant", "value": true }, "operator": "is_true", "right": { "type": "constant", "value": true } } }, "ports": { "yes": { "id": "yes" }, "no": { "id": "no" } } },
|
||||
{ "id": "B", "kind": "http", "name": "http B", "task": "http", "config": { "url": "https://httpbin.org/anything/B", "method": "GET" } },
|
||||
{ "id": "C", "kind": "http", "name": "http C", "task": "http", "config": { "url": "https://httpbin.org/anything/C", "method": "GET" } }
|
||||
],
|
||||
"edges": [
|
||||
{ "from": "A", "to": "COND" },
|
||||
{ "from": "COND", "to": "B", "condition": { "left": { "type": "constant", "value": true }, "operator": "is_true", "right": { "type": "constant", "value": true } } },
|
||||
{ "from": "COND", "to": "C", "condition": { "left": { "type": "constant", "value": true }, "operator": "is_false", "right": { "type": "constant", "value": true } } }
|
||||
]
|
||||
}
|
||||
}
|
||||
22
docs/test/branch-sync-create.json
Normal file
22
docs/test/branch-sync-create.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "branch-sync",
|
||||
"code": "branch_sync_1",
|
||||
"design_json": {
|
||||
"name": "branch-sync",
|
||||
"executionMode": "sync",
|
||||
"nodes": [
|
||||
{ "id": "A", "type": "http", "data": { "title": "A-GET-x", "api": { "method": "GET", "url": "https://httpbin.org/get?x=hello" } } },
|
||||
{ "id": "COND", "type": "condition", "data": { "title": "COND", "conditions": [
|
||||
{ "key": "yes", "value": { "left": { "type": "constant", "content": true }, "operator": "is_true" } },
|
||||
{ "key": "no", "value": { "left": { "type": "constant", "content": false }, "operator": "is_true" } }
|
||||
] } },
|
||||
{ "id": "B", "type": "http", "data": { "title": "B-UUID", "api": { "method": "GET", "url": "https://httpbin.org/uuid" } } },
|
||||
{ "id": "C", "type": "http", "data": { "title": "C-Delay1s", "api": { "method": "GET", "url": "https://httpbin.org/delay/1" } } }
|
||||
],
|
||||
"edges": [
|
||||
{ "sourceNodeID": "A", "targetNodeID": "COND" },
|
||||
{ "sourceNodeID": "COND", "targetNodeID": "B", "sourcePortID": "yes" },
|
||||
{ "sourceNodeID": "COND", "targetNodeID": "C", "sourcePortID": "no" }
|
||||
]
|
||||
}
|
||||
}
|
||||
5
docs/test/cookies.txt
Normal file
5
docs/test/cookies.txt
Normal file
@ -0,0 +1,5 @@
|
||||
# Netscape HTTP Cookie File
|
||||
# https://curl.se/docs/http-cookies.html
|
||||
# This file was generated by libcurl! Edit at your own risk.
|
||||
|
||||
#HttpOnly_localhost FALSE / FALSE 1756997209 refresh_token eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsInVpZCI6MSwiaXNzIjoidWRtaW4iLCJleHAiOjE3NTY5OTcyMDksInR5cCI6InJlZnJlc2gifQ.1zYcf-hETforh0jdyTGhuTA7_8U9EUASYaeGXfC-Jkw
|
||||
5
docs/test/cookies_admin.txt
Normal file
5
docs/test/cookies_admin.txt
Normal file
@ -0,0 +1,5 @@
|
||||
# Netscape HTTP Cookie File
|
||||
# https://curl.se/docs/http-cookies.html
|
||||
# This file was generated by libcurl! Edit at your own risk.
|
||||
|
||||
#HttpOnly_127.0.0.1 FALSE / FALSE 1759594896 refresh_token eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsInVpZCI6MSwiaXNzIjoidWRtaW4iLCJleHAiOjE3NTk1OTQ4OTYsInR5cCI6InJlZnJlc2gifQ.zH6gGProbzh4U7RzgYNH4DqD2-EyzvotbkGUfMBzp4k
|
||||
3
docs/test/flow_create.json
Normal file
3
docs/test/flow_create.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"yaml": "# demo flow\nnodes:\n - { id: start, kind: start, name: 开始 }\n - { id: assign, kind: custom, name: 赋值, script: ctx.x = ctx.x + 1; }\n - { id: cond, kind: custom, name: 条件 }\n - { id: end, kind: end, name: 结束 }\nedges:\n - { from: start, to: assign }\n - { from: assign, to: cond }\n - { from: cond, to: end, condition: ctx.x >= 1 }\n - { from: cond, to: end }\n"
|
||||
}
|
||||
15
docs/test/linear-async-create.json
Normal file
15
docs/test/linear-async-create.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "linear-async",
|
||||
"code": "linear_async_1",
|
||||
"design_json": {
|
||||
"name": "linear-async",
|
||||
"executionMode": "async",
|
||||
"nodes": [
|
||||
{ "id": "N1", "type": "http", "data": { "title": "A-GET", "api": { "method": "GET", "url": "https://httpbin.org/delay/1" } } },
|
||||
{ "id": "N2", "type": "http", "data": { "title": "B-UUID", "api": { "method": "GET", "url": "https://httpbin.org/uuid" } } }
|
||||
],
|
||||
"edges": [
|
||||
{ "sourceNodeID": "N1", "targetNodeID": "N2" }
|
||||
]
|
||||
}
|
||||
}
|
||||
15
docs/test/linear-sync-create.json
Normal file
15
docs/test/linear-sync-create.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "linear-sync",
|
||||
"code": "linear_sync_1",
|
||||
"design_json": {
|
||||
"name": "linear-sync",
|
||||
"executionMode": "sync",
|
||||
"nodes": [
|
||||
{ "id": "N1", "type": "http", "data": { "title": "A-GET", "api": { "method": "GET", "url": "https://httpbin.org/get" } } },
|
||||
{ "id": "N2", "type": "http", "data": { "title": "B-UUID", "api": { "method": "GET", "url": "https://httpbin.org/uuid" } } }
|
||||
],
|
||||
"edges": [
|
||||
{ "sourceNodeID": "N1", "targetNodeID": "N2" }
|
||||
]
|
||||
}
|
||||
}
|
||||
5
docs/test/test_flow_create.json
Normal file
5
docs/test/test_flow_create.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "测试流程",
|
||||
"code": "test-flow",
|
||||
"yaml": "# demo flow\nnodes:\n - { id: start, kind: start, name: 开始 }\n - { id: assign, kind: custom, name: 赋值, script: ctx.x = ctx.input.name || 'default'; }\n - { id: end, kind: end, name: 结束 }\nedges:\n - { from: start, to: assign }\n - { from: assign, to: end }\n"
|
||||
}
|
||||
Reference in New Issue
Block a user