feat(机柜管理): 添加机柜数据导出功能并优化设备删除逻辑
添加机柜数据导出功能,支持Excel格式下载 在设备删除逻辑中增加网卡删除处理 调整设备ID字段为非必填且隐藏
This commit is contained in:
@@ -8,9 +8,9 @@ const defaultDeviceFields = [
|
|||||||
fieldName: 'deviceId',
|
fieldName: 'deviceId',
|
||||||
displayName: '设备ID',
|
displayName: '设备ID',
|
||||||
fieldType: 'string',
|
fieldType: 'string',
|
||||||
required: true,
|
required: false,
|
||||||
order: 1,
|
order: 1,
|
||||||
visible: true,
|
visible: false,
|
||||||
isSystem: true
|
isSystem: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ const DeviceField = require('../models/DeviceField');
|
|||||||
const Ticket = require('../models/Ticket');
|
const Ticket = require('../models/Ticket');
|
||||||
const DevicePort = require('../models/DevicePort'); // Import DevicePort
|
const DevicePort = require('../models/DevicePort'); // Import DevicePort
|
||||||
const Cable = require('../models/Cable'); // Import Cable
|
const Cable = require('../models/Cable'); // Import Cable
|
||||||
|
const NetworkCard = require('../models/NetworkCard'); // Import NetworkCard
|
||||||
const { validateBody, validateQuery } = require('../middleware/validation');
|
const { validateBody, validateQuery } = require('../middleware/validation');
|
||||||
const {
|
const {
|
||||||
createDeviceSchema,
|
createDeviceSchema,
|
||||||
@@ -1078,19 +1079,25 @@ router.delete('/batch-delete', validateBody(batchDeviceIdsSchema), async (req, r
|
|||||||
transaction: t
|
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({
|
await DevicePort.destroy({
|
||||||
where: { deviceId: { [Op.in]: deviceIds } },
|
where: { deviceId: { [Op.in]: deviceIds } },
|
||||||
transaction: t
|
transaction: t
|
||||||
});
|
});
|
||||||
|
|
||||||
// 3. 解除工单关联
|
// 4. 解除工单关联
|
||||||
await Ticket.update(
|
await Ticket.update(
|
||||||
{ deviceId: null },
|
{ deviceId: null },
|
||||||
{ where: { deviceId: { [Op.in]: deviceIds } }, transaction: t }
|
{ where: { deviceId: { [Op.in]: deviceIds } }, transaction: t }
|
||||||
);
|
);
|
||||||
|
|
||||||
// 4. 删除设备
|
// 5. 删除设备
|
||||||
const deletedCount = await Device.destroy({
|
const deletedCount = await Device.destroy({
|
||||||
where: { deviceId: { [Op.in]: deviceIds } },
|
where: { deviceId: { [Op.in]: deviceIds } },
|
||||||
transaction: t
|
transaction: t
|
||||||
@@ -1116,7 +1123,8 @@ router.delete('/batch-delete', validateBody(batchDeviceIdsSchema), async (req, r
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await t.rollback();
|
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
|
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({
|
const deletedPorts = await DevicePort.destroy({
|
||||||
where: { deviceId: deviceId },
|
where: { deviceId: deviceId },
|
||||||
transaction: t
|
transaction: t
|
||||||
});
|
});
|
||||||
|
|
||||||
// 3. 解除工单关联 (Unlink Tickets)
|
// 4. 解除工单关联 (Unlink Tickets)
|
||||||
await Ticket.update(
|
await Ticket.update(
|
||||||
{ deviceId: null },
|
{ deviceId: null },
|
||||||
{ where: { deviceId: deviceId }, transaction: t }
|
{ where: { deviceId: deviceId }, transaction: t }
|
||||||
);
|
);
|
||||||
|
|
||||||
// 4. 删除设备 (Delete Device)
|
// 5. 删除设备 (Delete Device)
|
||||||
await Device.destroy({
|
await Device.destroy({
|
||||||
where: { deviceId: deviceId },
|
where: { deviceId: deviceId },
|
||||||
transaction: t
|
transaction: t
|
||||||
|
|||||||
@@ -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;
|
module.exports = router;
|
||||||
@@ -404,6 +404,48 @@ function RackManagement() {
|
|||||||
message.success('模板下载成功');
|
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) => {
|
const handleImport = useCallback(async (file) => {
|
||||||
try {
|
try {
|
||||||
setIsImporting(true);
|
setIsImporting(true);
|
||||||
@@ -703,6 +745,13 @@ function RackManagement() {
|
|||||||
>
|
>
|
||||||
导入
|
导入
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
icon={<DownloadOutlined />}
|
||||||
|
onClick={handleExport}
|
||||||
|
style={actionButtonStyle}
|
||||||
|
>
|
||||||
|
导出
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
icon={<PlusOutlined />}
|
icon={<PlusOutlined />}
|
||||||
|
|||||||
Reference in New Issue
Block a user