Files
zhang1106 979c28fdcd feat(floorplan): 新增机房平面图功能模块
refactor(models): 为Room和Rack模型添加布局相关字段
feat(routes): 实现机房平面图相关API接口
feat(components): 添加平面图编辑器、画布渲染及交互组件
feat(hooks): 实现平面图数据管理和状态管理
feat(pages): 新增机房平面图页面入口
style(components): 优化平面图组件样式和交互体验
2026-04-30 16:26:39 +08:00

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;