1
This commit is contained in:
parent
b843b4603f
commit
b0dadc9a24
110
frontend/src/pages/Workflow/Definition/Design/index.less
Normal file
110
frontend/src/pages/Workflow/Definition/Design/index.less
Normal file
@ -0,0 +1,110 @@
|
||||
.workflow-design {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #fff;
|
||||
|
||||
.header {
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
|
||||
.back-button {
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
.ant-space-compact {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
padding: 16px;
|
||||
gap: 16px;
|
||||
|
||||
.sidebar {
|
||||
width: 280px;
|
||||
flex-shrink: 0;
|
||||
border-right: 1px solid #f0f0f0;
|
||||
overflow-y: auto;
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
|
||||
:global {
|
||||
.ant-collapse {
|
||||
border: none;
|
||||
background: transparent;
|
||||
|
||||
.ant-collapse-item {
|
||||
border-radius: 0;
|
||||
|
||||
.ant-collapse-header {
|
||||
padding: 8px 16px;
|
||||
}
|
||||
|
||||
.ant-collapse-content-box {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.node-item {
|
||||
padding: 8px 16px;
|
||||
margin: 4px 8px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 4px;
|
||||
cursor: move;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
background: #f5f5f5;
|
||||
border-color: #1890ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.main-area {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
|
||||
.workflow-container {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 4px;
|
||||
background: #f5f5f5;
|
||||
overflow: hidden;
|
||||
|
||||
.workflow-canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.minimap-container {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
bottom: 20px;
|
||||
width: 200px;
|
||||
height: 150px;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
import React, {useEffect, useState, useRef} from 'react';
|
||||
import {useParams, useNavigate} from 'react-router-dom';
|
||||
import {Button, Space, Card, Row, Col, message, Modal} from 'antd';
|
||||
import {Button, Space, Card, Row, Col, message, Modal, Collapse} from 'antd';
|
||||
import {
|
||||
ArrowLeftOutlined,
|
||||
SaveOutlined,
|
||||
@ -18,6 +18,7 @@ import '@antv/x6-plugin-snapline';
|
||||
import '@antv/x6-plugin-clipboard';
|
||||
import '@antv/x6-plugin-history';
|
||||
import { Selection } from '@antv/x6-plugin-selection';
|
||||
import { MiniMap } from '@antv/x6-plugin-minimap';
|
||||
import {getDefinitionDetail, saveDefinition} from '../service';
|
||||
import {getNodeDefinitionList} from './service';
|
||||
import NodePanel from './components/NodePanel';
|
||||
@ -32,12 +33,14 @@ import {
|
||||
HIGHLIGHTING_CONFIG,
|
||||
DEFAULT_STYLES,
|
||||
} from './constants';
|
||||
import './index.less';
|
||||
|
||||
const WorkflowDesign: React.FC = () => {
|
||||
const {id} = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const [title, setTitle] = useState<string>('工作流设计');
|
||||
const graphContainerRef = useRef<HTMLDivElement>(null);
|
||||
const minimapContainerRef = useRef<HTMLDivElement>(null);
|
||||
const [graph, setGraph] = useState<Graph | null>(null);
|
||||
const [selectedNode, setSelectedNode] = useState<Cell | null>(null);
|
||||
const [selectedNodeDefinition, setSelectedNodeDefinition] = useState<NodeDefinition | null>(null);
|
||||
@ -79,6 +82,8 @@ const WorkflowDesign: React.FC = () => {
|
||||
background: {
|
||||
color: '#f5f5f5',
|
||||
},
|
||||
width: graphContainerRef.current.clientWidth,
|
||||
height: graphContainerRef.current.clientHeight,
|
||||
});
|
||||
|
||||
// 注册选择插件
|
||||
@ -94,6 +99,18 @@ const WorkflowDesign: React.FC = () => {
|
||||
})
|
||||
);
|
||||
|
||||
// 注册小地图插件
|
||||
if (minimapContainerRef.current) {
|
||||
graph.use(
|
||||
new MiniMap({
|
||||
container: minimapContainerRef.current,
|
||||
width: 200,
|
||||
height: 150,
|
||||
padding: 10,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// 添加右键菜单
|
||||
graph.on('node:contextmenu', ({cell, view, e}) => {
|
||||
e.preventDefault();
|
||||
@ -233,6 +250,11 @@ const WorkflowDesign: React.FC = () => {
|
||||
};
|
||||
}, [graphContainerRef, id, nodeDefinitions, isNodeDefinitionsLoaded]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!graph || !minimapContainerRef.current) return;
|
||||
|
||||
}, [graph]);
|
||||
|
||||
const loadDefinitionDetail = async (graphInstance: Graph, definitionId: string) => {
|
||||
try {
|
||||
const response = await getDefinitionDetail(Number(definitionId));
|
||||
@ -399,164 +421,156 @@ const WorkflowDesign: React.FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Row gutter={16}>
|
||||
<Col span={4}>
|
||||
<div className="workflow-design">
|
||||
<div className="header">
|
||||
<Space>
|
||||
<Button
|
||||
icon={<ArrowLeftOutlined />}
|
||||
onClick={() => navigate('/workflow/definition')}
|
||||
className="back-button"
|
||||
>
|
||||
返回
|
||||
</Button>
|
||||
<span>工作流设计器</span>
|
||||
</Space>
|
||||
<div className="actions">
|
||||
<Space>
|
||||
<Space.Compact>
|
||||
<Button
|
||||
icon={<SelectOutlined />}
|
||||
onClick={() => {
|
||||
if (!graph) return;
|
||||
const cells = graph.getCells();
|
||||
if (cells.length === 0) {
|
||||
message.info('当前没有可选择的元素');
|
||||
return;
|
||||
}
|
||||
graph.resetSelection();
|
||||
graph.select(cells);
|
||||
}}
|
||||
title="全选"
|
||||
/>
|
||||
<Button
|
||||
icon={<CopyOutlined />}
|
||||
onClick={() => {
|
||||
if (!graph) return;
|
||||
const cells = graph.getSelectedCells();
|
||||
if (cells.length === 0) {
|
||||
message.info('请先选择要复制的元素');
|
||||
return;
|
||||
}
|
||||
graph.copy(cells);
|
||||
}}
|
||||
title="复制"
|
||||
/>
|
||||
<Button
|
||||
icon={<ScissorOutlined />}
|
||||
onClick={() => {
|
||||
if (!graph) return;
|
||||
const cells = graph.getSelectedCells();
|
||||
if (cells.length === 0) {
|
||||
message.info('请先选择要剪切的元素');
|
||||
return;
|
||||
}
|
||||
graph.cut(cells);
|
||||
}}
|
||||
title="剪切"
|
||||
/>
|
||||
<Button
|
||||
icon={<SnippetsOutlined />}
|
||||
onClick={() => {
|
||||
if (!graph) return;
|
||||
if (!graph.isClipboardEmpty()) {
|
||||
const cells = graph.paste({ offset: 32 });
|
||||
graph.cleanSelection();
|
||||
graph.select(cells);
|
||||
} else {
|
||||
message.info('剪贴板为空');
|
||||
}
|
||||
}}
|
||||
title="粘贴"
|
||||
/>
|
||||
<Button
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => {
|
||||
if (!graph) return;
|
||||
const cells = graph.getSelectedCells();
|
||||
if (cells.length === 0) {
|
||||
message.info('请先选择要删除的元素');
|
||||
return;
|
||||
}
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: '确定要删除选中的元素吗?',
|
||||
onOk: () => {
|
||||
graph.removeCells(cells);
|
||||
}
|
||||
});
|
||||
}}
|
||||
danger
|
||||
title="删除"
|
||||
/>
|
||||
</Space.Compact>
|
||||
<Space.Compact>
|
||||
<Button
|
||||
icon={<UndoOutlined />}
|
||||
onClick={() => {
|
||||
if (!graph) return;
|
||||
if (graph.canUndo()) {
|
||||
graph.undo();
|
||||
} else {
|
||||
message.info('没有可撤销的操作');
|
||||
}
|
||||
}}
|
||||
title="撤销"
|
||||
/>
|
||||
<Button
|
||||
icon={<RedoOutlined />}
|
||||
onClick={() => {
|
||||
if (!graph) return;
|
||||
if (graph.canRedo()) {
|
||||
graph.redo();
|
||||
} else {
|
||||
message.info('没有可重做的操作');
|
||||
}
|
||||
}}
|
||||
title="重做"
|
||||
/>
|
||||
</Space.Compact>
|
||||
<Button onClick={handleSaveWorkflow} type="primary" icon={<SaveOutlined />}>
|
||||
保存
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
<div className="content">
|
||||
<div className="sidebar">
|
||||
<NodePanel
|
||||
nodeDefinitions={nodeDefinitions}
|
||||
onNodeDragStart={handleNodeDragStart}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={20}>
|
||||
<Card
|
||||
styles={{
|
||||
body: {
|
||||
padding: 0,
|
||||
height: 'calc(100vh - 250px)',
|
||||
background: '#f5f5f5',
|
||||
border: '1px solid #d9d9d9',
|
||||
borderRadius: '4px',
|
||||
}
|
||||
}}
|
||||
extra={
|
||||
<Space>
|
||||
<Space.Compact>
|
||||
<Button
|
||||
icon={<SelectOutlined />}
|
||||
onClick={() => {
|
||||
if (!graph) return;
|
||||
const cells = graph.getCells();
|
||||
if (cells.length === 0) {
|
||||
message.info('当前没有可选择的元素');
|
||||
return;
|
||||
}
|
||||
graph.resetSelection();
|
||||
graph.select(cells);
|
||||
}}
|
||||
title="全选"
|
||||
/>
|
||||
<Button
|
||||
icon={<CopyOutlined />}
|
||||
onClick={() => {
|
||||
if (!graph) return;
|
||||
const cells = graph.getSelectedCells();
|
||||
if (cells.length === 0) {
|
||||
message.info('请先选择要复制的元素');
|
||||
return;
|
||||
}
|
||||
graph.copy(cells);
|
||||
}}
|
||||
title="复制"
|
||||
/>
|
||||
<Button
|
||||
icon={<ScissorOutlined />}
|
||||
onClick={() => {
|
||||
if (!graph) return;
|
||||
const cells = graph.getSelectedCells();
|
||||
if (cells.length === 0) {
|
||||
message.info('请先选择要剪切的元素');
|
||||
return;
|
||||
}
|
||||
graph.cut(cells);
|
||||
}}
|
||||
title="剪切"
|
||||
/>
|
||||
<Button
|
||||
icon={<SnippetsOutlined />}
|
||||
onClick={() => {
|
||||
if (!graph) return;
|
||||
if (!graph.isClipboardEmpty()) {
|
||||
const cells = graph.paste({ offset: 32 });
|
||||
graph.cleanSelection();
|
||||
graph.select(cells);
|
||||
} else {
|
||||
message.info('剪贴板为空');
|
||||
}
|
||||
}}
|
||||
title="粘贴"
|
||||
/>
|
||||
<Button
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => {
|
||||
if (!graph) return;
|
||||
const cells = graph.getSelectedCells();
|
||||
if (cells.length === 0) {
|
||||
message.info('请先选择要删除的元素');
|
||||
return;
|
||||
}
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: '确定要删除选中的元素吗?',
|
||||
onOk: () => {
|
||||
graph.removeCells(cells);
|
||||
}
|
||||
});
|
||||
}}
|
||||
danger
|
||||
title="删除"
|
||||
/>
|
||||
</Space.Compact>
|
||||
<Space.Compact>
|
||||
<Button
|
||||
icon={<UndoOutlined />}
|
||||
onClick={() => {
|
||||
if (!graph) return;
|
||||
if (graph.canUndo()) {
|
||||
graph.undo();
|
||||
} else {
|
||||
message.info('没有可撤销的操作');
|
||||
}
|
||||
}}
|
||||
title="撤销"
|
||||
/>
|
||||
<Button
|
||||
icon={<RedoOutlined />}
|
||||
onClick={() => {
|
||||
if (!graph) return;
|
||||
if (graph.canRedo()) {
|
||||
graph.redo();
|
||||
} else {
|
||||
message.info('没有可重做的操作');
|
||||
}
|
||||
}}
|
||||
title="重做"
|
||||
/>
|
||||
</Space.Compact>
|
||||
<Button
|
||||
icon={<SaveOutlined/>}
|
||||
type="primary"
|
||||
onClick={handleSaveWorkflow}
|
||||
>
|
||||
保存
|
||||
</Button>
|
||||
<Button
|
||||
icon={<ArrowLeftOutlined/>}
|
||||
onClick={() => navigate('/workflow/definition')}
|
||||
>
|
||||
返回
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
</div>
|
||||
<div className="main-area">
|
||||
<div className="workflow-container">
|
||||
<div
|
||||
ref={graphContainerRef}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}}
|
||||
className="workflow-canvas"
|
||||
onDrop={handleDrop}
|
||||
onDragOver={handleDragOver}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
<NodeConfigDrawer
|
||||
visible={configModalVisible}
|
||||
node={selectedNode}
|
||||
nodeDefinition={selectedNodeDefinition}
|
||||
onOk={handleNodeConfigUpdate}
|
||||
onCancel={() => setConfigModalVisible(false)}
|
||||
/>
|
||||
<div ref={minimapContainerRef} className="minimap-container" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{configModalVisible && selectedNode && selectedNodeDefinition && (
|
||||
<NodeConfigDrawer
|
||||
visible={configModalVisible}
|
||||
node={selectedNode}
|
||||
nodeDefinition={selectedNodeDefinition}
|
||||
onCancel={() => setConfigModalVisible(false)}
|
||||
onOk={handleNodeConfigUpdate}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user