feat(机柜管理): 增强Excel导入功能并完善状态显示
- 在导出功能中增加"维护中"状态的中文显示 - 重构导入功能以支持多种Excel格式和列名映射 - 添加状态值的中英文自动转换 - 改进数据验证逻辑和错误提示信息
This commit is contained in:
+94
-17
@@ -124,7 +124,7 @@ router.get('/export', async (req, res) => {
|
||||
'当前功耗(W)': rack.currentPower || 0,
|
||||
'设备数量': deviceCount,
|
||||
'设备总功耗(W)': totalPower,
|
||||
'状态': rack.status === 'active' ? '启用' : '停用',
|
||||
'状态': rack.status === 'active' ? '启用' : rack.status === 'maintenance' ? '维护中' : '停用',
|
||||
'创建时间': rack.createdAt ? new Date(rack.createdAt).toLocaleString() : ''
|
||||
};
|
||||
});
|
||||
@@ -357,12 +357,62 @@ router.post('/import', async (req, res) => {
|
||||
|
||||
const worksheet = workbook.Sheets[workbook.SheetNames[0]];
|
||||
|
||||
// 转换为JSON格式
|
||||
const jsonData = XLSX.utils.sheet_to_json(worksheet, {
|
||||
header: ['rackId', 'name', 'roomName', 'height', 'maxPower', 'status'],
|
||||
// 读取第一行作为列头
|
||||
const headerRow = XLSX.utils.sheet_to_json(worksheet, { header: 1, range: 0, limit: 1 })[0] || [];
|
||||
|
||||
// 定义列名映射(支持导入模板格式和导出文件格式)
|
||||
const columnMapping = {
|
||||
// 机柜ID - 支持多种列名
|
||||
rackId: ['机柜ID(留空自动生成)', '机柜ID'],
|
||||
// 机柜名称
|
||||
name: ['机柜名称'],
|
||||
// 所属机房名称
|
||||
roomName: ['所属机房名称', '所属机房'],
|
||||
// 高度(U)
|
||||
height: ['高度(U)', '机柜高度(U)'],
|
||||
// 最大功率/最大功耗
|
||||
maxPower: ['最大功率(W)', '最大功耗(W)'],
|
||||
// 状态
|
||||
status: ['状态']
|
||||
};
|
||||
|
||||
// 根据列头自动检测列索引映射
|
||||
const columnIndexMap = {};
|
||||
Object.keys(columnMapping).forEach(field => {
|
||||
const possibleNames = columnMapping[field];
|
||||
const index = headerRow.findIndex(h => possibleNames.includes(String(h).trim()));
|
||||
if (index !== -1) {
|
||||
columnIndexMap[field] = index;
|
||||
}
|
||||
});
|
||||
|
||||
// 检查必需的列是否存在
|
||||
const requiredColumns = ['name', 'roomName', 'height', 'maxPower', 'status'];
|
||||
const missingColumns = requiredColumns.filter(col => columnIndexMap[col] === undefined);
|
||||
if (missingColumns.length > 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Excel列名格式不正确',
|
||||
error: `缺少必需的列: ${missingColumns.join(', ')},请使用系统导出的文件或下载导入模板`
|
||||
});
|
||||
}
|
||||
|
||||
// 转换为JSON格式(使用检测到的列索引)
|
||||
const rawData = XLSX.utils.sheet_to_json(worksheet, {
|
||||
header: headerRow.map((h, i) => `col_${i}`),
|
||||
range: 1, // 从第2行开始读取数据
|
||||
blankrows: false // 跳过空行
|
||||
});
|
||||
|
||||
// 根据列映射转换数据
|
||||
const jsonData = rawData.map(row => {
|
||||
const item = {};
|
||||
Object.keys(columnIndexMap).forEach(field => {
|
||||
const colIndex = columnIndexMap[field];
|
||||
item[field] = row[`col_${colIndex}`];
|
||||
});
|
||||
return item;
|
||||
});
|
||||
|
||||
if (jsonData.length === 0) {
|
||||
return res.status(400).json({
|
||||
@@ -372,7 +422,18 @@ router.post('/import', async (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
// 验证数据
|
||||
// 状态值转换映射(中文 → 英文)
|
||||
const statusMapping = {
|
||||
'启用': 'active',
|
||||
'在用': 'active',
|
||||
'停用': 'inactive',
|
||||
'禁用': 'inactive',
|
||||
'维护中': 'maintenance',
|
||||
'active': 'active',
|
||||
'inactive': 'inactive',
|
||||
'maintenance': 'maintenance'
|
||||
};
|
||||
|
||||
const validStatuses = ['active', 'maintenance', 'inactive'];
|
||||
const validationResults = [];
|
||||
|
||||
@@ -381,38 +442,54 @@ router.post('/import', async (req, res) => {
|
||||
const roomNameToIdMap = new Map(allRooms.map(room => [room.name, room.roomId]));
|
||||
const validRoomNames = new Set(roomNameToIdMap.keys());
|
||||
|
||||
// 为没有rackId的记录自动生成
|
||||
let autoGeneratedIdIndex = 0;
|
||||
// 处理数据(转换状态值、处理空rackId)
|
||||
const processedData = jsonData.map((item, index) => {
|
||||
const rowNumber = index + 2;
|
||||
|
||||
// 如果rackId为空,先标记为null,后续统一生成
|
||||
if (!item.rackId || item.rackId.trim() === '') {
|
||||
return { ...item, rackId: null, rowNumber, _autoGenerate: true };
|
||||
// 转换状态值(中文转英文)
|
||||
const rawStatus = String(item.status || '').trim();
|
||||
const normalizedStatus = statusMapping[rawStatus] || rawStatus.toLowerCase();
|
||||
|
||||
// 如果rackId为空或列不存在,标记为需要自动生成
|
||||
const rawRackId = item.rackId ? String(item.rackId).trim() : '';
|
||||
if (!rawRackId || rawRackId === '') {
|
||||
return {
|
||||
...item,
|
||||
rackId: null,
|
||||
status: normalizedStatus,
|
||||
rowNumber,
|
||||
_autoGenerate: true
|
||||
};
|
||||
}
|
||||
|
||||
return { ...item, rowNumber, _autoGenerate: false };
|
||||
return {
|
||||
...item,
|
||||
rackId: rawRackId,
|
||||
status: normalizedStatus,
|
||||
rowNumber,
|
||||
_autoGenerate: false
|
||||
};
|
||||
});
|
||||
|
||||
// 验证数据
|
||||
processedData.forEach((item) => {
|
||||
const errors = [];
|
||||
|
||||
// rackId为null表示需要自动生成,跳过验证
|
||||
// rackId为null表示需要自动生成,跳过格式验证
|
||||
if (item.rackId !== null) {
|
||||
// 验证机柜ID格式
|
||||
if (!/^RACK\d+$/.test(item.rackId.trim())) {
|
||||
if (!/^RACK\d+$/.test(item.rackId)) {
|
||||
errors.push('机柜ID格式应为RACK+数字,如RACK001');
|
||||
}
|
||||
}
|
||||
|
||||
if (!item.name || item.name.trim() === '') {
|
||||
if (!item.name || String(item.name).trim() === '') {
|
||||
errors.push('机柜名称不能为空');
|
||||
}
|
||||
|
||||
if (!item.roomName || item.roomName.trim() === '') {
|
||||
if (!item.roomName || String(item.roomName).trim() === '') {
|
||||
errors.push('所属机房名称不能为空');
|
||||
} else if (!validRoomNames.has(item.roomName.trim())) {
|
||||
} else if (!validRoomNames.has(String(item.roomName).trim())) {
|
||||
errors.push(`所属机房名称不存在: ${item.roomName}`);
|
||||
}
|
||||
|
||||
@@ -425,7 +502,7 @@ router.post('/import', async (req, res) => {
|
||||
}
|
||||
|
||||
if (!item.status || !validStatuses.includes(item.status)) {
|
||||
errors.push(`状态必须是以下值之一: ${validStatuses.join(', ')}`);
|
||||
errors.push(`状态必须是以下值之一: ${validStatuses.join(', ')} (或对应中文: 启用/在用、停用/禁用、维护中)`);
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
|
||||
Reference in New Issue
Block a user