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:watch": "exit 0",
|
||||
"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:fix": "eslint ./src --fix",
|
||||
"start": "NODE_ENV=development rsbuild dev --open",
|
||||
"start": "cross-env NODE_ENV=development rsbuild dev --open",
|
||||
"test": "exit",
|
||||
"test:cov": "exit",
|
||||
"watch": "exit 0"
|
||||
|
||||
@ -19,10 +19,10 @@
|
||||
"build:fast": "exit 0",
|
||||
"build:watch": "exit 0",
|
||||
"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:fix": "eslint ./src --fix",
|
||||
"start": "NODE_ENV=development rsbuild dev --open",
|
||||
"start": "cross-env NODE_ENV=development rsbuild dev --open",
|
||||
"test": "exit",
|
||||
"test:cov": "exit",
|
||||
"watch": "exit 0"
|
||||
|
||||
@ -20,13 +20,7 @@ const indexCode = {
|
||||
export const FixedLayoutSimplePreview = () => (
|
||||
<PreviewEditor
|
||||
files={{
|
||||
'App.js': `import React from 'react';
|
||||
import { Editor } from './index.tsx';
|
||||
const App = () => {
|
||||
return <Editor />
|
||||
}
|
||||
export default App;`,
|
||||
'index.tsx': indexCode,
|
||||
'editor.tsx': indexCode,
|
||||
'index.css': indexCssCode,
|
||||
'initial-data.ts': initialDataCode,
|
||||
'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 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 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 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';
|
||||
|
||||
export const FreeLayoutSimplePreview = () => {
|
||||
const files = {
|
||||
'index.tsx': indexCode,
|
||||
'editor.tsx': editorCode,
|
||||
'use-editor-props.tsx': useEditorPropsCode,
|
||||
'initial-data.ts': dataCode,
|
||||
'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
|
||||
import { WorkflowNodeRegistry } from '@flowgram.ai/free-layout-editor';
|
||||
|
||||
export const nodeRegistries: Record<string, WorkflowNodeRegistry> = {
|
||||
// Start node
|
||||
start: {
|
||||
/**
|
||||
* You can customize your own node registry
|
||||
*/
|
||||
export const nodeRegistries: WorkflowNodeRegistry[] = [
|
||||
{
|
||||
type: 'start',
|
||||
meta: {
|
||||
defaultWidth: 200,
|
||||
defaultHeight: 100,
|
||||
canDelete: false, // Prohibit deletion
|
||||
backgroundColor: '#E6F7FF',
|
||||
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
|
||||
},
|
||||
},
|
||||
// Custom node
|
||||
custom: {
|
||||
type: 'custom',
|
||||
meta: {
|
||||
defaultWidth: 200,
|
||||
defaultHeight: 100,
|
||||
backgroundColor: '#FFF7E6',
|
||||
defaultExpanded: true,
|
||||
},
|
||||
},
|
||||
// End node
|
||||
end: {
|
||||
{
|
||||
type: 'end',
|
||||
meta: {
|
||||
defaultWidth: 200,
|
||||
defaultHeight: 100,
|
||||
canDelete: false, // Prohibit deletion
|
||||
backgroundColor: '#FFF1F0',
|
||||
defaultExpanded: true,
|
||||
deleteDisable: true,
|
||||
copyDisable: true,
|
||||
defaultPorts: [{ type: 'input' }],
|
||||
},
|
||||
},
|
||||
};
|
||||
{
|
||||
type: 'custom',
|
||||
meta: {},
|
||||
defaultPorts: [{ type: 'output' }, { type: 'input' }], // A normal node has two ports
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
#### Step 3: Create Editor Configuration
|
||||
@ -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,
|
||||
}),
|
||||
// Auto-alignment plugin
|
||||
createFreeSnapPlugin({
|
||||
edgeColor: '#00B2B2',
|
||||
alignColor: '#00B2B2',
|
||||
edgeLineWidth: 1,
|
||||
alignLineWidth: 1,
|
||||
alignCrossWidth: 8,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
@ -311,37 +319,76 @@ export const NodeAddPanel: React.FC = () => {
|
||||
#### Step 5: Create Toolbar and Minimap
|
||||
|
||||
```tsx
|
||||
// src/components/tools.tsx
|
||||
import React 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}>Zoom In</button>
|
||||
<button onClick={zoomOut}>Zoom Out</button>
|
||||
<button onClick={resetZoom}>Reset Zoom</button>
|
||||
<button onClick={undo} disabled={!history?.canUndo()}>Undo</button>
|
||||
<button onClick={redo} disabled={!history?.canRedo()}>Redo</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,85 +427,123 @@ 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>Free Layout Editor Example</h1>
|
||||
<Editor />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
ReactDOM.render(<Editor />, document.getElementById('root'))
|
||||
```
|
||||
|
||||
#### Step 8: Add Styles
|
||||
|
||||
```css
|
||||
/* src/index.css */
|
||||
.demo-free-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 600px;
|
||||
border: 1px solid #eee;
|
||||
}
|
||||
|
||||
.demo-free-layout {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.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 {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.demo-free-tools {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.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);
|
||||
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 {
|
||||
padding: 8px 12px;
|
||||
font-weight: bold;
|
||||
border-bottom: 1px solid #eee;
|
||||
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-content {
|
||||
padding: 8px 12px;
|
||||
.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;
|
||||
}
|
||||
```
|
||||
|
||||
@ -515,24 +600,19 @@ Use `nodeRegistries` to define the behavior and appearance of different types of
|
||||
// Node registration
|
||||
import { WorkflowNodeRegistry } from '@flowgram.ai/free-layout-editor';
|
||||
|
||||
export const nodeRegistries: Record<string, WorkflowNodeRegistry> = {
|
||||
export const nodeRegistries: WorkflowNodeRegistry[] = [
|
||||
// Start node definition
|
||||
start: {
|
||||
{
|
||||
type: 'start',
|
||||
meta: {
|
||||
defaultWidth: 200,
|
||||
defaultHeight: 100,
|
||||
canDelete: false, // Prohibit deletion
|
||||
backgroundColor: '#fff',
|
||||
defaultExpanded: true, // Default expanded
|
||||
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: {
|
||||
// Node form definition
|
||||
render: () => <>Form content</>
|
||||
}
|
||||
},
|
||||
// More node types...
|
||||
};
|
||||
];
|
||||
```
|
||||
|
||||
### 3. Editor Components
|
||||
@ -549,7 +629,7 @@ const editorProps = {
|
||||
background: true, // Enable background grid
|
||||
readonly: false, // Non-readonly mode, allow editing
|
||||
initialData: {...}, // Initial data: definition of nodes and edges
|
||||
nodeRegistries: {...}, // Node type registration
|
||||
nodeRegistries: [...], // Node type registration
|
||||
nodeEngine: {
|
||||
enable: true, // Enable node form engine
|
||||
},
|
||||
@ -581,10 +661,10 @@ const dragService = useService<WorkflowDragService>(WorkflowDragService);
|
||||
dragService.startDragCard('nodeType', event, { data: {...} });
|
||||
|
||||
// Get editor context
|
||||
const { document, services } = useClientContext();
|
||||
const { document, playground } = useClientContext();
|
||||
// Manipulate canvas
|
||||
document.fitView(); // Fit view
|
||||
document.zoomTo(1.5); // Zoom canvas
|
||||
playground.config.zoomin(); // Zoom canvas
|
||||
document.fromJSON(newData); // Update data
|
||||
```
|
||||
|
||||
|
||||
@ -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-layout {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.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 {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.demo-free-tools {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.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);
|
||||
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 {
|
||||
padding: 8px 12px;
|
||||
font-weight: bold;
|
||||
border-bottom: 1px solid #eee;
|
||||
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-content {
|
||||
padding: 8px 12px;
|
||||
.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. 运行项目
|
||||
@ -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); // 更新数据
|
||||
```
|
||||
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
"build": "tsc -b --force",
|
||||
"dev": "npm run build -- -w",
|
||||
"lint": "eslint ./src --cache",
|
||||
"watch": "exit",
|
||||
"test": "exit",
|
||||
"test:cov": "exit"
|
||||
},
|
||||
|
||||
@ -9,6 +9,7 @@
|
||||
"build": "exit",
|
||||
"test": "exit",
|
||||
"lint": "exit",
|
||||
"watch": "exit",
|
||||
"test:cov": "exit 0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@ -28,7 +28,7 @@
|
||||
"build:watch": "npm run build:fast -- --dts-resolve",
|
||||
"clean": "rimraf dist",
|
||||
"test": "vitest run",
|
||||
"test:cov": "exit 0",
|
||||
"test:cov": "vitest run --coverage",
|
||||
"ts-check": "tsc --noEmit",
|
||||
"watch": "npm run build:fast -- --dts-resolve --watch --ignore-watch dist"
|
||||
},
|
||||
|
||||
@ -33,7 +33,7 @@ export function fromNodeJSON(
|
||||
return;
|
||||
}
|
||||
|
||||
return WorkflowDocumentOptionsDefault.fromNodeJSON?.(node, json, isFirstCreate);
|
||||
return WorkflowDocumentOptionsDefault.fromNodeJSON!(node, json, isFirstCreate);
|
||||
}
|
||||
|
||||
export function toNodeJSON(opts: FreeLayoutProps, node: FlowNodeEntity): FlowNodeJSON {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user