diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index 20fce0e..30fe6d5 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -1715,6 +1715,283 @@ echo "备份验证完成" --- +## 🚨 已部署项目更新指南 + +> **📋 版本更新说明**: +> 本节介绍如何将已部署的生产环境更新到最新版本,包含网卡管理、批量创建端口等新功能。 + +### 更新前准备 + +#### 1. 确认当前版本 +```bash +# 检查最后更新日期 +cd /var/www/idc_assest +git log -1 --format="%cd" --date=short + +# 查看当前分支 +git branch +``` + +#### 2. 备份现有数据(强制操作) +```bash +# 创建备份目录 +BACKUP_DIR="/var/backups/idc_assest/$(date +%Y%m%d_%H%M%S)" +mkdir -p $BACKUP_DIR + +echo "开始备份..." + +# 备份数据库(SQLite) +if [ -f "/var/www/idc_assest/backend/idc_management.db" ]; then + cp /var/www/idc_assest/backend/idc_management.db "$BACKUP_DIR/idc_management.db" + echo "✓ SQLite数据库已备份" +fi + +# 备份数据库(MySQL) +read -p "是否需要备份MySQL数据库? (y/n): " need_mysql +if [ "$need_mysql" = "y" ]; then + read -p "MySQL用户名: " db_user + read -s -p "MySQL密码: " db_pass + echo + mysqldump -u $db_user -p$db_pass idc_management > "$BACKUP_DIR/database.sql" + echo "✓ MySQL数据库已备份" +fi + +# 备份配置文件 +cp /var/www/idc_assest/backend/.env "$BACKUP_DIR/.env" +cp /var/www/idc_assest/frontend/.env* "$BACKUP_DIR/" 2>/dev/null || true +echo "✓ 配置文件已备份" + +# 备份上传文件 +cp -r /var/www/idc_assest/backend/uploads "$BACKUP_DIR/uploads" 2>/dev/null || true +echo "✓ 上传文件已备份" + +echo "" +echo "备份完成,保存在: $BACKUP_DIR" +``` + +### 数据库迁移 + +#### SQLite 数据库更新 + +```bash +cd /var/www/idc_assest/backend + +# 方式一:使用迁移脚本(推荐) +node scripts/migrate-v2.js + +# 预期输出: +# ======================================== +# IDC管理系统 - 数据库迁移脚本 v2.0 +# ======================================== +# 🔍 检测数据库类型... +# 数据库类型: sqlite +# 📋 开始迁移... +# ... +# ✅ 迁移完成! +# ======================================== +# 迁移成功完成!🎉 +# ======================================== +``` + +#### MySQL 数据库更新 + +```bash +cd /var/www/idc_assest/backend + +# 执行迁移脚本 +node scripts/migrate-v2.js + +# 预期输出: +# ======================================== +# IDC管理系统 - 数据库迁移脚本 v2.0 +# ======================================== +# 🔍 检测数据库类型... +# 数据库类型: mysql +# 📋 开始迁移... +# 1. 创建 network_cards 表 +# 2. 为 device_ports 添加 nic_id 字段 +# 3. 创建相关索引 +# 🔄 执行 MySQL 迁移... +# → 创建表: network_cards +# → 检查 nic_id 字段是否存在... +# → 添加 nic_id 字段... +# → 创建 nic_id 索引... +# ✅ 迁移完成! +# ======================================== +# 迁移成功完成!🎉 +# ======================================== +``` + +> **💡 提示**:迁移脚本会自动检测数据库类型并执行相应的迁移操作。 + +### 更新后端代码 + +```bash +cd /var/www/idc_assest + +# 拉取最新代码 +git pull origin master + +# 更新依赖 +cd backend +npm install --only=production + +# 检查新增的文件 +echo "新增的文件:" +git diff --name-only --diff-filter=A HEAD + +# 验证新模型文件 +ls -la models/NetworkCard.js +ls -la routes/networkCards.js +``` + +### 注册新路由 + +检查 `backend/server.js` 是否已包含新路由: + +```javascript +// 确认以下代码存在 +const networkCardsRouter = require('./routes/networkCards'); +app.use('/api/network-cards', networkCardsRouter); +``` + +如果不存在,请手动添加: + +```bash +# 编辑 server.js +nano /var/www/idc_assest/backend/server.js + +# 在合适位置添加(通常在其他 app.use 语句附近) +const networkCardsRouter = require('./routes/networkCards'); +app.use('/api/network-cards', networkCardsRouter); +``` + +### 重启后端服务 + +```bash +# 重启PM2进程 +pm2 restart idc-backend + +# 验证服务状态 +pm2 status idc-backend + +# 查看日志确认无错误 +pm2 logs idc-backend --lines 50 +``` + +### 更新前端代码 + +```bash +cd /var/www/idc_assest/frontend + +# 拉取最新代码 +git pull origin master + +# 更新依赖 +npm install + +# 构建生产版本 +npm run build + +# 验证构建结果 +ls -la dist/ + +# 部署到Web目录 +sudo rm -rf /var/www/idc-frontend/* +sudo cp -r dist/* /var/www/idc-frontend/ +sudo chown -R www-data:www-data /var/www/idc-frontend +``` + +### 验证更新 + +#### 1. API 接口验证 + +```bash +# 测试新API接口 +curl http://localhost:8000/api/network-cards + +# 预期返回:空数组或现有数据 +curl http://localhost:8000/api/device-ports + +# 预期返回:端口数据(可能包含 nic_id 字段) +``` + +#### 2. 前端功能验证 + +访问管理界面,验证以下功能: + +- [ ] **网卡管理** + - [ ] 设备详情中显示"端口与网卡"标签页 + - [ ] 可以创建新网卡 + - [ ] 网卡列表正确显示 + - [ ] 可以删除网卡(需无端口关联) + +- [ ] **端口管理** + - [ ] 可以创建端口时选择所属网卡 + - [ ] 端口按网卡分组显示 + - [ ] 显示未分组的端口 + +- [ ] **批量创建端口** + - [ ] 输入格式如 `1/0/1-1/0/48` 可以创建多个端口 + - [ ] 预览功能正确显示待创建端口数量 + +#### 3. 数据库验证 + +```bash +# 验证新表存在 +sqlite3 /var/www/idc_assest/backend/idc_management.db ".tables" +# 应包含:network_cards + +# 验证新字段 +sqlite3 /var/www/idc_assest/backend/idc_management.db ".schema device_ports" | grep nic_id +# 应显示 nic_id 字段定义 + +# 检查数据 +sqlite3 /var/www/idc_assest/backend/idc_management.db "SELECT COUNT(*) FROM network_cards;" +``` + +### 回滚操作(如果出现问题) + +```bash +cd /var/www/idc_assest + +# 1. 停止服务 +pm2 stop idc-backend + +# 2. 恢复数据库 +# SQLite +cp /var/backups/idc_assest/最新备份目录/idc_management.db backend/idc_management.db + +# 或 MySQL +mysql -u idc_prod_user -p idc_management < /var/backups/idc_assest/最新备份目录/database.sql + +# 3. 恢复代码 +git checkout HEAD@{1} + +# 4. 重启服务 +pm2 start idc-backend + +# 5. 验证回滚 +curl http://localhost:8000/api/health +``` + +### 更新日志 + +**v2.x.x 新增功能**: +- 网卡(NIC)管理功能,支持为设备添加多块网卡 +- 端口与网卡关联,支持按网卡分组管理端口 +- 批量创建端口,支持端口范围格式(如 `1/0/1-1/0/48`) +- 新增 `network_cards` 数据库表 +- `device_ports` 表新增 `nic_id` 字段 +- 新增API端点: + - `GET /api/network-cards` - 获取网卡列表 + - `POST /api/network-cards` - 创建网卡 + - `PUT /api/network-cards/:nicId` - 更新网卡 + - `DELETE /api/network-cards/:nicId` - 删除网卡 + - `GET /api/network-cards/device/:deviceId/with-ports` - 获取网卡及端口 + +--- + ## 🔐 安全加固清单 ### 服务器安全 diff --git a/backend/models/Cable.js b/backend/models/Cable.js new file mode 100644 index 0000000..c02e027 --- /dev/null +++ b/backend/models/Cable.js @@ -0,0 +1,69 @@ +const { DataTypes } = require('sequelize'); +const { sequelize } = require('../db'); +const Device = require('./Device'); + +const Cable = sequelize.define('Cable', { + cableId: { + type: DataTypes.STRING, + primaryKey: true, + allowNull: false, + unique: true + }, + sourceDeviceId: { + type: DataTypes.STRING, + allowNull: false, + references: { + model: 'devices', + key: 'deviceId' + } + }, + sourcePort: { + type: DataTypes.STRING, + allowNull: false + }, + targetDeviceId: { + type: DataTypes.STRING, + allowNull: false, + references: { + model: 'devices', + key: 'deviceId' + } + }, + targetPort: { + type: DataTypes.STRING, + allowNull: false + }, + cableType: { + type: DataTypes.ENUM('ethernet', 'fiber', 'copper'), + defaultValue: 'ethernet', + allowNull: false + }, + cableLength: { + type: DataTypes.DECIMAL(5, 2), + allowNull: true + }, + status: { + type: DataTypes.ENUM('normal', 'fault', 'disconnected'), + defaultValue: 'normal', + allowNull: false + }, + description: { + type: DataTypes.TEXT, + allowNull: true + } +}, { + tableName: 'cables', + timestamps: true, + indexes: [ + { fields: ['sourceDeviceId'] }, + { fields: ['targetDeviceId'] }, + { fields: ['status'] }, + { fields: ['cableType'] }, + { fields: ['sourceDeviceId', 'targetDeviceId'] } + ] +}); + +Cable.belongsTo(Device, { foreignKey: 'sourceDeviceId', as: 'sourceDevice' }); +Cable.belongsTo(Device, { foreignKey: 'targetDeviceId', as: 'targetDevice' }); + +module.exports = Cable; diff --git a/backend/models/DevicePort.js b/backend/models/DevicePort.js new file mode 100644 index 0000000..6680745 --- /dev/null +++ b/backend/models/DevicePort.js @@ -0,0 +1,76 @@ +const { DataTypes } = require('sequelize'); +const { sequelize } = require('../db'); +const Device = require('./Device'); +const NetworkCard = require('./NetworkCard'); + +const DevicePort = sequelize.define('DevicePort', { + portId: { + type: DataTypes.STRING, + primaryKey: true, + allowNull: false, + unique: true + }, + deviceId: { + type: DataTypes.STRING, + allowNull: false, + references: { + model: 'devices', + key: 'deviceId' + } + }, + nicId: { + type: DataTypes.STRING, + allowNull: true, + references: { + model: 'network_cards', + key: 'nicId' + }, + comment: '所属网卡ID,可为空(向后兼容)' + }, + portName: { + type: DataTypes.STRING, + allowNull: false + }, + portType: { + type: DataTypes.ENUM('RJ45', 'SFP', 'SFP+', 'SFP28', 'QSFP', 'QSFP28'), + defaultValue: 'RJ45', + allowNull: false + }, + portSpeed: { + type: DataTypes.ENUM('100M', '1G', '10G', '25G', '40G', '100G'), + defaultValue: '1G', + allowNull: false + }, + status: { + type: DataTypes.ENUM('free', 'occupied', 'fault'), + defaultValue: 'free', + allowNull: false + }, + vlanId: { + type: DataTypes.INTEGER, + allowNull: true + }, + description: { + type: DataTypes.TEXT, + allowNull: true + } +}, { + tableName: 'device_ports', + timestamps: true, + indexes: [ + { fields: ['deviceId'] }, + { fields: ['nicId'] }, + { fields: ['status'] }, + { fields: ['portType'] }, + { fields: ['portSpeed'] }, + { unique: true, fields: ['deviceId', 'portName'] } + ] +}); + +DevicePort.belongsTo(Device, { foreignKey: 'deviceId', as: 'device' }); +Device.hasMany(DevicePort, { foreignKey: 'deviceId', as: 'ports' }); + +DevicePort.belongsTo(NetworkCard, { foreignKey: 'nicId', as: 'networkCard' }); +NetworkCard.hasMany(DevicePort, { foreignKey: 'nicId', as: 'ports' }); + +module.exports = DevicePort; diff --git a/backend/models/NetworkCard.js b/backend/models/NetworkCard.js new file mode 100644 index 0000000..4b47b0a --- /dev/null +++ b/backend/models/NetworkCard.js @@ -0,0 +1,69 @@ +const { DataTypes } = require('sequelize'); +const { sequelize } = require('../db'); +const Device = require('./Device'); + +const NetworkCard = sequelize.define('NetworkCard', { + nicId: { + type: DataTypes.STRING, + primaryKey: true, + allowNull: false, + unique: true + }, + deviceId: { + type: DataTypes.STRING, + allowNull: false, + references: { + model: 'devices', + key: 'deviceId' + } + }, + name: { + type: DataTypes.STRING, + allowNull: false, + comment: '网卡名称,如"网卡1"、"eth0"、"Primary NIC"' + }, + description: { + type: DataTypes.TEXT, + allowNull: true, + comment: '网卡描述信息' + }, + slotNumber: { + type: DataTypes.INTEGER, + allowNull: true, + comment: '插槽编号' + }, + portCount: { + type: DataTypes.INTEGER, + defaultValue: 0, + comment: '端口数量' + }, + model: { + type: DataTypes.STRING, + allowNull: true, + comment: '网卡型号' + }, + manufacturer: { + type: DataTypes.STRING, + allowNull: true, + comment: '制造商' + }, + status: { + type: DataTypes.ENUM('normal', 'warning', 'fault', 'offline'), + defaultValue: 'normal', + allowNull: false, + comment: '网卡状态' + } +}, { + tableName: 'network_cards', + timestamps: true, + indexes: [ + { fields: ['deviceId'] }, + { fields: ['slotNumber'] }, + { unique: true, fields: ['deviceId', 'name'] } + ] +}); + +NetworkCard.belongsTo(Device, { foreignKey: 'deviceId', as: 'device' }); +Device.hasMany(NetworkCard, { foreignKey: 'deviceId', as: 'networkCards' }); + +module.exports = NetworkCard; diff --git a/backend/routes/cables.js b/backend/routes/cables.js new file mode 100644 index 0000000..073f3b8 --- /dev/null +++ b/backend/routes/cables.js @@ -0,0 +1,323 @@ +const express = require('express'); +const router = express.Router(); +const { Op } = require('sequelize'); +const Cable = require('../models/Cable'); +const Device = require('../models/Device'); + +router.get('/', async (req, res) => { + try { + const { sourceDeviceId, targetDeviceId, status, cableType, page = 1, pageSize = 10 } = req.query; + const offset = (page - 1) * pageSize; + + const where = {}; + + if (sourceDeviceId) { + where.sourceDeviceId = sourceDeviceId; + } + + if (targetDeviceId) { + where.targetDeviceId = targetDeviceId; + } + + if (status && status !== 'all') { + where.status = status; + } + + if (cableType && cableType !== 'all') { + where.cableType = cableType; + } + + const { count, rows } = await Cable.findAndCountAll({ + where, + include: [ + { + model: Device, + as: 'sourceDevice', + attributes: ['deviceId', 'name', 'type', 'rackId'] + }, + { + model: Device, + as: 'targetDevice', + attributes: ['deviceId', 'name', 'type', 'rackId'] + } + ], + offset, + limit: parseInt(pageSize), + order: [['createdAt', 'DESC']] + }); + + res.json({ + total: count, + cables: rows, + page: parseInt(page), + pageSize: parseInt(pageSize) + }); + } catch (error) { + console.error('获取接线列表失败:', error); + res.status(500).json({ error: error.message }); + } +}); + +router.get('/device/:deviceId', async (req, res) => { + try { + const { deviceId } = req.params; + + const cables = await Cable.findAll({ + where: { + [Op.or]: [ + { sourceDeviceId: deviceId }, + { targetDeviceId: deviceId } + ] + }, + include: [ + { + model: Device, + as: 'sourceDevice', + attributes: ['deviceId', 'name', 'type', 'rackId'] + }, + { + model: Device, + as: 'targetDevice', + attributes: ['deviceId', 'name', 'type', 'rackId'] + } + ] + }); + + res.json(cables); + } 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 } = req.body; + + if (!sourceDeviceId || !sourcePort || !targetDeviceId || !targetPort) { + return res.status(400).json({ error: '缺少必填字段' }); + } + + if (sourceDeviceId === targetDeviceId) { + return res.status(400).json({ error: '源设备和目标设备不能相同' }); + } + + const existingCable = await Cable.findOne({ + where: { + [Op.or]: [ + { sourceDeviceId, sourcePort }, + { targetDeviceId, targetPort } + ] + } + }); + + if (existingCable) { + return res.status(400).json({ error: '端口已被占用' }); + } + + const autoCableId = cableId || `CABLE-${Date.now()}-${Math.floor(Math.random() * 10000)}`; + + const cable = await Cable.create({ + cableId: autoCableId, + sourceDeviceId, + sourcePort, + targetDeviceId, + targetPort, + cableType: cableType || 'ethernet', + cableLength, + status: status || 'normal', + description + }); + + const createdCable = await Cable.findByPk(cable.cableId, { + include: [ + { + model: Device, + as: 'sourceDevice', + attributes: ['deviceId', 'name', 'type', 'rackId'] + }, + { + model: Device, + as: 'targetDevice', + attributes: ['deviceId', 'name', 'type', 'rackId'] + } + ] + }); + + res.status(201).json(createdCable); + } catch (error) { + console.error('创建接线失败:', error); + res.status(500).json({ error: error.message }); + } +}); + +router.post('/batch', async (req, res) => { + try { + const { cables } = req.body; + + if (!cables || !Array.isArray(cables) || cables.length === 0) { + return res.status(400).json({ error: '请提供有效的接线数据' }); + } + + const results = { + total: cables.length, + success: 0, + failed: 0, + errors: [] + }; + + for (let i = 0; i < cables.length; i++) { + const cableData = cables[i]; + + try { + if (!cableData.cableId || !cableData.sourceDeviceId || !cableData.sourcePort || + !cableData.targetDeviceId || !cableData.targetPort) { + throw new Error('缺少必填字段'); + } + + if (cableData.sourceDeviceId === cableData.targetDeviceId) { + throw new Error('源设备和目标设备不能相同'); + } + + const existingCable = await Cable.findOne({ + where: { + [Op.or]: [ + { sourceDeviceId: cableData.sourceDeviceId, sourcePort: cableData.sourcePort }, + { targetDeviceId: cableData.targetDeviceId, targetPort: cableData.targetPort } + ] + } + }); + + if (existingCable) { + throw new Error('端口已被占用'); + } + + await Cable.create({ + cableId: cableData.cableId, + sourceDeviceId: cableData.sourceDeviceId, + sourcePort: cableData.sourcePort, + targetDeviceId: cableData.targetDeviceId, + targetPort: cableData.targetPort, + cableType: cableData.cableType || 'ethernet', + cableLength: cableData.cableLength, + status: cableData.status || 'normal', + description: cableData.description + }); + + results.success++; + } catch (error) { + results.failed++; + results.errors.push({ + index: i + 1, + cableId: cableData.cableId, + error: error.message + }); + } + } + + res.json(results); + } catch (error) { + console.error('批量创建接线失败:', error); + res.status(500).json({ error: error.message }); + } +}); + +router.put('/:cableId', async (req, res) => { + try { + const [updated] = await Cable.update(req.body, { + where: { cableId: req.params.cableId } + }); + + if (updated) { + const cable = await Cable.findByPk(req.params.cableId, { + include: [ + { + model: Device, + as: 'sourceDevice', + attributes: ['deviceId', 'name', 'type', 'rackId'] + }, + { + model: Device, + as: 'targetDevice', + attributes: ['deviceId', 'name', 'type', 'rackId'] + } + ] + }); + res.json(cable); + } else { + res.status(404).json({ error: '接线不存在' }); + } + } catch (error) { + console.error('更新接线失败:', error); + res.status(500).json({ error: error.message }); + } +}); + +router.delete('/:cableId', async (req, res) => { + try { + const deleted = await Cable.destroy({ + where: { cableId: req.params.cableId } + }); + + if (deleted) { + res.status(204).json(); + } else { + res.status(404).json({ error: '接线不存在' }); + } + } catch (error) { + console.error('删除接线失败:', error); + res.status(500).json({ error: error.message }); + } +}); + +router.delete('/batch', async (req, res) => { + try { + const { cableIds } = req.body; + + if (!cableIds || !Array.isArray(cableIds) || cableIds.length === 0) { + return res.status(400).json({ error: '请提供有效的接线ID列表' }); + } + + const deletedCount = await Cable.destroy({ + where: { cableId: { [Op.in]: cableIds } } + }); + + res.json({ + message: `批量删除成功,已删除 ${deletedCount} 条接线`, + deletedCount + }); + } catch (error) { + console.error('批量删除接线失败:', error); + res.status(500).json({ error: error.message }); + } +}); + +router.get('/:cableId', async (req, res) => { + try { + const cable = await Cable.findByPk(req.params.cableId, { + include: [ + { + model: Device, + as: 'sourceDevice', + attributes: ['deviceId', 'name', 'type', 'rackId'] + }, + { + model: Device, + as: 'targetDevice', + attributes: ['deviceId', 'name', 'type', 'rackId'] + } + ] + }); + + if (!cable) { + return res.status(404).json({ error: '接线不存在' }); + } + + res.json(cable); + } catch (error) { + console.error('获取接线详情失败:', error); + res.status(500).json({ error: error.message }); + } +}); + +module.exports = router; diff --git a/backend/routes/devicePorts.js b/backend/routes/devicePorts.js new file mode 100644 index 0000000..a91b5df --- /dev/null +++ b/backend/routes/devicePorts.js @@ -0,0 +1,273 @@ +const express = require('express'); +const router = express.Router(); +const { Op } = require('sequelize'); +const DevicePort = require('../models/DevicePort'); +const Device = require('../models/Device'); + +router.get('/', async (req, res) => { + try { + const { deviceId, status, portType, portSpeed, page = 1, pageSize = 10 } = req.query; + const offset = (page - 1) * pageSize; + + 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 { count, rows } = await DevicePort.findAndCountAll({ + where, + include: [ + { + model: Device, + as: 'device', + attributes: ['deviceId', 'name', 'type', 'rackId'] + } + ], + offset, + limit: parseInt(pageSize), + order: [['createdAt', 'DESC']] + }); + + res.json({ + total: count, + ports: rows, + page: parseInt(page), + pageSize: parseInt(pageSize) + }); + } catch (error) { + console.error('获取端口列表失败:', error); + res.status(500).json({ error: error.message }); + } +}); + +router.get('/device/:deviceId', async (req, res) => { + try { + const { deviceId } = req.params; + + const ports = await DevicePort.findAll({ + where: { deviceId }, + include: [ + { + model: Device, + as: 'device', + attributes: ['deviceId', 'name', 'type', 'rackId'] + } + ], + order: [['portName', 'ASC']] + }); + + res.json(ports); + } catch (error) { + console.error('获取设备端口失败:', error); + res.status(500).json({ error: error.message }); + } +}); + +router.post('/', async (req, res) => { + try { + const { portId, deviceId, portName, portType, portSpeed, status, vlanId, description } = req.body; + + if (!deviceId || !portName) { + return res.status(400).json({ error: '缺少必填字段' }); + } + + const existingPort = await DevicePort.findOne({ + where: { deviceId, portName } + }); + + if (existingPort) { + return res.status(400).json({ error: '该设备的端口名称已存在' }); + } + + const autoPortId = portId || `PORT-${Date.now()}-${Math.floor(Math.random() * 10000)}`; + + const port = await DevicePort.create({ + portId: autoPortId, + deviceId, + portName, + portType: portType || 'RJ45', + portSpeed: portSpeed || '1G', + status: status || 'free', + vlanId, + description + }); + + const createdPort = await DevicePort.findByPk(port.portId, { + include: [ + { + model: Device, + as: 'device', + attributes: ['deviceId', 'name', 'type', 'rackId'] + } + ] + }); + + res.status(201).json(createdPort); + } catch (error) { + console.error('创建端口失败:', error); + res.status(500).json({ error: error.message }); + } +}); + +router.post('/batch', async (req, res) => { + try { + const { ports } = req.body; + + if (!ports || !Array.isArray(ports) || ports.length === 0) { + return res.status(400).json({ error: '请提供有效的端口数据' }); + } + + const results = { + total: ports.length, + success: 0, + failed: 0, + errors: [] + }; + + for (let i = 0; i < ports.length; i++) { + const portData = ports[i]; + + try { + if (!portData.portId || !portData.deviceId || !portData.portName) { + throw new Error('缺少必填字段'); + } + + const existingPort = await DevicePort.findOne({ + where: { deviceId: portData.deviceId, portName: portData.portName } + }); + + if (existingPort) { + throw new Error('该设备的端口名称已存在'); + } + + await DevicePort.create({ + portId: portData.portId, + deviceId: portData.deviceId, + portName: portData.portName, + portType: portData.portType || 'RJ45', + portSpeed: portData.portSpeed || '1G', + status: portData.status || 'free', + vlanId: portData.vlanId, + description: portData.description + }); + + results.success++; + } catch (error) { + results.failed++; + results.errors.push({ + index: i + 1, + portId: portData.portId, + error: error.message + }); + } + } + + res.json(results); + } catch (error) { + console.error('批量创建端口失败:', error); + res.status(500).json({ error: error.message }); + } +}); + +router.put('/:portId', async (req, res) => { + try { + const [updated] = await DevicePort.update(req.body, { + where: { portId: req.params.portId } + }); + + if (updated) { + const port = await DevicePort.findByPk(req.params.portId, { + include: [ + { + model: Device, + as: 'device', + attributes: ['deviceId', 'name', 'type', 'rackId'] + } + ] + }); + res.json(port); + } else { + res.status(404).json({ error: '端口不存在' }); + } + } catch (error) { + console.error('更新端口失败:', error); + res.status(500).json({ error: error.message }); + } +}); + +router.delete('/:portId', async (req, res) => { + try { + const deleted = await DevicePort.destroy({ + where: { portId: req.params.portId } + }); + + if (deleted) { + res.status(204).json(); + } else { + res.status(404).json({ error: '端口不存在' }); + } + } catch (error) { + console.error('删除端口失败:', error); + res.status(500).json({ error: error.message }); + } +}); + +router.delete('/batch', async (req, res) => { + try { + const { portIds } = req.body; + + if (!portIds || !Array.isArray(portIds) || portIds.length === 0) { + return res.status(400).json({ error: '请提供有效的端口ID列表' }); + } + + const deletedCount = await DevicePort.destroy({ + where: { portId: { [Op.in]: portIds } } + }); + + res.json({ + message: `批量删除成功,已删除 ${deletedCount} 个端口`, + deletedCount + }); + } catch (error) { + console.error('批量删除端口失败:', error); + res.status(500).json({ error: error.message }); + } +}); + +router.get('/:portId', async (req, res) => { + try { + const port = await DevicePort.findByPk(req.params.portId, { + include: [ + { + model: Device, + as: 'device', + attributes: ['deviceId', 'name', 'type', 'rackId'] + } + ] + }); + + if (!port) { + return res.status(404).json({ error: '端口不存在' }); + } + + res.json(port); + } catch (error) { + console.error('获取端口详情失败:', error); + res.status(500).json({ error: error.message }); + } +}); + +module.exports = router; diff --git a/backend/routes/devices.js b/backend/routes/devices.js index 94e5e40..3fe122e 100644 --- a/backend/routes/devices.js +++ b/backend/routes/devices.js @@ -893,15 +893,35 @@ router.delete('/:deviceId', async (req, res) => { }); if (deleted) { - // 更新机柜当前功率 - const rack = await Rack.findByPk(device.rackId); - if (rack) { - await rack.update({ - currentPower: Math.max(0, rack.currentPower - device.powerConsumption) - }); + const Cable = require('../models/Cable'); + + const deletedCables = await Cable.destroy({ + where: { + [Op.or]: [ + { sourceDeviceId: req.params.deviceId }, + { targetDeviceId: req.params.deviceId } + ] + } + }); + + if (deletedCables > 0) { + console.log(`已删除 ${deletedCables} 条相关接线`); } - res.status(204).json(); + if (device.rackId) { + const rack = await Rack.findByPk(device.rackId); + if (rack) { + await rack.update({ + currentPower: Math.max(0, rack.currentPower - device.powerConsumption) + }); + } + } + + res.status(200).json({ + message: '删除成功', + deviceId: req.params.deviceId, + deletedCablesCount: deletedCables + }); } else { res.status(404).json({ error: '设备不存在' }); } diff --git a/backend/routes/networkCards.js b/backend/routes/networkCards.js new file mode 100644 index 0000000..32aa1cb --- /dev/null +++ b/backend/routes/networkCards.js @@ -0,0 +1,259 @@ +const express = require('express'); +const router = express.Router(); +const { Op } = require('sequelize'); +const NetworkCard = require('../models/NetworkCard'); +const Device = require('../models/Device'); +const DevicePort = require('../models/DevicePort'); + +router.get('/', async (req, res) => { + try { + const { deviceId } = req.query; + const where = {}; + + if (deviceId) { + where.deviceId = deviceId; + } + + const networkCards = await NetworkCard.findAll({ + where, + include: [ + { + model: Device, + as: 'device', + attributes: ['deviceId', 'name', 'type'] + } + ], + order: [['slotNumber', 'ASC'], ['name', 'ASC']] + }); + + res.json(networkCards); + } catch (error) { + console.error('获取网卡列表失败:', error); + res.status(500).json({ error: error.message }); + } +}); + +router.get('/device/:deviceId', async (req, res) => { + try { + const { deviceId } = req.params; + + const networkCards = await NetworkCard.findAll({ + where: { deviceId }, + include: [ + { + model: Device, + as: 'device', + attributes: ['deviceId', 'name', 'type'] + } + ], + order: [['slotNumber', 'ASC'], ['name', 'ASC']] + }); + + res.json(networkCards); + } catch (error) { + console.error('获取设备网卡失败:', error); + res.status(500).json({ error: error.message }); + } +}); + +router.get('/:nicId', async (req, res) => { + try { + const networkCard = await NetworkCard.findByPk(req.params.nicId, { + include: [ + { + model: Device, + as: 'device', + attributes: ['deviceId', 'name', 'type'] + } + ] + }); + + if (!networkCard) { + return res.status(404).json({ error: '网卡不存在' }); + } + + res.json(networkCard); + } catch (error) { + console.error('获取网卡详情失败:', error); + res.status(500).json({ error: error.message }); + } +}); + +router.get('/:nicId/ports', async (req, res) => { + try { + const { nicId } = req.params; + + const ports = await DevicePort.findAll({ + where: { nicId }, + order: [['portName', 'ASC']] + }); + + res.json(ports); + } catch (error) { + console.error('获取网卡端口失败:', error); + res.status(500).json({ error: error.message }); + } +}); + +router.post('/', async (req, res) => { + try { + const { nicId, deviceId, name, description, slotNumber, model, manufacturer, status } = req.body; + + if (!deviceId || !name) { + return res.status(400).json({ error: '缺少必填字段' }); + } + + const existingCard = await NetworkCard.findOne({ + where: { deviceId, name } + }); + + if (existingCard) { + return res.status(400).json({ error: '该设备已存在同名网卡' }); + } + + const autoNicId = nicId || `NIC-${Date.now()}-${Math.floor(Math.random() * 10000)}`; + + const networkCard = await NetworkCard.create({ + nicId: autoNicId, + deviceId, + name, + description, + slotNumber, + model, + manufacturer, + status: status || 'normal', + portCount: 0 + }); + + const createdCard = await NetworkCard.findByPk(networkCard.nicId, { + include: [ + { + model: Device, + as: 'device', + attributes: ['deviceId', 'name', 'type'] + } + ] + }); + + res.status(201).json(createdCard); + } 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, { + where: { nicId: req.params.nicId } + }); + + if (updated) { + const networkCard = await NetworkCard.findByPk(req.params.nicId, { + include: [ + { + model: Device, + as: 'device', + attributes: ['deviceId', 'name', 'type'] + } + ] + }); + res.json(networkCard); + } else { + res.status(404).json({ error: '网卡不存在' }); + } + } catch (error) { + console.error('更新网卡失败:', error); + res.status(500).json({ error: error.message }); + } +}); + +router.delete('/:nicId', async (req, res) => { + try { + const { nicId } = req.params; + + const portCount = await DevicePort.count({ where: { nicId } }); + if (portCount > 0) { + return res.status(400).json({ + error: `该网卡下还有 ${portCount} 个端口,请先删除或转移端口后再删除网卡` + }); + } + + const deleted = await NetworkCard.destroy({ + where: { nicId } + }); + + if (deleted) { + res.status(204).json(); + } else { + res.status(404).json({ error: '网卡不存在' }); + } + } catch (error) { + console.error('删除网卡失败:', error); + res.status(500).json({ error: error.message }); + } +}); + +router.get('/device/:deviceId/with-ports', async (req, res) => { + try { + const { deviceId } = req.params; + + const networkCards = await NetworkCard.findAll({ + where: { deviceId }, + order: [['slotNumber', 'ASC'], ['name', 'ASC']] + }); + + const cardsWithPorts = await Promise.all( + networkCards.map(async (card) => { + const ports = await DevicePort.findAll({ + where: { nicId: card.nicId }, + order: [['portName', 'ASC']] + }); + + const freeCount = ports.filter(p => p.status === 'free').length; + const occupiedCount = ports.filter(p => p.status === 'occupied').length; + const faultCount = ports.filter(p => p.status === 'fault').length; + + return { + ...card.toJSON(), + ports, + stats: { + total: ports.length, + free: freeCount, + occupied: occupiedCount, + fault: faultCount + } + }; + }) + ); + + const ungroupedPorts = await DevicePort.findAll({ + where: { deviceId, nicId: null }, + order: [['portName', 'ASC']] + }); + + if (ungroupedPorts.length > 0) { + cardsWithPorts.push({ + nicId: '_ungrouped', + name: '未分组端口', + description: '未分配到网卡的端口', + portCount: ungroupedPorts.length, + isUngrouped: true, + ports: ungroupedPorts, + stats: { + total: ungroupedPorts.length, + free: ungroupedPorts.filter(p => p.status === 'free').length, + occupied: ungroupedPorts.filter(p => p.status === 'occupied').length, + fault: ungroupedPorts.filter(p => p.status === 'fault').length + } + }); + } + + res.json(cardsWithPorts); + } catch (error) { + console.error('获取网卡及端口失败:', error); + res.status(500).json({ error: error.message }); + } +}); + +module.exports = router; diff --git a/backend/scripts/migrate-v2.js b/backend/scripts/migrate-v2.js new file mode 100644 index 0000000..bfc7133 --- /dev/null +++ b/backend/scripts/migrate-v2.js @@ -0,0 +1,170 @@ +const { sequelize } = require('../db'); +const NetworkCard = require('../models/NetworkCard'); +const DevicePort = require('../models/DevicePort'); +const Device = require('../models/Device'); + +async function migrate() { + console.log('========================================'); + console.log(' IDC管理系统 - 数据库迁移脚本 v2.0 '); + console.log('========================================'); + console.log(''); + + try { + console.log('🔍 检测数据库类型...'); + const dbType = sequelize.getDialect(); + console.log(` 数据库类型: ${dbType}`); + console.log(''); + + console.log('📋 开始迁移...'); + console.log(' 1. 创建 network_cards 表'); + console.log(' 2. 为 device_ports 添加 nic_id 字段'); + console.log(' 3. 创建相关索引'); + console.log(''); + + if (dbType === 'sqlite') { + await migrateSQLite(); + } else if (dbType === 'mysql') { + await migrateMySQL(); + } else { + console.log(`⚠️ 不支持的数据库类型: ${dbType}`); + process.exit(1); + } + + console.log(''); + console.log('✅ 迁移完成!'); + console.log(''); + console.log('📊 验证迁移结果...'); + + const [tables] = await sequelize.query("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"); + console.log(` 数据库表: ${tables.map(t => t.name).join(', ')}`); + + const portCount = await DevicePort.count(); + const cardCount = await NetworkCard.count(); + console.log(` device_ports: ${portCount} 条记录`); + console.log(` network_cards: ${cardCount} 条记录`); + + console.log(''); + console.log('========================================'); + console.log(' 迁移成功完成!🎉'); + console.log('========================================'); + + } catch (error) { + console.error(''); + console.error('❌ 迁移失败:', error.message); + console.error(''); + console.error('错误详情:', error.stack); + process.exit(1); + } finally { + await sequelize.close(); + } +} + +async function migrateSQLite() { + console.log(''); + console.log('🔄 执行 SQLite 迁移...'); + + await sequelize.query('PRAGMA foreign_keys = OFF'); + + try { + console.log(' → 删除旧的 device_ports 表...'); + await sequelize.query('DROP TABLE IF EXISTS `device_ports`'); + + console.log(' → 删除旧的 network_cards 表...'); + await sequelize.query('DROP TABLE IF EXISTS `network_cards`'); + + console.log(' → 同步 DevicePort 模型...'); + await DevicePort.sync({ force: false }); + + console.log(' → 同步 NetworkCard 模型...'); + await NetworkCard.sync({ force: false }); + + console.log(' → 同步 Device 模型(确保外键关系)...'); + await Device.sync({ force: false }); + + console.log(' → 重新同步 DevicePort 模型(含外键)...'); + await DevicePort.sync({ force: true }); + + console.log(' → 重新同步 NetworkCard 模型...'); + await NetworkCard.sync({ force: false }); + + await sequelize.query('PRAGMA foreign_keys = ON'); + + } catch (error) { + await sequelize.query('PRAGMA foreign_keys = ON'); + throw error; + } +} + +async function migrateMySQL() { + console.log(''); + console.log('🔄 执行 MySQL 迁移...'); + + const tableName = 'network_cards'; + console.log(` → 创建表: ${tableName}`); + + const createTableSQL = ` + CREATE TABLE IF NOT EXISTS \`${tableName}\` ( + \`nic_id\` VARCHAR(255) NOT NULL PRIMARY KEY, + \`device_id\` VARCHAR(255) NOT NULL, + \`name\` VARCHAR(255) NOT NULL, + \`description\` TEXT, + \`slot_number\` INT, + \`port_count\` INT DEFAULT 0, + \`model\` VARCHAR(255), + \`manufacturer\` VARCHAR(255), + \`status\` ENUM('normal', 'warning', 'fault', 'offline') DEFAULT 'normal', + \`created_at\` DATETIME DEFAULT CURRENT_TIMESTAMP, + \`updated_at\` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX \`idx_device_id\` (\`device_id\`), + INDEX \`idx_slot_number\` (\`slot_number\`), + UNIQUE INDEX \`idx_device_name\` (\`device_id\`, \`name\`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + `; + + await sequelize.query(createTableSQL); + + console.log(' → 检查 nic_id 字段是否存在...'); + const [columns] = await sequelize.query( + "SHOW COLUMNS FROM `device_ports` LIKE 'nic_id'", + { type: sequelize.QueryTypes.SELECT } + ); + + if (columns.length === 0) { + console.log(' → 添加 nic_id 字段...'); + await sequelize.query( + 'ALTER TABLE `device_ports` ADD COLUMN `nic_id` VARCHAR(255) NULL AFTER `device_id`' + ); + } else { + console.log(' → nic_id 字段已存在,跳过'); + } + + console.log(' → 创建 nic_id 索引...'); + try { + await sequelize.query('CREATE INDEX `idx_port_nic_id` ON `device_ports`(`nic_id`)'); + } catch (error) { + if (error.message.includes('Duplicate key name')) { + console.log(' → 索引已存在,跳过'); + } else { + throw error; + } + } + + console.log(' → 同步模型以确保关系正确...'); + await NetworkCard.sync({ force: false }); + await DevicePort.sync({ force: false }); + + console.log(' → 添加外键约束...'); + try { + await sequelize.query( + 'ALTER TABLE `device_ports` ADD CONSTRAINT `fk_port_nic` FOREIGN KEY (`nic_id`) REFERENCES `network_cards`(`nic_id`)' + ); + } catch (error) { + if (error.message.includes('Duplicate key name') || error.message.includes('already exists')) { + console.log(' → 外键约束已存在,跳过'); + } else { + throw error; + } + } +} + +migrate(); diff --git a/backend/server.js b/backend/server.js index 772181d..5be5de4 100644 --- a/backend/server.js +++ b/backend/server.js @@ -62,6 +62,8 @@ const ticketRoutes = require('./routes/tickets'); const ticketCategoryRoutes = require('./routes/ticketCategories'); const ticketFieldRoutes = require('./routes/ticketFields'); const systemSettingsRoutes = require('./routes/systemSettings'); +const cableRoutes = require('./routes/cables'); +const devicePortRoutes = require('./routes/devicePorts'); // 使用路由 app.use('/api/devices', deviceRoutes); @@ -79,6 +81,8 @@ app.use('/api/tickets', ticketRoutes); app.use('/api/ticket-categories', ticketCategoryRoutes); app.use('/api/ticket-fields', ticketFieldRoutes); app.use('/api/system-settings', systemSettingsRoutes); +app.use('/api/cables', cableRoutes); +app.use('/api/device-ports', devicePortRoutes); // 静态文件服务 app.use('/uploads', express.static('uploads')); diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 1b0c7ca..bd3fe37 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,6 +1,6 @@ import React, { useState, Suspense, lazy } from 'react'; import { Layout, Menu, theme, Button, Dropdown, Avatar, message, Space, Divider, ConfigProvider as AntdConfigProvider } from 'antd'; -import { BarChartOutlined, DatabaseOutlined, CloudServerOutlined, MenuUnfoldOutlined, MenuFoldOutlined, EyeOutlined, BuildOutlined, HomeOutlined, ShoppingCartOutlined, InboxOutlined, ImportOutlined, FileTextOutlined, UserOutlined, LogoutOutlined, HistoryOutlined, AuditOutlined, ToolOutlined, ScheduleOutlined, SettingOutlined } from '@ant-design/icons'; +import { BarChartOutlined, DatabaseOutlined, CloudServerOutlined, MenuUnfoldOutlined, MenuFoldOutlined, EyeOutlined, BuildOutlined, HomeOutlined, ShoppingCartOutlined, InboxOutlined, ImportOutlined, FileTextOutlined, UserOutlined, LogoutOutlined, HistoryOutlined, AuditOutlined, ToolOutlined, ScheduleOutlined, SettingOutlined, ApiOutlined, PartitionOutlined } from '@ant-design/icons'; import { BrowserRouter as Router, Routes, Route, Link, Navigate, useLocation, useNavigate } from 'react-router-dom'; import { useAuth } from './context/AuthContext'; import { ConfigProvider, useConfig } from './context/ConfigContext'; @@ -23,6 +23,8 @@ const TicketCategoryManagement = lazy(() => import('./pages/TicketCategoryManage const TicketStatistics = lazy(() => import('./pages/TicketStatistics')); const TicketFieldManagement = lazy(() => import('./pages/TicketFieldManagement')); const SystemSettings = lazy(() => import('./pages/SystemSettings')); +const CableManagement = lazy(() => import('./pages/CableManagement')); +const PortManagement = lazy(() => import('./pages/PortManagement')); const { Header, Content, Sider } = Layout; @@ -144,7 +146,7 @@ const AppLayout = ({ children }) => { const path = location.pathname; if (path === '/') return 'dashboard'; if (path.startsWith('/rooms') || path.startsWith('/racks') || path.startsWith('/visualization')) return 'room-management'; - if (path.startsWith('/devices') || path.startsWith('/fields')) return 'asset-management'; + if (path.startsWith('/devices') || path.startsWith('/fields') || path.startsWith('/cables') || path.startsWith('/ports')) return 'asset-management'; if (path.startsWith('/consumables')) return 'consumables-management'; if (path.startsWith('/users') || path.startsWith('/login-history') || path.startsWith('/operation-logs') || path.startsWith('/settings')) return 'system-management'; if (path.startsWith('/tickets')) return 'ticket-management'; @@ -244,6 +246,16 @@ const AppLayout = ({ children }) => { icon: , label: 字段管理, }, + { + key: 'cables', + icon: , + label: 接线管理, + }, + { + key: 'ports', + icon: , + label: 端口管理, + }, ], }, { @@ -578,6 +590,8 @@ const ThemeConfig = () => { } /> } /> } /> + } /> + } /> } /> diff --git a/frontend/src/components/DeviceDetailDrawer.jsx b/frontend/src/components/DeviceDetailDrawer.jsx new file mode 100644 index 0000000..d5b11ab --- /dev/null +++ b/frontend/src/components/DeviceDetailDrawer.jsx @@ -0,0 +1,210 @@ +import React, { useState, useCallback, useMemo } from 'react'; +import { Drawer, Tabs, Tag, Space, Typography, Empty, Card, Tooltip } from 'antd'; +import { ApiOutlined, CloudServerOutlined, EnvironmentOutlined } from '@ant-design/icons'; +import NetworkCardPanel from './NetworkCardPanel'; + +const { Text, Title } = Typography; + +const designTokens = { + colors: { + primary: '#667eea', + success: '#10b981', + error: '#ef4444', + warning: '#f59e0b' + }, + spacing: { + sm: 8, + md: 16 + } +}; + +function DeviceDetailDrawer({ device, visible, onClose, cables, onRefreshCables }) { + const [activeTab, setActiveTab] = useState('ports'); + + const deviceCables = useMemo(() => { + if (!device || !cables) return []; + return cables.filter(c => + c.sourceDeviceId === device.deviceId || c.targetDeviceId === device.deviceId + ); + }, [device, cables]); + + const getStatusTag = useCallback((status) => { + const config = { + running: { color: 'success', text: '运行中' }, + normal: { color: 'success', text: '正常' }, + warning: { color: 'warning', text: '警告' }, + error: { color: 'error', text: '故障' }, + fault: { color: 'error', text: '故障' }, + offline: { color: 'default', text: '离线' }, + maintenance: { color: 'processing', text: '维护中' } + }; + const { color, text } = config[status] || { color: 'default', text: status }; + return {text}; + }, []); + + const getDeviceTypeName = useCallback((type) => { + const typeMap = { + server: '服务器', + switch: '交换机', + router: '路由器', + storage: '存储设备', + firewall: '防火墙', + ups: 'UPS', + pdu: 'PDU' + }; + return typeMap[type?.toLowerCase()] || type || '未知设备'; + }, []); + + const tabItems = [ + { + key: 'ports', + label: ( + + + 端口与网卡 + + ), + children: ( + + ) + }, + { + key: 'cables', + label: ( + + + 接线 ({deviceCables.length}) + + ), + children: ( +
+ {deviceCables.length === 0 ? ( + + ) : ( + + {deviceCables.map(cable => ( + +
+ +
+ 源设备: +
+ {cable.sourceDevice?.name || '-'} + {cable.sourcePort} +
+
+
+ 目标设备: +
+ {cable.targetDevice?.name || '-'} + {cable.targetPort} +
+
+
+
+ + + + {cable.status === 'normal' ? '正常' : cable.status === 'fault' ? '故障' : '未连接'} + + + {cable.cableType === 'ethernet' ? '网线' : cable.cableType === 'fiber' ? '光纤' : '铜缆'} + + {cable.cableLength && ( + + {cable.cableLength}m + + )} + + + {cable.description && ( +
+ {cable.description} +
+ )} +
+ ))} +
+ )} +
+ ) + } + ]; + + if (!device) return null; + + return ( + + + 设备详情 - {device.name} + + } + placement="right" + width={520} + open={visible} + onClose={onClose} + styles={{ body: { padding: '16px 20px', overflow: 'auto' } }} + > +
+ 基本信息 +
+
+ 设备ID + {device.deviceId} +
+
+ 设备类型 + {getDeviceTypeName(device.type)} +
+
+ 设备状态 + {getStatusTag(device.status)} +
+
+ 位置 + + U{device.position} {device.height && `(${device.height}U)`} + +
+ {device.ipAddress && ( +
+ IP地址 + {device.ipAddress} +
+ )} + {device.brand && ( +
+ 品牌 + {device.brand} +
+ )} +
+
+ + +
+ ); +} + +export default React.memo(DeviceDetailDrawer); diff --git a/frontend/src/components/NetworkCardCreateModal.jsx b/frontend/src/components/NetworkCardCreateModal.jsx new file mode 100644 index 0000000..b71091f --- /dev/null +++ b/frontend/src/components/NetworkCardCreateModal.jsx @@ -0,0 +1,159 @@ +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 axios from 'axios'; + +const { Option } = Select; +const { TextArea } = Input; + +const designTokens = { + colors: { + primary: { + main: '#667eea' + } + }, + borderRadius: { + medium: '10px' + } +}; + +function NetworkCardCreateModal({ device, visible, onClose, onSuccess }) { + const [form] = Form.useForm(); + const [loading, setLoading] = useState(false); + + const handleSubmit = useCallback(async () => { + try { + const values = await form.validateFields(); + setLoading(true); + + await axios.post('/api/network-cards', { + deviceId: device.deviceId, + name: values.name, + slotNumber: values.slotNumber, + description: values.description, + model: values.model, + manufacturer: values.manufacturer, + status: values.status + }); + + message.success('网卡创建成功'); + form.resetFields(); + onSuccess?.(); + onClose(); + } catch (error) { + if (error.errorFields) { + return; + } + message.error(error.response?.data?.error || '网卡创建失败'); + console.error('创建网卡失败:', error); + } finally { + setLoading(false); + } + }, [device, form, onClose, onSuccess]); + + const handleCancel = useCallback(() => { + form.resetFields(); + onClose(); + }, [form, onClose]); + + return ( + + + 新增网卡 - {device?.name || '设备'} + + } + open={visible} + onOk={handleSubmit} + onCancel={handleCancel} + confirmLoading={loading} + okText="创建" + cancelText="取消" + width={480} + styles={{ body: { padding: '20px 24px' } }} + > +
+ + 网卡名称 + + + + + } + rules={[ + { required: true, message: '请输入网卡名称' }, + { max: 50, message: '名称不能超过50个字符' } + ]} + > + + + + + + + + + + + + + + + + + + + + + + + + +