|
|
@@ -127,40 +127,33 @@ export const initialData: WorkflowJSON = {
|
|
|
// src/node-registries.ts
|
|
|
import { WorkflowNodeRegistry } from '@flowgram.ai/free-layout-editor';
|
|
|
|
|
|
-export const nodeRegistries: Record<string, WorkflowNodeRegistry> = {
|
|
|
- // 开始节点
|
|
|
- start: {
|
|
|
+/**
|
|
|
+ * 你可以自定义节点的注册器
|
|
|
+ */
|
|
|
+export const nodeRegistries: WorkflowNodeRegistry[] = [
|
|
|
+ {
|
|
|
type: 'start',
|
|
|
meta: {
|
|
|
- defaultWidth: 200,
|
|
|
- defaultHeight: 100,
|
|
|
- canDelete: false, // 禁止删除
|
|
|
- backgroundColor: '#E6F7FF',
|
|
|
- defaultExpanded: true,
|
|
|
+ isStart: true, // 开始节点标记
|
|
|
+ deleteDisable: true, // 开始节点不能被删除
|
|
|
+ copyDisable: true, // 开始节点不能被 copy
|
|
|
+ defaultPorts: [{ type: 'output' }], // 定义 input 和 output 端口,开始节点只有 output 端口
|
|
|
},
|
|
|
},
|
|
|
- // 自定义节点
|
|
|
- custom: {
|
|
|
- type: 'custom',
|
|
|
- meta: {
|
|
|
- defaultWidth: 200,
|
|
|
- defaultHeight: 100,
|
|
|
- backgroundColor: '#FFF7E6',
|
|
|
- defaultExpanded: true,
|
|
|
- },
|
|
|
- },
|
|
|
- // 结束节点
|
|
|
- end: {
|
|
|
+ {
|
|
|
type: 'end',
|
|
|
meta: {
|
|
|
- defaultWidth: 200,
|
|
|
- defaultHeight: 100,
|
|
|
- canDelete: false, // 禁止删除
|
|
|
- backgroundColor: '#FFF1F0',
|
|
|
- defaultExpanded: true,
|
|
|
+ deleteDisable: true,
|
|
|
+ copyDisable: true,
|
|
|
+ defaultPorts: [{ type: 'input' }], // 结束节点只有 input 端口
|
|
|
},
|
|
|
},
|
|
|
-};
|
|
|
+ {
|
|
|
+ type: 'custom',
|
|
|
+ meta: {},
|
|
|
+ defaultPorts: [{ type: 'output' }, { type: 'input' }], // 普通节点有两个端口
|
|
|
+ },
|
|
|
+];
|
|
|
```
|
|
|
|
|
|
#### 步骤三:创建编辑器配置
|
|
|
@@ -260,14 +253,29 @@ export const useEditorProps = () =>
|
|
|
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,
|
|
|
}),
|
|
|
],
|
|
|
}),
|
|
|
@@ -293,7 +301,7 @@ export const NodeAddPanel: React.FC = () => {
|
|
|
<div
|
|
|
key={nodeType}
|
|
|
className="demo-free-card"
|
|
|
- onMouseDown={e => dragService.startDragCard('custom', e, {
|
|
|
+ onMouseDown={e => dragService.startDragCard(nodeType, e, {
|
|
|
data: {
|
|
|
title: nodeType,
|
|
|
content: '拖拽创建的节点'
|
|
|
@@ -313,35 +321,76 @@ export const NodeAddPanel: React.FC = () => {
|
|
|
```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 { zoomIn, zoomOut, resetZoom, undo, redo } = usePlaygroundTools();
|
|
|
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 className="demo-free-tools">
|
|
|
- <button onClick={zoomIn}>放大</button>
|
|
|
- <button onClick={zoomOut}>缩小</button>
|
|
|
- <button onClick={resetZoom}>重置缩放</button>
|
|
|
- <button onClick={undo} disabled={!history?.canUndo()}>撤销</button>
|
|
|
- <button onClick={redo} disabled={!history?.canRedo()}>重做</button>
|
|
|
+ <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';
|
|
|
-import { MinimapService } from '@flowgram.ai/minimap-plugin';
|
|
|
-
|
|
|
-export const Minimap: React.FC = () => {
|
|
|
- const minimapService = useService<MinimapService>(MinimapService);
|
|
|
|
|
|
+export const Minimap = () => {
|
|
|
+ const minimapService = useService(FlowMinimapService);
|
|
|
return (
|
|
|
<div
|
|
|
- className="demo-free-minimap"
|
|
|
- ref={minimapService?.setContainer}
|
|
|
- />
|
|
|
+ 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>
|
|
|
);
|
|
|
};
|
|
|
```
|
|
|
@@ -380,86 +429,125 @@ export const Editor = () => {
|
|
|
|
|
|
```tsx
|
|
|
// src/app.tsx
|
|
|
+import React from 'react';
|
|
|
+import ReactDOM from 'react-dom';
|
|
|
+
|
|
|
import { Editor } from './editor';
|
|
|
|
|
|
-export function App() {
|
|
|
- return (
|
|
|
- <div className="app">
|
|
|
- <h1>自由布局编辑器示例</h1>
|
|
|
- <Editor />
|
|
|
- </div>
|
|
|
- );
|
|
|
-}
|
|
|
+ReactDOM.render(<Editor />, document.getElementById('root'))
|
|
|
```
|
|
|
|
|
|
#### 步骤八:添加样式
|
|
|
|
|
|
```css
|
|
|
/* src/index.css */
|
|
|
-.demo-free-container {
|
|
|
- position: relative;
|
|
|
- width: 100%;
|
|
|
- height: 600px;
|
|
|
- border: 1px solid #eee;
|
|
|
+.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-layout {
|
|
|
- display: flex;
|
|
|
- height: 100%;
|
|
|
+.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-sidebar {
|
|
|
- width: 200px;
|
|
|
- padding: 16px;
|
|
|
- border-right: 1px solid #eee;
|
|
|
- overflow-y: auto;
|
|
|
+.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-card {
|
|
|
- margin-bottom: 8px;
|
|
|
- padding: 8px 12px;
|
|
|
- border: 1px solid #ddd;
|
|
|
- border-radius: 4px;
|
|
|
- cursor: grab;
|
|
|
- background: #fff;
|
|
|
+.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-editor {
|
|
|
- flex: 1;
|
|
|
- height: 100%;
|
|
|
+.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-tools {
|
|
|
- position: absolute;
|
|
|
- top: 16px;
|
|
|
- right: 16px;
|
|
|
- display: flex;
|
|
|
- gap: 8px;
|
|
|
- z-index: 10;
|
|
|
+.demo-free-right-top-panel {
|
|
|
+ position: fixed;
|
|
|
+ right: 10px;
|
|
|
+ top: 70px;
|
|
|
+ width: 300px;
|
|
|
+ z-index: 999;
|
|
|
}
|
|
|
|
|
|
-.demo-free-minimap {
|
|
|
- position: absolute;
|
|
|
- right: 16px;
|
|
|
- bottom: 16px;
|
|
|
- z-index: 10;
|
|
|
+.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-node {
|
|
|
- background: #fff;
|
|
|
- border-radius: 4px;
|
|
|
- box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
|
|
+.demo-free-layout {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: row;
|
|
|
+ flex-grow: 1;
|
|
|
}
|
|
|
|
|
|
-.demo-free-node-title {
|
|
|
- padding: 8px 12px;
|
|
|
- font-weight: bold;
|
|
|
- border-bottom: 1px solid #eee;
|
|
|
+.demo-free-editor {
|
|
|
+ flex-grow: 1;
|
|
|
+ position: relative;
|
|
|
+ height: 100%;
|
|
|
}
|
|
|
|
|
|
-.demo-free-node-content {
|
|
|
- padding: 8px 12px;
|
|
|
+.demo-free-container {
|
|
|
+ position: absolute;
|
|
|
+ left: 0;
|
|
|
+ top: 0;
|
|
|
+ display: flex;
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ flex-direction: column;
|
|
|
}
|
|
|
+
|
|
|
```
|
|
|
|
|
|
### 4. 运行项目
|
|
|
@@ -515,24 +603,19 @@ const initialData: WorkflowJSON = {
|
|
|
// 节点注册
|
|
|
import { WorkflowNodeRegistry } from '@flowgram.ai/free-layout-editor';
|
|
|
|
|
|
-export const nodeRegistries: Record<string, WorkflowNodeRegistry> = {
|
|
|
+export const nodeRegistries: WorkflowNodeRegistry[] = [
|
|
|
// 开始节点定义
|
|
|
- start: {
|
|
|
+ {
|
|
|
type: 'start',
|
|
|
meta: {
|
|
|
- defaultWidth: 200,
|
|
|
- defaultHeight: 100,
|
|
|
- canDelete: false, // 禁止删除
|
|
|
- backgroundColor: '#fff',
|
|
|
- defaultExpanded: true, // 默认展开
|
|
|
+ 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
|
|
|
},
|
|
|
- formMeta: {
|
|
|
- // 节点表单定义
|
|
|
- render: () => <>表单内容</>
|
|
|
- }
|
|
|
},
|
|
|
// 更多节点类型...
|
|
|
-};
|
|
|
+];
|
|
|
```
|
|
|
|
|
|
### 3. 编辑器组件
|
|
|
@@ -549,7 +632,7 @@ const editorProps = {
|
|
|
background: true, // 启用背景网格
|
|
|
readonly: false, // 非只读模式,允许编辑
|
|
|
initialData: {...}, // 初始化数据:节点和边的定义
|
|
|
- nodeRegistries: {...}, // 节点类型注册
|
|
|
+ nodeRegistries: [...], // 节点类型注册
|
|
|
nodeEngine: {
|
|
|
enable: true, // 启用节点表单引擎
|
|
|
},
|
|
|
@@ -581,10 +664,10 @@ const dragService = useService<WorkflowDragService>(WorkflowDragService);
|
|
|
dragService.startDragCard('nodeType', event, { data: {...} });
|
|
|
|
|
|
// 获取编辑器上下文
|
|
|
-const { document, services } = useClientContext();
|
|
|
+const { document, playground } = useClientContext();
|
|
|
// 操作画布
|
|
|
document.fitView(); // 适应视图
|
|
|
-document.zoomTo(1.5); // 缩放画布
|
|
|
+playground.config.zoomin(); // 缩放画布
|
|
|
document.fromJSON(newData); // 更新数据
|
|
|
```
|
|
|
|