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;
const designTokens = {
colors: {
primary: {
main: '#667eea',
gradient: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
light: '#8b9ff0',
dark: '#4f5db8'
},
success: {
main: '#10b981',
gradient: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
light: '#34d399',
dark: '#047857'
},
warning: {
main: '#f59e0b',
gradient: 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)',
light: '#fbbf24',
dark: '#b45309'
},
error: {
main: '#ef4444',
gradient: 'linear-gradient(135deg, #ef4444 0%, #dc2626 100%)',
light: '#f87171',
dark: '#b91c1c'
}
},
borderRadius: {
small: '6px',
medium: '10px',
large: '16px'
},
shadows: {
medium: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)'
}
};
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({
deviceId: '',
status: 'all',
portType: 'all',
portSpeed: 'all'
});
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([]);
const [importProgress, setImportProgress] = useState({ current: 0, total: 0 });
const [importing, setImporting] = useState(false);
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 = {
pageSize: 1000 // 获取所有端口,不分页
};
if (filters.deviceId) params.deviceId = filters.deviceId;
if (filters.status !== 'all') params.status = filters.status;
if (filters.portType !== 'all') params.portType = filters.portType;
if (filters.portSpeed !== 'all') params.portSpeed = filters.portSpeed;
const response = await axios.get('/api/device-ports', { params });
setPorts(response.data.ports || response.data || []);
} catch (error) {
message.error('获取端口列表失败');
console.error('获取端口列表失败:', error);
} finally {
setLoading(false);
}
}, [filters]);
const fetchDevices = useCallback(async () => {
try {
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();
fetchCables();
}, [fetchPorts, fetchDevices, fetchCables]);
useEffect(() => {
const grouped = {};
ports.forEach(port => {
const deviceId = port.deviceId;
if (!grouped[deviceId]) {
grouped[deviceId] = {
device: devices.find(d => d.deviceId === deviceId),
ports: []
};
}
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]);
const handleSearch = () => {
fetchPorts();
};
const handleReset = () => {
setFilters({
deviceId: '',
status: 'all',
portType: 'all',
portSpeed: 'all'
});
};
const handleAdd = () => {
setEditingPort(null);
form.resetFields();
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({
portId: port.portId,
deviceId: port.deviceId,
portName: port.portName,
portType: port.portType,
portSpeed: port.portSpeed,
status: port.status,
vlanId: port.vlanId,
description: port.description
});
setModalVisible(true);
};
const handleDelete = async (portId) => {
try {
await axios.delete(`/api/device-ports/${portId}`);
message.success('删除成功');
fetchPorts();
} catch (error) {
message.error('删除失败');
console.error('删除失败:', error);
}
};
// 解析端口名称范围,例如 "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();
if (editingPort) {
await axios.put(`/api/device-ports/${editingPort.portId}`, values);
message.success('更新成功');
} else {
// 解析端口名称范围
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);
form.resetFields();
fetchPorts();
} catch (error) {
message.error(editingPort ? '更新失败' : '创建失败');
console.error('提交失败:', error);
}
};
const handleImport = () => {
setImportModalVisible(true);
setImportPreview([]);
setImportProgress({ current: 0, total: 0 });
};
const handleFileUpload = (info) => {
const { file } = info;
setImportFileList([file]);
const reader = new FileReader();
reader.onload = async (e) => {
try {
const data = e.target.result;
let parsedData = [];
if (file.name.endsWith('.xlsx') || file.name.endsWith('.xls')) {
const workbook = XLSX.read(data, { type: 'binary' });
const sheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[sheetName];
parsedData = XLSX.utils.sheet_to_json(worksheet);
} else if (file.name.endsWith('.csv')) {
Papa.parse(data, {
header: true,
skipEmptyLines: true,
complete: (results) => {
parsedData = results.data;
}
});
} else {
message.error('不支持的文件格式,请上传 .xlsx 或 .csv 文件');
return;
}
const validatedData = await validateImportData(parsedData);
setImportPreview(validatedData);
setImportProgress({ current: 0, total: validatedData.length });
} catch (error) {
message.error('文件解析失败');
console.error('文件解析失败:', error);
}
};
reader.readAsBinaryString(file);
};
const validateImportData = async (data) => {
const validatedData = [];
const errors = [];
for (let i = 0; i < data.length; i++) {
const row = data[i];
const error = await validatePortRow(row, i);
if (error) {
errors.push(error);
} else {
validatedData.push(row);
}
}
if (errors.length > 0) {
message.warning(`发现 ${errors.length} 条数据错误,已跳过`);
console.log('导入错误:', errors);
}
return validatedData;
};
const validatePortRow = async (row, index) => {
const errors = [];
if (!row['设备ID'] || !row['端口名称']) {
return { valid: false, error: `第 ${index + 1} 行:缺少必填字段(设备ID或端口名称)` };
}
const device = devices.find(d => d.deviceId === row['设备ID']);
if (!device) {
return { valid: false, error: `第 ${index + 1} 行:设备不存在` };
}
const validPortTypes = ['RJ45', 'SFP', 'SFP+', 'SFP28', 'QSFP', 'QSFP28'];
if (!validPortTypes.includes(row['端口类型'])) {
return { valid: false, error: `第 ${index + 1} 行:无效的端口类型` };
}
const validPortSpeeds = ['100M', '1G', '10G', '25G', '40G', '100G'];
if (!validPortSpeeds.includes(row['端口速率'])) {
return { valid: false, error: `第 ${index + 1} 行:无效的端口速率` };
}
const validStatuses = ['空闲', '占用', '故障'];
if (!validStatuses.includes(row['状态'])) {
return { valid: false, error: `第 ${index + 1} 行:无效的状态` };
}
if (errors.length > 0) {
return { valid: false, error: errors.join('; ') };
}
return { valid: true };
};
const handleBatchImport = async () => {
if (importPreview.length === 0) {
message.warning('请先选择要导入的数据');
return;
}
setImporting(true);
setImportProgress({ current: 0, total: importPreview.length });
try {
const statusMap = {
'空闲': 'free',
'占用': 'occupied',
'故障': 'fault'
};
const portsData = importPreview.map((row, index) => ({
portId: `PORT-${Date.now()}-${index}`,
deviceId: row['设备ID'],
portName: row['端口名称'],
portType: row['端口类型'],
portSpeed: row['端口速率'],
status: statusMap[row['状态']] || 'free',
vlanId: row['VLAN ID'],
description: row['描述']
}));
const response = await axios.post('/api/device-ports/batch', { ports: portsData });
const { total, success, failed, errors } = response.data;
setImportProgress({ current: total, total: total });
if (failed > 0) {
console.error('导入错误:', errors);
message.warning(`导入完成!成功 ${success} 条,失败 ${failed} 条`);
} else {
message.success(`导入完成!成功 ${success} 条`);
}
fetchPorts();
setImportModalVisible(false);
setImportPreview([]);
} catch (error) {
console.error('批量导入失败:', error);
message.error('批量导入失败,请检查数据格式');
} finally {
setImporting(false);
}
};
const handleDownloadTemplate = () => {
const templateData = [
{
'设备ID': 'DEV001',
'端口名称': 'eth0/1',
'端口类型': 'RJ45',
'端口速率': '1G',
'状态': '空闲',
'VLAN ID': '100',
'描述': '示例端口'
}
];
const worksheet = XLSX.utils.json_to_sheet(templateData);
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, '端口数据');
XLSX.writeFile(workbook, '端口导入模板.xlsx');
};
const getStatusTag = (status) => {
const statusMap = {
'free': { color: 'success', text: '空闲' },
'occupied': { color: 'processing', text: '占用' },
'fault': { color: 'error', text: '故障' },
'空闲': { color: 'success', text: '空闲' },
'占用': { color: 'processing', text: '占用' },
'故障': { color: 'error', text: '故障' }
};
const config = statusMap[status] || { color: 'default', text: status };
return