feat(拓扑图): 新增网络拓扑图功能模块
This commit is contained in:
@@ -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';
|
||||
Reference in New Issue
Block a user