mirror of
https://gitee.com/ByteDance/flowgram.ai.git
synced 2025-07-07 17:43:29 +08:00
docs: free-layout-simple docs error (#245)
* chore: simple-layout-demo add cross-env * docs: free-layout-simple docs error
This commit is contained in:
parent
766fdc1597
commit
ec6e5abe23
@ -19,10 +19,10 @@
|
|||||||
"build:fast": "exit 0",
|
"build:fast": "exit 0",
|
||||||
"build:watch": "exit 0",
|
"build:watch": "exit 0",
|
||||||
"clean": "rimraf dist",
|
"clean": "rimraf dist",
|
||||||
"dev": "MODE=app NODE_ENV=development rsbuild dev --open",
|
"dev": "cross-env MODE=app NODE_ENV=development rsbuild dev --open",
|
||||||
"lint": "eslint ./src --cache",
|
"lint": "eslint ./src --cache",
|
||||||
"lint:fix": "eslint ./src --fix",
|
"lint:fix": "eslint ./src --fix",
|
||||||
"start": "NODE_ENV=development rsbuild dev --open",
|
"start": "cross-env NODE_ENV=development rsbuild dev --open",
|
||||||
"test": "exit",
|
"test": "exit",
|
||||||
"test:cov": "exit",
|
"test:cov": "exit",
|
||||||
"watch": "exit 0"
|
"watch": "exit 0"
|
||||||
|
|||||||
@ -19,10 +19,10 @@
|
|||||||
"build:fast": "exit 0",
|
"build:fast": "exit 0",
|
||||||
"build:watch": "exit 0",
|
"build:watch": "exit 0",
|
||||||
"clean": "rimraf dist",
|
"clean": "rimraf dist",
|
||||||
"dev": "MODE=app NODE_ENV=development rsbuild dev --open",
|
"dev": "cross-env MODE=app NODE_ENV=development rsbuild dev --open",
|
||||||
"lint": "eslint ./src --cache",
|
"lint": "eslint ./src --cache",
|
||||||
"lint:fix": "eslint ./src --fix",
|
"lint:fix": "eslint ./src --fix",
|
||||||
"start": "NODE_ENV=development rsbuild dev --open",
|
"start": "cross-env NODE_ENV=development rsbuild dev --open",
|
||||||
"test": "exit",
|
"test": "exit",
|
||||||
"test:cov": "exit",
|
"test:cov": "exit",
|
||||||
"watch": "exit 0"
|
"watch": "exit 0"
|
||||||
|
|||||||
@ -20,13 +20,7 @@ const indexCode = {
|
|||||||
export const FixedLayoutSimplePreview = () => (
|
export const FixedLayoutSimplePreview = () => (
|
||||||
<PreviewEditor
|
<PreviewEditor
|
||||||
files={{
|
files={{
|
||||||
'App.js': `import React from 'react';
|
'editor.tsx': indexCode,
|
||||||
import { Editor } from './index.tsx';
|
|
||||||
const App = () => {
|
|
||||||
return <Editor />
|
|
||||||
}
|
|
||||||
export default App;`,
|
|
||||||
'index.tsx': indexCode,
|
|
||||||
'index.css': indexCssCode,
|
'index.css': indexCssCode,
|
||||||
'initial-data.ts': initialDataCode,
|
'initial-data.ts': initialDataCode,
|
||||||
'node-registries.ts': nodeRegistriesCode,
|
'node-registries.ts': nodeRegistriesCode,
|
||||||
|
|||||||
@ -4,14 +4,14 @@ import { FreeLayoutSimple } from '.';
|
|||||||
import nodeRegistriesCode from '!!raw-loader!@flowgram.ai/demo-free-layout-simple/src/node-registries.ts';
|
import nodeRegistriesCode from '!!raw-loader!@flowgram.ai/demo-free-layout-simple/src/node-registries.ts';
|
||||||
import dataCode from '!!raw-loader!@flowgram.ai/demo-free-layout-simple/src/initial-data.ts';
|
import dataCode from '!!raw-loader!@flowgram.ai/demo-free-layout-simple/src/initial-data.ts';
|
||||||
import useEditorPropsCode from '!!raw-loader!@flowgram.ai/demo-free-layout-simple/src/hooks/use-editor-props.tsx';
|
import useEditorPropsCode from '!!raw-loader!@flowgram.ai/demo-free-layout-simple/src/hooks/use-editor-props.tsx';
|
||||||
import indexCode from '!!raw-loader!@flowgram.ai/demo-free-layout-simple/src/editor.tsx';
|
import editorCode from '!!raw-loader!@flowgram.ai/demo-free-layout-simple/src/editor.tsx';
|
||||||
import toolsCode from '!!raw-loader!@flowgram.ai/demo-free-layout-simple/src/components/tools.tsx';
|
import toolsCode from '!!raw-loader!@flowgram.ai/demo-free-layout-simple/src/components/tools.tsx';
|
||||||
import nodeAddPanelCode from '!!raw-loader!@flowgram.ai/demo-free-layout-simple/src/components/node-add-panel.tsx';
|
import nodeAddPanelCode from '!!raw-loader!@flowgram.ai/demo-free-layout-simple/src/components/node-add-panel.tsx';
|
||||||
import minimapCode from '!!raw-loader!@flowgram.ai/demo-free-layout-simple/src/components/minimap.tsx';
|
import minimapCode from '!!raw-loader!@flowgram.ai/demo-free-layout-simple/src/components/minimap.tsx';
|
||||||
|
|
||||||
export const FreeLayoutSimplePreview = () => {
|
export const FreeLayoutSimplePreview = () => {
|
||||||
const files = {
|
const files = {
|
||||||
'index.tsx': indexCode,
|
'editor.tsx': editorCode,
|
||||||
'use-editor-props.tsx': useEditorPropsCode,
|
'use-editor-props.tsx': useEditorPropsCode,
|
||||||
'initial-data.ts': dataCode,
|
'initial-data.ts': dataCode,
|
||||||
'node-registries.ts': nodeRegistriesCode,
|
'node-registries.ts': nodeRegistriesCode,
|
||||||
|
|||||||
@ -127,40 +127,33 @@ Next, we need to define the behavior and appearance of different types of nodes:
|
|||||||
// src/node-registries.ts
|
// src/node-registries.ts
|
||||||
import { WorkflowNodeRegistry } from '@flowgram.ai/free-layout-editor';
|
import { WorkflowNodeRegistry } from '@flowgram.ai/free-layout-editor';
|
||||||
|
|
||||||
export const nodeRegistries: Record<string, WorkflowNodeRegistry> = {
|
/**
|
||||||
// Start node
|
* You can customize your own node registry
|
||||||
start: {
|
*/
|
||||||
|
export const nodeRegistries: WorkflowNodeRegistry[] = [
|
||||||
|
{
|
||||||
type: 'start',
|
type: 'start',
|
||||||
meta: {
|
meta: {
|
||||||
defaultWidth: 200,
|
isStart: true, // Mark as start
|
||||||
defaultHeight: 100,
|
deleteDisable: true, // The start node cannot be deleted
|
||||||
canDelete: false, // Prohibit deletion
|
copyDisable: true, // The start node cannot be copied
|
||||||
backgroundColor: '#E6F7FF',
|
defaultPorts: [{ type: 'output' }], // Used to define the input and output ports, the start node only has the output port
|
||||||
defaultExpanded: true,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// Custom node
|
{
|
||||||
custom: {
|
|
||||||
type: 'custom',
|
|
||||||
meta: {
|
|
||||||
defaultWidth: 200,
|
|
||||||
defaultHeight: 100,
|
|
||||||
backgroundColor: '#FFF7E6',
|
|
||||||
defaultExpanded: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// End node
|
|
||||||
end: {
|
|
||||||
type: 'end',
|
type: 'end',
|
||||||
meta: {
|
meta: {
|
||||||
defaultWidth: 200,
|
deleteDisable: true,
|
||||||
defaultHeight: 100,
|
copyDisable: true,
|
||||||
canDelete: false, // Prohibit deletion
|
defaultPorts: [{ type: 'input' }],
|
||||||
backgroundColor: '#FFF1F0',
|
|
||||||
defaultExpanded: true,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
{
|
||||||
|
type: 'custom',
|
||||||
|
meta: {},
|
||||||
|
defaultPorts: [{ type: 'output' }, { type: 'input' }], // A normal node has two ports
|
||||||
|
},
|
||||||
|
];
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Step 3: Create Editor Configuration
|
#### Step 3: Create Editor Configuration
|
||||||
@ -260,14 +253,29 @@ export const useEditorProps = () =>
|
|||||||
canvasStyle: {
|
canvasStyle: {
|
||||||
canvasWidth: 182,
|
canvasWidth: 182,
|
||||||
canvasHeight: 102,
|
canvasHeight: 102,
|
||||||
|
canvasPadding: 50,
|
||||||
canvasBackground: 'rgba(245, 245, 245, 1)',
|
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,
|
||||||
}),
|
}),
|
||||||
// Auto-alignment plugin
|
// Auto-alignment plugin
|
||||||
createFreeSnapPlugin({
|
createFreeSnapPlugin({
|
||||||
edgeColor: '#00B2B2',
|
edgeColor: '#00B2B2',
|
||||||
alignColor: '#00B2B2',
|
alignColor: '#00B2B2',
|
||||||
edgeLineWidth: 1,
|
edgeLineWidth: 1,
|
||||||
|
alignLineWidth: 1,
|
||||||
|
alignCrossWidth: 8,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
@ -311,37 +319,76 @@ export const NodeAddPanel: React.FC = () => {
|
|||||||
#### Step 5: Create Toolbar and Minimap
|
#### Step 5: Create Toolbar and Minimap
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
// src/components/tools.tsx
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { usePlaygroundTools, useClientContext } from '@flowgram.ai/free-layout-editor';
|
import { usePlaygroundTools, useClientContext } from '@flowgram.ai/free-layout-editor';
|
||||||
|
|
||||||
export const Tools: React.FC = () => {
|
export const Tools: React.FC = () => {
|
||||||
const { zoomIn, zoomOut, resetZoom, undo, redo } = usePlaygroundTools();
|
|
||||||
const { history } = useClientContext();
|
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 (
|
return (
|
||||||
<div className="demo-free-tools">
|
<div
|
||||||
<button onClick={zoomIn}>Zoom In</button>
|
style={{ position: 'absolute', zIndex: 10, bottom: 16, left: 226, display: 'flex', gap: 8 }}
|
||||||
<button onClick={zoomOut}>Zoom Out</button>
|
>
|
||||||
<button onClick={resetZoom}>Reset Zoom</button>
|
<button onClick={() => tools.zoomin()}>ZoomIn</button>
|
||||||
<button onClick={undo} disabled={!history?.canUndo()}>Undo</button>
|
<button onClick={() => tools.zoomout()}>ZoomOut</button>
|
||||||
<button onClick={redo} disabled={!history?.canRedo()}>Redo</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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// src/components/minimap.tsx
|
// src/components/minimap.tsx
|
||||||
|
import { FlowMinimapService, MinimapRender } from '@flowgram.ai/minimap-plugin';
|
||||||
import { useService } from '@flowgram.ai/free-layout-editor';
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
className="demo-free-minimap"
|
style={{
|
||||||
ref={minimapService?.setContainer}
|
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,85 +427,123 @@ export const Editor = () => {
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
// src/app.tsx
|
// src/app.tsx
|
||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
|
||||||
import { Editor } from './editor';
|
import { Editor } from './editor';
|
||||||
|
|
||||||
export function App() {
|
ReactDOM.render(<Editor />, document.getElementById('root'))
|
||||||
return (
|
|
||||||
<div className="app">
|
|
||||||
<h1>Free Layout Editor Example</h1>
|
|
||||||
<Editor />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Step 8: Add Styles
|
#### Step 8: Add Styles
|
||||||
|
|
||||||
```css
|
```css
|
||||||
/* src/index.css */
|
/* src/index.css */
|
||||||
.demo-free-container {
|
.demo-free-node {
|
||||||
position: relative;
|
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%;
|
width: 100%;
|
||||||
height: 600px;
|
border-radius: 8px 8px 0 0;
|
||||||
border: 1px solid #eee;
|
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 {
|
.demo-free-layout {
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 100%;
|
flex-direction: row;
|
||||||
}
|
flex-grow: 1;
|
||||||
|
|
||||||
.demo-free-sidebar {
|
|
||||||
width: 200px;
|
|
||||||
padding: 16px;
|
|
||||||
border-right: 1px solid #eee;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.demo-free-card {
|
|
||||||
margin-bottom: 8px;
|
|
||||||
padding: 8px 12px;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: grab;
|
|
||||||
background: #fff;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.demo-free-editor {
|
.demo-free-editor {
|
||||||
flex: 1;
|
flex-grow: 1;
|
||||||
|
position: relative;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.demo-free-tools {
|
.demo-free-container {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 16px;
|
left: 0;
|
||||||
right: 16px;
|
top: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
width: 100%;
|
||||||
z-index: 10;
|
height: 100%;
|
||||||
}
|
flex-direction: column;
|
||||||
|
|
||||||
.demo-free-minimap {
|
|
||||||
position: absolute;
|
|
||||||
right: 16px;
|
|
||||||
bottom: 16px;
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
.demo-free-node {
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 4px;
|
|
||||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.demo-free-node-title {
|
|
||||||
padding: 8px 12px;
|
|
||||||
font-weight: bold;
|
|
||||||
border-bottom: 1px solid #eee;
|
|
||||||
}
|
|
||||||
|
|
||||||
.demo-free-node-content {
|
|
||||||
padding: 8px 12px;
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -515,24 +600,19 @@ Use `nodeRegistries` to define the behavior and appearance of different types of
|
|||||||
// Node registration
|
// Node registration
|
||||||
import { WorkflowNodeRegistry } from '@flowgram.ai/free-layout-editor';
|
import { WorkflowNodeRegistry } from '@flowgram.ai/free-layout-editor';
|
||||||
|
|
||||||
export const nodeRegistries: Record<string, WorkflowNodeRegistry> = {
|
export const nodeRegistries: WorkflowNodeRegistry[] = [
|
||||||
// Start node definition
|
// Start node definition
|
||||||
start: {
|
{
|
||||||
type: 'start',
|
type: 'start',
|
||||||
meta: {
|
meta: {
|
||||||
defaultWidth: 200,
|
isStart: true, // Mark as start
|
||||||
defaultHeight: 100,
|
deleteDisable: true, // The start node cannot be deleted
|
||||||
canDelete: false, // Prohibit deletion
|
copyDisable: true, // The start node cannot be copied
|
||||||
backgroundColor: '#fff',
|
defaultPorts: [{ type: 'output' }], // Used to define the input and output ports, the start node only has the output port
|
||||||
defaultExpanded: true, // Default expanded
|
|
||||||
},
|
},
|
||||||
formMeta: {
|
|
||||||
// Node form definition
|
|
||||||
render: () => <>Form content</>
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
// More node types...
|
// More node types...
|
||||||
};
|
];
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Editor Components
|
### 3. Editor Components
|
||||||
@ -549,7 +629,7 @@ const editorProps = {
|
|||||||
background: true, // Enable background grid
|
background: true, // Enable background grid
|
||||||
readonly: false, // Non-readonly mode, allow editing
|
readonly: false, // Non-readonly mode, allow editing
|
||||||
initialData: {...}, // Initial data: definition of nodes and edges
|
initialData: {...}, // Initial data: definition of nodes and edges
|
||||||
nodeRegistries: {...}, // Node type registration
|
nodeRegistries: [...], // Node type registration
|
||||||
nodeEngine: {
|
nodeEngine: {
|
||||||
enable: true, // Enable node form engine
|
enable: true, // Enable node form engine
|
||||||
},
|
},
|
||||||
@ -581,10 +661,10 @@ const dragService = useService<WorkflowDragService>(WorkflowDragService);
|
|||||||
dragService.startDragCard('nodeType', event, { data: {...} });
|
dragService.startDragCard('nodeType', event, { data: {...} });
|
||||||
|
|
||||||
// Get editor context
|
// Get editor context
|
||||||
const { document, services } = useClientContext();
|
const { document, playground } = useClientContext();
|
||||||
// Manipulate canvas
|
// Manipulate canvas
|
||||||
document.fitView(); // Fit view
|
document.fitView(); // Fit view
|
||||||
document.zoomTo(1.5); // Zoom canvas
|
playground.config.zoomin(); // Zoom canvas
|
||||||
document.fromJSON(newData); // Update data
|
document.fromJSON(newData); // Update data
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@ -127,40 +127,33 @@ export const initialData: WorkflowJSON = {
|
|||||||
// src/node-registries.ts
|
// src/node-registries.ts
|
||||||
import { WorkflowNodeRegistry } from '@flowgram.ai/free-layout-editor';
|
import { WorkflowNodeRegistry } from '@flowgram.ai/free-layout-editor';
|
||||||
|
|
||||||
export const nodeRegistries: Record<string, WorkflowNodeRegistry> = {
|
/**
|
||||||
// 开始节点
|
* 你可以自定义节点的注册器
|
||||||
start: {
|
*/
|
||||||
|
export const nodeRegistries: WorkflowNodeRegistry[] = [
|
||||||
|
{
|
||||||
type: 'start',
|
type: 'start',
|
||||||
meta: {
|
meta: {
|
||||||
defaultWidth: 200,
|
isStart: true, // 开始节点标记
|
||||||
defaultHeight: 100,
|
deleteDisable: true, // 开始节点不能被删除
|
||||||
canDelete: false, // 禁止删除
|
copyDisable: true, // 开始节点不能被 copy
|
||||||
backgroundColor: '#E6F7FF',
|
defaultPorts: [{ type: 'output' }], // 定义 input 和 output 端口,开始节点只有 output 端口
|
||||||
defaultExpanded: true,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// 自定义节点
|
{
|
||||||
custom: {
|
|
||||||
type: 'custom',
|
|
||||||
meta: {
|
|
||||||
defaultWidth: 200,
|
|
||||||
defaultHeight: 100,
|
|
||||||
backgroundColor: '#FFF7E6',
|
|
||||||
defaultExpanded: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// 结束节点
|
|
||||||
end: {
|
|
||||||
type: 'end',
|
type: 'end',
|
||||||
meta: {
|
meta: {
|
||||||
defaultWidth: 200,
|
deleteDisable: true,
|
||||||
defaultHeight: 100,
|
copyDisable: true,
|
||||||
canDelete: false, // 禁止删除
|
defaultPorts: [{ type: 'input' }], // 结束节点只有 input 端口
|
||||||
backgroundColor: '#FFF1F0',
|
|
||||||
defaultExpanded: true,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
{
|
||||||
|
type: 'custom',
|
||||||
|
meta: {},
|
||||||
|
defaultPorts: [{ type: 'output' }, { type: 'input' }], // 普通节点有两个端口
|
||||||
|
},
|
||||||
|
];
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 步骤三:创建编辑器配置
|
#### 步骤三:创建编辑器配置
|
||||||
@ -260,14 +253,29 @@ export const useEditorProps = () =>
|
|||||||
canvasStyle: {
|
canvasStyle: {
|
||||||
canvasWidth: 182,
|
canvasWidth: 182,
|
||||||
canvasHeight: 102,
|
canvasHeight: 102,
|
||||||
|
canvasPadding: 50,
|
||||||
canvasBackground: 'rgba(245, 245, 245, 1)',
|
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({
|
createFreeSnapPlugin({
|
||||||
edgeColor: '#00B2B2',
|
edgeColor: '#00B2B2',
|
||||||
alignColor: '#00B2B2',
|
alignColor: '#00B2B2',
|
||||||
edgeLineWidth: 1,
|
edgeLineWidth: 1,
|
||||||
|
alignLineWidth: 1,
|
||||||
|
alignCrossWidth: 8,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
@ -293,7 +301,7 @@ export const NodeAddPanel: React.FC = () => {
|
|||||||
<div
|
<div
|
||||||
key={nodeType}
|
key={nodeType}
|
||||||
className="demo-free-card"
|
className="demo-free-card"
|
||||||
onMouseDown={e => dragService.startDragCard('custom', e, {
|
onMouseDown={e => dragService.startDragCard(nodeType, e, {
|
||||||
data: {
|
data: {
|
||||||
title: nodeType,
|
title: nodeType,
|
||||||
content: '拖拽创建的节点'
|
content: '拖拽创建的节点'
|
||||||
@ -313,35 +321,76 @@ export const NodeAddPanel: React.FC = () => {
|
|||||||
```tsx
|
```tsx
|
||||||
// src/components/tools.tsx
|
// src/components/tools.tsx
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
import { usePlaygroundTools, useClientContext } from '@flowgram.ai/free-layout-editor';
|
import { usePlaygroundTools, useClientContext } from '@flowgram.ai/free-layout-editor';
|
||||||
|
|
||||||
export const Tools: React.FC = () => {
|
export const Tools: React.FC = () => {
|
||||||
const { zoomIn, zoomOut, resetZoom, undo, redo } = usePlaygroundTools();
|
|
||||||
const { history } = useClientContext();
|
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 (
|
return (
|
||||||
<div className="demo-free-tools">
|
<div
|
||||||
<button onClick={zoomIn}>放大</button>
|
style={{ position: 'absolute', zIndex: 10, bottom: 16, left: 226, display: 'flex', gap: 8 }}
|
||||||
<button onClick={zoomOut}>缩小</button>
|
>
|
||||||
<button onClick={resetZoom}>重置缩放</button>
|
<button onClick={() => tools.zoomin()}>ZoomIn</button>
|
||||||
<button onClick={undo} disabled={!history?.canUndo()}>撤销</button>
|
<button onClick={() => tools.zoomout()}>ZoomOut</button>
|
||||||
<button onClick={redo} disabled={!history?.canRedo()}>重做</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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// src/components/minimap.tsx
|
// src/components/minimap.tsx
|
||||||
|
import { FlowMinimapService, MinimapRender } from '@flowgram.ai/minimap-plugin';
|
||||||
import { useService } from '@flowgram.ai/free-layout-editor';
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
className="demo-free-minimap"
|
style={{
|
||||||
ref={minimapService?.setContainer}
|
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
|
```tsx
|
||||||
// src/app.tsx
|
// src/app.tsx
|
||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
|
||||||
import { Editor } from './editor';
|
import { Editor } from './editor';
|
||||||
|
|
||||||
export function App() {
|
ReactDOM.render(<Editor />, document.getElementById('root'))
|
||||||
return (
|
|
||||||
<div className="app">
|
|
||||||
<h1>自由布局编辑器示例</h1>
|
|
||||||
<Editor />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 步骤八:添加样式
|
#### 步骤八:添加样式
|
||||||
|
|
||||||
```css
|
```css
|
||||||
/* src/index.css */
|
/* src/index.css */
|
||||||
.demo-free-container {
|
.demo-free-node {
|
||||||
position: relative;
|
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%;
|
width: 100%;
|
||||||
height: 600px;
|
border-radius: 8px 8px 0 0;
|
||||||
border: 1px solid #eee;
|
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 {
|
.demo-free-layout {
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 100%;
|
flex-direction: row;
|
||||||
}
|
flex-grow: 1;
|
||||||
|
|
||||||
.demo-free-sidebar {
|
|
||||||
width: 200px;
|
|
||||||
padding: 16px;
|
|
||||||
border-right: 1px solid #eee;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.demo-free-card {
|
|
||||||
margin-bottom: 8px;
|
|
||||||
padding: 8px 12px;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: grab;
|
|
||||||
background: #fff;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.demo-free-editor {
|
.demo-free-editor {
|
||||||
flex: 1;
|
flex-grow: 1;
|
||||||
|
position: relative;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.demo-free-tools {
|
.demo-free-container {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 16px;
|
left: 0;
|
||||||
right: 16px;
|
top: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
width: 100%;
|
||||||
z-index: 10;
|
height: 100%;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.demo-free-minimap {
|
|
||||||
position: absolute;
|
|
||||||
right: 16px;
|
|
||||||
bottom: 16px;
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
.demo-free-node {
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 4px;
|
|
||||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.demo-free-node-title {
|
|
||||||
padding: 8px 12px;
|
|
||||||
font-weight: bold;
|
|
||||||
border-bottom: 1px solid #eee;
|
|
||||||
}
|
|
||||||
|
|
||||||
.demo-free-node-content {
|
|
||||||
padding: 8px 12px;
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. 运行项目
|
### 4. 运行项目
|
||||||
@ -515,24 +603,19 @@ const initialData: WorkflowJSON = {
|
|||||||
// 节点注册
|
// 节点注册
|
||||||
import { WorkflowNodeRegistry } from '@flowgram.ai/free-layout-editor';
|
import { WorkflowNodeRegistry } from '@flowgram.ai/free-layout-editor';
|
||||||
|
|
||||||
export const nodeRegistries: Record<string, WorkflowNodeRegistry> = {
|
export const nodeRegistries: WorkflowNodeRegistry[] = [
|
||||||
// 开始节点定义
|
// 开始节点定义
|
||||||
start: {
|
{
|
||||||
type: 'start',
|
type: 'start',
|
||||||
meta: {
|
meta: {
|
||||||
defaultWidth: 200,
|
isStart: true, // Mark as start
|
||||||
defaultHeight: 100,
|
deleteDisable: true, // The start node cannot be deleted
|
||||||
canDelete: false, // 禁止删除
|
copyDisable: true, // The start node cannot be copied
|
||||||
backgroundColor: '#fff',
|
defaultPorts: [{ type: 'output' }], // Used to define the input and output ports, the start node only has the output port
|
||||||
defaultExpanded: true, // 默认展开
|
|
||||||
},
|
},
|
||||||
formMeta: {
|
|
||||||
// 节点表单定义
|
|
||||||
render: () => <>表单内容</>
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
// 更多节点类型...
|
// 更多节点类型...
|
||||||
};
|
];
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. 编辑器组件
|
### 3. 编辑器组件
|
||||||
@ -549,7 +632,7 @@ const editorProps = {
|
|||||||
background: true, // 启用背景网格
|
background: true, // 启用背景网格
|
||||||
readonly: false, // 非只读模式,允许编辑
|
readonly: false, // 非只读模式,允许编辑
|
||||||
initialData: {...}, // 初始化数据:节点和边的定义
|
initialData: {...}, // 初始化数据:节点和边的定义
|
||||||
nodeRegistries: {...}, // 节点类型注册
|
nodeRegistries: [...], // 节点类型注册
|
||||||
nodeEngine: {
|
nodeEngine: {
|
||||||
enable: true, // 启用节点表单引擎
|
enable: true, // 启用节点表单引擎
|
||||||
},
|
},
|
||||||
@ -581,10 +664,10 @@ const dragService = useService<WorkflowDragService>(WorkflowDragService);
|
|||||||
dragService.startDragCard('nodeType', event, { data: {...} });
|
dragService.startDragCard('nodeType', event, { data: {...} });
|
||||||
|
|
||||||
// 获取编辑器上下文
|
// 获取编辑器上下文
|
||||||
const { document, services } = useClientContext();
|
const { document, playground } = useClientContext();
|
||||||
// 操作画布
|
// 操作画布
|
||||||
document.fitView(); // 适应视图
|
document.fitView(); // 适应视图
|
||||||
document.zoomTo(1.5); // 缩放画布
|
playground.config.zoomin(); // 缩放画布
|
||||||
document.fromJSON(newData); // 更新数据
|
document.fromJSON(newData); // 更新数据
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@ -8,6 +8,7 @@
|
|||||||
"build": "tsc -b --force",
|
"build": "tsc -b --force",
|
||||||
"dev": "npm run build -- -w",
|
"dev": "npm run build -- -w",
|
||||||
"lint": "eslint ./src --cache",
|
"lint": "eslint ./src --cache",
|
||||||
|
"watch": "exit",
|
||||||
"test": "exit",
|
"test": "exit",
|
||||||
"test:cov": "exit"
|
"test:cov": "exit"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -9,6 +9,7 @@
|
|||||||
"build": "exit",
|
"build": "exit",
|
||||||
"test": "exit",
|
"test": "exit",
|
||||||
"lint": "exit",
|
"lint": "exit",
|
||||||
|
"watch": "exit",
|
||||||
"test:cov": "exit 0"
|
"test:cov": "exit 0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@ -28,7 +28,7 @@
|
|||||||
"build:watch": "npm run build:fast -- --dts-resolve",
|
"build:watch": "npm run build:fast -- --dts-resolve",
|
||||||
"clean": "rimraf dist",
|
"clean": "rimraf dist",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"test:cov": "exit 0",
|
"test:cov": "vitest run --coverage",
|
||||||
"ts-check": "tsc --noEmit",
|
"ts-check": "tsc --noEmit",
|
||||||
"watch": "npm run build:fast -- --dts-resolve --watch --ignore-watch dist"
|
"watch": "npm run build:fast -- --dts-resolve --watch --ignore-watch dist"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -33,7 +33,7 @@ export function fromNodeJSON(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return WorkflowDocumentOptionsDefault.fromNodeJSON?.(node, json, isFirstCreate);
|
return WorkflowDocumentOptionsDefault.fromNodeJSON!(node, json, isFirstCreate);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toNodeJSON(opts: FreeLayoutProps, node: FlowNodeEntity): FlowNodeJSON {
|
export function toNodeJSON(opts: FreeLayoutProps, node: FlowNodeEntity): FlowNodeJSON {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user