feat: 端口管理页面添加网卡功能和服务器背板可视化
- 在端口管理页面添加网卡管理功能,与3D可视化页面同步 - 新增 ServerBackplanePanel 组件,按真实服务器背板布局展示网卡和端口 - 支持板载网卡、管理口、PCIe扩展插槽的可视化展示 - 添加网卡-端口层级展示,点击网卡可查看端口详情 - 更新 VirtualDeviceList 组件,集成服务器背板视图
This commit is contained in:
@@ -73,16 +73,17 @@ function CableManagement() {
|
||||
try {
|
||||
setLoading(true);
|
||||
const params = {};
|
||||
|
||||
|
||||
if (filters.switchDeviceId) params.sourceDeviceId = filters.switchDeviceId;
|
||||
if (filters.status !== 'all') params.status = filters.status;
|
||||
if (filters.cableType !== 'all') params.cableType = filters.cableType;
|
||||
|
||||
|
||||
const response = await axios.get('/api/cables', { params });
|
||||
setCables(response.data.cables || []);
|
||||
|
||||
const cablesData = response.data.cables || [];
|
||||
setCables(cablesData);
|
||||
|
||||
const grouped = {};
|
||||
response.data.cables.forEach(cable => {
|
||||
cablesData.forEach(cable => {
|
||||
const switchId = cable.sourceDeviceId;
|
||||
if (!grouped[switchId]) {
|
||||
grouped[switchId] = {
|
||||
@@ -93,22 +94,36 @@ function CableManagement() {
|
||||
grouped[switchId].cables.push(cable);
|
||||
});
|
||||
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) {
|
||||
message.error('获取接线列表失败');
|
||||
console.error('获取接线列表失败:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [filters]);
|
||||
}, [filters, devicePorts]);
|
||||
|
||||
const fetchDevices = useCallback(async () => {
|
||||
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 switches = allDevices.filter(device => device.type === 'switch');
|
||||
setDevices(allDevices);
|
||||
setSwitchDevices(switches);
|
||||
} catch (error) {
|
||||
message.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 () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
|
||||
|
||||
// 如果是编辑模式,直接提交
|
||||
if (editingCable) {
|
||||
await axios.put(`/api/cables/${editingCable.cableId}`, values);
|
||||
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);
|
||||
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) {
|
||||
message.error(editingCable ? '更新失败' : '创建失败');
|
||||
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 = () => {
|
||||
setImportModalVisible(true);
|
||||
setImportPreview([]);
|
||||
@@ -562,9 +637,12 @@ function CableManagement() {
|
||||
onChange={(value) => setFilters(prev => ({ ...prev, switchDeviceId: value }))}
|
||||
allowClear
|
||||
showSearch
|
||||
filterOption={(input, option) =>
|
||||
option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
}
|
||||
filterOption={(input, option) => {
|
||||
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 => (
|
||||
<Option key={device.deviceId} value={device.deviceId}>
|
||||
@@ -572,7 +650,7 @@ function CableManagement() {
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
|
||||
<Select
|
||||
placeholder="线缆类型"
|
||||
style={{ width: 120 }}
|
||||
@@ -762,12 +840,15 @@ function CableManagement() {
|
||||
label="源设备"
|
||||
rules={[{ required: true, message: '请选择源设备' }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="请选择源设备"
|
||||
<Select
|
||||
placeholder="请选择源设备"
|
||||
showSearch
|
||||
filterOption={(input, option) =>
|
||||
option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
}
|
||||
filterOption={(input, option) => {
|
||||
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) => {
|
||||
fetchDevicePorts(value);
|
||||
form.setFieldsValue({ sourcePort: undefined });
|
||||
@@ -780,18 +861,22 @@ function CableManagement() {
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
|
||||
<Form.Item
|
||||
name="sourcePort"
|
||||
label="源设备端口"
|
||||
rules={[{ required: true, message: '请选择源设备端口' }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="请先选择源设备"
|
||||
<Select
|
||||
placeholder="请先选择源设备"
|
||||
showSearch
|
||||
filterOption={(input, option) =>
|
||||
option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
}
|
||||
filterOption={(input, option) => {
|
||||
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')}
|
||||
>
|
||||
{(devicePorts[form.getFieldValue('sourceDeviceId')] || []).map(port => (
|
||||
@@ -801,18 +886,21 @@ function CableManagement() {
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
|
||||
<Form.Item
|
||||
name="targetDeviceId"
|
||||
label="目标设备"
|
||||
rules={[{ required: true, message: '请选择目标设备' }]}
|
||||
>
|
||||
<Select
|
||||
<Select
|
||||
placeholder="请选择目标设备"
|
||||
showSearch
|
||||
filterOption={(input, option) =>
|
||||
option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
}
|
||||
filterOption={(input, option) => {
|
||||
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) => {
|
||||
fetchDevicePorts(value);
|
||||
form.setFieldsValue({ targetPort: undefined });
|
||||
@@ -825,18 +913,22 @@ function CableManagement() {
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
|
||||
<Form.Item
|
||||
name="targetPort"
|
||||
label="目标设备端口"
|
||||
rules={[{ required: true, message: '请选择目标设备端口' }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="请先选择目标设备"
|
||||
<Select
|
||||
placeholder="请先选择目标设备"
|
||||
showSearch
|
||||
filterOption={(input, option) =>
|
||||
option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
}
|
||||
filterOption={(input, option) => {
|
||||
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')}
|
||||
>
|
||||
{(devicePorts[form.getFieldValue('targetDeviceId')] || []).map(port => (
|
||||
@@ -846,7 +938,7 @@ function CableManagement() {
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
|
||||
<Form.Item
|
||||
name="cableType"
|
||||
label="线缆类型"
|
||||
@@ -1052,6 +1144,85 @@ function CableManagement() {
|
||||
)}
|
||||
</div>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import React, { useState, useEffect, useCallback } 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 { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined, ReloadOutlined, ExportOutlined, ImportOutlined, DownloadOutlined, UploadOutlined as UploadIcon } from '@ant-design/icons';
|
||||
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, Tabs, Badge, List, Typography } from 'antd';
|
||||
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 * as XLSX from 'xlsx';
|
||||
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 { Panel } = Collapse;
|
||||
@@ -48,6 +53,7 @@ const designTokens = {
|
||||
function PortManagement() {
|
||||
const [ports, setPorts] = useState([]);
|
||||
const [devices, setDevices] = useState([]);
|
||||
const [cables, setCables] = useState([]);
|
||||
const [groupedPorts, setGroupedPorts] = useState({});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [filters, setFilters] = useState({
|
||||
@@ -59,7 +65,7 @@ function PortManagement() {
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [editingPort, setEditingPort] = useState(null);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
|
||||
const [importModalVisible, setImportModalVisible] = useState(false);
|
||||
const [importFileList, setImportFileList] = useState([]);
|
||||
const [importPreview, setImportPreview] = useState([]);
|
||||
@@ -68,10 +74,30 @@ function PortManagement() {
|
||||
const [skipExisting, setSkipExisting] = 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 () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const params = {};
|
||||
const params = {
|
||||
pageSize: 1000 // 获取所有端口,不分页
|
||||
};
|
||||
|
||||
if (filters.deviceId) params.deviceId = filters.deviceId;
|
||||
if (filters.status !== 'all') params.status = filters.status;
|
||||
@@ -90,17 +116,28 @@ function PortManagement() {
|
||||
|
||||
const fetchDevices = useCallback(async () => {
|
||||
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 || []);
|
||||
} catch (error) {
|
||||
message.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(() => {
|
||||
fetchPorts();
|
||||
fetchDevices();
|
||||
}, [fetchPorts, fetchDevices]);
|
||||
fetchCables();
|
||||
}, [fetchPorts, fetchDevices, fetchCables]);
|
||||
|
||||
useEffect(() => {
|
||||
const grouped = {};
|
||||
@@ -114,6 +151,25 @@ function PortManagement() {
|
||||
}
|
||||
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);
|
||||
}, [ports, devices]);
|
||||
|
||||
@@ -136,6 +192,41 @@ function PortManagement() {
|
||||
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) => {
|
||||
setEditingPort(port);
|
||||
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 () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
@@ -170,8 +276,35 @@ function PortManagement() {
|
||||
await axios.put(`/api/device-ports/${editingPort.portId}`, values);
|
||||
message.success('更新成功');
|
||||
} 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);
|
||||
@@ -480,9 +613,12 @@ function PortManagement() {
|
||||
onChange={(value) => setFilters(prev => ({ ...prev, deviceId: value }))}
|
||||
allowClear
|
||||
showSearch
|
||||
filterOption={(input, option) =>
|
||||
option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
}
|
||||
filterOption={(input, option) => {
|
||||
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 => (
|
||||
<Option key={device.deviceId} value={device.deviceId}>
|
||||
@@ -490,7 +626,7 @@ function PortManagement() {
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
|
||||
<Select
|
||||
placeholder="端口类型"
|
||||
style={{ width: 120 }}
|
||||
@@ -548,32 +684,51 @@ function PortManagement() {
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Space>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={handleAdd}
|
||||
style={{ background: designTokens.colors.primary.gradient, border: 'none' }}
|
||||
>
|
||||
新增端口
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<ImportOutlined />}
|
||||
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<ImportOutlined />}
|
||||
onClick={handleImport}
|
||||
style={{ background: designTokens.colors.primary.gradient, border: 'none' }}
|
||||
>
|
||||
批量导入
|
||||
</Button>
|
||||
|
||||
|
||||
<Button icon={<ExportOutlined />}>
|
||||
导出
|
||||
</Button>
|
||||
</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>
|
||||
|
||||
|
||||
{loading ? (
|
||||
<div style={{ textAlign: 'center', padding: '40px' }}>
|
||||
<Spin size="large" tip="加载端口数据中..." />
|
||||
@@ -582,7 +737,21 @@ function PortManagement() {
|
||||
<div style={{ textAlign: 'center', padding: '60px' }}>
|
||||
<Empty description="暂无端口数据" />
|
||||
</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
|
||||
defaultActiveKey={Object.keys(groupedPorts).slice(0, 5)}
|
||||
style={{ background: '#f5f5f5' }}
|
||||
@@ -593,7 +762,7 @@ function PortManagement() {
|
||||
const freeCount = devicePorts.filter(p => p.status === 'free').length;
|
||||
const occupiedCount = devicePorts.filter(p => p.status === 'occupied').length;
|
||||
const faultCount = devicePorts.filter(p => p.status === 'fault').length;
|
||||
|
||||
|
||||
return (
|
||||
<Panel
|
||||
key={deviceId}
|
||||
@@ -611,8 +780,8 @@ function PortManagement() {
|
||||
color: '#fff',
|
||||
fontSize: '18px'
|
||||
}}>
|
||||
{device?.type?.toLowerCase()?.includes('server') ? '🖥️' :
|
||||
device?.type?.toLowerCase()?.includes('switch') ? '🔀' :
|
||||
{device?.type?.toLowerCase()?.includes('server') ? '🖥️' :
|
||||
device?.type?.toLowerCase()?.includes('switch') ? '🔀' :
|
||||
device?.type?.toLowerCase()?.includes('router') ? '🌐' : '📦'}
|
||||
</div>
|
||||
<div>
|
||||
@@ -629,6 +798,21 @@ function PortManagement() {
|
||||
<Tag color="processing">占用: {occupiedCount}</Tag>
|
||||
<Tag color="error">故障: {faultCount}</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>
|
||||
</div>
|
||||
}
|
||||
@@ -637,7 +821,12 @@ function PortManagement() {
|
||||
columns={portColumns}
|
||||
dataSource={devicePorts}
|
||||
rowKey="portId"
|
||||
pagination={false}
|
||||
pagination={{
|
||||
pageSize: 10,
|
||||
showSizeChanger: true,
|
||||
showTotal: (total) => `共 ${total} 个端口`,
|
||||
pageSizeOptions: ['10', '20', '50', '100']
|
||||
}}
|
||||
size="small"
|
||||
scroll={{ x: 1000 }}
|
||||
/>
|
||||
@@ -666,12 +855,15 @@ function PortManagement() {
|
||||
label="设备"
|
||||
rules={[{ required: true, message: '请选择设备' }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="请选择设备"
|
||||
<Select
|
||||
placeholder="请选择设备"
|
||||
showSearch
|
||||
filterOption={(input, option) =>
|
||||
option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
}
|
||||
filterOption={(input, option) => {
|
||||
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 => (
|
||||
<Option key={device.deviceId} value={device.deviceId}>
|
||||
@@ -685,8 +877,9 @@ function PortManagement() {
|
||||
name="portName"
|
||||
label="端口名称"
|
||||
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
|
||||
@@ -896,8 +1089,8 @@ function PortManagement() {
|
||||
<div style={{ textAlign: 'center', padding: '24px' }}>
|
||||
<Spin size="large" tip="导入中..." />
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<Progress
|
||||
percent={Math.round((importProgress.current / importProgress.total) * 100)}
|
||||
<Progress
|
||||
percent={Math.round((importProgress.current / importProgress.total) * 100)}
|
||||
status="active"
|
||||
strokeColor={{
|
||||
'0%': designTokens.colors.primary.main,
|
||||
@@ -919,6 +1112,44 @@ function PortManagement() {
|
||||
)}
|
||||
</div>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user