增加右键跟小地图

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-history": "^2.2.4",
"@antv/x6-plugin-keyboard": "^2.2.3",
"@antv/x6-plugin-minimap": "^2.0.7",
"@antv/x6-plugin-selection": "^2.2.2",
"@antv/x6-plugin-snapline": "^2.1.7",
"@antv/x6-plugin-transform": "^2.1.8",
@ -264,6 +265,15 @@
"@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": {
"version": "2.2.2",
"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-history": "^2.2.4",
"@antv/x6-plugin-keyboard": "^2.2.3",
"@antv/x6-plugin-minimap": "^2.0.7",
"@antv/x6-plugin-selection": "^2.2.2",
"@antv/x6-plugin-snapline": "^2.1.7",
"@antv/x6-plugin-transform": "^2.1.8",

View File

@ -1,5 +1,7 @@
:global {
.workflow-designer {
height: 100%;
&-container {
height: calc(100vh - 170px);
background: #fff;
@ -8,21 +10,43 @@
&-sider {
background: #fff;
border-right: 1px solid #e8e8e8;
border-right: 1px solid #f0f0f0;
}
&-content {
position: relative;
background: #fafafa;
height: 100%;
padding: 16px;
background: #fafafa;
}
&-graph {
width: 100%;
height: 100%;
background: #fff;
border: 1px solid #e8e8e8;
border: 1px solid #f0f0f0;
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 {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 {getDefinition, updateDefinition} from '../../service';
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 {Keyboard} from '@antv/x6-plugin-keyboard';
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 NodePanel from './components/NodePanel';
import NodeConfig from './components/NodeConfig';
import Toolbar from './components/Toolbar';
import {NodeType, getNodeTypes} from './service';
import {DeleteOutlined, CopyOutlined, SettingOutlined, ClearOutlined, FullscreenOutlined} from '@ant-design/icons';
const {Sider, Content} = Layout;
@ -52,6 +55,107 @@ const FlowDesigner: React.FC = () => {
const [nodeTypes, setNodeTypes] = useState<NodeType[]>([]);
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 () => {
try {
@ -80,50 +184,46 @@ const FlowDesigner: React.FC = () => {
width: 800,
height: 600,
grid: {
size: 10,
visible: true,
type: 'dot',
type: 'mesh',
size: 10,
args: {
color: '#ccc',
thickness: 1,
color: '#e5e5e5', // 网格线颜色
thickness: 1, // 网格线宽度
},
},
mousewheel: {
enabled: true,
modifiers: ['ctrl', 'meta'],
minScale: 0.5,
maxScale: 2,
},
connecting: {
router: 'manhattan',
snap: true, // 连线时自动吸附
allowBlank: false, // 禁止连接到空白位置
allowLoop: false, // 禁止自环
allowNode: false, // 禁止直接连接到节点(必须连接到连接桩)
allowEdge: false, // 禁止边连接到边
connector: {
name: 'rounded',
args: {
radius: 8,
},
},
anchor: 'center',
connectionPoint: 'anchor',
allowBlank: false,
snap: {
radius: 20,
},
createEdge() {
return this.createEdge({
attrs: {
line: {
stroke: '#5F95FF',
strokeWidth: 1,
targetMarker: {
name: 'classic',
size: 8,
},
},
},
router: {
name: 'manhattan',
},
connector: {
name: 'rounded',
name: 'manhattan', // 使用曼哈顿路由
args: {
radius: 8,
padding: 1,
},
},
});
validateConnection({sourceCell, targetCell, sourceMagnet, targetMagnet}) {
if (sourceCell === targetCell) {
return false; // 禁止自环
}
if (!sourceMagnet || !targetMagnet) {
return false; // 必须使用连接桩
}
return true;
},
},
highlighting: {
@ -137,22 +237,16 @@ const FlowDesigner: React.FC = () => {
},
},
},
magnetAdsorbed: {
name: 'stroke',
args: {
padding: 4,
attrs: {
strokeWidth: 4,
stroke: '#1890ff',
},
},
mousewheel: {
enabled: true,
modifiers: ['ctrl', 'meta'],
minScale: 0.5,
maxScale: 2,
},
interacting: {
nodeMovable: true,
edgeMovable: true,
edgeLabelMovable: true,
arrowheadMovable: true,
vertexMovable: true,
vertexAddable: true,
vertexDeletable: true,
magnetConnectable: true,
},
keyboard: {
enabled: true,
@ -170,7 +264,7 @@ const FlowDesigner: React.FC = () => {
restrict: true,
},
background: {
color: '#F8F9FA',
color: '#ffffff', // 画布背景色
},
});
@ -238,12 +332,58 @@ const FlowDesigner: React.FC = () => {
})
);
// 启用基础功能
graph.enableSelection();
graph.enableRubberband();
graph.enableKeyboard();
graph.enableClipboard();
graph.enableHistory();
// 启用小地图
graph.use(
new MiniMap({
container: document.getElementById('workflow-minimap'),
width: 200,
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;
setGraph(graph);
@ -583,31 +723,43 @@ const FlowDesigner: React.FC = () => {
return (
<Card
title="流程设计器"
extra={
title={
<Space>
<Button
icon={<SaveOutlined/>}
type="primary"
onClick={handleSave}
disabled={detail?.status !== WorkflowStatus.DRAFT}
>
</Button>
<Button icon={<ArrowLeftOutlined/>} onClick={handleBack}>
</Button>
<Button icon={<ArrowLeftOutlined/>} onClick={() => navigate(-1)}></Button>
<Button type="primary" icon={<SaveOutlined/>} onClick={handleSave}></Button>
</Space>
}
className="workflow-designer"
>
<Layout className="workflow-designer-container">
<Sider width={280} className="workflow-designer-sider">
<Layout>
<Sider width={250} className="workflow-designer-sider">
<NodePanel onNodeDragStart={handleNodeDragStart}/>
</Sider>
<Layout>
<Toolbar graph={graph}/>
<Content className="workflow-designer-content">
<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>
</Layout>
</Layout>