From e525fa4f11b24577804eb8bda9744bf8748ec326 Mon Sep 17 00:00:00 2001 From: zhang1106 <849185023@qq.com> Date: Fri, 6 Feb 2026 16:23:56 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E6=9C=BA=E6=9F=9C=E7=AE=A1=E7=90=86):=20?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=9C=BA=E6=9F=9C=E6=95=B0=E6=8D=AE=E5=AF=BC?= =?UTF-8?q?=E5=87=BA=E5=8A=9F=E8=83=BD=E5=B9=B6=E4=BC=98=E5=8C=96=E8=AE=BE?= =?UTF-8?q?=E5=A4=87=E5=88=A0=E9=99=A4=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加机柜数据导出功能,支持Excel格式下载 在设备删除逻辑中增加网卡删除处理 调整设备ID字段为非必填且隐藏 --- backend/initDeviceFields.js | 4 +- backend/routes/devices.js | 28 ++++++-- backend/routes/racks.js | 97 +++++++++++++++++++++++++++ frontend/src/pages/RackManagement.jsx | 49 ++++++++++++++ 4 files changed, 169 insertions(+), 9 deletions(-) diff --git a/backend/initDeviceFields.js b/backend/initDeviceFields.js index b7a602c..0c25599 100644 --- a/backend/initDeviceFields.js +++ b/backend/initDeviceFields.js @@ -8,9 +8,9 @@ const defaultDeviceFields = [ fieldName: 'deviceId', displayName: '设备ID', fieldType: 'string', - required: true, + required: false, order: 1, - visible: true, + visible: false, isSystem: true }, { diff --git a/backend/routes/devices.js b/backend/routes/devices.js index 5bf5fd9..9be186e 100644 --- a/backend/routes/devices.js +++ b/backend/routes/devices.js @@ -14,6 +14,7 @@ const DeviceField = require('../models/DeviceField'); const Ticket = require('../models/Ticket'); const DevicePort = require('../models/DevicePort'); // Import DevicePort const Cable = require('../models/Cable'); // Import Cable +const NetworkCard = require('../models/NetworkCard'); // Import NetworkCard const { validateBody, validateQuery } = require('../middleware/validation'); const { createDeviceSchema, @@ -1078,19 +1079,25 @@ router.delete('/batch-delete', validateBody(batchDeviceIdsSchema), async (req, r transaction: t }); - // 2. 删除相关端口 (Delete associated DevicePorts) + // 2. 删除相关网卡 (Delete associated NetworkCards) + await NetworkCard.destroy({ + where: { deviceId: { [Op.in]: deviceIds } }, + transaction: t + }); + + // 3. 删除相关端口 (Delete associated DevicePorts) await DevicePort.destroy({ where: { deviceId: { [Op.in]: deviceIds } }, transaction: t }); - // 3. 解除工单关联 + // 4. 解除工单关联 await Ticket.update( { deviceId: null }, { where: { deviceId: { [Op.in]: deviceIds } }, transaction: t } ); - // 4. 删除设备 + // 5. 删除设备 const deletedCount = await Device.destroy({ where: { deviceId: { [Op.in]: deviceIds } }, transaction: t @@ -1116,7 +1123,8 @@ router.delete('/batch-delete', validateBody(batchDeviceIdsSchema), async (req, r }); } catch (error) { await t.rollback(); - res.status(500).json({ error: error.message }); + console.error('批量删除设备错误:', error); + res.status(500).json({ error: error.message, stack: error.stack }); } }); @@ -1145,20 +1153,26 @@ router.delete('/:deviceId', async (req, res) => { transaction: t }); - // 2. 删除相关端口 (Delete associated DevicePorts) + // 2. 删除相关网卡 (Delete associated NetworkCards) + const deletedNetworkCards = await NetworkCard.destroy({ + where: { deviceId: deviceId }, + transaction: t + }); + + // 3. 删除相关端口 (Delete associated DevicePorts) // 必须在删除设备之前删除,否则触发外键约束错误 const deletedPorts = await DevicePort.destroy({ where: { deviceId: deviceId }, transaction: t }); - // 3. 解除工单关联 (Unlink Tickets) + // 4. 解除工单关联 (Unlink Tickets) await Ticket.update( { deviceId: null }, { where: { deviceId: deviceId }, transaction: t } ); - // 4. 删除设备 (Delete Device) + // 5. 删除设备 (Delete Device) await Device.destroy({ where: { deviceId: deviceId }, transaction: t diff --git a/backend/routes/racks.js b/backend/routes/racks.js index 0e0d29c..589b97a 100644 --- a/backend/routes/racks.js +++ b/backend/routes/racks.js @@ -452,4 +452,101 @@ router.post('/import', async (req, res) => { } }); +// 导出租机柜数据 +router.get('/export', async (req, res) => { + try { + // 获取所有机柜数据(包含机房信息) + const racks = await Rack.findAll({ + include: [ + { model: Room, attributes: ['name'] }, + { model: Device, attributes: ['deviceId', 'name', 'powerConsumption'] } + ], + order: [['rackId', 'ASC']] + }); + + // 准备导出数据 + const exportData = racks.map(rack => { + const deviceCount = rack.Devices ? rack.Devices.length : 0; + const totalPower = rack.Devices ? rack.Devices.reduce((sum, d) => sum + (d.powerConsumption || 0), 0) : 0; + + return { + '机柜ID': rack.rackId, + '机柜名称': rack.name, + '所属机房': rack.Room ? rack.Room.name : '', + '机柜高度(U)': rack.height, + '最大功耗(W)': rack.maxPower, + '当前功耗(W)': rack.currentPower || 0, + '设备数量': deviceCount, + '设备总功耗(W)': totalPower, + '状态': rack.status === 'active' ? '启用' : '停用', + '创建时间': rack.createdAt ? new Date(rack.createdAt).toLocaleString() : '' + }; + }); + + // 创建工作簿 + const wb = XLSX.utils.book_new(); + const ws = XLSX.utils.json_to_sheet(exportData); + + // 设置列宽 + ws['!cols'] = [ + { wch: 15 }, // 机柜ID + { wch: 20 }, // 机柜名称 + { wch: 20 }, // 所属机房 + { wch: 12 }, // 机柜高度 + { wch: 15 }, // 最大功耗 + { wch: 15 }, // 当前功耗 + { wch: 12 }, // 设备数量 + { wch: 15 }, // 设备总功耗 + { wch: 10 }, // 状态 + { wch: 20 } // 创建时间 + ]; + + XLSX.utils.book_append_sheet(wb, ws, '机柜列表'); + + // 生成文件名 + const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19); + const fileName = `机柜导出_${timestamp}.xlsx`; + + // 确保temp目录存在 + const tempDir = path.join(__dirname, '../temp'); + if (!fs.existsSync(tempDir)) { + fs.mkdirSync(tempDir, { recursive: true }); + } + + const filePath = path.join(tempDir, fileName); + + // 写入文件 + XLSX.writeFile(wb, filePath); + + // 发送文件 + res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); + res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${encodeURIComponent(fileName)}`); + + const fileStream = fs.createReadStream(filePath); + fileStream.pipe(res); + + // 发送完成后删除临时文件 + fileStream.on('close', () => { + if (fs.existsSync(filePath)) { + fs.unlinkSync(filePath); + } + }); + + fileStream.on('error', (err) => { + console.error('文件流错误:', err); + if (fs.existsSync(filePath)) { + fs.unlinkSync(filePath); + } + }); + + } catch (error) { + console.error('导出租机柜数据失败:', error); + res.status(500).json({ + success: false, + message: '导出失败', + error: error.message + }); + } +}); + module.exports = router; \ No newline at end of file diff --git a/frontend/src/pages/RackManagement.jsx b/frontend/src/pages/RackManagement.jsx index a0c40f8..5ee9f3f 100644 --- a/frontend/src/pages/RackManagement.jsx +++ b/frontend/src/pages/RackManagement.jsx @@ -404,6 +404,48 @@ function RackManagement() { message.success('模板下载成功'); }, []); + // 导出租机柜数据 + const handleExport = useCallback(async () => { + try { + message.loading('正在导出租机柜数据...', 0); + + const response = await axios.get('/api/racks/export', { + responseType: 'blob' + }); + + // 创建下载链接 + const blob = new Blob([response.data], { + type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' + }); + const url = window.URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + + // 从响应头获取文件名,或使用默认文件名 + const contentDisposition = response.headers['content-disposition']; + let fileName = '机柜导出.xlsx'; + if (contentDisposition) { + const match = contentDisposition.match(/filename\*?=(?:UTF-8'')?([^;]+)/); + if (match) { + fileName = decodeURIComponent(match[1].replace(/['"]/g, '')); + } + } + + link.setAttribute('download', fileName); + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + window.URL.revokeObjectURL(url); + + message.destroy(); + message.success('机柜导出成功'); + } catch (error) { + message.destroy(); + message.error('机柜导出失败'); + console.error('机柜导出失败:', error); + } + }, []); + const handleImport = useCallback(async (file) => { try { setIsImporting(true); @@ -703,6 +745,13 @@ function RackManagement() { > 导入 +