增加右键跟小地图
This commit is contained in:
parent
d6f3648229
commit
cf72081575
10
frontend/package-lock.json
generated
10
frontend/package-lock.json
generated
@ -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",
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user