增加右键跟小地图
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-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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
router: {
|
||||
name: 'manhattan', // 使用曼哈顿路由
|
||||
args: {
|
||||
padding: 1,
|
||||
},
|
||||
},
|
||||
createEdge() {
|
||||
return this.createEdge({
|
||||
attrs: {
|
||||
line: {
|
||||
stroke: '#5F95FF',
|
||||
strokeWidth: 1,
|
||||
targetMarker: {
|
||||
name: 'classic',
|
||||
size: 8,
|
||||
},
|
||||
},
|
||||
},
|
||||
router: {
|
||||
name: 'manhattan',
|
||||
},
|
||||
connector: {
|
||||
name: 'rounded',
|
||||
args: {
|
||||
radius: 8,
|
||||
},
|
||||
},
|
||||
});
|
||||
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 = () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
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,
|
||||
magnetAdsorbed: {
|
||||
name: 'stroke',
|
||||
args: {
|
||||
padding: 4,
|
||||
attrs: {
|
||||
strokeWidth: 4,
|
||||
stroke: '#1890ff',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user