增加右键跟小地图

This commit is contained in:
dengqichen 2024-12-06 14:52:35 +08:00
parent d6f3648229
commit cf72081575
4 changed files with 263 additions and 76 deletions

View File

@ -15,6 +15,7 @@
"@antv/x6-plugin-export": "^2.1.6", "@antv/x6-plugin-export": "^2.1.6",
"@antv/x6-plugin-history": "^2.2.4", "@antv/x6-plugin-history": "^2.2.4",
"@antv/x6-plugin-keyboard": "^2.2.3", "@antv/x6-plugin-keyboard": "^2.2.3",
"@antv/x6-plugin-minimap": "^2.0.7",
"@antv/x6-plugin-selection": "^2.2.2", "@antv/x6-plugin-selection": "^2.2.2",
"@antv/x6-plugin-snapline": "^2.1.7", "@antv/x6-plugin-snapline": "^2.1.7",
"@antv/x6-plugin-transform": "^2.1.8", "@antv/x6-plugin-transform": "^2.1.8",
@ -264,6 +265,15 @@
"@antv/x6": "^2.x" "@antv/x6": "^2.x"
} }
}, },
"node_modules/@antv/x6-plugin-minimap": {
"version": "2.0.7",
"resolved": "https://registry.npmmirror.com/@antv/x6-plugin-minimap/-/x6-plugin-minimap-2.0.7.tgz",
"integrity": "sha512-8zzESCx0jguFPCOKCA0gPFb6JmRgq81CXXtgJYe34XhySdZ6PB23I5Po062lNsEuTjTjnzli4lju8vvU+jzlGw==",
"license": "MIT",
"peerDependencies": {
"@antv/x6": "^2.x"
}
},
"node_modules/@antv/x6-plugin-selection": { "node_modules/@antv/x6-plugin-selection": {
"version": "2.2.2", "version": "2.2.2",
"resolved": "https://registry.npmmirror.com/@antv/x6-plugin-selection/-/x6-plugin-selection-2.2.2.tgz", "resolved": "https://registry.npmmirror.com/@antv/x6-plugin-selection/-/x6-plugin-selection-2.2.2.tgz",

View File

@ -17,6 +17,7 @@
"@antv/x6-plugin-export": "^2.1.6", "@antv/x6-plugin-export": "^2.1.6",
"@antv/x6-plugin-history": "^2.2.4", "@antv/x6-plugin-history": "^2.2.4",
"@antv/x6-plugin-keyboard": "^2.2.3", "@antv/x6-plugin-keyboard": "^2.2.3",
"@antv/x6-plugin-minimap": "^2.0.7",
"@antv/x6-plugin-selection": "^2.2.2", "@antv/x6-plugin-selection": "^2.2.2",
"@antv/x6-plugin-snapline": "^2.1.7", "@antv/x6-plugin-snapline": "^2.1.7",
"@antv/x6-plugin-transform": "^2.1.8", "@antv/x6-plugin-transform": "^2.1.8",

View File

@ -1,5 +1,7 @@
:global { :global {
.workflow-designer { .workflow-designer {
height: 100%;
&-container { &-container {
height: calc(100vh - 170px); height: calc(100vh - 170px);
background: #fff; background: #fff;
@ -8,21 +10,43 @@
&-sider { &-sider {
background: #fff; background: #fff;
border-right: 1px solid #e8e8e8; border-right: 1px solid #f0f0f0;
} }
&-content { &-content {
position: relative; position: relative;
background: #fafafa; height: 100%;
padding: 16px; padding: 16px;
background: #fafafa;
} }
&-graph { &-graph {
width: 100%;
height: 100%; height: 100%;
background: #fff; background: #fff;
border: 1px solid #e8e8e8; border: 1px solid #f0f0f0;
border-radius: 2px; border-radius: 2px;
} }
&-minimap {
position: absolute;
right: 24px;
bottom: 24px;
width: 200px;
height: 150px;
background-color: #fff;
border: 1px solid #f0f0f0;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
} }
}
.ant-card-body {
height: calc(100% - 57px);
padding: 0;
}
.ant-layout {
height: 100%;
background: #fff;
}
}

View File

@ -1,6 +1,6 @@
import React, {useEffect, useRef, useState} from 'react'; import React, {useEffect, useRef, useState} from 'react';
import {useNavigate, useParams} from 'react-router-dom'; import {useNavigate, useParams} from 'react-router-dom';
import {Button, Card, Layout, message, Space, Spin, Drawer, Form} from 'antd'; import {Button, Card, Layout, message, Space, Spin, Drawer, Form, Dropdown} from 'antd';
import {ArrowLeftOutlined, SaveOutlined} from '@ant-design/icons'; import {ArrowLeftOutlined, SaveOutlined} from '@ant-design/icons';
import {getDefinition, updateDefinition} from '../../service'; import {getDefinition, updateDefinition} from '../../service';
import {WorkflowDefinition, WorkflowStatus} from '../../../Workflow/types'; import {WorkflowDefinition, WorkflowStatus} from '../../../Workflow/types';
@ -12,11 +12,14 @@ import {Clipboard} from '@antv/x6-plugin-clipboard';
import {Transform} from '@antv/x6-plugin-transform'; import {Transform} from '@antv/x6-plugin-transform';
import {Keyboard} from '@antv/x6-plugin-keyboard'; import {Keyboard} from '@antv/x6-plugin-keyboard';
import {Snapline} from '@antv/x6-plugin-snapline'; import {Snapline} from '@antv/x6-plugin-snapline';
import {MiniMap} from '@antv/x6-plugin-minimap';
import {Menu} from '@antv/x6-plugin-menu';
import './index.module.less'; import './index.module.less';
import NodePanel from './components/NodePanel'; import NodePanel from './components/NodePanel';
import NodeConfig from './components/NodeConfig'; import NodeConfig from './components/NodeConfig';
import Toolbar from './components/Toolbar'; import Toolbar from './components/Toolbar';
import {NodeType, getNodeTypes} from './service'; import {NodeType, getNodeTypes} from './service';
import {DeleteOutlined, CopyOutlined, SettingOutlined, ClearOutlined, FullscreenOutlined} from '@ant-design/icons';
const {Sider, Content} = Layout; const {Sider, Content} = Layout;
@ -52,6 +55,107 @@ const FlowDesigner: React.FC = () => {
const [nodeTypes, setNodeTypes] = useState<NodeType[]>([]); const [nodeTypes, setNodeTypes] = useState<NodeType[]>([]);
const [form] = Form.useForm(); const [form] = Form.useForm();
// 右键菜单状态
const [contextMenu, setContextMenu] = useState<{
x: number;
y: number;
visible: boolean;
type: 'node' | 'edge' | 'canvas';
cell?: Cell;
}>({
x: 0,
y: 0,
visible: false,
type: 'canvas',
});
// 右键菜单项
const menuItems = {
node: [
{
key: 'delete',
label: '删除节点',
icon: <DeleteOutlined />,
onClick: () => {
if (contextMenu.cell) {
contextMenu.cell.remove();
}
setContextMenu(prev => ({ ...prev, visible: false }));
},
},
{
key: 'copy',
label: '复制节点',
icon: <CopyOutlined />,
onClick: () => {
if (contextMenu.cell && contextMenu.cell.isNode()) {
const pos = contextMenu.cell.position();
const newCell = contextMenu.cell.clone();
newCell.position(pos.x + 20, pos.y + 20);
graph?.addCell(newCell);
}
setContextMenu(prev => ({ ...prev, visible: false }));
},
},
{
key: 'config',
label: '配置节点',
icon: <SettingOutlined />,
onClick: () => {
if (contextMenu.cell && contextMenu.cell.isNode()) {
setCurrentNode(contextMenu.cell);
const data = contextMenu.cell.getData() as NodeData;
const nodeType = nodeTypes.find(type => type.code === data.type);
if (nodeType) {
setCurrentNodeType(nodeType);
const formValues = {
name: data.name || nodeType.name,
description: data.description,
...data.config
};
form.setFieldsValue(formValues);
}
setConfigVisible(true);
}
setContextMenu(prev => ({ ...prev, visible: false }));
},
},
],
edge: [
{
key: 'delete',
label: '删除连线',
icon: <DeleteOutlined />,
onClick: () => {
if (contextMenu.cell) {
contextMenu.cell.remove();
}
setContextMenu(prev => ({ ...prev, visible: false }));
},
},
],
canvas: [
{
key: 'clear',
label: '清空画布',
icon: <ClearOutlined />,
onClick: () => {
graph?.clearCells();
setContextMenu(prev => ({ ...prev, visible: false }));
},
},
{
key: 'fit',
label: '适应画布',
icon: <FullscreenOutlined />,
onClick: () => {
graph?.zoomToFit({ padding: 20 });
setContextMenu(prev => ({ ...prev, visible: false }));
},
},
],
};
// 获取所有节点类型 // 获取所有节点类型
const fetchNodeTypes = async () => { const fetchNodeTypes = async () => {
try { try {
@ -80,50 +184,46 @@ const FlowDesigner: React.FC = () => {
width: 800, width: 800,
height: 600, height: 600,
grid: { grid: {
size: 10,
visible: true, visible: true,
type: 'dot', type: 'mesh',
size: 10,
args: { args: {
color: '#ccc', color: '#e5e5e5', // 网格线颜色
thickness: 1, thickness: 1, // 网格线宽度
}, },
}, },
mousewheel: {
enabled: true,
modifiers: ['ctrl', 'meta'],
minScale: 0.5,
maxScale: 2,
},
connecting: { connecting: {
router: 'manhattan', snap: true, // 连线时自动吸附
allowBlank: false, // 禁止连接到空白位置
allowLoop: false, // 禁止自环
allowNode: false, // 禁止直接连接到节点(必须连接到连接桩)
allowEdge: false, // 禁止边连接到边
connector: { connector: {
name: 'rounded', name: 'rounded',
args: { args: {
radius: 8, radius: 8,
}, },
}, },
anchor: 'center', router: {
connectionPoint: 'anchor', name: 'manhattan', // 使用曼哈顿路由
allowBlank: false, args: {
snap: { padding: 1,
radius: 20, },
}, },
createEdge() { validateConnection({sourceCell, targetCell, sourceMagnet, targetMagnet}) {
return this.createEdge({ if (sourceCell === targetCell) {
attrs: { return false; // 禁止自环
line: { }
stroke: '#5F95FF', if (!sourceMagnet || !targetMagnet) {
strokeWidth: 1, return false; // 必须使用连接桩
targetMarker: { }
name: 'classic', return true;
size: 8,
},
},
},
router: {
name: 'manhattan',
},
connector: {
name: 'rounded',
args: {
radius: 8,
},
},
});
}, },
}, },
highlighting: { highlighting: {
@ -137,22 +237,16 @@ const FlowDesigner: React.FC = () => {
}, },
}, },
}, },
}, magnetAdsorbed: {
mousewheel: { name: 'stroke',
enabled: true, args: {
modifiers: ['ctrl', 'meta'], padding: 4,
minScale: 0.5, attrs: {
maxScale: 2, strokeWidth: 4,
}, stroke: '#1890ff',
interacting: { },
nodeMovable: true, },
edgeMovable: true, },
edgeLabelMovable: true,
arrowheadMovable: true,
vertexMovable: true,
vertexAddable: true,
vertexDeletable: true,
magnetConnectable: true,
}, },
keyboard: { keyboard: {
enabled: true, enabled: true,
@ -170,7 +264,7 @@ const FlowDesigner: React.FC = () => {
restrict: true, restrict: true,
}, },
background: { background: {
color: '#F8F9FA', color: '#ffffff', // 画布背景色
}, },
}); });
@ -238,12 +332,58 @@ const FlowDesigner: React.FC = () => {
}) })
); );
// 启用基础功能 // 启用小地图
graph.enableSelection(); graph.use(
graph.enableRubberband(); new MiniMap({
graph.enableKeyboard(); container: document.getElementById('workflow-minimap'),
graph.enableClipboard(); width: 200,
graph.enableHistory(); height: 150,
padding: 10,
scalable: false,
minScale: 0.5,
maxScale: 2,
graphOptions: {
async: true,
// 简化节点渲染
grid: false,
background: {
color: '#f5f5f5',
},
},
})
);
// 绑定右键菜单事件
graph.on('cell:contextmenu', ({ cell, e }) => {
e.preventDefault();
setContextMenu({
x: e.clientX,
y: e.clientY,
visible: true,
type: cell.isNode() ? 'node' : 'edge',
cell,
});
});
graph.on('blank:contextmenu', ({ e }) => {
e.preventDefault();
setContextMenu({
x: e.clientX,
y: e.clientY,
visible: true,
type: 'canvas',
});
});
// 点击画布时隐藏右键菜单
graph.on('blank:click', () => {
setContextMenu(prev => ({ ...prev, visible: false }));
});
// 点击节点时隐藏右键菜单
graph.on('cell:click', () => {
setContextMenu(prev => ({ ...prev, visible: false }));
});
graphRef.current = graph; graphRef.current = graph;
setGraph(graph); setGraph(graph);
@ -583,31 +723,43 @@ const FlowDesigner: React.FC = () => {
return ( return (
<Card <Card
title="流程设计器" title={
extra={
<Space> <Space>
<Button <Button icon={<ArrowLeftOutlined/>} onClick={() => navigate(-1)}></Button>
icon={<SaveOutlined/>} <Button type="primary" icon={<SaveOutlined/>} onClick={handleSave}></Button>
type="primary"
onClick={handleSave}
disabled={detail?.status !== WorkflowStatus.DRAFT}
>
</Button>
<Button icon={<ArrowLeftOutlined/>} onClick={handleBack}>
</Button>
</Space> </Space>
} }
className="workflow-designer"
> >
<Layout className="workflow-designer-container"> <Layout>
<Sider width={280} className="workflow-designer-sider"> <Sider width={250} className="workflow-designer-sider">
<NodePanel onNodeDragStart={handleNodeDragStart}/> <NodePanel onNodeDragStart={handleNodeDragStart}/>
</Sider> </Sider>
<Layout> <Layout>
<Toolbar graph={graph}/> <Toolbar graph={graph}/>
<Content className="workflow-designer-content"> <Content className="workflow-designer-content">
<div ref={containerRef} className="workflow-designer-graph"/> <div ref={containerRef} className="workflow-designer-graph"/>
<div id="workflow-minimap" className="workflow-designer-minimap"/>
<Dropdown
menu={{
items: menuItems[contextMenu.type],
onClick: ({ key, domEvent }) => {
domEvent.stopPropagation();
},
}}
open={contextMenu.visible}
trigger={['contextMenu']}
>
<div
style={{
position: 'fixed',
left: contextMenu.x,
top: contextMenu.y,
width: 1,
height: 1,
}}
/>
</Dropdown>
</Content> </Content>
</Layout> </Layout>
</Layout> </Layout>