refactor(models): 为Room和Rack模型添加布局相关字段 feat(routes): 实现机房平面图相关API接口 feat(components): 添加平面图编辑器、画布渲染及交互组件 feat(hooks): 实现平面图数据管理和状态管理 feat(pages): 新增机房平面图页面入口 style(components): 优化平面图组件样式和交互体验
261 lines
7.5 KiB
JavaScript
261 lines
7.5 KiB
JavaScript
const express = require('express');
|
|
const router = express.Router();
|
|
const { Room, Rack, Device } = require('../models');
|
|
const { sequelize } = require('../db');
|
|
const { validateBody } = require('../middleware/validation');
|
|
const { createRoomSchema, updateRoomSchema } = require('../validation/roomSchema');
|
|
|
|
// 获取所有机房
|
|
router.get('/', async (req, res) => {
|
|
try {
|
|
const page = parseInt(req.query.page) || 1;
|
|
const pageSize = parseInt(req.query.pageSize) || 100;
|
|
const offset = (page - 1) * pageSize;
|
|
|
|
const { count, rows } = await Room.findAndCountAll({
|
|
include: [{ model: Rack, attributes: ['rackId', 'name'] }],
|
|
offset: offset,
|
|
limit: pageSize,
|
|
});
|
|
|
|
res.json({
|
|
rooms: rows,
|
|
total: count,
|
|
});
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// 获取单个机房
|
|
router.get('/:roomId', async (req, res) => {
|
|
try {
|
|
const room = await Room.findByPk(req.params.roomId, {
|
|
include: Rack,
|
|
});
|
|
if (!room) {
|
|
return res.status(404).json({ error: '机房不存在' });
|
|
}
|
|
res.json(room);
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// 创建机房
|
|
router.post('/', async (req, res) => {
|
|
try {
|
|
const room = await Room.create(req.body);
|
|
res.status(201).json(room);
|
|
} catch (error) {
|
|
res.status(400).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// 更新机房
|
|
router.put('/:roomId', validateBody(updateRoomSchema), async (req, res) => {
|
|
try {
|
|
const [updated] = await Room.update(req.body, {
|
|
where: { roomId: req.params.roomId },
|
|
});
|
|
if (updated) {
|
|
const updatedRoom = await Room.findByPk(req.params.roomId);
|
|
res.json(updatedRoom);
|
|
} else {
|
|
res.status(404).json({ error: '机房不存在' });
|
|
}
|
|
} catch (error) {
|
|
res.status(400).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// 删除机房
|
|
router.delete('/:roomId', async (req, res) => {
|
|
try {
|
|
const racks = await Rack.findAll({ where: { roomId: req.params.roomId } });
|
|
if (racks.length > 0) {
|
|
return res.status(400).json({ error: '该机房下有机柜,无法删除' });
|
|
}
|
|
|
|
const deleted = await Room.destroy({
|
|
where: { roomId: req.params.roomId },
|
|
});
|
|
if (deleted) {
|
|
res.status(204).json();
|
|
} else {
|
|
res.status(404).json({ error: '机房不存在' });
|
|
}
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// 获取机房平面图布局数据
|
|
router.get('/:roomId/layout', async (req, res) => {
|
|
try {
|
|
const room = await Room.findByPk(req.params.roomId);
|
|
if (!room) {
|
|
return res.status(404).json({ error: '机房不存在' });
|
|
}
|
|
|
|
const racks = await Rack.findAll({
|
|
where: { roomId: req.params.roomId },
|
|
include: [{
|
|
model: Device,
|
|
as: 'Devices',
|
|
attributes: ['deviceId', 'name', 'type', 'status', 'position', 'height', 'powerConsumption', 'ipAddress', 'model'],
|
|
}],
|
|
});
|
|
|
|
const rackData = racks.map(rack => {
|
|
const rackJson = rack.toJSON();
|
|
const devices = rackJson.Devices || [];
|
|
const usedU = devices.reduce((sum, d) => sum + (d.height || 1), 0);
|
|
const totalPower = devices.reduce((sum, d) => sum + (d.powerConsumption || 0), 0);
|
|
return {
|
|
rackId: rackJson.rackId,
|
|
name: rackJson.name,
|
|
rowPos: rackJson.rowPos,
|
|
colPos: rackJson.colPos,
|
|
facing: rackJson.facing,
|
|
status: rackJson.status,
|
|
height: rackJson.height,
|
|
maxPower: rackJson.maxPower,
|
|
currentPower: rackJson.currentPower,
|
|
deviceCount: devices.length,
|
|
usedU,
|
|
totalU: rackJson.height,
|
|
utilization: rackJson.height > 0 ? usedU / rackJson.height : 0,
|
|
totalPower,
|
|
Devices: devices,
|
|
};
|
|
});
|
|
|
|
const activeRacks = rackData.filter(r => r.status === 'active').length;
|
|
const maintenanceRacks = rackData.filter(r => r.status === 'maintenance').length;
|
|
const inactiveRacks = rackData.filter(r => r.status === 'inactive').length;
|
|
const totalMaxPower = rackData.reduce((sum, r) => sum + (r.maxPower || 0), 0);
|
|
const totalCurrentPower = rackData.reduce((sum, r) => sum + (r.currentPower || 0), 0);
|
|
const avgUtilization = rackData.length > 0
|
|
? rackData.reduce((sum, r) => sum + r.utilization, 0) / rackData.length
|
|
: 0;
|
|
|
|
res.json({
|
|
room: {
|
|
roomId: room.roomId,
|
|
name: room.name,
|
|
location: room.location,
|
|
area: room.area,
|
|
capacity: room.capacity,
|
|
gridRows: room.gridRows,
|
|
gridCols: room.gridCols,
|
|
layoutConfig: room.layoutConfig,
|
|
},
|
|
racks: rackData,
|
|
stats: {
|
|
totalRacks: rackData.length,
|
|
activeRacks,
|
|
maintenanceRacks,
|
|
inactiveRacks,
|
|
avgUtilization: Math.round(avgUtilization * 100) / 100,
|
|
totalCurrentPower,
|
|
totalMaxPower,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// 更新机房布局参数
|
|
router.put('/:roomId/layout', async (req, res) => {
|
|
try {
|
|
const { gridRows, gridCols, layoutConfig } = req.body;
|
|
const room = await Room.findByPk(req.params.roomId);
|
|
if (!room) {
|
|
return res.status(404).json({ error: '机房不存在' });
|
|
}
|
|
|
|
const updateData = {};
|
|
if (gridRows !== undefined) updateData.gridRows = gridRows;
|
|
if (gridCols !== undefined) updateData.gridCols = gridCols;
|
|
if (layoutConfig !== undefined) updateData.layoutConfig = layoutConfig;
|
|
|
|
await Room.update(updateData, { where: { roomId: req.params.roomId } });
|
|
const updatedRoom = await Room.findByPk(req.params.roomId);
|
|
res.json(updatedRoom);
|
|
} catch (error) {
|
|
res.status(400).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// 批量更新机房内机柜位置
|
|
router.put('/:roomId/racks-positions', async (req, res) => {
|
|
try {
|
|
const { positions } = req.body;
|
|
if (!Array.isArray(positions)) {
|
|
return res.status(400).json({ error: 'positions 必须是数组' });
|
|
}
|
|
|
|
const results = [];
|
|
for (const pos of positions) {
|
|
const { rackId, rowPos, colPos, facing } = pos;
|
|
if (!rackId) continue;
|
|
|
|
const rack = await Rack.findByPk(rackId);
|
|
if (!rack || rack.roomId !== req.params.roomId) continue;
|
|
|
|
await Rack.update(
|
|
{ rowPos, colPos, facing },
|
|
{ where: { rackId } }
|
|
);
|
|
results.push(rackId);
|
|
}
|
|
|
|
res.json({ success: true, updated: results.length });
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// 初始化机房布局(自动分配所有机柜位置)
|
|
router.post('/:roomId/init-layout', async (req, res) => {
|
|
try {
|
|
const { gridRows, gridCols } = req.body;
|
|
const rows = gridRows || 10;
|
|
const cols = gridCols || 10;
|
|
|
|
const racks = await Rack.findAll({
|
|
where: { roomId: req.params.roomId },
|
|
order: [['name', 'ASC']],
|
|
});
|
|
|
|
const positions = [];
|
|
let index = 0;
|
|
for (let row = 0; row < rows && index < racks.length; row++) {
|
|
for (let col = 0; col < cols && index < racks.length; col++) {
|
|
positions.push({
|
|
rackId: racks[index].rackId,
|
|
rowPos: row,
|
|
colPos: col,
|
|
facing: 'front',
|
|
});
|
|
index++;
|
|
}
|
|
}
|
|
|
|
for (const pos of positions) {
|
|
await Rack.update(
|
|
{ rowPos: pos.rowPos, colPos: pos.colPos, facing: pos.facing },
|
|
{ where: { rackId: pos.rackId } }
|
|
);
|
|
}
|
|
|
|
res.json({ success: true, assigned: positions.length });
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
module.exports = router;
|