feat: 端口管理页面添加网卡功能和服务器背板可视化
- 在端口管理页面添加网卡管理功能,与3D可视化页面同步 - 新增 ServerBackplanePanel 组件,按真实服务器背板布局展示网卡和端口 - 支持板载网卡、管理口、PCIe扩展插槽的可视化展示 - 添加网卡-端口层级展示,点击网卡可查看端口详情 - 更新 VirtualDeviceList 组件,集成服务器背板视图
This commit is contained in:
@@ -12,6 +12,7 @@ build/
|
|||||||
.env.test.local
|
.env.test.local
|
||||||
.env.production.local
|
.env.production.local
|
||||||
.gitignore
|
.gitignore
|
||||||
|
.vercelignore
|
||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
logs
|
logs
|
||||||
|
|||||||
+212
-19
@@ -3,6 +3,29 @@ const router = express.Router();
|
|||||||
const { Op } = require('sequelize');
|
const { Op } = require('sequelize');
|
||||||
const Cable = require('../models/Cable');
|
const Cable = require('../models/Cable');
|
||||||
const Device = require('../models/Device');
|
const Device = require('../models/Device');
|
||||||
|
const DevicePort = require('../models/DevicePort');
|
||||||
|
|
||||||
|
// 辅助函数:更新端口状态
|
||||||
|
async function updatePortStatus(deviceId, portName, status) {
|
||||||
|
try {
|
||||||
|
await DevicePort.update(
|
||||||
|
{ status },
|
||||||
|
{ where: { deviceId, portName } }
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`更新端口状态失败: ${deviceId}:${portName} -> ${status}`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助函数:将端口状态设为occupied
|
||||||
|
async function occupyPort(deviceId, portName) {
|
||||||
|
await updatePortStatus(deviceId, portName, 'occupied');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助函数:将端口状态恢复为free
|
||||||
|
async function freePort(deviceId, portName) {
|
||||||
|
await updatePortStatus(deviceId, portName, 'free');
|
||||||
|
}
|
||||||
|
|
||||||
router.get('/', async (req, res) => {
|
router.get('/', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
@@ -136,33 +159,146 @@ router.get('/rack/:rackId', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post('/', async (req, res) => {
|
// 检查接线冲突
|
||||||
|
router.post('/check-conflict', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { cableId, sourceDeviceId, sourcePort, targetDeviceId, targetPort, cableType, cableLength, status, description } = req.body;
|
const { sourceDeviceId, sourcePort, targetDeviceId, targetPort, excludeCableId } = req.body;
|
||||||
|
|
||||||
if (!sourceDeviceId || !sourcePort || !targetDeviceId || !targetPort) {
|
if (!sourceDeviceId || !sourcePort || !targetDeviceId || !targetPort) {
|
||||||
return res.status(400).json({ error: '缺少必填字段' });
|
return res.status(400).json({ error: '缺少必填字段' });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sourceDeviceId === targetDeviceId) {
|
const conflicts = [];
|
||||||
return res.status(400).json({ error: '源设备和目标设备不能相同' });
|
|
||||||
}
|
// 检查源端口冲突
|
||||||
|
const sourceConflict = await Cable.findOne({
|
||||||
const existingCable = await Cable.findOne({
|
|
||||||
where: {
|
where: {
|
||||||
[Op.or]: [
|
[Op.or]: [
|
||||||
{ sourceDeviceId, sourcePort },
|
{ sourceDeviceId, sourcePort },
|
||||||
{ targetDeviceId, targetPort }
|
{ targetDeviceId: sourceDeviceId, targetPort: sourcePort }
|
||||||
]
|
],
|
||||||
}
|
...(excludeCableId && { cableId: { [Op.ne]: excludeCableId } })
|
||||||
|
},
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: Device,
|
||||||
|
as: 'sourceDevice',
|
||||||
|
attributes: ['deviceId', 'name', 'type']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: Device,
|
||||||
|
as: 'targetDevice',
|
||||||
|
attributes: ['deviceId', 'name', 'type']
|
||||||
|
}
|
||||||
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
if (existingCable) {
|
if (sourceConflict) {
|
||||||
return res.status(400).json({ error: '端口已被占用' });
|
conflicts.push({
|
||||||
|
type: 'source',
|
||||||
|
port: sourcePort,
|
||||||
|
deviceId: sourceDeviceId,
|
||||||
|
existingCable: sourceConflict
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查目标端口冲突
|
||||||
|
const targetConflict = await Cable.findOne({
|
||||||
|
where: {
|
||||||
|
[Op.or]: [
|
||||||
|
{ sourceDeviceId: targetDeviceId, sourcePort: targetPort },
|
||||||
|
{ targetDeviceId, targetPort }
|
||||||
|
],
|
||||||
|
...(excludeCableId && { cableId: { [Op.ne]: excludeCableId } })
|
||||||
|
},
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: Device,
|
||||||
|
as: 'sourceDevice',
|
||||||
|
attributes: ['deviceId', 'name', 'type']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: Device,
|
||||||
|
as: 'targetDevice',
|
||||||
|
attributes: ['deviceId', 'name', 'type']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
if (targetConflict) {
|
||||||
|
conflicts.push({
|
||||||
|
type: 'target',
|
||||||
|
port: targetPort,
|
||||||
|
deviceId: targetDeviceId,
|
||||||
|
existingCable: targetConflict
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
hasConflict: conflicts.length > 0,
|
||||||
|
conflicts
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('检查接线冲突失败:', error);
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { cableId, sourceDeviceId, sourcePort, targetDeviceId, targetPort, cableType, cableLength, status, description, force } = req.body;
|
||||||
|
|
||||||
|
if (!sourceDeviceId || !sourcePort || !targetDeviceId || !targetPort) {
|
||||||
|
return res.status(400).json({ error: '缺少必填字段' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sourceDeviceId === targetDeviceId) {
|
||||||
|
return res.status(400).json({ error: '源设备和目标设备不能相同' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果不是强制模式,检查冲突
|
||||||
|
if (!force) {
|
||||||
|
const existingCable = await Cable.findOne({
|
||||||
|
where: {
|
||||||
|
[Op.or]: [
|
||||||
|
{ sourceDeviceId, sourcePort },
|
||||||
|
{ targetDeviceId, targetPort }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingCable) {
|
||||||
|
return res.status(409).json({
|
||||||
|
error: '端口已被占用',
|
||||||
|
conflict: true,
|
||||||
|
existingCable
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是强制模式,先断开原有连接
|
||||||
|
if (force) {
|
||||||
|
const existingCables = await Cable.findAll({
|
||||||
|
where: {
|
||||||
|
[Op.or]: [
|
||||||
|
{ sourceDeviceId, sourcePort },
|
||||||
|
{ targetDeviceId: sourceDeviceId, targetPort: sourcePort },
|
||||||
|
{ sourceDeviceId: targetDeviceId, sourcePort: targetPort },
|
||||||
|
{ targetDeviceId, targetPort }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const cable of existingCables) {
|
||||||
|
await cable.destroy();
|
||||||
|
// 释放原有端口
|
||||||
|
await freePort(cable.sourceDeviceId, cable.sourcePort);
|
||||||
|
await freePort(cable.targetDeviceId, cable.targetPort);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const autoCableId = cableId || `CABLE-${Date.now()}-${Math.floor(Math.random() * 10000)}`;
|
const autoCableId = cableId || `CABLE-${Date.now()}-${Math.floor(Math.random() * 10000)}`;
|
||||||
|
|
||||||
const cable = await Cable.create({
|
const cable = await Cable.create({
|
||||||
cableId: autoCableId,
|
cableId: autoCableId,
|
||||||
sourceDeviceId,
|
sourceDeviceId,
|
||||||
@@ -174,7 +310,7 @@ router.post('/', async (req, res) => {
|
|||||||
status: status || 'normal',
|
status: status || 'normal',
|
||||||
description
|
description
|
||||||
});
|
});
|
||||||
|
|
||||||
const createdCable = await Cable.findByPk(cable.cableId, {
|
const createdCable = await Cable.findByPk(cable.cableId, {
|
||||||
include: [
|
include: [
|
||||||
{
|
{
|
||||||
@@ -189,7 +325,11 @@ router.post('/', async (req, res) => {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 自动将源端口和目标端口状态设为occupied
|
||||||
|
await occupyPort(sourceDeviceId, sourcePort);
|
||||||
|
await occupyPort(targetDeviceId, targetPort);
|
||||||
|
|
||||||
res.status(201).json(createdCable);
|
res.status(201).json(createdCable);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('创建接线失败:', error);
|
console.error('创建接线失败:', error);
|
||||||
@@ -250,6 +390,10 @@ router.post('/batch', async (req, res) => {
|
|||||||
description: cableData.description
|
description: cableData.description
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 自动将源端口和目标端口状态设为occupied
|
||||||
|
await occupyPort(cableData.sourceDeviceId, cableData.sourcePort);
|
||||||
|
await occupyPort(cableData.targetDeviceId, cableData.targetPort);
|
||||||
|
|
||||||
results.success++;
|
results.success++;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
results.failed++;
|
results.failed++;
|
||||||
@@ -270,6 +414,15 @@ router.post('/batch', async (req, res) => {
|
|||||||
|
|
||||||
router.put('/:cableId', async (req, res) => {
|
router.put('/:cableId', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
// 获取更新前的接线信息
|
||||||
|
const oldCable = await Cable.findByPk(req.params.cableId);
|
||||||
|
|
||||||
|
if (!oldCable) {
|
||||||
|
return res.status(404).json({ error: '接线不存在' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { sourceDeviceId: oldSourceDeviceId, sourcePort: oldSourcePort, targetDeviceId: oldTargetDeviceId, targetPort: oldTargetPort } = oldCable;
|
||||||
|
|
||||||
const [updated] = await Cable.update(req.body, {
|
const [updated] = await Cable.update(req.body, {
|
||||||
where: { cableId: req.params.cableId }
|
where: { cableId: req.params.cableId }
|
||||||
});
|
});
|
||||||
@@ -289,6 +442,22 @@ router.put('/:cableId', async (req, res) => {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { sourceDeviceId, sourcePort, targetDeviceId, targetPort } = cable;
|
||||||
|
|
||||||
|
// 同步更新端口状态
|
||||||
|
// 源端口变更:释放旧端口,占用新端口
|
||||||
|
if (oldSourceDeviceId !== sourceDeviceId || oldSourcePort !== sourcePort) {
|
||||||
|
await freePort(oldSourceDeviceId, oldSourcePort);
|
||||||
|
await occupyPort(sourceDeviceId, sourcePort);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 目标端口变更:释放旧端口,占用新端口
|
||||||
|
if (oldTargetDeviceId !== targetDeviceId || oldTargetPort !== targetPort) {
|
||||||
|
await freePort(oldTargetDeviceId, oldTargetPort);
|
||||||
|
await occupyPort(targetDeviceId, targetPort);
|
||||||
|
}
|
||||||
|
|
||||||
res.json(cable);
|
res.json(cable);
|
||||||
} else {
|
} else {
|
||||||
res.status(404).json({ error: '接线不存在' });
|
res.status(404).json({ error: '接线不存在' });
|
||||||
@@ -301,11 +470,24 @@ router.put('/:cableId', async (req, res) => {
|
|||||||
|
|
||||||
router.delete('/:cableId', async (req, res) => {
|
router.delete('/:cableId', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
// 先获取接线信息,用于后续恢复端口状态
|
||||||
|
const cable = await Cable.findByPk(req.params.cableId);
|
||||||
|
|
||||||
|
if (!cable) {
|
||||||
|
return res.status(404).json({ error: '接线不存在' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { sourceDeviceId, sourcePort, targetDeviceId, targetPort } = cable;
|
||||||
|
|
||||||
const deleted = await Cable.destroy({
|
const deleted = await Cable.destroy({
|
||||||
where: { cableId: req.params.cableId }
|
where: { cableId: req.params.cableId }
|
||||||
});
|
});
|
||||||
|
|
||||||
if (deleted) {
|
if (deleted) {
|
||||||
|
// 自动将源端口和目标端口状态恢复为free
|
||||||
|
await freePort(sourceDeviceId, sourcePort);
|
||||||
|
await freePort(targetDeviceId, targetPort);
|
||||||
|
|
||||||
res.status(204).json();
|
res.status(204).json();
|
||||||
} else {
|
} else {
|
||||||
res.status(404).json({ error: '接线不存在' });
|
res.status(404).json({ error: '接线不存在' });
|
||||||
@@ -324,10 +506,21 @@ router.delete('/batch', async (req, res) => {
|
|||||||
return res.status(400).json({ error: '请提供有效的接线ID列表' });
|
return res.status(400).json({ error: '请提供有效的接线ID列表' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 先获取所有要删除的接线信息,用于后续恢复端口状态
|
||||||
|
const cables = await Cable.findAll({
|
||||||
|
where: { cableId: { [Op.in]: cableIds } }
|
||||||
|
});
|
||||||
|
|
||||||
const deletedCount = await Cable.destroy({
|
const deletedCount = await Cable.destroy({
|
||||||
where: { cableId: { [Op.in]: cableIds } }
|
where: { cableId: { [Op.in]: cableIds } }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 自动将所有相关端口状态恢复为free
|
||||||
|
for (const cable of cables) {
|
||||||
|
await freePort(cable.sourceDeviceId, cable.sourcePort);
|
||||||
|
await freePort(cable.targetDeviceId, cable.targetPort);
|
||||||
|
}
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
message: `批量删除成功,已删除 ${deletedCount} 条接线`,
|
message: `批量删除成功,已删除 ${deletedCount} 条接线`,
|
||||||
deletedCount
|
deletedCount
|
||||||
|
|||||||
@@ -183,9 +183,13 @@ function DeviceDetailDrawer({ device, visible, onClose, cables, onRefreshCables,
|
|||||||
return (
|
return (
|
||||||
<Drawer
|
<Drawer
|
||||||
title={
|
title={
|
||||||
<Space>
|
<Space style={{ maxWidth: '280px', overflow: 'hidden' }}>
|
||||||
<CloudServerOutlined style={{ color: designTokens.colors.primary }} />
|
<CloudServerOutlined style={{ color: designTokens.colors.primary, flexShrink: 0 }} />
|
||||||
<span>设备详情 - {device.name}</span>
|
<span style={{
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap'
|
||||||
|
}}>设备详情 - {device.name}</span>
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
placement="right"
|
placement="right"
|
||||||
|
|||||||
@@ -0,0 +1,517 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Tooltip, Badge, Divider, Pagination } from 'antd';
|
||||||
|
import { LinkOutlined, SwapRightOutlined, AimOutlined, NodeIndexOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
|
const PortPanel = ({ ports, deviceName, deviceId, cables = [], devices = [], onPortClick, compact = false }) => {
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const [pageSize, setPageSize] = useState(48); // 默认每页48个端口
|
||||||
|
|
||||||
|
// 按端口名称排序(升序)
|
||||||
|
const sortedPorts = [...ports].sort((a, b) => {
|
||||||
|
// 尝试按数字部分排序,支持格式如:1/0/1, eth0/1, GigabitEthernet1/0/1 等
|
||||||
|
const extractNumbers = (str) => {
|
||||||
|
const matches = str.match(/\d+/g);
|
||||||
|
return matches ? matches.map(Number) : [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const numsA = extractNumbers(a.portName);
|
||||||
|
const numsB = extractNumbers(b.portName);
|
||||||
|
|
||||||
|
// 逐个比较数字部分
|
||||||
|
for (let i = 0; i < Math.min(numsA.length, numsB.length); i++) {
|
||||||
|
if (numsA[i] !== numsB[i]) {
|
||||||
|
return numsA[i] - numsB[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果数字部分相同,按字符串排序
|
||||||
|
return a.portName.localeCompare(b.portName);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 分页数据
|
||||||
|
const totalPorts = sortedPorts.length;
|
||||||
|
const startIndex = (currentPage - 1) * pageSize;
|
||||||
|
const endIndex = startIndex + pageSize;
|
||||||
|
const paginatedPorts = sortedPorts.slice(startIndex, endIndex);
|
||||||
|
|
||||||
|
// 获取端口状态颜色
|
||||||
|
const getPortStatusColor = (status) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'free':
|
||||||
|
return '#6b7280'; // 灰色 - 空闲
|
||||||
|
case 'occupied':
|
||||||
|
return '#10b981'; // 绿色 - 占用
|
||||||
|
case 'fault':
|
||||||
|
return '#ef4444'; // 红色 - 故障
|
||||||
|
case 'disabled':
|
||||||
|
return '#374151'; // 深灰色 - 禁用
|
||||||
|
default:
|
||||||
|
return '#6b7280';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取端口状态文本
|
||||||
|
const getPortStatusText = (status) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'free':
|
||||||
|
return '空闲';
|
||||||
|
case 'occupied':
|
||||||
|
return '已连接';
|
||||||
|
case 'fault':
|
||||||
|
return '故障';
|
||||||
|
case 'disabled':
|
||||||
|
return '禁用';
|
||||||
|
default:
|
||||||
|
return '空闲';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取端口类型图标 - 使用更真实的端口符号
|
||||||
|
const getPortTypeIcon = (portType) => {
|
||||||
|
switch (portType) {
|
||||||
|
case 'RJ45':
|
||||||
|
return '⬡'; // 六边形表示网口
|
||||||
|
case 'SFP':
|
||||||
|
case 'SFP+':
|
||||||
|
case 'SFP28':
|
||||||
|
return '▭'; // 矩形表示SFP
|
||||||
|
case 'QSFP':
|
||||||
|
case 'QSFP28':
|
||||||
|
return '▯'; // 宽矩形表示QSFP
|
||||||
|
default:
|
||||||
|
return '⬡';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取简化端口显示名称(只显示数字)
|
||||||
|
const getPortDisplayName = (portName) => {
|
||||||
|
// 提取最后的数字
|
||||||
|
const match = portName.match(/(\d+)$/);
|
||||||
|
if (match) {
|
||||||
|
return match[1];
|
||||||
|
}
|
||||||
|
// 如果没有数字,返回原名称
|
||||||
|
return portName;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取线缆类型文本
|
||||||
|
const getCableTypeText = (cableType) => {
|
||||||
|
const typeMap = {
|
||||||
|
'ethernet': '网线',
|
||||||
|
'fiber': '光纤',
|
||||||
|
'copper': '铜缆',
|
||||||
|
'power': '电源线'
|
||||||
|
};
|
||||||
|
return typeMap[cableType] || cableType || '未知';
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取线缆类型颜色
|
||||||
|
const getCableTypeColor = (cableType) => {
|
||||||
|
const colorMap = {
|
||||||
|
'ethernet': '#52c41a',
|
||||||
|
'fiber': '#1890ff',
|
||||||
|
'copper': '#faad14',
|
||||||
|
'power': '#ff4d4f'
|
||||||
|
};
|
||||||
|
return colorMap[cableType] || '#999';
|
||||||
|
};
|
||||||
|
|
||||||
|
// 查找端口关联的接线
|
||||||
|
const findPortCable = (port) => {
|
||||||
|
if (!cables || cables.length === 0) return null;
|
||||||
|
|
||||||
|
return cables.find(cable =>
|
||||||
|
(cable.sourceDeviceId === deviceId && cable.sourcePortId === port.portId) ||
|
||||||
|
(cable.targetDeviceId === deviceId && cable.targetPortId === port.portId) ||
|
||||||
|
(cable.sourceDeviceId === deviceId && cable.sourcePort === port.portName) ||
|
||||||
|
(cable.targetDeviceId === deviceId && cable.targetPort === port.portName)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取连接的对端信息
|
||||||
|
const getPeerInfo = (cable, currentPort) => {
|
||||||
|
if (!cable) return null;
|
||||||
|
|
||||||
|
const isSource = cable.sourceDeviceId === deviceId ||
|
||||||
|
(cable.sourcePortId && cable.sourcePortId === currentPort.portId) ||
|
||||||
|
cable.sourcePort === currentPort.portName;
|
||||||
|
|
||||||
|
if (isSource) {
|
||||||
|
// 当前是源端,返回目标端信息
|
||||||
|
const targetDevice = devices.find(d => d.deviceId === cable.targetDeviceId);
|
||||||
|
return {
|
||||||
|
deviceName: targetDevice?.name || cable.targetDeviceId,
|
||||||
|
deviceId: cable.targetDeviceId,
|
||||||
|
portName: cable.targetPort || cable.targetPortId,
|
||||||
|
direction: 'out'
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// 当前是目标端,返回源端信息
|
||||||
|
const sourceDevice = devices.find(d => d.deviceId === cable.sourceDeviceId);
|
||||||
|
return {
|
||||||
|
deviceName: sourceDevice?.name || cable.sourceDeviceId,
|
||||||
|
deviceId: cable.sourceDeviceId,
|
||||||
|
portName: cable.sourcePort || cable.sourcePortId,
|
||||||
|
direction: 'in'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 渲染端口详情提示
|
||||||
|
const renderPortTooltip = (port) => {
|
||||||
|
const cable = findPortCable(port);
|
||||||
|
const peerInfo = cable ? getPeerInfo(cable, port) : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '8px 4px', minWidth: '220px' }}>
|
||||||
|
{/* 端口基本信息 */}
|
||||||
|
<div style={{ fontWeight: 600, fontSize: 14, marginBottom: 8, borderBottom: '1px solid rgba(255,255,255,0.2)', paddingBottom: 4 }}>
|
||||||
|
<NodeIndexOutlined style={{ marginRight: 6 }} />
|
||||||
|
{port.portName}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: 12, lineHeight: '1.8' }}>
|
||||||
|
<div><span style={{ opacity: 0.7 }}>端口类型:</span> {port.portType}</div>
|
||||||
|
<div><span style={{ opacity: 0.7 }}>端口速率:</span> {port.portSpeed}</div>
|
||||||
|
<div><span style={{ opacity: 0.7 }}>状态:</span>
|
||||||
|
<span style={{
|
||||||
|
color: getPortStatusColor(port.status),
|
||||||
|
marginLeft: 4,
|
||||||
|
fontWeight: 500
|
||||||
|
}}>
|
||||||
|
{getPortStatusText(port.status)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{port.vlanId && <div><span style={{ opacity: 0.7 }}>VLAN:</span> {port.vlanId}</div>}
|
||||||
|
{port.description && <div><span style={{ opacity: 0.7 }}>描述:</span> {port.description}</div>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 接线信息 */}
|
||||||
|
{cable && peerInfo && (
|
||||||
|
<>
|
||||||
|
<Divider style={{ margin: '12px 0', borderColor: 'rgba(255,255,255,0.1)' }} />
|
||||||
|
<div style={{ fontWeight: 600, fontSize: 13, marginBottom: 8, color: '#69c0ff' }}>
|
||||||
|
<LinkOutlined style={{ marginRight: 6 }} />
|
||||||
|
接线详情
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: 12, lineHeight: '1.8' }}>
|
||||||
|
{/* 线缆类型和长度 */}
|
||||||
|
<div style={{ marginBottom: 6 }}>
|
||||||
|
<span style={{
|
||||||
|
display: 'inline-block',
|
||||||
|
padding: '2px 8px',
|
||||||
|
borderRadius: '4px',
|
||||||
|
background: getCableTypeColor(cable.cableType) + '20',
|
||||||
|
color: getCableTypeColor(cable.cableType),
|
||||||
|
fontSize: '11px',
|
||||||
|
fontWeight: 500
|
||||||
|
}}>
|
||||||
|
{getCableTypeText(cable.cableType)}
|
||||||
|
</span>
|
||||||
|
{cable.cableLength && (
|
||||||
|
<span style={{ marginLeft: 8, opacity: 0.8 }}>
|
||||||
|
{cable.cableLength}m
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 连接方向 */}
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '8px',
|
||||||
|
padding: '8px',
|
||||||
|
background: 'rgba(255,255,255,0.05)',
|
||||||
|
borderRadius: '6px',
|
||||||
|
marginTop: '8px'
|
||||||
|
}}>
|
||||||
|
<div style={{ textAlign: 'center' }}>
|
||||||
|
<div style={{
|
||||||
|
width: '32px',
|
||||||
|
height: '32px',
|
||||||
|
borderRadius: '50%',
|
||||||
|
background: peerInfo.direction === 'out' ? '#52c41a20' : '#1890ff20',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
fontSize: '14px'
|
||||||
|
}}>
|
||||||
|
{peerInfo.direction === 'out' ? '📤' : '📥'}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '10px', marginTop: '2px', opacity: 0.6 }}>
|
||||||
|
{peerInfo.direction === 'out' ? '输出' : '输入'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
<div style={{ fontWeight: 500, color: '#fff' }}>
|
||||||
|
{peerInfo.deviceName}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '11px', opacity: 0.7 }}>
|
||||||
|
端口: {peerInfo.portName}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '10px', opacity: 0.5 }}>
|
||||||
|
ID: {peerInfo.deviceId}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 线缆标签/备注 */}
|
||||||
|
{cable.label && (
|
||||||
|
<div style={{ marginTop: 8, opacity: 0.8 }}>
|
||||||
|
<span style={{ opacity: 0.7 }}>标签:</span> {cable.label}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{cable.notes && (
|
||||||
|
<div style={{ marginTop: 4, opacity: 0.8 }}>
|
||||||
|
<span style={{ opacity: 0.7 }}>备注:</span> {cable.notes}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 空闲端口提示 */}
|
||||||
|
{port.status === 'free' && !cable && (
|
||||||
|
<>
|
||||||
|
<Divider style={{ margin: '12px 0', borderColor: 'rgba(255,255,255,0.1)' }} />
|
||||||
|
<div style={{ fontSize: 12, opacity: 0.6, textAlign: 'center', padding: '4px 0' }}>
|
||||||
|
<AimOutlined style={{ marginRight: 4 }} />
|
||||||
|
端口空闲,暂无接线
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
background: 'linear-gradient(145deg, #1e293b 0%, #0f172a 100%)',
|
||||||
|
borderRadius: compact ? '12px' : '16px',
|
||||||
|
padding: compact ? '16px' : '24px',
|
||||||
|
boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.5), inset 0 1px 0 rgba(255, 255, 255, 0.1)',
|
||||||
|
border: '1px solid rgba(255, 255, 255, 0.1)'
|
||||||
|
}}>
|
||||||
|
{/* 设备标题 - compact 模式下隐藏 */}
|
||||||
|
{!compact && (
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
marginBottom: '20px',
|
||||||
|
paddingBottom: '16px',
|
||||||
|
borderBottom: '1px solid rgba(255, 255, 255, 0.1)'
|
||||||
|
}}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||||
|
<div style={{
|
||||||
|
width: '40px',
|
||||||
|
height: '40px',
|
||||||
|
borderRadius: '10px',
|
||||||
|
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
fontSize: '20px'
|
||||||
|
}}>
|
||||||
|
🔌
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: '16px', fontWeight: 600, color: '#fff' }}>
|
||||||
|
{deviceName || '交换机'}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '12px', color: 'rgba(255,255,255,0.5)', marginTop: '2px' }}>
|
||||||
|
{sortedPorts.length} 个端口
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 状态图例 */}
|
||||||
|
<div style={{ display: 'flex', gap: '16px' }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
||||||
|
<div style={{
|
||||||
|
width: '8px',
|
||||||
|
height: '8px',
|
||||||
|
borderRadius: '50%',
|
||||||
|
background: '#6b7280',
|
||||||
|
boxShadow: '0 0 8px #6b7280'
|
||||||
|
}} />
|
||||||
|
<span style={{ fontSize: '12px', color: 'rgba(255,255,255,0.6)' }}>空闲</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
||||||
|
<div style={{
|
||||||
|
width: '8px',
|
||||||
|
height: '8px',
|
||||||
|
borderRadius: '50%',
|
||||||
|
background: '#10b981',
|
||||||
|
boxShadow: '0 0 8px #10b981'
|
||||||
|
}} />
|
||||||
|
<span style={{ fontSize: '12px', color: 'rgba(255,255,255,0.6)' }}>已连接</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
||||||
|
<div style={{
|
||||||
|
width: '8px',
|
||||||
|
height: '8px',
|
||||||
|
borderRadius: '50%',
|
||||||
|
background: '#ef4444',
|
||||||
|
boxShadow: '0 0 8px #ef4444'
|
||||||
|
}} />
|
||||||
|
<span style={{ fontSize: '12px', color: 'rgba(255,255,255,0.6)' }}>故障</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 端口网格 - 固定每行24个端口 */}
|
||||||
|
<div style={{
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: 'repeat(24, 1fr)',
|
||||||
|
gap: '8px',
|
||||||
|
padding: '16px',
|
||||||
|
background: 'rgba(0, 0, 0, 0.3)',
|
||||||
|
borderRadius: '12px',
|
||||||
|
border: '1px solid rgba(255, 255, 255, 0.05)'
|
||||||
|
}}>
|
||||||
|
{paginatedPorts.map((port) => {
|
||||||
|
const statusColor = getPortStatusColor(port.status);
|
||||||
|
const isClickable = onPortClick && port.status !== 'disabled';
|
||||||
|
const cable = findPortCable(port);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
key={port.portId}
|
||||||
|
title={renderPortTooltip(port)}
|
||||||
|
placement="top"
|
||||||
|
color="#1e293b"
|
||||||
|
overlayStyle={{
|
||||||
|
borderRadius: '8px',
|
||||||
|
border: '1px solid rgba(255, 255, 255, 0.1)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
onClick={() => isClickable && onPortClick(port)}
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: '4px',
|
||||||
|
cursor: isClickable ? 'pointer' : 'not-allowed',
|
||||||
|
transition: 'all 0.2s ease',
|
||||||
|
position: 'relative',
|
||||||
|
minWidth: '0'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* LED 指示灯 - 在端口上方 */}
|
||||||
|
<div style={{
|
||||||
|
width: '6px',
|
||||||
|
height: '6px',
|
||||||
|
borderRadius: '50%',
|
||||||
|
background: statusColor,
|
||||||
|
boxShadow: `0 0 6px ${statusColor}, 0 0 12px ${statusColor}50`,
|
||||||
|
marginBottom: '4px',
|
||||||
|
animation: port.status === 'fault' ? 'pulse 1.5s infinite' : 'none'
|
||||||
|
}} />
|
||||||
|
|
||||||
|
{/* 端口主体 - 矩形样式 */}
|
||||||
|
<div style={{
|
||||||
|
width: '100%',
|
||||||
|
aspectRatio: '1 / 1.2',
|
||||||
|
background: 'linear-gradient(180deg, #2a3441 0%, #1e2530 100%)',
|
||||||
|
border: `2px solid ${statusColor}`,
|
||||||
|
borderRadius: '2px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
position: 'relative',
|
||||||
|
boxShadow: `inset 0 1px 0 rgba(255,255,255,0.1), 0 2px 4px rgba(0,0,0,0.3)`
|
||||||
|
}}>
|
||||||
|
{/* 端口内部图标 */}
|
||||||
|
<div style={{
|
||||||
|
fontSize: '10px',
|
||||||
|
color: statusColor,
|
||||||
|
opacity: 0.8
|
||||||
|
}}>
|
||||||
|
{getPortTypeIcon(port.portType)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 接线指示标记 */}
|
||||||
|
{cable && (
|
||||||
|
<div style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '1px',
|
||||||
|
right: '1px',
|
||||||
|
width: '4px',
|
||||||
|
height: '4px',
|
||||||
|
borderRadius: '50%',
|
||||||
|
background: getCableTypeColor(cable.cableType),
|
||||||
|
boxShadow: `0 0 3px ${getCableTypeColor(cable.cableType)}`
|
||||||
|
}} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 端口名称 - 在端口下方 */}
|
||||||
|
<div style={{
|
||||||
|
fontSize: '9px',
|
||||||
|
fontWeight: 500,
|
||||||
|
color: 'rgba(255, 255, 255, 0.7)',
|
||||||
|
textAlign: 'center',
|
||||||
|
marginTop: '3px',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
maxWidth: '100%'
|
||||||
|
}}>
|
||||||
|
{getPortDisplayName(port.portName)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 分页 */}
|
||||||
|
{totalPorts > pageSize && (
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
padding: '16px 0 0 0',
|
||||||
|
borderTop: '1px solid rgba(255, 255, 255, 0.1)',
|
||||||
|
marginTop: '16px'
|
||||||
|
}}>
|
||||||
|
<Pagination
|
||||||
|
current={currentPage}
|
||||||
|
total={totalPorts}
|
||||||
|
pageSize={pageSize}
|
||||||
|
onChange={(page, size) => {
|
||||||
|
setCurrentPage(page);
|
||||||
|
if (size) setPageSize(size);
|
||||||
|
}}
|
||||||
|
showSizeChanger
|
||||||
|
showQuickJumper
|
||||||
|
showTotal={(total) => `共 ${total} 个端口`}
|
||||||
|
pageSizeOptions={['24', '48', '96']}
|
||||||
|
size="small"
|
||||||
|
style={{
|
||||||
|
color: 'rgba(255, 255, 255, 0.8)'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 添加脉冲动画 */}
|
||||||
|
<style>{`
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% {
|
||||||
|
opacity: 1;
|
||||||
|
box-shadow: 0 0 10px #ef4444, 0 0 20px #ef444450;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.5;
|
||||||
|
box-shadow: 0 0 5px #ef4444, 0 0 10px #ef444450;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PortPanel;
|
||||||
@@ -0,0 +1,590 @@
|
|||||||
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
|
import { Badge, Space, Typography, Spin, Empty, Button, Tooltip, Tag, Modal } from 'antd';
|
||||||
|
import {
|
||||||
|
CloudServerOutlined,
|
||||||
|
PlusOutlined,
|
||||||
|
ReloadOutlined,
|
||||||
|
ApiOutlined,
|
||||||
|
ThunderboltOutlined,
|
||||||
|
DesktopOutlined,
|
||||||
|
UsbOutlined,
|
||||||
|
MonitorOutlined,
|
||||||
|
SettingOutlined
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import PortPanel from './PortPanel';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
const { Text } = Typography;
|
||||||
|
|
||||||
|
const designTokens = {
|
||||||
|
colors: {
|
||||||
|
primary: { main: '#667eea', gradient: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' },
|
||||||
|
success: '#10b981',
|
||||||
|
error: '#ef4444',
|
||||||
|
warning: '#f59e0b',
|
||||||
|
metal: { light: '#9ca3af', DEFAULT: '#6b7280', dark: '#4b5563' },
|
||||||
|
slot: { empty: '#d1d5db', occupied: '#3b82f6' }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 服务器背板可视化组件
|
||||||
|
* 按照真实服务器背板布局展示网卡和端口
|
||||||
|
*
|
||||||
|
* @param {string} deviceId - 设备ID
|
||||||
|
* @param {string} deviceName - 设备名称
|
||||||
|
* @param {Object[]} cables - 接线列表
|
||||||
|
* @param {Object[]} allDevices - 所有设备列表
|
||||||
|
* @param {Function} onPortClick - 端口点击回调
|
||||||
|
* @param {Function} onManageNetworkCards - 网卡管理回调
|
||||||
|
*/
|
||||||
|
const ServerBackplanePanel = ({
|
||||||
|
deviceId,
|
||||||
|
deviceName,
|
||||||
|
cables,
|
||||||
|
allDevices,
|
||||||
|
onPortClick,
|
||||||
|
onManageNetworkCards
|
||||||
|
}) => {
|
||||||
|
const [cards, setCards] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [selectedSlot, setSelectedSlot] = useState(null);
|
||||||
|
|
||||||
|
// 获取网卡及端口数据
|
||||||
|
const fetchData = useCallback(async () => {
|
||||||
|
if (!deviceId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await axios.get(`/api/network-cards/device/${deviceId}/with-ports`);
|
||||||
|
const cardsData = response.data || [];
|
||||||
|
setCards(cardsData);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取网卡数据失败:', error);
|
||||||
|
setCards([]);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [deviceId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchData();
|
||||||
|
}, [fetchData]);
|
||||||
|
|
||||||
|
// 按类型和槽位号对网卡进行分类
|
||||||
|
const categorizeCards = () => {
|
||||||
|
const onboard = []; // 板载网卡
|
||||||
|
const management = []; // 管理口
|
||||||
|
const expansionSlots = []; // 扩展插槽
|
||||||
|
|
||||||
|
cards.forEach(card => {
|
||||||
|
const slotNum = card.slotNumber;
|
||||||
|
const name = (card.name || '').toLowerCase();
|
||||||
|
|
||||||
|
// 判断网卡类型
|
||||||
|
if (name.includes('idrac') || name.includes('ilo') || name.includes('bmc') || name.includes('mgmt') || name.includes('管理')) {
|
||||||
|
management.push({ ...card, type: 'management' });
|
||||||
|
} else if (slotNum === 0 || name.includes('onboard') || name.includes('板载') || name.includes('内置')) {
|
||||||
|
onboard.push({ ...card, type: 'onboard' });
|
||||||
|
} else {
|
||||||
|
expansionSlots.push({ ...card, type: 'expansion', slotIndex: slotNum });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 按槽位号排序扩展插槽
|
||||||
|
expansionSlots.sort((a, b) => (a.slotNumber || 0) - (b.slotNumber || 0));
|
||||||
|
|
||||||
|
return { onboard, management, expansionSlots };
|
||||||
|
};
|
||||||
|
|
||||||
|
const { onboard, management, expansionSlots } = categorizeCards();
|
||||||
|
|
||||||
|
// 渲染管理口区域(左侧)
|
||||||
|
const renderManagementArea = () => {
|
||||||
|
const mgmtCard = management[0];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: '80px',
|
||||||
|
background: 'linear-gradient(180deg, #374151 0%, #1f2937 100%)',
|
||||||
|
borderRadius: '4px',
|
||||||
|
padding: '8px 4px',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '6px',
|
||||||
|
border: '2px solid #4b5563',
|
||||||
|
boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.1)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ fontSize: '10px', color: '#9ca3af', fontWeight: 600 }}>MGMT</div>
|
||||||
|
{mgmtCard ? (
|
||||||
|
<div
|
||||||
|
onClick={() => setSelectedSlot(mgmtCard)}
|
||||||
|
style={{
|
||||||
|
width: '48px',
|
||||||
|
height: '48px',
|
||||||
|
background: 'linear-gradient(145deg, #1f2937 0%, #111827 100%)',
|
||||||
|
borderRadius: '4px',
|
||||||
|
border: `2px solid ${mgmtCard.ports?.length > 0 ? designTokens.colors.success : '#6b7280'}`,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
cursor: 'pointer',
|
||||||
|
transition: 'all 0.2s',
|
||||||
|
boxShadow: '0 2px 4px rgba(0,0,0,0.3)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SettingOutlined style={{ fontSize: 16, color: '#10b981' }} />
|
||||||
|
<span style={{ fontSize: '9px', color: '#9ca3af', marginTop: 2 }}>
|
||||||
|
{mgmtCard.ports?.length || 0}口
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: '48px',
|
||||||
|
height: '48px',
|
||||||
|
background: '#374151',
|
||||||
|
borderRadius: '4px',
|
||||||
|
border: '2px dashed #6b7280',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PlusOutlined style={{ fontSize: 14, color: '#6b7280' }} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 其他接口占位 */}
|
||||||
|
<div style={{ width: '48px', height: '24px', background: '#1f2937', borderRadius: '2px', border: '1px solid #4b5563' }}>
|
||||||
|
<Text style={{ fontSize: '8px', color: '#6b7280', display: 'block', textAlign: 'center', lineHeight: '22px' }}>VGA</Text>
|
||||||
|
</div>
|
||||||
|
<div style={{ width: '48px', height: '16px', background: '#1f2937', borderRadius: '2px', border: '1px solid #4b5563' }}>
|
||||||
|
<Text style={{ fontSize: '8px', color: '#6b7280', display: 'block', textAlign: 'center', lineHeight: '14px' }}>USB</Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 渲染板载网卡区域
|
||||||
|
const renderOnboardArea = () => {
|
||||||
|
const onboardCard = onboard[0];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
background: 'linear-gradient(180deg, #4b5563 0%, #374151 100%)',
|
||||||
|
borderRadius: '4px',
|
||||||
|
padding: '12px',
|
||||||
|
border: '2px solid #6b7280',
|
||||||
|
boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.1)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '8px' }}>
|
||||||
|
<Text style={{ fontSize: '11px', color: '#d1d5db', fontWeight: 600 }}>板载网卡 (Onboard)</Text>
|
||||||
|
{onboardCard && (
|
||||||
|
<Badge
|
||||||
|
count={onboardCard.ports?.length || 0}
|
||||||
|
style={{ backgroundColor: designTokens.colors.primary.main }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{onboardCard ? (
|
||||||
|
<div
|
||||||
|
onClick={() => setSelectedSlot(onboardCard)}
|
||||||
|
style={{
|
||||||
|
background: 'linear-gradient(145deg, #1f2937 0%, #111827 100%)',
|
||||||
|
borderRadius: '6px',
|
||||||
|
padding: '12px',
|
||||||
|
border: `2px solid ${onboardCard.ports?.length > 0 ? designTokens.colors.primary.main : '#6b7280'}`,
|
||||||
|
cursor: 'pointer',
|
||||||
|
transition: 'all 0.2s'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* 4个RJ45端口布局 */}
|
||||||
|
<div style={{ display: 'flex', gap: '12px', justifyContent: 'center' }}>
|
||||||
|
{[0, 1, 2, 3].map((idx) => {
|
||||||
|
const port = onboardCard.ports?.[idx];
|
||||||
|
const hasPort = !!port;
|
||||||
|
const isOccupied = hasPort && port.status === 'occupied';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip key={idx} title={hasPort ? `${port.portName} - ${port.status}` : '未配置'}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: '40px',
|
||||||
|
height: '32px',
|
||||||
|
background: hasPort
|
||||||
|
? 'linear-gradient(180deg, #374151 0%, #1f2937 100%)'
|
||||||
|
: '#374151',
|
||||||
|
borderRadius: '4px',
|
||||||
|
border: `2px solid ${hasPort
|
||||||
|
? (isOccupied ? designTokens.colors.success : designTokens.colors.metal.light)
|
||||||
|
: '#4b5563'
|
||||||
|
}`,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
position: 'relative'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* LED指示灯 */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: '4px',
|
||||||
|
height: '4px',
|
||||||
|
borderRadius: '50%',
|
||||||
|
background: hasPort
|
||||||
|
? (isOccupied ? '#10b981' : '#6b7280')
|
||||||
|
: '#374151',
|
||||||
|
position: 'absolute',
|
||||||
|
top: '2px',
|
||||||
|
right: '2px',
|
||||||
|
boxShadow: isOccupied ? '0 0 4px #10b981' : 'none'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span style={{ fontSize: '8px', color: '#9ca3af' }}>⬡</span>
|
||||||
|
<span style={{ fontSize: '7px', color: '#6b7280', marginTop: '1px' }}>
|
||||||
|
{hasPort ? idx + 1 : '-'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
onClick={onManageNetworkCards}
|
||||||
|
style={{
|
||||||
|
background: '#374151',
|
||||||
|
borderRadius: '6px',
|
||||||
|
padding: '20px',
|
||||||
|
border: '2px dashed #6b7280',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
cursor: 'pointer',
|
||||||
|
gap: '8px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PlusOutlined style={{ fontSize: 20, color: '#6b7280' }} />
|
||||||
|
<Text style={{ fontSize: '11px', color: '#9ca3af' }}>添加板载网卡</Text>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 渲染扩展插槽区域
|
||||||
|
const renderExpansionSlots = () => {
|
||||||
|
// 标准2U服务器通常有4-8个PCIe插槽
|
||||||
|
const totalSlots = 6;
|
||||||
|
const slots = [];
|
||||||
|
|
||||||
|
for (let i = 1; i <= totalSlots; i++) {
|
||||||
|
const card = expansionSlots.find(c => c.slotNumber === i);
|
||||||
|
slots.push({ slotNumber: i, card });
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
flex: 1.5,
|
||||||
|
background: 'linear-gradient(180deg, #4b5563 0%, #374151 100%)',
|
||||||
|
borderRadius: '4px',
|
||||||
|
padding: '12px',
|
||||||
|
border: '2px solid #6b7280',
|
||||||
|
boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.1)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '8px' }}>
|
||||||
|
<Text style={{ fontSize: '11px', color: '#d1d5db', fontWeight: 600 }}>PCIe 扩展插槽</Text>
|
||||||
|
<Space size={4}>
|
||||||
|
<Badge count={expansionSlots.length} style={{ backgroundColor: designTokens.colors.primary.main }} />
|
||||||
|
<Text style={{ fontSize: '10px', color: '#9ca3af' }}>/{totalSlots}</Text>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
|
||||||
|
{slots.map(({ slotNumber, card }) => (
|
||||||
|
<Tooltip
|
||||||
|
key={slotNumber}
|
||||||
|
title={card ? `${card.name} (${card.ports?.length || 0}口)` : `插槽 ${slotNumber} (空闲)`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
onClick={() => card && setSelectedSlot(card)}
|
||||||
|
style={{
|
||||||
|
width: '70px',
|
||||||
|
height: '90px',
|
||||||
|
background: card
|
||||||
|
? 'linear-gradient(145deg, #1f2937 0%, #111827 100%)'
|
||||||
|
: '#374151',
|
||||||
|
borderRadius: '4px',
|
||||||
|
border: `2px solid ${card ? designTokens.colors.slot.occupied : designTokens.colors.slot.empty}`,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
padding: '6px',
|
||||||
|
cursor: card ? 'pointer' : 'default',
|
||||||
|
transition: 'all 0.2s',
|
||||||
|
boxShadow: card ? '0 2px 8px rgba(59, 130, 246, 0.3)' : 'none'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={{ fontSize: '9px', color: '#6b7280', fontWeight: 600 }}>Slot {slotNumber}</Text>
|
||||||
|
|
||||||
|
{card ? (
|
||||||
|
<>
|
||||||
|
<CloudServerOutlined style={{ fontSize: 20, color: '#3b82f6' }} />
|
||||||
|
<div style={{ display: 'flex', gap: '2px', flexWrap: 'wrap', justifyContent: 'center' }}>
|
||||||
|
{card.ports?.slice(0, 4).map((port, idx) => (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
style={{
|
||||||
|
width: '8px',
|
||||||
|
height: '8px',
|
||||||
|
borderRadius: '1px',
|
||||||
|
background: port.status === 'occupied' ? '#10b981' : '#6b7280',
|
||||||
|
boxShadow: port.status === 'occupied' ? '0 0 2px #10b981' : 'none'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{card.ports?.length > 4 && (
|
||||||
|
<Text style={{ fontSize: '8px', color: '#9ca3af' }}>+{card.ports.length - 4}</Text>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Text style={{ fontSize: '8px', color: '#9ca3af' }}>{card.ports?.length || 0}口</Text>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div style={{ width: '40px', height: '40px', border: '2px dashed #4b5563', borderRadius: '4px' }} />
|
||||||
|
<Text style={{ fontSize: '8px', color: '#6b7280' }}>空闲</Text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 渲染电源区域(右侧)
|
||||||
|
const renderPowerArea = () => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: '100px',
|
||||||
|
background: 'linear-gradient(180deg, #374151 0%, #1f2937 100%)',
|
||||||
|
borderRadius: '4px',
|
||||||
|
padding: '8px',
|
||||||
|
border: '2px solid #4b5563',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: '8px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={{ fontSize: '10px', color: '#9ca3af', fontWeight: 600, textAlign: 'center' }}>电源</Text>
|
||||||
|
{[1, 2].map((psu) => (
|
||||||
|
<div
|
||||||
|
key={psu}
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
background: 'linear-gradient(145deg, #1f2937 0%, #111827 100%)',
|
||||||
|
borderRadius: '4px',
|
||||||
|
border: '2px solid #10b981',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
gap: '4px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ThunderboltOutlined style={{ fontSize: 20, color: '#10b981' }} />
|
||||||
|
<Text style={{ fontSize: '9px', color: '#10b981' }}>PSU {psu}</Text>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: '6px',
|
||||||
|
height: '6px',
|
||||||
|
borderRadius: '50%',
|
||||||
|
background: '#10b981',
|
||||||
|
boxShadow: '0 0 6px #10b981'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div style={{ textAlign: 'center', padding: '40px' }}>
|
||||||
|
<Spin size="large" tip="加载背板数据中..." />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
||||||
|
{/* 工具栏 */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: '8px 12px',
|
||||||
|
background: '#f8fafc',
|
||||||
|
borderRadius: '8px',
|
||||||
|
border: '1px solid #e2e8f0'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Space>
|
||||||
|
<Badge count={cards.filter(c => !c.isUngrouped).length} style={{ backgroundColor: designTokens.colors.primary.main }} />
|
||||||
|
<Text type="secondary" style={{ fontSize: '13px' }}>个网卡</Text>
|
||||||
|
<Badge
|
||||||
|
count={cards.reduce((acc, card) => acc + (card.ports?.length || 0), 0)}
|
||||||
|
style={{ backgroundColor: '#667eea' }}
|
||||||
|
/>
|
||||||
|
<Text type="secondary" style={{ fontSize: '13px' }}>个端口</Text>
|
||||||
|
</Space>
|
||||||
|
<Space>
|
||||||
|
<Button size="small" icon={<ReloadOutlined />} onClick={fetchData}>
|
||||||
|
刷新
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
icon={<CloudServerOutlined />}
|
||||||
|
onClick={onManageNetworkCards}
|
||||||
|
style={{ background: designTokens.colors.primary.gradient, border: 'none' }}
|
||||||
|
>
|
||||||
|
网卡管理
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 服务器背板主体 */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
background: 'linear-gradient(180deg, #6b7280 0%, #4b5563 100%)',
|
||||||
|
borderRadius: '8px',
|
||||||
|
padding: '16px',
|
||||||
|
border: '3px solid #374151',
|
||||||
|
boxShadow: 'inset 0 2px 4px rgba(0,0,0,0.2), 0 4px 12px rgba(0,0,0,0.3)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* 服务器标识 */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
marginBottom: '12px',
|
||||||
|
padding: '6px 12px',
|
||||||
|
background: 'rgba(0,0,0,0.3)',
|
||||||
|
borderRadius: '4px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DesktopOutlined style={{ fontSize: 14, color: '#9ca3af', marginRight: 8 }} />
|
||||||
|
<Text style={{ fontSize: '12px', color: '#d1d5db', fontWeight: 600 }}>
|
||||||
|
{deviceName || '服务器'} - 背板视图
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 背板布局 */}
|
||||||
|
<div style={{ display: 'flex', gap: '12px', alignItems: 'stretch' }}>
|
||||||
|
{/* 左侧:管理口区域 */}
|
||||||
|
{renderManagementArea()}
|
||||||
|
|
||||||
|
{/* 中间左:板载网卡 */}
|
||||||
|
{renderOnboardArea()}
|
||||||
|
|
||||||
|
{/* 中间:扩展插槽 */}
|
||||||
|
{renderExpansionSlots()}
|
||||||
|
|
||||||
|
{/* 右侧:电源 */}
|
||||||
|
{renderPowerArea()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 选中插槽的端口详情模态框 */}
|
||||||
|
<Modal
|
||||||
|
title={
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||||
|
<CloudServerOutlined style={{ color: '#667eea' }} />
|
||||||
|
<span>
|
||||||
|
{selectedSlot?.name}
|
||||||
|
{selectedSlot?.slotNumber > 0 && ` (Slot ${selectedSlot.slotNumber})`}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
open={!!selectedSlot}
|
||||||
|
onCancel={() => setSelectedSlot(null)}
|
||||||
|
footer={null}
|
||||||
|
width={700}
|
||||||
|
destroyOnClose
|
||||||
|
>
|
||||||
|
{selectedSlot && (
|
||||||
|
<div>
|
||||||
|
<div style={{ marginBottom: '16px', padding: '12px', background: '#f8fafc', borderRadius: '8px' }}>
|
||||||
|
<Space direction="vertical" size={4} style={{ width: '100%' }}>
|
||||||
|
<Text type="secondary">类型: {selectedSlot.type === 'onboard' ? '板载网卡' : selectedSlot.type === 'management' ? '管理口' : '扩展网卡'}</Text>
|
||||||
|
{selectedSlot.description && <Text type="secondary">描述: {selectedSlot.description}</Text>}
|
||||||
|
<div>
|
||||||
|
<Text type="secondary">端口统计: </Text>
|
||||||
|
<Space size={8}>
|
||||||
|
<Tag color="success">空闲: {selectedSlot.stats?.free || 0}</Tag>
|
||||||
|
<Tag color="processing">占用: {selectedSlot.stats?.occupied || 0}</Tag>
|
||||||
|
{selectedSlot.stats?.fault > 0 && <Tag color="error">故障: {selectedSlot.stats.fault}</Tag>}
|
||||||
|
<Tag color="blue">总计: {selectedSlot.ports?.length || 0}</Tag>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedSlot.ports && selectedSlot.ports.length > 0 ? (
|
||||||
|
<PortPanel
|
||||||
|
ports={selectedSlot.ports}
|
||||||
|
deviceName={deviceName}
|
||||||
|
deviceId={deviceId}
|
||||||
|
cables={cables}
|
||||||
|
devices={allDevices}
|
||||||
|
onPortClick={onPortClick}
|
||||||
|
compact={true}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Empty
|
||||||
|
description={
|
||||||
|
<span>
|
||||||
|
该网卡暂无端口
|
||||||
|
<br />
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
icon={<PlusOutlined />}
|
||||||
|
onClick={onManageNetworkCards}
|
||||||
|
style={{ padding: 0, marginTop: 8 }}
|
||||||
|
>
|
||||||
|
前往网卡管理添加端口
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ServerBackplanePanel;
|
||||||
@@ -0,0 +1,345 @@
|
|||||||
|
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||||
|
import { Button, Empty, Spin, Badge, Typography, Space, Checkbox, Tooltip } from 'antd';
|
||||||
|
import { DownOutlined, UpOutlined, EyeOutlined, EyeInvisibleOutlined, PlusOutlined, CloudServerOutlined } from '@ant-design/icons';
|
||||||
|
import ServerBackplanePanel from './ServerBackplanePanel';
|
||||||
|
|
||||||
|
const { Text } = Typography;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 虚拟设备列表组件
|
||||||
|
* 用于优化大量设备面板的渲染性能
|
||||||
|
*
|
||||||
|
* @param {Object[]} devices - 设备列表
|
||||||
|
* @param {Object} groupedPorts - 按设备分组的端口数据
|
||||||
|
* @param {Object[]} cables - 接线列表
|
||||||
|
* @param {Object[]} allDevices - 所有设备列表(用于查找设备信息)
|
||||||
|
* @param {Function} onPortClick - 端口点击回调
|
||||||
|
* @param {Function} onAddPort - 添加端口回调 (device) => void
|
||||||
|
* @param {Function} onManageNetworkCards - 网卡管理回调 (device) => void
|
||||||
|
* @param {number} initialVisibleCount - 初始显示数量
|
||||||
|
* @param {number} loadMoreCount - 每次加载更多数量
|
||||||
|
*/
|
||||||
|
const VirtualDeviceList = ({
|
||||||
|
devices,
|
||||||
|
groupedPorts,
|
||||||
|
cables,
|
||||||
|
allDevices,
|
||||||
|
onPortClick,
|
||||||
|
onAddPort,
|
||||||
|
onManageNetworkCards,
|
||||||
|
initialVisibleCount = 5,
|
||||||
|
loadMoreCount = 5
|
||||||
|
}) => {
|
||||||
|
const [visibleCount, setVisibleCount] = useState(initialVisibleCount);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [expandedDevices, setExpandedDevices] = useState({});
|
||||||
|
const [showAll, setShowAll] = useState(false);
|
||||||
|
const containerRef = useRef(null);
|
||||||
|
const observerRef = useRef(null);
|
||||||
|
|
||||||
|
// 初始化展开状态
|
||||||
|
useEffect(() => {
|
||||||
|
const initialExpanded = {};
|
||||||
|
devices.slice(0, initialVisibleCount).forEach((device, index) => {
|
||||||
|
initialExpanded[device.deviceId] = index < 3; // 前3个默认展开
|
||||||
|
});
|
||||||
|
setExpandedDevices(initialExpanded);
|
||||||
|
}, [devices, initialVisibleCount]);
|
||||||
|
|
||||||
|
// 无限滚动观察器
|
||||||
|
useEffect(() => {
|
||||||
|
if (showAll) return;
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
root: null,
|
||||||
|
rootMargin: '100px',
|
||||||
|
threshold: 0.1
|
||||||
|
};
|
||||||
|
|
||||||
|
observerRef.current = new IntersectionObserver((entries) => {
|
||||||
|
entries.forEach(entry => {
|
||||||
|
if (entry.isIntersecting && !loading && visibleCount < devices.length) {
|
||||||
|
loadMore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, options);
|
||||||
|
|
||||||
|
const loadMoreTrigger = document.getElementById('load-more-trigger');
|
||||||
|
if (loadMoreTrigger) {
|
||||||
|
observerRef.current.observe(loadMoreTrigger);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (observerRef.current) {
|
||||||
|
observerRef.current.disconnect();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [visibleCount, devices.length, loading, showAll]);
|
||||||
|
|
||||||
|
const loadMore = useCallback(() => {
|
||||||
|
if (loading || visibleCount >= devices.length) return;
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
// 模拟异步加载,实际可以直接同步更新
|
||||||
|
setTimeout(() => {
|
||||||
|
setVisibleCount(prev => Math.min(prev + loadMoreCount, devices.length));
|
||||||
|
setLoading(false);
|
||||||
|
}, 100);
|
||||||
|
}, [loading, visibleCount, devices.length, loadMoreCount]);
|
||||||
|
|
||||||
|
const handleShowAll = useCallback(() => {
|
||||||
|
// 先显示所有设备
|
||||||
|
setVisibleCount(devices.length);
|
||||||
|
// 展开所有设备
|
||||||
|
const allExpanded = {};
|
||||||
|
devices.forEach(device => {
|
||||||
|
allExpanded[device.deviceId] = true;
|
||||||
|
});
|
||||||
|
setExpandedDevices(allExpanded);
|
||||||
|
setShowAll(true);
|
||||||
|
}, [devices]);
|
||||||
|
|
||||||
|
const handleCollapseAll = useCallback(() => {
|
||||||
|
// 收起所有面板(折叠所有设备),但保持当前显示的设备数量
|
||||||
|
const allCollapsed = {};
|
||||||
|
devices.forEach(device => {
|
||||||
|
allCollapsed[device.deviceId] = false;
|
||||||
|
});
|
||||||
|
setExpandedDevices(allCollapsed);
|
||||||
|
setShowAll(false);
|
||||||
|
}, [devices]);
|
||||||
|
|
||||||
|
const toggleDeviceExpand = (deviceId) => {
|
||||||
|
setExpandedDevices(prev => ({
|
||||||
|
...prev,
|
||||||
|
[deviceId]: !prev[deviceId]
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const visibleDevices = devices.slice(0, visibleCount);
|
||||||
|
const hasMore = visibleCount < devices.length;
|
||||||
|
|
||||||
|
if (devices.length === 0) {
|
||||||
|
return (
|
||||||
|
<Empty
|
||||||
|
description="暂无设备数据"
|
||||||
|
style={{ padding: '60px 0' }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={containerRef} style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
|
||||||
|
{/* 控制栏 */}
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: '12px 16px',
|
||||||
|
background: '#f8fafc',
|
||||||
|
borderRadius: '8px',
|
||||||
|
border: '1px solid #e2e8f0'
|
||||||
|
}}>
|
||||||
|
<Space align="center">
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||||
|
<Text strong style={{ fontSize: '14px' }}>设备列表</Text>
|
||||||
|
<Badge
|
||||||
|
count={devices.length}
|
||||||
|
style={{ backgroundColor: '#667eea' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Text type="secondary" style={{ fontSize: '12px' }}>
|
||||||
|
显示 {visibleDevices.length} / {devices.length}
|
||||||
|
</Text>
|
||||||
|
</Space>
|
||||||
|
|
||||||
|
<Space>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
icon={showAll ? <UpOutlined /> : <DownOutlined />}
|
||||||
|
onClick={showAll ? handleCollapseAll : handleShowAll}
|
||||||
|
>
|
||||||
|
{showAll ? '收起全部' : '展开全部'}
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 设备面板列表 */}
|
||||||
|
{visibleDevices.map((device) => {
|
||||||
|
const deviceId = device.deviceId;
|
||||||
|
const data = groupedPorts[deviceId] || { device, ports: [] };
|
||||||
|
const isExpanded = expandedDevices[deviceId];
|
||||||
|
const portCount = data.ports?.length || 0;
|
||||||
|
const occupiedCount = data.ports?.filter(p => p.status === 'occupied').length || 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={deviceId}
|
||||||
|
style={{
|
||||||
|
border: '1px solid #e2e8f0',
|
||||||
|
borderRadius: '12px',
|
||||||
|
overflow: 'hidden',
|
||||||
|
background: '#fff',
|
||||||
|
transition: 'all 0.3s ease'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* 设备标题栏 */}
|
||||||
|
<div
|
||||||
|
onClick={() => toggleDeviceExpand(deviceId)}
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
padding: '16px 20px',
|
||||||
|
background: isExpanded ? '#f1f5f9' : '#fff',
|
||||||
|
cursor: 'pointer',
|
||||||
|
borderBottom: isExpanded ? '1px solid #e2e8f0' : 'none',
|
||||||
|
transition: 'background 0.2s'
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
e.currentTarget.style.background = '#f1f5f9';
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
if (!isExpanded) {
|
||||||
|
e.currentTarget.style.background = '#fff';
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||||
|
<div style={{
|
||||||
|
width: '40px',
|
||||||
|
height: '40px',
|
||||||
|
borderRadius: '10px',
|
||||||
|
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
fontSize: '20px'
|
||||||
|
}}>
|
||||||
|
{device.type?.toLowerCase()?.includes('server') ? '🖥️' :
|
||||||
|
device.type?.toLowerCase()?.includes('switch') ? '🔀' :
|
||||||
|
device.type?.toLowerCase()?.includes('router') ? '🌐' : '📦'}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontWeight: 600, fontSize: '15px', color: '#1e293b' }}>
|
||||||
|
{device.name || '未知设备'}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '12px', color: '#64748b', marginTop: '2px' }}>
|
||||||
|
{device.deviceId} · {device.type || '未知类型'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Space size="middle">
|
||||||
|
<Space size="small">
|
||||||
|
<Badge
|
||||||
|
count={occupiedCount}
|
||||||
|
style={{ backgroundColor: '#3b82f6' }}
|
||||||
|
overflowCount={999}
|
||||||
|
/>
|
||||||
|
<Text type="secondary" style={{ fontSize: '12px' }}>
|
||||||
|
已用
|
||||||
|
</Text>
|
||||||
|
<Badge
|
||||||
|
count={portCount}
|
||||||
|
style={{ backgroundColor: '#10b981' }}
|
||||||
|
overflowCount={999}
|
||||||
|
/>
|
||||||
|
<Text type="secondary" style={{ fontSize: '12px' }}>
|
||||||
|
端口
|
||||||
|
</Text>
|
||||||
|
</Space>
|
||||||
|
|
||||||
|
{/* 网卡管理按钮 - 只有服务器显示 */}
|
||||||
|
{device.type?.toLowerCase()?.includes('server') && (
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
icon={<CloudServerOutlined />}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation(); // 防止触发折叠
|
||||||
|
onManageNetworkCards && onManageNetworkCards(device);
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||||
|
border: 'none'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
网卡管理
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 添加端口按钮 */}
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
icon={<PlusOutlined />}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation(); // 防止触发折叠
|
||||||
|
onAddPort && onAddPort(device);
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
background: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
|
||||||
|
border: 'none'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
添加端口
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
icon={isExpanded ? <UpOutlined /> : <DownOutlined />}
|
||||||
|
style={{ color: '#64748b' }}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 面板内容 - 可折叠 */}
|
||||||
|
{isExpanded && (
|
||||||
|
<div style={{ padding: '16px' }}>
|
||||||
|
<ServerBackplanePanel
|
||||||
|
deviceId={deviceId}
|
||||||
|
deviceName={device.name}
|
||||||
|
cables={cables}
|
||||||
|
allDevices={allDevices}
|
||||||
|
onPortClick={onPortClick}
|
||||||
|
onManageNetworkCards={() => onManageNetworkCards && onManageNetworkCards(device)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{/* 加载更多触发器 */}
|
||||||
|
{hasMore && !showAll && (
|
||||||
|
<div
|
||||||
|
id="load-more-trigger"
|
||||||
|
style={{
|
||||||
|
textAlign: 'center',
|
||||||
|
padding: '20px',
|
||||||
|
color: '#64748b'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<Spin size="small" tip="加载更多设备..." />
|
||||||
|
) : (
|
||||||
|
<Text type="secondary">
|
||||||
|
向下滚动加载更多 ({devices.length - visibleCount} 个设备)
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 已显示全部提示 */}
|
||||||
|
{!hasMore && devices.length > initialVisibleCount && (
|
||||||
|
<div style={{ textAlign: 'center', padding: '20px', color: '#94a3b8' }}>
|
||||||
|
<Text type="secondary">已显示全部 {devices.length} 个设备</Text>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default VirtualDeviceList;
|
||||||
@@ -73,16 +73,17 @@ function CableManagement() {
|
|||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const params = {};
|
const params = {};
|
||||||
|
|
||||||
if (filters.switchDeviceId) params.sourceDeviceId = filters.switchDeviceId;
|
if (filters.switchDeviceId) params.sourceDeviceId = filters.switchDeviceId;
|
||||||
if (filters.status !== 'all') params.status = filters.status;
|
if (filters.status !== 'all') params.status = filters.status;
|
||||||
if (filters.cableType !== 'all') params.cableType = filters.cableType;
|
if (filters.cableType !== 'all') params.cableType = filters.cableType;
|
||||||
|
|
||||||
const response = await axios.get('/api/cables', { params });
|
const response = await axios.get('/api/cables', { params });
|
||||||
setCables(response.data.cables || []);
|
const cablesData = response.data.cables || [];
|
||||||
|
setCables(cablesData);
|
||||||
|
|
||||||
const grouped = {};
|
const grouped = {};
|
||||||
response.data.cables.forEach(cable => {
|
cablesData.forEach(cable => {
|
||||||
const switchId = cable.sourceDeviceId;
|
const switchId = cable.sourceDeviceId;
|
||||||
if (!grouped[switchId]) {
|
if (!grouped[switchId]) {
|
||||||
grouped[switchId] = {
|
grouped[switchId] = {
|
||||||
@@ -93,22 +94,36 @@ function CableManagement() {
|
|||||||
grouped[switchId].cables.push(cable);
|
grouped[switchId].cables.push(cable);
|
||||||
});
|
});
|
||||||
setGroupedCables(grouped);
|
setGroupedCables(grouped);
|
||||||
|
|
||||||
|
// 自动为每个交换机加载端口数据
|
||||||
|
const switchIds = Object.keys(grouped);
|
||||||
|
for (const switchId of switchIds) {
|
||||||
|
if (!devicePorts[switchId]) {
|
||||||
|
try {
|
||||||
|
const portsResponse = await axios.get(`/api/device-ports/device/${switchId}`);
|
||||||
|
setDevicePorts(prev => ({ ...prev, [switchId]: portsResponse.data || [] }));
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`获取交换机 ${switchId} 端口失败:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error('获取接线列表失败');
|
message.error('获取接线列表失败');
|
||||||
console.error('获取接线列表失败:', error);
|
console.error('获取接线列表失败:', error);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [filters]);
|
}, [filters, devicePorts]);
|
||||||
|
|
||||||
const fetchDevices = useCallback(async () => {
|
const fetchDevices = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get('/api/devices', { params: { pageSize: 1000 } });
|
const response = await axios.get('/api/devices', { params: { pageSize: 100 } });
|
||||||
const allDevices = response.data.devices || [];
|
const allDevices = response.data.devices || [];
|
||||||
const switches = allDevices.filter(device => device.type === 'switch');
|
const switches = allDevices.filter(device => device.type === 'switch');
|
||||||
setDevices(allDevices);
|
setDevices(allDevices);
|
||||||
setSwitchDevices(switches);
|
setSwitchDevices(switches);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
message.error('获取设备列表失败');
|
||||||
console.error('获取设备列表失败:', error);
|
console.error('获取设备列表失败:', error);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
@@ -189,27 +204,87 @@ function CableManagement() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const [conflictModalVisible, setConflictModalVisible] = useState(false);
|
||||||
|
const [conflictInfo, setConflictInfo] = useState(null);
|
||||||
|
const [pendingSubmitValues, setPendingSubmitValues] = useState(null);
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
try {
|
try {
|
||||||
const values = await form.validateFields();
|
const values = await form.validateFields();
|
||||||
|
|
||||||
|
// 如果是编辑模式,直接提交
|
||||||
if (editingCable) {
|
if (editingCable) {
|
||||||
await axios.put(`/api/cables/${editingCable.cableId}`, values);
|
await axios.put(`/api/cables/${editingCable.cableId}`, values);
|
||||||
message.success('更新成功');
|
message.success('更新成功');
|
||||||
} else {
|
setModalVisible(false);
|
||||||
|
form.resetFields();
|
||||||
|
fetchCables();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建模式:先检查冲突
|
||||||
|
try {
|
||||||
|
const checkResponse = await axios.post('/api/cables/check-conflict', {
|
||||||
|
sourceDeviceId: values.sourceDeviceId,
|
||||||
|
sourcePort: values.sourcePort,
|
||||||
|
targetDeviceId: values.targetDeviceId,
|
||||||
|
targetPort: values.targetPort
|
||||||
|
});
|
||||||
|
|
||||||
|
if (checkResponse.data.hasConflict) {
|
||||||
|
setConflictInfo(checkResponse.data.conflicts);
|
||||||
|
setPendingSubmitValues(values);
|
||||||
|
setConflictModalVisible(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 无冲突,直接创建
|
||||||
await axios.post('/api/cables', values);
|
await axios.post('/api/cables', values);
|
||||||
message.success('创建成功');
|
message.success('创建成功');
|
||||||
|
setModalVisible(false);
|
||||||
|
form.resetFields();
|
||||||
|
fetchCables();
|
||||||
|
} catch (error) {
|
||||||
|
if (error.response?.status === 409) {
|
||||||
|
// 冲突错误
|
||||||
|
setConflictInfo([{
|
||||||
|
type: 'unknown',
|
||||||
|
existingCable: error.response.data.existingCable
|
||||||
|
}]);
|
||||||
|
setPendingSubmitValues(values);
|
||||||
|
setConflictModalVisible(true);
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setModalVisible(false);
|
|
||||||
form.resetFields();
|
|
||||||
fetchCables();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error(editingCable ? '更新失败' : '创建失败');
|
message.error(editingCable ? '更新失败' : '创建失败');
|
||||||
console.error('提交失败:', error);
|
console.error('提交失败:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleForceSubmit = async () => {
|
||||||
|
try {
|
||||||
|
if (!pendingSubmitValues) return;
|
||||||
|
|
||||||
|
await axios.post('/api/cables', {
|
||||||
|
...pendingSubmitValues,
|
||||||
|
force: true
|
||||||
|
});
|
||||||
|
|
||||||
|
message.success('接线已强制接管并创建成功');
|
||||||
|
setConflictModalVisible(false);
|
||||||
|
setModalVisible(false);
|
||||||
|
form.resetFields();
|
||||||
|
setPendingSubmitValues(null);
|
||||||
|
setConflictInfo(null);
|
||||||
|
fetchCables();
|
||||||
|
} catch (error) {
|
||||||
|
message.error('强制接管失败');
|
||||||
|
console.error('强制接管失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleImport = () => {
|
const handleImport = () => {
|
||||||
setImportModalVisible(true);
|
setImportModalVisible(true);
|
||||||
setImportPreview([]);
|
setImportPreview([]);
|
||||||
@@ -562,9 +637,12 @@ function CableManagement() {
|
|||||||
onChange={(value) => setFilters(prev => ({ ...prev, switchDeviceId: value }))}
|
onChange={(value) => setFilters(prev => ({ ...prev, switchDeviceId: value }))}
|
||||||
allowClear
|
allowClear
|
||||||
showSearch
|
showSearch
|
||||||
filterOption={(input, option) =>
|
filterOption={(input, option) => {
|
||||||
option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
const device = switchDevices.find(d => d.deviceId === option.value);
|
||||||
}
|
if (!device) return false;
|
||||||
|
const searchText = `${device.name} ${device.deviceId}`.toLowerCase();
|
||||||
|
return searchText.indexOf(input.toLowerCase()) >= 0;
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{switchDevices.map(device => (
|
{switchDevices.map(device => (
|
||||||
<Option key={device.deviceId} value={device.deviceId}>
|
<Option key={device.deviceId} value={device.deviceId}>
|
||||||
@@ -572,7 +650,7 @@ function CableManagement() {
|
|||||||
</Option>
|
</Option>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
placeholder="线缆类型"
|
placeholder="线缆类型"
|
||||||
style={{ width: 120 }}
|
style={{ width: 120 }}
|
||||||
@@ -762,12 +840,15 @@ function CableManagement() {
|
|||||||
label="源设备"
|
label="源设备"
|
||||||
rules={[{ required: true, message: '请选择源设备' }]}
|
rules={[{ required: true, message: '请选择源设备' }]}
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
placeholder="请选择源设备"
|
placeholder="请选择源设备"
|
||||||
showSearch
|
showSearch
|
||||||
filterOption={(input, option) =>
|
filterOption={(input, option) => {
|
||||||
option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
const device = switchDevices.find(d => d.deviceId === option.value);
|
||||||
}
|
if (!device) return false;
|
||||||
|
const searchText = `${device.name} ${device.deviceId}`.toLowerCase();
|
||||||
|
return searchText.indexOf(input.toLowerCase()) >= 0;
|
||||||
|
}}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
fetchDevicePorts(value);
|
fetchDevicePorts(value);
|
||||||
form.setFieldsValue({ sourcePort: undefined });
|
form.setFieldsValue({ sourcePort: undefined });
|
||||||
@@ -780,18 +861,22 @@ function CableManagement() {
|
|||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="sourcePort"
|
name="sourcePort"
|
||||||
label="源设备端口"
|
label="源设备端口"
|
||||||
rules={[{ required: true, message: '请选择源设备端口' }]}
|
rules={[{ required: true, message: '请选择源设备端口' }]}
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
placeholder="请先选择源设备"
|
placeholder="请先选择源设备"
|
||||||
showSearch
|
showSearch
|
||||||
filterOption={(input, option) =>
|
filterOption={(input, option) => {
|
||||||
option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
const ports = devicePorts[form.getFieldValue('sourceDeviceId')] || [];
|
||||||
}
|
const port = ports.find(p => p.portName === option.value);
|
||||||
|
if (!port) return false;
|
||||||
|
const searchText = `${port.portName} ${port.portType} ${port.portSpeed}`.toLowerCase();
|
||||||
|
return searchText.indexOf(input.toLowerCase()) >= 0;
|
||||||
|
}}
|
||||||
disabled={!form.getFieldValue('sourceDeviceId')}
|
disabled={!form.getFieldValue('sourceDeviceId')}
|
||||||
>
|
>
|
||||||
{(devicePorts[form.getFieldValue('sourceDeviceId')] || []).map(port => (
|
{(devicePorts[form.getFieldValue('sourceDeviceId')] || []).map(port => (
|
||||||
@@ -801,18 +886,21 @@ function CableManagement() {
|
|||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="targetDeviceId"
|
name="targetDeviceId"
|
||||||
label="目标设备"
|
label="目标设备"
|
||||||
rules={[{ required: true, message: '请选择目标设备' }]}
|
rules={[{ required: true, message: '请选择目标设备' }]}
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
placeholder="请选择目标设备"
|
placeholder="请选择目标设备"
|
||||||
showSearch
|
showSearch
|
||||||
filterOption={(input, option) =>
|
filterOption={(input, option) => {
|
||||||
option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
const device = devices.find(d => d.deviceId === option.value);
|
||||||
}
|
if (!device) return false;
|
||||||
|
const searchText = `${device.name} ${device.deviceId}`.toLowerCase();
|
||||||
|
return searchText.indexOf(input.toLowerCase()) >= 0;
|
||||||
|
}}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
fetchDevicePorts(value);
|
fetchDevicePorts(value);
|
||||||
form.setFieldsValue({ targetPort: undefined });
|
form.setFieldsValue({ targetPort: undefined });
|
||||||
@@ -825,18 +913,22 @@ function CableManagement() {
|
|||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="targetPort"
|
name="targetPort"
|
||||||
label="目标设备端口"
|
label="目标设备端口"
|
||||||
rules={[{ required: true, message: '请选择目标设备端口' }]}
|
rules={[{ required: true, message: '请选择目标设备端口' }]}
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
placeholder="请先选择目标设备"
|
placeholder="请先选择目标设备"
|
||||||
showSearch
|
showSearch
|
||||||
filterOption={(input, option) =>
|
filterOption={(input, option) => {
|
||||||
option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
const ports = devicePorts[form.getFieldValue('targetDeviceId')] || [];
|
||||||
}
|
const port = ports.find(p => p.portName === option.value);
|
||||||
|
if (!port) return false;
|
||||||
|
const searchText = `${port.portName} ${port.portType} ${port.portSpeed}`.toLowerCase();
|
||||||
|
return searchText.indexOf(input.toLowerCase()) >= 0;
|
||||||
|
}}
|
||||||
disabled={!form.getFieldValue('targetDeviceId')}
|
disabled={!form.getFieldValue('targetDeviceId')}
|
||||||
>
|
>
|
||||||
{(devicePorts[form.getFieldValue('targetDeviceId')] || []).map(port => (
|
{(devicePorts[form.getFieldValue('targetDeviceId')] || []).map(port => (
|
||||||
@@ -846,7 +938,7 @@ function CableManagement() {
|
|||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="cableType"
|
name="cableType"
|
||||||
label="线缆类型"
|
label="线缆类型"
|
||||||
@@ -1052,6 +1144,85 @@ function CableManagement() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
{/* 冲突提示弹窗 */}
|
||||||
|
<Modal
|
||||||
|
title="端口冲突警告"
|
||||||
|
open={conflictModalVisible}
|
||||||
|
onCancel={() => {
|
||||||
|
setConflictModalVisible(false);
|
||||||
|
setConflictInfo(null);
|
||||||
|
setPendingSubmitValues(null);
|
||||||
|
}}
|
||||||
|
footer={[
|
||||||
|
<Button
|
||||||
|
key="cancel"
|
||||||
|
onClick={() => {
|
||||||
|
setConflictModalVisible(false);
|
||||||
|
setConflictInfo(null);
|
||||||
|
setPendingSubmitValues(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</Button>,
|
||||||
|
<Button
|
||||||
|
key="force"
|
||||||
|
type="primary"
|
||||||
|
danger
|
||||||
|
onClick={handleForceSubmit}
|
||||||
|
>
|
||||||
|
强制接管
|
||||||
|
</Button>
|
||||||
|
]}
|
||||||
|
width={600}
|
||||||
|
>
|
||||||
|
{conflictInfo && (
|
||||||
|
<div>
|
||||||
|
<div style={{ marginBottom: 16, color: '#ef4444', fontWeight: 500 }}>
|
||||||
|
<span style={{ fontSize: 20, marginRight: 8 }}>⚠️</span>
|
||||||
|
检测到端口冲突,以下端口已被占用:
|
||||||
|
</div>
|
||||||
|
{conflictInfo.map((conflict, index) => (
|
||||||
|
<Card
|
||||||
|
key={index}
|
||||||
|
size="small"
|
||||||
|
style={{ marginBottom: 12, background: '#fef2f2', border: '1px solid #fecaca' }}
|
||||||
|
>
|
||||||
|
<div style={{ marginBottom: 8 }}>
|
||||||
|
<Tag color="error">
|
||||||
|
{conflict.type === 'source' ? '源端口' : conflict.type === 'target' ? '目标端口' : '端口'}
|
||||||
|
</Tag>
|
||||||
|
<span style={{ fontWeight: 500, marginLeft: 8 }}>{conflict.port}</span>
|
||||||
|
</div>
|
||||||
|
{conflict.existingCable && (
|
||||||
|
<div style={{ fontSize: 13, color: '#666' }}>
|
||||||
|
<div>当前连接:</div>
|
||||||
|
<div style={{ marginTop: 4, paddingLeft: 12 }}>
|
||||||
|
<div>
|
||||||
|
源设备:{conflict.existingCable.sourceDevice?.name || conflict.existingCable.sourceDeviceId}
|
||||||
|
({conflict.existingCable.sourcePort})
|
||||||
|
</div>
|
||||||
|
<div style={{ marginTop: 2 }}>
|
||||||
|
目标设备:{conflict.existingCable.targetDevice?.name || conflict.existingCable.targetDeviceId}
|
||||||
|
({conflict.existingCable.targetPort})
|
||||||
|
</div>
|
||||||
|
<div style={{ marginTop: 2 }}>
|
||||||
|
线缆类型:{getCableTypeTag(conflict.existingCable.cableType)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
<div style={{ marginTop: 16, padding: 12, background: '#fff7ed', borderRadius: 6, border: '1px solid #fed7aa' }}>
|
||||||
|
<span style={{ color: '#ea580c' }}>💡</span>
|
||||||
|
<span style={{ marginLeft: 8, color: '#9a3412' }}>
|
||||||
|
点击"强制接管"将断开原有连接并创建新接线。此操作不可恢复!
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
import React, { useState, useEffect, useCallback } from 'react';
|
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
||||||
import { Table, Button, Modal, Form, Input, Select, message, Card, Space, Popconfirm, Tag, Tooltip, InputNumber, Collapse, Empty, Spin, Upload, Progress, Checkbox } from 'antd';
|
import { Table, Button, Modal, Form, Input, Select, message, Card, Space, Popconfirm, Tag, Tooltip, InputNumber, Collapse, Empty, Spin, Upload, Progress, Checkbox, Tabs, Badge, List, Typography } from 'antd';
|
||||||
import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined, ReloadOutlined, ExportOutlined, ImportOutlined, DownloadOutlined, UploadOutlined as UploadIcon } from '@ant-design/icons';
|
import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined, ReloadOutlined, ExportOutlined, ImportOutlined, DownloadOutlined, UploadOutlined as UploadIcon, AppstoreOutlined, UnorderedListOutlined, FilterOutlined, EyeOutlined, CompressOutlined, CloudServerOutlined } from '@ant-design/icons';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import * as XLSX from 'xlsx';
|
import * as XLSX from 'xlsx';
|
||||||
import Papa from 'papaparse';
|
import Papa from 'papaparse';
|
||||||
|
import PortPanel from '../components/PortPanel';
|
||||||
|
import VirtualDeviceList from '../components/VirtualDeviceList';
|
||||||
|
import NetworkCardPanel from '../components/NetworkCardPanel';
|
||||||
|
import NetworkCardCreateModal from '../components/NetworkCardCreateModal';
|
||||||
|
import PortCreateModal from '../components/PortCreateModal';
|
||||||
|
|
||||||
const { Option } = Select;
|
const { Option } = Select;
|
||||||
const { Panel } = Collapse;
|
const { Panel } = Collapse;
|
||||||
@@ -48,6 +53,7 @@ const designTokens = {
|
|||||||
function PortManagement() {
|
function PortManagement() {
|
||||||
const [ports, setPorts] = useState([]);
|
const [ports, setPorts] = useState([]);
|
||||||
const [devices, setDevices] = useState([]);
|
const [devices, setDevices] = useState([]);
|
||||||
|
const [cables, setCables] = useState([]);
|
||||||
const [groupedPorts, setGroupedPorts] = useState({});
|
const [groupedPorts, setGroupedPorts] = useState({});
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [filters, setFilters] = useState({
|
const [filters, setFilters] = useState({
|
||||||
@@ -59,7 +65,7 @@ function PortManagement() {
|
|||||||
const [modalVisible, setModalVisible] = useState(false);
|
const [modalVisible, setModalVisible] = useState(false);
|
||||||
const [editingPort, setEditingPort] = useState(null);
|
const [editingPort, setEditingPort] = useState(null);
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
const [importModalVisible, setImportModalVisible] = useState(false);
|
const [importModalVisible, setImportModalVisible] = useState(false);
|
||||||
const [importFileList, setImportFileList] = useState([]);
|
const [importFileList, setImportFileList] = useState([]);
|
||||||
const [importPreview, setImportPreview] = useState([]);
|
const [importPreview, setImportPreview] = useState([]);
|
||||||
@@ -68,10 +74,30 @@ function PortManagement() {
|
|||||||
const [skipExisting, setSkipExisting] = useState(false);
|
const [skipExisting, setSkipExisting] = useState(false);
|
||||||
const [updateExisting, setUpdateExisting] = useState(false);
|
const [updateExisting, setUpdateExisting] = useState(false);
|
||||||
|
|
||||||
|
// 视图模式:list 或 panel
|
||||||
|
const [viewMode, setViewMode] = useState('list');
|
||||||
|
|
||||||
|
// 面板视图优化状态
|
||||||
|
const [panelFilters, setPanelFilters] = useState({
|
||||||
|
deviceType: 'all',
|
||||||
|
searchText: '',
|
||||||
|
showOnlyOccupied: false
|
||||||
|
});
|
||||||
|
const [visibleDeviceCount, setVisibleDeviceCount] = useState(10);
|
||||||
|
const [expandedDevices, setExpandedDevices] = useState({});
|
||||||
|
|
||||||
|
// 网卡管理相关状态
|
||||||
|
const [networkCardModalVisible, setNetworkCardModalVisible] = useState(false);
|
||||||
|
const [portCreateModalVisible, setPortCreateModalVisible] = useState(false);
|
||||||
|
const [selectedDeviceForNic, setSelectedDeviceForNic] = useState(null);
|
||||||
|
const [refreshTrigger, setRefreshTrigger] = useState(0);
|
||||||
|
|
||||||
const fetchPorts = useCallback(async () => {
|
const fetchPorts = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const params = {};
|
const params = {
|
||||||
|
pageSize: 1000 // 获取所有端口,不分页
|
||||||
|
};
|
||||||
|
|
||||||
if (filters.deviceId) params.deviceId = filters.deviceId;
|
if (filters.deviceId) params.deviceId = filters.deviceId;
|
||||||
if (filters.status !== 'all') params.status = filters.status;
|
if (filters.status !== 'all') params.status = filters.status;
|
||||||
@@ -90,17 +116,28 @@ function PortManagement() {
|
|||||||
|
|
||||||
const fetchDevices = useCallback(async () => {
|
const fetchDevices = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get('/api/devices', { params: { pageSize: 1000 } });
|
const response = await axios.get('/api/devices', { params: { pageSize: 100 } });
|
||||||
setDevices(response.data.devices || response.data || []);
|
setDevices(response.data.devices || response.data || []);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
message.error('获取设备列表失败');
|
||||||
console.error('获取设备列表失败:', error);
|
console.error('获取设备列表失败:', error);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const fetchCables = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get('/api/cables');
|
||||||
|
setCables(response.data.cables || response.data || []);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取接线列表失败:', error);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchPorts();
|
fetchPorts();
|
||||||
fetchDevices();
|
fetchDevices();
|
||||||
}, [fetchPorts, fetchDevices]);
|
fetchCables();
|
||||||
|
}, [fetchPorts, fetchDevices, fetchCables]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const grouped = {};
|
const grouped = {};
|
||||||
@@ -114,6 +151,25 @@ function PortManagement() {
|
|||||||
}
|
}
|
||||||
grouped[deviceId].ports.push(port);
|
grouped[deviceId].ports.push(port);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 对每个设备的端口按名称升序排序
|
||||||
|
Object.keys(grouped).forEach(deviceId => {
|
||||||
|
grouped[deviceId].ports.sort((a, b) => {
|
||||||
|
const extractNumbers = (str) => {
|
||||||
|
const matches = str.match(/\d+/g);
|
||||||
|
return matches ? matches.map(Number) : [];
|
||||||
|
};
|
||||||
|
const numsA = extractNumbers(a.portName);
|
||||||
|
const numsB = extractNumbers(b.portName);
|
||||||
|
for (let i = 0; i < Math.min(numsA.length, numsB.length); i++) {
|
||||||
|
if (numsA[i] !== numsB[i]) {
|
||||||
|
return numsA[i] - numsB[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return a.portName.localeCompare(b.portName);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
setGroupedPorts(grouped);
|
setGroupedPorts(grouped);
|
||||||
}, [ports, devices]);
|
}, [ports, devices]);
|
||||||
|
|
||||||
@@ -136,6 +192,41 @@ function PortManagement() {
|
|||||||
setModalVisible(true);
|
setModalVisible(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleAddPortForDevice = (device) => {
|
||||||
|
setEditingPort(null);
|
||||||
|
form.resetFields();
|
||||||
|
// 自动选中当前设备
|
||||||
|
form.setFieldsValue({
|
||||||
|
deviceId: device.deviceId
|
||||||
|
});
|
||||||
|
setModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 打开网卡管理模态框
|
||||||
|
const handleManageNetworkCards = (device) => {
|
||||||
|
setSelectedDeviceForNic(device);
|
||||||
|
setNetworkCardModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 打开添加网卡模态框
|
||||||
|
const handleAddNetworkCard = (device) => {
|
||||||
|
setSelectedDeviceForNic(device);
|
||||||
|
setPortCreateModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 网卡/端口创建成功回调
|
||||||
|
const handleNicSuccess = () => {
|
||||||
|
message.success('操作成功');
|
||||||
|
setRefreshTrigger(prev => prev + 1);
|
||||||
|
fetchPorts();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePortSuccess = () => {
|
||||||
|
message.success('端口添加成功');
|
||||||
|
setRefreshTrigger(prev => prev + 1);
|
||||||
|
fetchPorts();
|
||||||
|
};
|
||||||
|
|
||||||
const handleEdit = (port) => {
|
const handleEdit = (port) => {
|
||||||
setEditingPort(port);
|
setEditingPort(port);
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
@@ -162,6 +253,21 @@ function PortManagement() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 解析端口名称范围,例如 "1/0/1-1/0/48" -> ["1/0/1", "1/0/2", ..., "1/0/48"]
|
||||||
|
const parsePortRange = (portName) => {
|
||||||
|
const rangeMatch = portName.match(/^(.*?)\/(\d+)-\1\/(\d+)$/);
|
||||||
|
if (rangeMatch) {
|
||||||
|
const prefix = rangeMatch[1];
|
||||||
|
const start = parseInt(rangeMatch[2]);
|
||||||
|
const end = parseInt(rangeMatch[3]);
|
||||||
|
|
||||||
|
if (start <= end && end - start < 100) { // 限制最多100个端口
|
||||||
|
return Array.from({ length: end - start + 1 }, (_, i) => `${prefix}/${start + i}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [portName]; // 如果不是范围格式,返回原名称
|
||||||
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
try {
|
try {
|
||||||
const values = await form.validateFields();
|
const values = await form.validateFields();
|
||||||
@@ -170,8 +276,35 @@ function PortManagement() {
|
|||||||
await axios.put(`/api/device-ports/${editingPort.portId}`, values);
|
await axios.put(`/api/device-ports/${editingPort.portId}`, values);
|
||||||
message.success('更新成功');
|
message.success('更新成功');
|
||||||
} else {
|
} else {
|
||||||
await axios.post('/api/device-ports', values);
|
// 解析端口名称范围
|
||||||
message.success('创建成功');
|
const portNames = parsePortRange(values.portName);
|
||||||
|
|
||||||
|
if (portNames.length > 1) {
|
||||||
|
// 批量创建端口
|
||||||
|
const portsData = portNames.map((name, index) => ({
|
||||||
|
portId: `PORT-${Date.now()}-${index}`,
|
||||||
|
deviceId: values.deviceId,
|
||||||
|
portName: name,
|
||||||
|
portType: values.portType,
|
||||||
|
portSpeed: values.portSpeed,
|
||||||
|
status: values.status,
|
||||||
|
vlanId: values.vlanId,
|
||||||
|
description: values.description
|
||||||
|
}));
|
||||||
|
|
||||||
|
const response = await axios.post('/api/device-ports/batch', { ports: portsData });
|
||||||
|
const { success, failed } = response.data;
|
||||||
|
|
||||||
|
if (failed > 0) {
|
||||||
|
message.warning(`批量创建完成!成功 ${success} 个,失败 ${failed} 个`);
|
||||||
|
} else {
|
||||||
|
message.success(`成功创建 ${success} 个端口`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 单个创建
|
||||||
|
await axios.post('/api/device-ports', values);
|
||||||
|
message.success('创建成功');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setModalVisible(false);
|
setModalVisible(false);
|
||||||
@@ -480,9 +613,12 @@ function PortManagement() {
|
|||||||
onChange={(value) => setFilters(prev => ({ ...prev, deviceId: value }))}
|
onChange={(value) => setFilters(prev => ({ ...prev, deviceId: value }))}
|
||||||
allowClear
|
allowClear
|
||||||
showSearch
|
showSearch
|
||||||
filterOption={(input, option) =>
|
filterOption={(input, option) => {
|
||||||
option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
const device = devices.find(d => d.deviceId === option.value);
|
||||||
}
|
if (!device) return false;
|
||||||
|
const searchText = `${device.name} ${device.deviceId}`.toLowerCase();
|
||||||
|
return searchText.indexOf(input.toLowerCase()) >= 0;
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{devices.map(device => (
|
{devices.map(device => (
|
||||||
<Option key={device.deviceId} value={device.deviceId}>
|
<Option key={device.deviceId} value={device.deviceId}>
|
||||||
@@ -490,7 +626,7 @@ function PortManagement() {
|
|||||||
</Option>
|
</Option>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
placeholder="端口类型"
|
placeholder="端口类型"
|
||||||
style={{ width: 120 }}
|
style={{ width: 120 }}
|
||||||
@@ -548,32 +684,51 @@ function PortManagement() {
|
|||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ marginBottom: 16 }}>
|
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
<Space>
|
<Space>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
icon={<PlusOutlined />}
|
icon={<PlusOutlined />}
|
||||||
onClick={handleAdd}
|
onClick={handleAdd}
|
||||||
style={{ background: designTokens.colors.primary.gradient, border: 'none' }}
|
style={{ background: designTokens.colors.primary.gradient, border: 'none' }}
|
||||||
>
|
>
|
||||||
新增端口
|
新增端口
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
icon={<ImportOutlined />}
|
icon={<ImportOutlined />}
|
||||||
onClick={handleImport}
|
onClick={handleImport}
|
||||||
style={{ background: designTokens.colors.primary.gradient, border: 'none' }}
|
style={{ background: designTokens.colors.primary.gradient, border: 'none' }}
|
||||||
>
|
>
|
||||||
批量导入
|
批量导入
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button icon={<ExportOutlined />}>
|
<Button icon={<ExportOutlined />}>
|
||||||
导出
|
导出
|
||||||
</Button>
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
|
|
||||||
|
<Space>
|
||||||
|
<Button.Group>
|
||||||
|
<Button
|
||||||
|
type={viewMode === 'list' ? 'primary' : 'default'}
|
||||||
|
icon={<UnorderedListOutlined />}
|
||||||
|
onClick={() => setViewMode('list')}
|
||||||
|
>
|
||||||
|
列表
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type={viewMode === 'panel' ? 'primary' : 'default'}
|
||||||
|
icon={<AppstoreOutlined />}
|
||||||
|
onClick={() => setViewMode('panel')}
|
||||||
|
>
|
||||||
|
面板
|
||||||
|
</Button>
|
||||||
|
</Button.Group>
|
||||||
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div style={{ textAlign: 'center', padding: '40px' }}>
|
<div style={{ textAlign: 'center', padding: '40px' }}>
|
||||||
<Spin size="large" tip="加载端口数据中..." />
|
<Spin size="large" tip="加载端口数据中..." />
|
||||||
@@ -582,7 +737,21 @@ function PortManagement() {
|
|||||||
<div style={{ textAlign: 'center', padding: '60px' }}>
|
<div style={{ textAlign: 'center', padding: '60px' }}>
|
||||||
<Empty description="暂无端口数据" />
|
<Empty description="暂无端口数据" />
|
||||||
</div>
|
</div>
|
||||||
|
) : viewMode === 'panel' ? (
|
||||||
|
// 面板视图 - 使用虚拟滚动优化
|
||||||
|
<VirtualDeviceList
|
||||||
|
devices={Object.values(groupedPorts).map(g => g.device).filter(Boolean)}
|
||||||
|
groupedPorts={groupedPorts}
|
||||||
|
cables={cables}
|
||||||
|
allDevices={devices}
|
||||||
|
onPortClick={(port) => handleEdit(port)}
|
||||||
|
onAddPort={(device) => handleAddPortForDevice(device)}
|
||||||
|
onManageNetworkCards={(device) => handleManageNetworkCards(device)}
|
||||||
|
initialVisibleCount={5}
|
||||||
|
loadMoreCount={5}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
// 列表视图
|
||||||
<Collapse
|
<Collapse
|
||||||
defaultActiveKey={Object.keys(groupedPorts).slice(0, 5)}
|
defaultActiveKey={Object.keys(groupedPorts).slice(0, 5)}
|
||||||
style={{ background: '#f5f5f5' }}
|
style={{ background: '#f5f5f5' }}
|
||||||
@@ -593,7 +762,7 @@ function PortManagement() {
|
|||||||
const freeCount = devicePorts.filter(p => p.status === 'free').length;
|
const freeCount = devicePorts.filter(p => p.status === 'free').length;
|
||||||
const occupiedCount = devicePorts.filter(p => p.status === 'occupied').length;
|
const occupiedCount = devicePorts.filter(p => p.status === 'occupied').length;
|
||||||
const faultCount = devicePorts.filter(p => p.status === 'fault').length;
|
const faultCount = devicePorts.filter(p => p.status === 'fault').length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Panel
|
<Panel
|
||||||
key={deviceId}
|
key={deviceId}
|
||||||
@@ -611,8 +780,8 @@ function PortManagement() {
|
|||||||
color: '#fff',
|
color: '#fff',
|
||||||
fontSize: '18px'
|
fontSize: '18px'
|
||||||
}}>
|
}}>
|
||||||
{device?.type?.toLowerCase()?.includes('server') ? '🖥️' :
|
{device?.type?.toLowerCase()?.includes('server') ? '🖥️' :
|
||||||
device?.type?.toLowerCase()?.includes('switch') ? '🔀' :
|
device?.type?.toLowerCase()?.includes('switch') ? '🔀' :
|
||||||
device?.type?.toLowerCase()?.includes('router') ? '🌐' : '📦'}
|
device?.type?.toLowerCase()?.includes('router') ? '🌐' : '📦'}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -629,6 +798,21 @@ function PortManagement() {
|
|||||||
<Tag color="processing">占用: {occupiedCount}</Tag>
|
<Tag color="processing">占用: {occupiedCount}</Tag>
|
||||||
<Tag color="error">故障: {faultCount}</Tag>
|
<Tag color="error">故障: {faultCount}</Tag>
|
||||||
<Tag color="blue">总计: {devicePorts.length}</Tag>
|
<Tag color="blue">总计: {devicePorts.length}</Tag>
|
||||||
|
{/* 网卡管理按钮 - 只有服务器显示 */}
|
||||||
|
{device?.type?.toLowerCase()?.includes('server') && (
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
icon={<CloudServerOutlined />}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleManageNetworkCards(device);
|
||||||
|
}}
|
||||||
|
style={{ background: designTokens.colors.primary.gradient, border: 'none' }}
|
||||||
|
>
|
||||||
|
网卡管理
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -637,7 +821,12 @@ function PortManagement() {
|
|||||||
columns={portColumns}
|
columns={portColumns}
|
||||||
dataSource={devicePorts}
|
dataSource={devicePorts}
|
||||||
rowKey="portId"
|
rowKey="portId"
|
||||||
pagination={false}
|
pagination={{
|
||||||
|
pageSize: 10,
|
||||||
|
showSizeChanger: true,
|
||||||
|
showTotal: (total) => `共 ${total} 个端口`,
|
||||||
|
pageSizeOptions: ['10', '20', '50', '100']
|
||||||
|
}}
|
||||||
size="small"
|
size="small"
|
||||||
scroll={{ x: 1000 }}
|
scroll={{ x: 1000 }}
|
||||||
/>
|
/>
|
||||||
@@ -666,12 +855,15 @@ function PortManagement() {
|
|||||||
label="设备"
|
label="设备"
|
||||||
rules={[{ required: true, message: '请选择设备' }]}
|
rules={[{ required: true, message: '请选择设备' }]}
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
placeholder="请选择设备"
|
placeholder="请选择设备"
|
||||||
showSearch
|
showSearch
|
||||||
filterOption={(input, option) =>
|
filterOption={(input, option) => {
|
||||||
option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
const device = devices.find(d => d.deviceId === option.value);
|
||||||
}
|
if (!device) return false;
|
||||||
|
const searchText = `${device.name} ${device.deviceId}`.toLowerCase();
|
||||||
|
return searchText.indexOf(input.toLowerCase()) >= 0;
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{devices.map(device => (
|
{devices.map(device => (
|
||||||
<Option key={device.deviceId} value={device.deviceId}>
|
<Option key={device.deviceId} value={device.deviceId}>
|
||||||
@@ -685,8 +877,9 @@ function PortManagement() {
|
|||||||
name="portName"
|
name="portName"
|
||||||
label="端口名称"
|
label="端口名称"
|
||||||
rules={[{ required: true, message: '请输入端口名称' }]}
|
rules={[{ required: true, message: '请输入端口名称' }]}
|
||||||
|
extra={!editingPort && "支持批量添加,例如: 1/0/1-1/0/48 将创建 48 个端口"}
|
||||||
>
|
>
|
||||||
<Input placeholder="例如: eth0/1" />
|
<Input placeholder="例如: eth0/1 或 1/0/1-1/0/48" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
@@ -896,8 +1089,8 @@ function PortManagement() {
|
|||||||
<div style={{ textAlign: 'center', padding: '24px' }}>
|
<div style={{ textAlign: 'center', padding: '24px' }}>
|
||||||
<Spin size="large" tip="导入中..." />
|
<Spin size="large" tip="导入中..." />
|
||||||
<div style={{ marginTop: 16 }}>
|
<div style={{ marginTop: 16 }}>
|
||||||
<Progress
|
<Progress
|
||||||
percent={Math.round((importProgress.current / importProgress.total) * 100)}
|
percent={Math.round((importProgress.current / importProgress.total) * 100)}
|
||||||
status="active"
|
status="active"
|
||||||
strokeColor={{
|
strokeColor={{
|
||||||
'0%': designTokens.colors.primary.main,
|
'0%': designTokens.colors.primary.main,
|
||||||
@@ -919,6 +1112,44 @@ function PortManagement() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
{/* 网卡管理模态框 */}
|
||||||
|
<Modal
|
||||||
|
title={
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||||
|
<CloudServerOutlined style={{ color: '#667eea' }} />
|
||||||
|
<span>网卡管理 - {selectedDeviceForNic?.name}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
open={networkCardModalVisible}
|
||||||
|
onCancel={() => {
|
||||||
|
setNetworkCardModalVisible(false);
|
||||||
|
setSelectedDeviceForNic(null);
|
||||||
|
}}
|
||||||
|
footer={null}
|
||||||
|
width={800}
|
||||||
|
destroyOnClose
|
||||||
|
>
|
||||||
|
{selectedDeviceForNic && (
|
||||||
|
<NetworkCardPanel
|
||||||
|
deviceId={selectedDeviceForNic.deviceId}
|
||||||
|
deviceName={selectedDeviceForNic.name}
|
||||||
|
onRefresh={fetchPorts}
|
||||||
|
refreshTrigger={refreshTrigger}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
{/* 创建网卡模态框 */}
|
||||||
|
<NetworkCardCreateModal
|
||||||
|
device={selectedDeviceForNic}
|
||||||
|
visible={portCreateModalVisible}
|
||||||
|
onClose={() => {
|
||||||
|
setPortCreateModalVisible(false);
|
||||||
|
setSelectedDeviceForNic(null);
|
||||||
|
}}
|
||||||
|
onSuccess={handleNicSuccess}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user