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() {
>
导入
+ }
+ onClick={handleExport}
+ style={actionButtonStyle}
+ >
+ 导出
+
}