feat(拓扑图): 新增网络拓扑图功能模块

This commit is contained in:
zhang1106
2026-04-13 16:54:07 +08:00
parent 1d297fc81a
commit 01a05719dc
25 changed files with 2453 additions and 145 deletions
@@ -0,0 +1,114 @@
import React from 'react';
import { Tooltip } from 'antd';
const NODE_WIDTH = 200;
const NODE_HEIGHT = 100;
const STATUS_COLORS = {
online: '#52c41a',
offline: '#d9d9d9',
fault: '#ff4d4f',
warning: '#faad14'
};
function RouterNode({ data }) {
if (!data) {
return (
<div style={{
width: NODE_WIDTH,
height: NODE_HEIGHT,
background: '#f0f0f0',
borderRadius: 8,
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}>
无数据
</div>
);
}
const nodeColor = '#eb595a';
const statusColor = STATUS_COLORS[data.status] || STATUS_COLORS.offline;
const isCenter = data.isCenter;
const isSelected = data.selected;
return (
<div
style={{
width: NODE_WIDTH,
height: NODE_HEIGHT,
background: '#fff',
borderRadius: 8,
border: isSelected
? `2px solid ${nodeColor}`
: isCenter
? `2px solid ${nodeColor}`
: `1px solid #d9d9d9`,
boxShadow: isCenter
? `0 4px 16px ${nodeColor}40`
: isSelected
? `0 4px 12px ${nodeColor}30`
: '0 2px 8px rgba(0,0,0,0.1)',
overflow: 'hidden',
transition: 'all 0.2s ease',
cursor: 'pointer',
display: 'flex',
flexDirection: 'column'
}}
>
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '8px' }}>
<img
src="/png/防火墙.png"
alt="防火墙"
style={{ width: 'auto', height: '60px', objectFit: 'contain' }}
onError={(e) => { e.target.style.display = 'none'; }}
/>
</div>
<div style={{
height: 32,
background: '#fff1f0',
padding: '4px 8px',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between'
}}>
<Tooltip title={data.name || '防火墙'} placement="bottomLeft">
<div style={{
fontSize: 11,
fontWeight: 'bold',
color: '#262626',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
flex: 1,
cursor: 'pointer'
}}>
{data.name?.substring(0, 14) || '防火墙'}
</div>
</Tooltip>
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
<span style={{
width: 8,
height: 8,
borderRadius: '50%',
background: statusColor,
display: 'inline-block'
}} />
{isCenter && (
<span style={{
fontSize: 9,
color: '#faad14',
fontWeight: 'bold'
}}>
</span>
)}
</div>
</div>
</div>
);
}
export default RouterNode;
@@ -0,0 +1,155 @@
import React from 'react';
const NODE_WIDTH = 200;
const NODE_HEIGHT = 80;
const STATUS_COLORS = {
online: '#52c41a',
offline: '#d9d9d9',
fault: '#ff4d4f',
warning: '#faad14'
};
const TYPE_COLORS = {
switch: '#1890ff',
router: '#722ed1',
server: '#52c41a',
storage: '#fa8c16',
default: '#8c8c8c'
};
function GenericNode({ data }) {
if (!data) {
return (
<div style={{
width: NODE_WIDTH,
height: NODE_HEIGHT,
background: '#f0f0f0',
borderRadius: 8,
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}>
无数据
</div>
);
}
const nodeColor = TYPE_COLORS[data.type] || TYPE_COLORS.default;
const statusColor = STATUS_COLORS[data.status] || STATUS_COLORS.offline;
const isCenter = data.isCenter;
const isSelected = data.selected;
return (
<div
style={{
width: NODE_WIDTH,
height: NODE_HEIGHT,
background: '#fff',
borderRadius: 8,
border: isSelected
? `2px solid ${nodeColor}`
: isCenter
? `2px solid ${nodeColor}`
: `1px solid #d9d9d9`,
boxShadow: isCenter
? `0 4px 16px ${nodeColor}40`
: isSelected
? `0 4px 12px ${nodeColor}30`
: '0 2px 8px rgba(0,0,0,0.1)',
overflow: 'hidden',
transition: 'all 0.2s ease',
cursor: 'pointer'
}}
>
<svg
width={NODE_WIDTH}
height={NODE_HEIGHT}
viewBox={`0 0 ${NODE_WIDTH} ${NODE_HEIGHT}`}
style={{ display: 'block' }}
>
<rect
x="10"
y="10"
width={NODE_WIDTH - 20}
height={NODE_HEIGHT - 20}
rx="4"
fill="#fafafa"
stroke={nodeColor}
strokeWidth="1"
/>
<rect
x="20"
y="20"
width="50"
height={NODE_HEIGHT - 40}
rx="4"
fill={nodeColor}
opacity="0.2"
/>
<rect
x="25"
y="25"
width="40"
height="30"
rx="2"
fill={nodeColor}
/>
<circle cx="35" cy="35" r="6" fill="#fff" />
<circle cx="35" cy="35" r="3" fill="#fff" opacity="0.5" />
<line x1="75" y1="20" x2="75" y2={NODE_HEIGHT - 20} stroke="#d9d9d9" strokeWidth="1" />
<text
x="85"
y="35"
fill="#262626"
fontSize="12"
fontWeight="bold"
>
<title>{data.name || '设备'}</title>
{data.name?.substring(0, 10) || '设备'}
</text>
<text
x="85"
y="50"
fill="#8c8c8c"
fontSize="10"
>
{data.type === 'switch' ? '交换机' :
data.type === 'router' ? '路由器' :
data.type === 'server' ? '服务器' :
data.type === 'storage' ? '存储' : '设备'}
</text>
{isCenter && (
<>
<circle cx={NODE_WIDTH - 20} cy="15" r="8" fill={nodeColor} />
<text
x={NODE_WIDTH - 20}
y="19"
fill="#fff"
fontSize="8"
fontWeight="bold"
textAnchor="middle"
>
</text>
</>
)}
<circle
cx={NODE_WIDTH - 15}
cy={NODE_HEIGHT - 15}
r="4"
fill={statusColor}
/>
</svg>
</div>
);
}
export default GenericNode;
@@ -0,0 +1,114 @@
import React from 'react';
import { Tooltip } from 'antd';
const NODE_WIDTH = 200;
const NODE_HEIGHT = 100;
const STATUS_COLORS = {
online: '#52c41a',
offline: '#d9d9d9',
fault: '#ff4d4f',
warning: '#faad14'
};
function RouterNode({ data }) {
if (!data) {
return (
<div style={{
width: NODE_WIDTH,
height: NODE_HEIGHT,
background: '#f0f0f0',
borderRadius: 8,
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}>
无数据
</div>
);
}
const nodeColor = '#722ed1';
const statusColor = STATUS_COLORS[data.status] || STATUS_COLORS.offline;
const isCenter = data.isCenter;
const isSelected = data.selected;
return (
<div
style={{
width: NODE_WIDTH,
height: NODE_HEIGHT,
background: '#fff',
borderRadius: 8,
border: isSelected
? `2px solid ${nodeColor}`
: isCenter
? `2px solid ${nodeColor}`
: `1px solid #d9d9d9`,
boxShadow: isCenter
? `0 4px 16px ${nodeColor}40`
: isSelected
? `0 4px 12px ${nodeColor}30`
: '0 2px 8px rgba(0,0,0,0.1)',
overflow: 'hidden',
transition: 'all 0.2s ease',
cursor: 'pointer',
display: 'flex',
flexDirection: 'column'
}}
>
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '8px' }}>
<img
src="/png/路由器.png"
alt="路由器"
style={{ width: 'auto', height: '60px', objectFit: 'contain' }}
onError={(e) => { e.target.style.display = 'none'; }}
/>
</div>
<div style={{
height: 32,
background: '#f9f0ff',
padding: '4px 8px',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between'
}}>
<Tooltip title={data.name || '路由器'} placement="bottomLeft">
<div style={{
fontSize: 11,
fontWeight: 'bold',
color: '#262626',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
flex: 1,
cursor: 'pointer'
}}>
{data.name?.substring(0, 14) || '路由器'}
</div>
</Tooltip>
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
<span style={{
width: 8,
height: 8,
borderRadius: '50%',
background: statusColor,
display: 'inline-block'
}} />
{isCenter && (
<span style={{
fontSize: 9,
color: '#faad14',
fontWeight: 'bold'
}}>
</span>
)}
</div>
</div>
</div>
);
}
export default RouterNode;
@@ -0,0 +1,114 @@
import React from 'react';
import { Tooltip } from 'antd';
const NODE_WIDTH = 200;
const NODE_HEIGHT = 100;
const STATUS_COLORS = {
online: '#52c41a',
offline: '#d9d9d9',
fault: '#ff4d4f',
warning: '#faad14'
};
function ServerNode({ data }) {
if (!data) {
return (
<div style={{
width: NODE_WIDTH,
height: NODE_HEIGHT,
background: '#f0f0f0',
borderRadius: 8,
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}>
无数据
</div>
);
}
const nodeColor = '#52c41a';
const statusColor = STATUS_COLORS[data.status] || STATUS_COLORS.offline;
const isCenter = data.isCenter;
const isSelected = data.selected;
return (
<div
style={{
width: NODE_WIDTH,
height: NODE_HEIGHT,
background: '#fff',
borderRadius: 8,
border: isSelected
? `2px solid ${nodeColor}`
: isCenter
? `2px solid ${nodeColor}`
: `1px solid #d9d9d9`,
boxShadow: isCenter
? `0 4px 16px ${nodeColor}40`
: isSelected
? `0 4px 12px ${nodeColor}30`
: '0 2px 8px rgba(0,0,0,0.1)',
overflow: 'hidden',
transition: 'all 0.2s ease',
cursor: 'pointer',
display: 'flex',
flexDirection: 'column'
}}
>
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '8px' }}>
<img
src="/png/服务器.png"
alt="服务器"
style={{ width: 'auto', height: '60px', objectFit: 'contain' }}
onError={(e) => { e.target.style.display = 'none'; }}
/>
</div>
<div style={{
height: 32,
background: '#f6ffed',
padding: '4px 8px',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between'
}}>
<Tooltip title={data.name || '服务器'} placement="bottomLeft">
<div style={{
fontSize: 11,
fontWeight: 'bold',
color: '#262626',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
flex: 1,
cursor: 'pointer'
}}>
{data.name?.substring(0, 14) || '服务器'}
</div>
</Tooltip>
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
<span style={{
width: 8,
height: 8,
borderRadius: '50%',
background: statusColor,
display: 'inline-block'
}} />
{isCenter && (
<span style={{
fontSize: 9,
color: '#faad14',
fontWeight: 'bold'
}}>
</span>
)}
</div>
</div>
</div>
);
}
export default ServerNode;
@@ -0,0 +1,226 @@
import React from 'react';
import { Tooltip } from 'antd';
const NODE_WIDTH = 200;
const NODE_HEIGHT = 110;
const STATUS_COLORS = {
online: '#52c41a',
offline: '#d9d9d9',
fault: '#ff4d4f',
warning: '#faad14'
};
function StorageNode({ data }) {
if (!data) {
return (
<div style={{
width: NODE_WIDTH,
height: NODE_HEIGHT,
background: '#f0f0f0',
borderRadius: 8,
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}>
无数据
</div>
);
}
const nodeColor = '#fa8c16';
const statusColor = STATUS_COLORS[data.status] || STATUS_COLORS.offline;
const isCenter = data.isCenter;
const isSelected = data.selected;
const diskCount = 8;
const activeDisks = Math.floor((data.portCount?.used || 12) / 4);
return (
<div
style={{
width: NODE_WIDTH,
height: NODE_HEIGHT,
background: '#fff',
borderRadius: 8,
border: isSelected
? `2px solid ${nodeColor}`
: isCenter
? `2px solid ${nodeColor}`
: `1px solid #d9d9d9`,
boxShadow: isCenter
? `0 4px 16px ${nodeColor}40`
: isSelected
? `0 4px 12px ${nodeColor}30`
: '0 2px 8px rgba(0,0,0,0.1)',
overflow: 'hidden',
transition: 'all 0.2s ease',
cursor: 'pointer'
}}
>
<svg
width={NODE_WIDTH}
height={NODE_HEIGHT}
viewBox={`0 0 ${NODE_WIDTH} ${NODE_HEIGHT}`}
style={{ display: 'block' }}
>
<rect
x="10"
y="10"
width={NODE_WIDTH - 20}
height={NODE_HEIGHT - 20}
rx="4"
fill="#fff7e6"
stroke={nodeColor}
strokeWidth="1"
/>
<rect
x="15"
y="15"
width={NODE_WIDTH - 30}
height="55"
rx="3"
fill="#fff"
stroke={nodeColor}
strokeWidth="1"
/>
<rect
x="20"
y="20"
width="35"
height="45"
rx="2"
fill="#262626"
/>
<rect x="23" y="23" width="29" height="4" rx="1" fill="#52c41a" />
{[...Array(6)].map((_, i) => (
<rect
key={i}
x="23"
y={29 + i * 6}
width="29"
height="4"
rx="1"
fill={i < activeDisks ? nodeColor : '#595959'}
/>
))}
<rect
x="60"
y="20"
width="35"
height="45"
rx="2"
fill="#262626"
/>
<rect x="63" y="23" width="29" height="4" rx="1" fill="#52c41a" />
{[...Array(6)].map((_, i) => (
<rect
key={i}
x="63"
y={29 + i * 6}
width="29"
height="4"
rx="1"
fill={i < activeDisks - 2 ? nodeColor : '#595959'}
/>
))}
<rect
x="100"
y="20"
width="35"
height="45"
rx="2"
fill="#262626"
/>
<rect x="103" y="23" width="29" height="4" rx="1" fill="#52c41a" />
{[...Array(6)].map((_, i) => (
<rect
key={i}
x="103"
y={29 + i * 6}
width="29"
height="4"
rx="1"
fill={i < activeDisks - 4 ? nodeColor : '#595959'}
/>
))}
<rect
x="140"
y="20"
width="35"
height="45"
rx="2"
fill="#262626"
/>
<rect x="143" y="23" width="29" height="4" rx="1" fill="#52c41a" />
{[...Array(6)].map((_, i) => (
<rect
key={i}
x="143"
y={29 + i * 6}
width="29"
height="4"
rx="1"
fill={i < activeDisks - 6 ? nodeColor : '#595959'}
/>
))}
<line x1="15" y1="75" x2={NODE_WIDTH - 15} y2="75" stroke="#d9d9d9" strokeWidth="1" />
<rect x="15" y="80" width="80" height="5" rx="1" fill="#262626" />
<rect x="100" y="80" width="40" height="5" rx="1" fill="#52c41a" />
<text
x="15"
y="98"
fill="#262626"
fontSize="10"
fontWeight="bold"
>
<title>{data.name || '存储设备'}</title>
{data.name?.substring(0, 14) || '存储设备'}
</text>
<text
x={NODE_WIDTH - 15}
y="98"
fill="#8c8c8c"
fontSize="9"
textAnchor="end"
>
{data.model || 'Storage'}
</text>
{isCenter && (
<>
<circle cx={NODE_WIDTH - 20} cy="12" r="8" fill={nodeColor} />
<text
x={NODE_WIDTH - 20}
y="16"
fill="#fff"
fontSize="8"
fontWeight="bold"
textAnchor="middle"
>
</text>
</>
)}
<circle
cx={NODE_WIDTH - 15}
cy={NODE_HEIGHT - 12}
r="4"
fill={statusColor}
/>
</svg>
</div>
);
}
export default StorageNode;
@@ -0,0 +1,120 @@
import React from 'react';
import { Tooltip } from 'antd';
const NODE_WIDTH = 200;
const NODE_HEIGHT = 120;
const STATUS_COLORS = {
online: '#52c41a',
offline: '#d9d9d9',
fault: '#ff4d4f',
warning: '#faad14'
};
function SwitchNode({ data }) {
if (!data) {
return (
<div style={{
width: NODE_WIDTH,
height: NODE_HEIGHT,
background: '#f0f0f0',
borderRadius: 8,
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}>
无数据
</div>
);
}
const nodeColor = '#1890ff';
const statusColor = STATUS_COLORS[data.status] || STATUS_COLORS.offline;
const isCenter = data.isCenter;
const isSelected = data.selected;
const portCount = data.portCount || { total: 24, used: 12, free: 12 };
return (
<div
style={{
width: NODE_WIDTH,
height: NODE_HEIGHT,
background: '#fff',
borderRadius: 8,
border: isSelected
? `2px solid ${nodeColor}`
: isCenter
? `2px solid ${nodeColor}`
: `1px solid #d9d9d9`,
boxShadow: isCenter
? `0 4px 16px ${nodeColor}40`
: isSelected
? `0 4px 12px ${nodeColor}30`
: '0 2px 8px rgba(0,0,0,0.1)',
overflow: 'hidden',
transition: 'all 0.2s ease',
cursor: 'pointer',
display: 'flex',
flexDirection: 'column'
}}
>
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '8px' }}>
<img
src="/png/交换机.png"
alt="交换机"
style={{ width: 'auto', height: '70px', objectFit: 'contain' }}
onError={(e) => { e.target.style.display = 'none'; }}
/>
</div>
<div style={{
height: 36,
background: '#e6f7ff',
padding: '4px 8px',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center'
}}>
<Tooltip title={data.name || '交换机'} placement="bottomLeft">
<div style={{
fontSize: 11,
fontWeight: 'bold',
color: '#262626',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
cursor: 'pointer'
}}>
{data.name?.substring(0, 16) || '交换机'}
</div>
</Tooltip>
<div style={{ display: 'flex', alignItems: 'center', gap: 4, marginTop: 2 }}>
<span style={{
fontSize: 9,
color: '#8c8c8c'
}}>
端口: {portCount.used}/{portCount.total}
</span>
<span style={{
width: 8,
height: 8,
borderRadius: '50%',
background: statusColor,
display: 'inline-block'
}} />
{isCenter && (
<span style={{
fontSize: 9,
color: '#faad14',
fontWeight: 'bold'
}}>
中心
</span>
)}
</div>
</div>
</div>
);
}
export default SwitchNode;
@@ -0,0 +1,6 @@
export { default as SwitchNode } from './SwitchNode';
export { default as ServerNode } from './ServerNode';
export { default as RouterNode } from './RouterNode';
export { default as StorageNode } from './StorageNode';
export { default as GenericNode } from './GenericNode';
export { default as FirewallNode } from './FirewallNode';