优化面板

This commit is contained in:
戚辰先生 2024-12-06 22:52:49 +08:00
parent e718f7f0ec
commit 8d4941e882
5 changed files with 232 additions and 161 deletions

View File

@ -1,6 +1,8 @@
.node-panel {
height: 100%;
background: #fff;
overflow: hidden;
display: flex;
flex-direction: column;
&-loading {
height: 100%;
@ -9,13 +11,17 @@
justify-content: center;
}
:global {
.ant-tabs {
height: 100%;
display: flex;
flex-direction: column;
.ant-tabs-nav {
margin: 0;
padding: 12px 12px 0;
background: #fafafa;
padding: 0 16px;
background-color: #fafafa;
border-bottom: 1px solid #f0f0f0;
.ant-tabs-tab {
padding: 8px 16px !important;
@ -25,6 +31,7 @@
&:hover {
color: #1890ff;
background: rgba(24, 144, 255, 0.1);
}
&.ant-tabs-tab-active {
@ -38,89 +45,70 @@
}
}
.ant-tabs-content-holder {
flex: 1;
overflow: auto;
.ant-tabs-content {
height: calc(100% - 44px); // 减去tab导航的高度
height: calc(100% - 44px);
}
}
}
}
.node-panel-content {
height: 100%;
padding: 16px;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 16px;
overflow-y: auto;
background: #fff;
&::-webkit-scrollbar {
width: 6px;
height: 6px;
}
&::-webkit-scrollbar-thumb {
background: #ccc;
border-radius: 3px;
}
&::-webkit-scrollbar-track {
background: #f5f5f5;
border-radius: 3px;
}
}
.node-card {
position: relative;
margin-bottom: 12px;
padding: 12px 16px;
background: #fff;
border: 1px solid #e6e6e6;
border-radius: 6px;
height: 100px;
padding: 12px;
background-color: #fff;
border: 2px solid transparent;
border-radius: 8px;
cursor: move;
transition: all 0.2s;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
transition: all 0.3s ease;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
&:hover {
border-color: #1890ff;
background: #e6f7ff;
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(24, 144, 255, 0.1);
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.node-icon {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
margin-right: 12px;
border-radius: 6px;
background: #f0f5ff;
font-size: 24px;
i {
font-size: 20px;
color: #1890ff;
img {
width: 32px;
height: 32px;
object-fit: contain;
}
}
.node-name {
font-size: 14px;
color: #262626;
font-weight: 500;
}
&::after {
content: '';
position: absolute;
right: 16px;
top: 50%;
transform: translateY(-50%);
width: 16px;
height: 16px;
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23999'%3E%3Cpath d='M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z'/%3E%3C/svg%3E") center/contain no-repeat;
opacity: 0.5;
transition: all 0.2s;
}
&:hover::after {
opacity: 1;
font-size: 12px;
color: #333;
text-align: center;
line-height: 1.2;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
}

View File

@ -1,6 +1,7 @@
import React, { useEffect, useState } from 'react';
import { Tabs, Spin } from 'antd';
import { NodeType, getNodeTypes } from '../../service';
import { NODE_CONFIG } from '../../configs/nodeConfig';
import './index.less';
interface NodePanelProps {
@ -17,7 +18,9 @@ const NodePanel: React.FC<NodePanelProps> = ({ onNodeDragStart }) => {
try {
const response = await getNodeTypes({ enabled: true });
if (response) {
setNodeTypes(response);
// 只保留有配置的节点类型
const filteredTypes = response.filter(type => NODE_CONFIG[type.code]);
setNodeTypes(filteredTypes);
}
} catch (error) {
console.error('获取节点类型失败:', error);
@ -45,23 +48,46 @@ const NodePanel: React.FC<NodePanelProps> = ({ onNodeDragStart }) => {
label: getCategoryLabel(category),
children: (
<div className="node-panel-content">
{types.map((nodeType) => (
{types.map((nodeType) => {
const config = NODE_CONFIG[nodeType.code];
if (!config) return null;
return (
<div
key={nodeType.code}
className="node-card"
draggable
onDragStart={(e) => {
e.dataTransfer.setData('node-type', JSON.stringify(nodeType));
e.dataTransfer.setData('node-type', JSON.stringify({
...nodeType,
config: config // 添加节点配置
}));
onNodeDragStart(nodeType);
}}
style={{ borderColor: nodeType.color }}
style={{
borderColor: config.theme.stroke,
backgroundColor: config.theme.fill
}}
>
<div className="node-icon" style={{ color: nodeType.color }}>
<div
className="node-icon"
style={{ color: config.theme.stroke }}
>
{config.extras?.icon ? (
<img
src={config.extras.icon['xlink:href']}
width={32}
height={32}
alt={nodeType.name}
/>
) : (
<i className={nodeType.icon} />
)}
</div>
<div className="node-name">{nodeType.name}</div>
</div>
))}
);
})}
</div>
),
}));
@ -78,7 +104,7 @@ const NodePanel: React.FC<NodePanelProps> = ({ onNodeDragStart }) => {
<div className="node-panel">
<Tabs
className="node-tabs"
defaultActiveKey="TASK"
defaultActiveKey="BASIC"
items={tabItems}
/>
</div>
@ -88,6 +114,7 @@ const NodePanel: React.FC<NodePanelProps> = ({ onNodeDragStart }) => {
// 获取类别标签
const getCategoryLabel = (category: string) => {
const labels: Record<string, string> = {
BASIC: '基础节点',
TASK: '任务节点',
EVENT: '事件节点',
GATEWAY: '网关节点',

View File

@ -8,7 +8,16 @@ export const NODE_CONFIG: Record<string, NodeConfig> = {
fill: '#f6ffed',
stroke: '#52c41a'
},
label: '开始'
label: '开始',
extras: {
icon: {
'xlink:href': 'data:image/svg+xml;base64,PHN2ZyB0PSIxNzAzODQ4NTM1NDY5IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjQxNjEiIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIj48cGF0aCBkPSJNNTEyIDY0QzI2NC42IDY0IDY0IDI2NC42IDY0IDUxMnMyMDAuNiA0NDggNDQ4IDQ0OCA0NDgtMjAwLjYgNDQ4LTQ0OFM3NTkuNCA2NCA1MTIgNjR6bTE5MiAzMzZjMCA0LjQtMy42IDgtOCA4SDU0NHYxNTJjMCA0LjQtMy42IDgtOCA4aC00OGMtNC40IDAtOC0zLjYtOC04VjQwOEgzMjhjLTQuNCAwLTgtMy42LTgtOHYtNDhjMC00LjQgMy42LTggOC04aDE1MlYxOTJjMC00LjQgMy42LTggOC04aDQ4YzQuNCAwIDggMy42IDggOHYxNTJoMTUyYzQuNCAwIDggMy42IDggOHY0OHoiIGZpbGw9IiM1MmM0MWEiIHAtaWQ9IjQxNjIiPjwvcGF0aD48L3N2Zz4=',
width: 32,
height: 32,
x: 24,
y: 24
}
}
},
END: {
size: { width: 80, height: 80 },
@ -17,7 +26,16 @@ export const NODE_CONFIG: Record<string, NodeConfig> = {
fill: '#fff1f0',
stroke: '#ff4d4f'
},
label: '结束'
label: '结束',
extras: {
icon: {
'xlink:href': 'data:image/svg+xml;base64,PHN2ZyB0PSIxNzAzODQ4NTM1NDY5IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjQxNjEiIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIj48cGF0aCBkPSJNNTEyIDY0QzI2NC42IDY0IDY0IDI2NC42IDY0IDUxMnMyMDAuNiA0NDggNDQ4IDQ0OCA0NDgtMjAwLjYgNDQ4LTQ0OFM3NTkuNCA2NCA1MTIgNjR6bTE5MiAyODhjMCA0LjQtMy42IDgtOCA4SDMyOGMtNC40IDAtOC0zLjYtOC04di00OGMwLTQuNCAzLjYtOCA4LThoMzY4YzQuNCAwIDggMy42IDggOHY0OHoiIGZpbGw9IiNmZjRkNGYiIHAtaWQ9IjQxNjIiPjwvcGF0aD48L3N2Zz4=',
width: 32,
height: 32,
x: 24,
y: 24
}
}
},
SHELL: {
size: { width: 200, height: 80 },
@ -26,12 +44,12 @@ export const NODE_CONFIG: Record<string, NodeConfig> = {
fill: '#e6f7ff',
stroke: '#1890ff'
},
label: '脚本',
label: 'Shell脚本',
extras: {
rx: 4,
ry: 4,
icon: {
'xlink:href': 'data:image/svg+xml;base64,PHN2ZyB0PSIxNzAzODQ4NTM1NDY5IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjQxNjEiIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIj48cGF0aCBkPSJNMTYwIDI1NnY1MTJoNzA0VjI1NkgxNjB6IG02NDAgNDQ4SDE5MlYyODhoNjA4djQxNnpNMjI0IDQ4MGwxMjgtMTI4IDQ1LjMgNDUuMy04Mi43IDgyLjcgODIuNyA4Mi43TDM1MiA2MDhsLTEyOC0xMjh6IG0yMzEuMSAxOTJsLTQ1LjMtNDUuMyAxNTItMTUyIDQ1LjMgNDUuM2wtMTUyIDE1MnoiIGZpbGw9IiMxODkwZmYiIHAtaWQ9IjQxNjIiPjwvcGF0aD48L3N2Zz4=',
'xlink:href': 'data:image/svg+xml;base64,PHN2ZyB0PSIxNzAzODQ4NTM1NDY5IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjQxNjEiIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIj48cGF0aCBkPSJNMTYwIDI1NnY1MTJoNzA0VjI1NkgxNjB6IG02NDAgNDQ4SDE5MlYyODhoNjA4djQxNnpNMjI0IDQ4MGwxMjgtMTI4IDQ1LjMgNDUuMy04Mi43IDgyLjcgODIuNyA4Mi43TDM1MiA2MDhsLTEyOC0xMjh6IG0yMzEuMSAxOTBsLTQ1LjMtNDUuMyAxNTItMTUyIDQ1LjMgNDUuM2wtMTUyIDE1MnoiIGZpbGw9IiMxODkwZmYiIHAtaWQ9IjQxNjIiPjwvcGF0aD48L3N2Zz4=',
width: 32,
height: 32,
x: 16,
@ -60,7 +78,7 @@ export const NODE_CONFIG: Record<string, NodeConfig> = {
}
},
GATEWAY: {
size: { width: 80, height: 80 },
size: { width: 60, height: 60 },
shape: 'polygon',
theme: {
fill: '#f9f0ff',
@ -68,7 +86,14 @@ export const NODE_CONFIG: Record<string, NodeConfig> = {
},
label: '网关',
extras: {
refPoints: '0,10 10,0 20,10 10,20'
refPoints: '0,30 30,0 60,30 30,60',
icon: {
'xlink:href': 'data:image/svg+xml;base64,PHN2ZyB0PSIxNzAzODQ4NzAzODY0IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjY1NTUiIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIj48cGF0aCBkPSJNNzgyLjcgNDQxLjRMNTQ1IDMwNC44YzAuMy0wLjMgMC40LTAuNyAwLjctMUw3ODIuNyA0NDEuNHogbTExOC45IDQzMi45TDIxNy43IDE1OC4xQzIwMi4xIDE0Mi41IDE3NiAxNDIuNSAxNjAuNCAxNTguMWMtMTUuNiAxNS42LTE1LjYgNDEuNyAwIDU3LjNsNjgzLjkgNzE2LjJjMTUuNiAxNS42IDQxLjcgMTUuNiA1Ny4zIDBzMTUuNi00MS43IDAtNTcuM3ogbS00MjYtMjQuNmMtMC4zIDAuMy0wLjQgMC43LTAuNyAxTDIzNy42IDU4Mi42YzAuMy0wLjMgMC40LTAuNyAwLjctMWwyMzcuMyAyNjguMXogbTIzNy4zLTI2Ny4xTDQ3NS42IDg1MC43YzAuMy0wLjMgMC40LTAuNyAwLjctMUw3MTIuOSA1ODIuNnogbS00NzUtMjY3LjFsMjM3LjMgMjY4LjFjLTAuMyAwLjMtMC40IDAuNy0wLjcgMUwyMzcuOSAzMTUuNXoiIGZpbGw9IiM3MjJlZDEiIHAtaWQ9IjY1NTYiPjwvcGF0aD48L3N2Zz4=',
width: 24,
height: 24,
x: 18,
y: 18
}
}
}
};

View File

@ -3,30 +3,42 @@ export interface Position {
y: number;
}
export interface Size {
width: number;
height: number;
}
export interface NodeConfig {
size: Size;
shape: 'circle' | 'rect' | 'polygon';
theme: {
fill: string;
stroke: string;
};
label: string;
style: {
width?: number;
height?: number;
[key: string]: any;
extras?: {
rx?: number;
ry?: number;
icon?: {
'xlink:href': string;
width: number;
height: number;
x: number;
y: number;
};
ports?: {
groups?: {
[key: string]: any;
};
items?: Array<{
group: string;
[key: string]: any;
}>;
};
}
export interface NodeType {
id?: number;
code: string;
name: string;
label: string;
config: NodeConfig;
category: string;
color?: string;
icon?: string;
enabled?: boolean;
description?: string;
config: NodeConfig;
}
export interface NodeData {

View File

@ -69,6 +69,8 @@ interface NodeStyle {
export const generateNodeStyle = (nodeType: string): NodeStyle => {
try {
const config = getNodeConfig(nodeType);
const isCircle = config.shape === 'circle';
return {
width: config.size.width,
height: config.size.height,
@ -85,15 +87,32 @@ export const generateNodeStyle = (nodeType: string): NodeStyle => {
fill: '#000000',
fontSize: 14,
fontWeight: 500,
...(config.shape === 'rect' ? {
refX: 0.5,
...(config.shape === 'circle' ? {
refY: 0.65,
textAnchor: 'middle',
textVerticalAnchor: 'middle'
} : config.shape === 'polygon' ? {
refY: 1.3,
textAnchor: 'middle',
textVerticalAnchor: 'middle'
} : {
refX: config.extras?.icon ? 0.3 : 0.5,
refY: 0.5,
textAnchor: 'middle',
textVerticalAnchor: 'middle'
} : {})
})
},
...(config.extras?.icon ? {
image: config.extras.icon
image: {
...config.extras.icon,
...(config.shape === 'circle' ? {
x: (config.size.width - config.extras.icon.width) / 2,
y: config.size.height * 0.25
} : config.shape === 'polygon' ? {
x: (config.size.width - config.extras.icon.width) / 2,
y: (config.size.height - config.extras.icon.height) / 2
} : config.extras.icon)
}
} : {})
}
};