diff --git a/.gitignore b/.gitignore index 9a09151..7f2f261 100644 --- a/.gitignore +++ b/.gitignore @@ -44,4 +44,8 @@ lerna-debug.log* # Backup files (generated by maintenance scripts) backend/backups/ -*.backup.json \ No newline at end of file +*.backup.json + +#数据文件 +*.xlsx +*.csv diff --git a/backend/routes/devicePorts.js b/backend/routes/devicePorts.js index d3ff830..9982365 100644 --- a/backend/routes/devicePorts.js +++ b/backend/routes/devicePorts.js @@ -60,7 +60,8 @@ router.get('/', async (req, res) => { }); } catch (error) { console.error('获取端口列表失败:', error); - res.status(500).json({ error: error.message }); + console.error('Error name:', error.name); + res.status(500).json({ error: error.message, errorType: error.name }); } }); @@ -167,6 +168,48 @@ router.post('/batch', async (req, res) => { throw new Error('缺少必填字段'); } + const device = await Device.findByPk(portData.deviceId, { transaction }); + if (!device) { + throw new Error(`设备 ${portData.deviceId} 不存在`); + } + + const isServer = device.type && device.type.toLowerCase().includes('server'); + + if (isServer) { + if (!portData.nicId && !portData.网卡名称) { + throw new Error(`服务器 ${portData.deviceId} 的端口必须关联网卡,请先在网卡管理中添加网卡`); + } + + let nicId = portData.nicId; + + if (!nicId && portData.网卡名称) { + const networkCard = await NetworkCard.findOne({ + where: { deviceId: portData.deviceId, name: portData.网卡名称 }, + transaction + }); + if (!networkCard) { + throw new Error(`服务器 ${portData.deviceId} 的网卡"${portData.网卡名称}"不存在,请先在网卡管理中添加该网卡`); + } + nicId = networkCard.nicId; + } + + if (nicId) { + const networkCard = await NetworkCard.findByPk(nicId, { transaction }); + if (!networkCard) { + throw new Error(`网卡 ${nicId} 不存在`); + } + if (networkCard.deviceId !== portData.deviceId) { + throw new Error(`网卡 ${nicId} 不属于设备 ${portData.deviceId}`); + } + } + + portData.nicId = nicId; + } else { + if (portData.nicId || portData.网卡名称) { + portData.nicId = null; + } + } + const existingPort = await DevicePort.findOne({ where: { deviceId: portData.deviceId, portName: portData.portName }, transaction @@ -183,7 +226,8 @@ router.post('/batch', async (req, res) => { portSpeed: portData.portSpeed || existingPort.portSpeed, status: portData.status || existingPort.status, vlanId: portData.vlanId !== undefined ? portData.vlanId : existingPort.vlanId, - description: portData.description !== undefined ? portData.description : existingPort.description + description: portData.description !== undefined ? portData.description : existingPort.description, + nicId: portData.nicId !== undefined ? portData.nicId : existingPort.nicId }, { where: { portId: existingPort.portId }, transaction @@ -213,6 +257,8 @@ router.post('/batch', async (req, res) => { results.errors.push({ index: i + 1, portId: portData.portId, + deviceId: portData.deviceId, + portName: portData.portName, error: error.message }); } @@ -351,11 +397,11 @@ router.get('/:portId', async (req, res) => { } ] }); - + if (!port) { return res.status(404).json({ error: '端口不存在' }); } - + res.json(port); } catch (error) { console.error('获取端口详情失败:', error); @@ -363,4 +409,120 @@ router.get('/:portId', async (req, res) => { } }); +router.get('/export/all', async (req, res) => { + try { + const { keyword, status, portType, portSpeed, deviceId, page = 1, pageSize = 5000 } = req.query; + + const parsedPage = Math.max(1, parseInt(page) || 1); + const parsedPageSize = Math.min(10000, Math.max(1, parseInt(pageSize) || 5000)); + const offset = (parsedPage - 1) * parsedPageSize; + + const where = {}; + + if (deviceId) { + where.deviceId = deviceId; + } + + if (status && status !== 'all') { + where.status = status; + } + + if (portType && portType !== 'all') { + where.portType = portType; + } + + if (portSpeed && portSpeed !== 'all') { + where.portSpeed = portSpeed; + } + + const timeoutMs = 30000; + const timeoutPromise = new Promise((_, reject) => { + setTimeout(() => reject(new Error('查询超时,请尝试缩小查询范围或减少pageSize')), timeoutMs); + }); + + const countResult = await Promise.race([ + DevicePort.findAll({ + where, + include: [ + { + model: Device, + as: 'device', + attributes: ['deviceId', 'name', 'type', 'rackId'], + include: [ + { + model: require('../models/Rack'), + as: 'rack', + attributes: ['rackId', 'name'], + include: [ + { + model: require('../models/Room'), + as: 'room', + attributes: ['roomId', 'name'] + } + ] + } + ] + }, + { + model: NetworkCard, + as: 'networkCard', + attributes: ['nicId', 'name'] + } + ], + order: [['createdAt', 'DESC']], + limit: parsedPageSize, + offset: offset, + subQuery: false + }), + timeoutPromise + ]); + + const ports = countResult; + + const statusMap = { + free: '空闲', + occupied: '占用', + fault: '故障' + }; + + const exportData = ports.map(port => ({ + 端口ID: port.portId, + 设备ID: port.deviceId, + 设备名称: port.device?.name || '-', + 设备类型: port.device?.type || '-', + 机房: port.device?.rack?.room?.name || '-', + 机架: port.device?.rack?.name || '-', + 网卡名称: port.networkCard?.name || '-', + 端口名称: port.portName, + 端口类型: port.portType, + 端口速率: port.portSpeed, + 状态: statusMap[port.status] || port.status, + VLAN_ID: port.vlanId || '-', + 描述: port.description || '-', + 创建时间: port.createdAt ? new Date(port.createdAt).toLocaleString('zh-CN') : '-' + })); + + let filteredExportData = exportData; + if (keyword) { + const searchLower = keyword.toLowerCase(); + filteredExportData = exportData.filter(item => + item.端口名称?.toLowerCase().includes(searchLower) || + item.端口类型?.toLowerCase().includes(searchLower) || + item.设备名称?.toLowerCase().includes(searchLower) || + item.描述?.toLowerCase().includes(searchLower) + ); + } + + res.json({ + page: parsedPage, + pageSize: parsedPageSize, + total: filteredExportData.length, + ports: filteredExportData + }); + } catch (error) { + console.error('导出端口失败:', error); + res.status(500).json({ error: error.message }); + } +}); + module.exports = router; diff --git a/backend/routes/networkCards.js b/backend/routes/networkCards.js index add8c33..05d2f85 100644 --- a/backend/routes/networkCards.js +++ b/backend/routes/networkCards.js @@ -231,6 +231,111 @@ router.post('/', async (req, res) => { } }); +router.post('/batch', async (req, res) => { + try { + const { networkCards, skipExisting = false, updateExisting = false } = req.body; + + if (!networkCards || !Array.isArray(networkCards) || networkCards.length === 0) { + return res.status(400).json({ error: '请提供有效的网卡数据' }); + } + + const results = { + total: networkCards.length, + success: 0, + failed: 0, + skipped: 0, + updated: 0, + errors: [] + }; + + const transaction = await NetworkCard.sequelize.transaction(); + + try { + for (let i = 0; i < networkCards.length; i++) { + const cardData = networkCards[i]; + + try { + if (!cardData.deviceId || !cardData.name) { + throw new Error('缺少必填字段:设备ID和网卡名称'); + } + + const device = await Device.findByPk(cardData.deviceId, { transaction }); + if (!device) { + throw new Error(`设备 ${cardData.deviceId} 不存在`); + } + + const isServer = device.type && device.type.toLowerCase().includes('server'); + const isSwitch = device.type && device.type.toLowerCase().includes('switch'); + if (!isServer && !isSwitch) { + throw new Error(`设备类型 ${device.type} 不支持网卡管理`); + } + + const existingCard = await NetworkCard.findOne({ + where: { deviceId: cardData.deviceId, name: cardData.name }, + transaction + }); + + if (existingCard) { + if (skipExisting) { + results.skipped++; + continue; + } + if (updateExisting) { + await NetworkCard.update({ + slotNumber: cardData.slotNumber !== undefined ? cardData.slotNumber : existingCard.slotNumber, + model: cardData.model !== undefined ? cardData.model : existingCard.model, + manufacturer: cardData.manufacturer !== undefined ? cardData.manufacturer : existingCard.manufacturer, + description: cardData.description !== undefined ? cardData.description : existingCard.description, + status: cardData.status || existingCard.status + }, { + where: { nicId: existingCard.nicId }, + transaction + }); + results.updated++; + results.success++; + continue; + } + throw new Error('该设备已存在同名网卡'); + } + + const autoNicId = cardData.nicId || `NIC-${Date.now()}-${Math.floor(Math.random() * 10000)}`; + + await NetworkCard.create({ + nicId: autoNicId, + deviceId: cardData.deviceId, + name: cardData.name, + slotNumber: cardData.slotNumber, + model: cardData.model, + manufacturer: cardData.manufacturer, + description: cardData.description, + status: cardData.status || 'normal', + portCount: 0 + }, { transaction }); + + results.success++; + } catch (error) { + results.failed++; + results.errors.push({ + index: i + 1, + deviceId: cardData.deviceId, + name: cardData.name, + error: error.message + }); + } + } + + await transaction.commit(); + res.json(results); + } catch (error) { + await transaction.rollback(); + throw error; + } + } catch (error) { + console.error('批量创建网卡失败:', error); + res.status(500).json({ error: error.message }); + } +}); + router.put('/:nicId', async (req, res) => { try { const [updated] = await NetworkCard.update(req.body, { diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 508d9d0..3c42935 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -261,16 +261,16 @@ const AppLayout = ({ children }) => { icon: , label: 字段管理, }, - { - key: 'cables', - icon: , - label: 接线管理, - }, { key: 'ports', icon: , label: 端口管理, }, + { + key: 'cables', + icon: , + label: 接线管理, + }, ], }, { diff --git a/frontend/src/components/BatchImportModal.jsx b/frontend/src/components/BatchImportModal.jsx new file mode 100644 index 0000000..70e9d66 --- /dev/null +++ b/frontend/src/components/BatchImportModal.jsx @@ -0,0 +1,152 @@ +import React from 'react'; +import { Modal, Button, Typography, Divider } from 'antd'; +import { + ImportOutlined, + CloudServerOutlined, + ApiOutlined, + InfoCircleOutlined, +} from '@ant-design/icons'; +import { designTokens } from '../config/theme'; +import CloseButton from './CloseButton'; + +const { Text, Paragraph } = Typography; + +function BatchImportModal({ visible, onClose, onImportNetworkCard, onImportPort }) { + return ( + +
+ +
+ 批量导入 + + } + open={visible} + closeIcon={} + onCancel={onClose} + footer={null} + width={500} + > +
+
+ + + +
+ + + +
+
+ +
+
+ 交换机与服务器导入说明 +
+ +
+ 交换机端口:可直接批量导入端口,无需先导入网卡 +
+
+ 服务器端口:必须先在"网卡管理"中添加网卡,才能导入端口。 + 服务器的网卡和端口是层级关系:设备 → 网卡 → 端口 +
+
+
+
+
+
+
+ ); +} + +export default BatchImportModal; diff --git a/frontend/src/components/NetworkCardCreateModal.jsx b/frontend/src/components/NetworkCardCreateModal.jsx index 8b097e0..a073c79 100644 --- a/frontend/src/components/NetworkCardCreateModal.jsx +++ b/frontend/src/components/NetworkCardCreateModal.jsx @@ -1,17 +1,44 @@ -import React, { useState, useCallback } from 'react'; -import { Modal, Form, Input, InputNumber, Select, message, Space, Tooltip } from 'antd'; -import { PlusOutlined, InfoCircleOutlined, CloudServerOutlined } from '@ant-design/icons'; +import React, { useState, useCallback, useEffect } from 'react'; +import { Modal, Form, Input, Select, message, Space, Tooltip, Card, Alert, AutoComplete } from 'antd'; +import { CloudServerOutlined, InfoCircleOutlined, QuestionCircleOutlined, ThunderboltOutlined } from '@ant-design/icons'; import axios from 'axios'; import { designTokens } from '../config/theme'; import CloseButton from './CloseButton'; -const { Option } = Select; const { TextArea } = Input; +const SLOT_TYPES = [ + { value: 'LOM', label: '板载网卡 (LOM)', description: '主板集成网卡,编号0' }, + { value: 'OCP', label: 'OCP 网卡', description: '开放式计算项目网卡槽位' }, + { value: '1', label: '插槽 1', description: 'PCIe x16/x8 插槽' }, + { value: '2', label: '插槽 2', description: 'PCIe x8 插槽' }, + { value: '3', label: '插槽 3', description: 'PCIe x4 插槽(需 Riser 卡)' }, + { value: '4', label: '插槽 4', description: 'PCIe x4 插槽(需 Riser 卡)' }, +]; + +const MANUFACTURER_OPTIONS = [ + { value: 'Intel', label: 'Intel' }, + { value: 'Broadcom', label: 'Broadcom' }, + { value: 'Mellanox', label: 'Mellanox (NVIDIA)' }, + { value: 'Realtek', label: 'Realtek' }, + { value: 'Cisco', label: 'Cisco' }, + { value: 'HP', label: 'HP/HPE' }, + { value: 'Dell', label: 'Dell' }, + { value: 'Qlogic', label: 'Qlogic' }, + { value: 'Marvell', label: 'Marvell' }, + { value: 'Other', label: '其他' }, +]; + function NetworkCardCreateModal({ device, visible, onClose, onSuccess }) { const [form] = Form.useForm(); const [loading, setLoading] = useState(false); + useEffect(() => { + if (visible) { + form.resetFields(); + } + }, [visible, form]); + const handleSubmit = useCallback(async () => { try { const values = await form.validateFields(); @@ -24,7 +51,6 @@ function NetworkCardCreateModal({ device, visible, onClose, onSuccess }) { description: values.description, model: values.model, manufacturer: values.manufacturer, - status: values.status, }); message.success('网卡创建成功'); @@ -62,67 +88,157 @@ function NetworkCardCreateModal({ device, visible, onClose, onSuccess }) { confirmLoading={loading} okText="创建" cancelText="取消" - width={480} - styles={{ body: { padding: '20px 24px' } }} + width={800} + styles={{ body: { padding: '0 24px 24px' } }} > +
+
+ 为服务器添加新的网卡,网卡创建后可关联端口 +
+
+ + + 插槽编号参考 +
    +
  • LOM (LAN on Motherboard):主板集成网卡,编号通常为 0
  • +
  • OCP (Open Compute Project):服务器前端维护网卡专用槽位
  • +
  • PCIe 插槽:从 1 开始编号,对应服务器物理插槽位置
  • +
+ + } + type="info" + style={{ + marginBottom: 20, + borderRadius: 10, + background: designTokens.colors.info.bg, + border: `1px solid ${designTokens.colors.info.light}40`, + }} + /> +
- + + + 基础信息 + + } + style={{ + flex: 1, + borderRadius: 12, + border: `1px solid ${designTokens.colors.neutral[200]}`, + }} + styles={{ body: { padding: '16px 20px' } }} + > + 网卡名称 *} + rules={[ + { required: true, message: '请输入网卡名称' }, + { max: 50, message: '名称不能超过50个字符' }, + ]} + > + + + + + 插槽位置 + + + + + } + > + ({ + value: slot.value, + label: ( +
+
{slot.label}
+
+ {slot.description} +
+
+ ), + }))} + filterOption={(input, option) => + option.value.toLowerCase().includes(input.toLowerCase()) || + option.label.props.children[0].props.children.toLowerCase().includes(input.toLowerCase()) + } + /> +
+
+ + + + 规格信息 + + } + style={{ + flex: 1, + borderRadius: 12, + border: `1px solid ${designTokens.colors.neutral[200]}`, + }} + styles={{ body: { padding: '16px 20px' } }} + > +
+ 制造商}> + ({ value: m.value, label: m.label }))} + filterOption={(input, option) => + option.label.toLowerCase().includes(input.toLowerCase()) + } + size="large" + /> + + + 型号}> + + +
+
+ + + - 网卡名称 - - - + + 附加信息 } - rules={[ - { required: true, message: '请输入网卡名称' }, - { max: 50, message: '名称不能超过50个字符' }, - ]} + style={{ + borderRadius: 12, + border: `1px solid ${designTokens.colors.neutral[200]}`, + }} + styles={{ body: { padding: '16px 20px' } }} > - -
- - - - + 描述}> +