feat: 添加空闲设备状态支持并优化耗材导入功能

refactor(设备管理): 重构状态筛选和高级搜索界面

perf(机柜管理): 优化U位计算方式为基于设备高度

fix(耗材管理): 修复导入时库存计算不准确的问题

style(耗材统计): 调整表格样式和实时刷新功能

docs: 更新设备状态映射包含空闲状态
This commit is contained in:
zhang1106
2026-03-24 14:54:39 +08:00
parent f82b8202fe
commit 1b898814ed
13 changed files with 1385 additions and 456 deletions
+14 -1
View File
@@ -104,7 +104,8 @@ const defaultDeviceFields = [
{ value: 'running', label: '运行中' },
{ value: 'maintenance', label: '维护中' },
{ value: 'offline', label: '离线' },
{ value: 'fault', label: '故障' }
{ value: 'fault', label: '故障' },
{ value: 'idle', label: '空闲' }
]
},
{
@@ -176,6 +177,18 @@ async function initDeviceFields() {
if (field.options && !existingField.options) {
await existingField.update({ options: field.options });
console.log(`更新字段 options: ${field.displayName}`);
} else if (field.options && existingField.options) {
// 如果系统字段已有 options,检查是否缺少默认选项,补充缺失的选项
const existingValues = existingField.options.map(o => o.value);
const defaultValues = field.options.map(o => o.value);
const missingOptions = field.options.filter(o => !existingValues.includes(o.value));
if (missingOptions.length > 0) {
const updatedOptions = [...existingField.options, ...missingOptions];
await existingField.update({ options: updatedOptions });
console.log(`补充缺失的 options: ${field.displayName},新增: ${missingOptions.map(o => o.label).join(', ')}`);
} else {
console.log(`跳过已存在字段: ${field.displayName}`);
}
} else {
console.log(`跳过已存在字段: ${field.displayName}`);
}
+76 -21
View File
@@ -67,6 +67,9 @@ router.post('/', async (req, res) => {
...req.body,
consumableId: req.body.consumableId || `CON${Date.now()}`
};
if (Array.isArray(consumableData.snList)) {
consumableData.currentStock = consumableData.snList.length;
}
const consumable = await Consumable.create(consumableData, { transaction });
await ConsumableLog.create({
@@ -107,7 +110,7 @@ router.post('/', async (req, res) => {
router.post('/import', async (req, res) => {
const transaction = await sequelize.transaction();
try {
const { items, operator = '系统' } = req.body;
const { items, operator = '系统', mode = 'create' } = req.body;
if (!items || !Array.isArray(items) || items.length === 0) {
await transaction.rollback();
@@ -117,45 +120,93 @@ router.post('/import', async (req, res) => {
const results = {
success: 0,
failed: 0,
errors: []
updated: 0,
skipped: 0,
errors: [],
details: []
};
for (let i = 0; i < items.length; i++) {
const item = items[i];
const rowNumber = i + 1;
try {
let consumableId = item.耗材ID || item.consumableId;
const name = item.名称 || item.name;
const category = item.分类 || item.category;
if (!name || !category) {
results.failed++;
results.errors.push(`${rowNumber} 行: 名称和分类为必填项`);
results.details.push({ row: rowNumber, status: 'failed', error: '名称和分类为必填项' });
continue;
}
let snList = [];
if (item.SN序列号 || item.snList) {
const snStr = item.SN序列号 || item.snList;
if (typeof snStr === 'string') {
snList = snStr.split(/[,;\n]/).map(s => s.trim()).filter(Boolean);
} else if (Array.isArray(snStr)) {
snList = snStr;
}
}
const consumableData = {
consumableId: item.耗材ID || item.consumableId || `CON${Date.now()}${i}`,
name: item.名称 || item.name,
category: item.分类 || item.category,
consumableId: consumableId || `CON${Date.now()}${i}`,
name,
category,
unit: item.单位 || item.unit || '个',
currentStock: parseInt(item.当前库存 || item.currentStock) || 0,
currentStock: snList.length > 0 ? snList.length : (parseInt(item.当前库存 || item.currentStock) || 0),
minStock: parseInt(item.最小库存 || item.minStock) || 10,
maxStock: parseInt(item.最大库存 || item.maxStock) || 100,
maxStock: parseInt(item.最大库存 || item.maxStock) || 0,
unitPrice: parseFloat(item.单价 || item.unitPrice) || 0,
supplier: item.供应商 || item.supplier || '',
location: item.存放位置 || item.location || '',
description: item.描述 || item.description || '',
status: item.状态 || item.status || 'active'
status: item.状态 || item.status || 'active',
snList
};
if (!consumableData.name || !consumableData.category) {
results.failed++;
results.errors.push(`${i + 1} 行: 名称和分类为必填项`);
continue;
let existingConsumable = null;
if (consumableId) {
existingConsumable = await Consumable.findByPk(consumableId, { transaction });
}
const consumable = await Consumable.create(consumableData, { transaction });
let consumable;
let operationType;
let previousStock = 0;
if (existingConsumable) {
if (mode === 'update') {
previousStock = existingConsumable.currentStock;
await existingConsumable.update(consumableData, { transaction });
consumable = existingConsumable;
operationType = 'import_update';
results.updated++;
results.details.push({ row: rowNumber, status: 'updated', consumableId: consumable.consumableId, name: consumable.name });
} else {
results.skipped++;
results.details.push({ row: rowNumber, status: 'skipped', reason: '耗材已存在', consumableId: consumableId });
continue;
}
} else {
consumable = await Consumable.create(consumableData, { transaction });
operationType = 'import';
results.success++;
results.details.push({ row: rowNumber, status: 'created', consumableId: consumable.consumableId, name: consumable.name });
}
await ConsumableLog.create({
consumableId: consumable.consumableId,
consumableName: consumable.name,
operationType: 'import',
operationType,
quantity: consumable.currentStock,
previousStock: 0,
previousStock,
currentStock: consumable.currentStock,
operator,
reason: '批量导入',
notes: '',
notes: existingConsumable ? '更新现有耗材' : '',
consumableSnapshot: {
category: consumable.category,
unit: consumable.unit,
@@ -167,16 +218,16 @@ router.post('/import', async (req, res) => {
}
}, { transaction });
results.success++;
} catch (error) {
results.failed++;
results.errors.push(`${i + 1} 行: ${error.message}`);
results.errors.push(`${rowNumber} 行: ${error.message}`);
results.details.push({ row: rowNumber, status: 'failed', error: error.message });
}
}
await transaction.commit();
res.json({
message: `导入完成,成功 ${results.success} 条,失败 ${results.failed}`,
message: `导入完成,成功 ${results.success} 条,更新 ${results.updated} 条,跳过 ${results.skipped} 条,失败 ${results.failed}`,
results
});
} catch (error) {
@@ -872,9 +923,13 @@ router.put('/:id', async (req, res) => {
await transaction.rollback();
return res.status(404).json({ error: '耗材不存在' });
}
const oldData = consumable.toJSON();
await consumable.update(req.body, { transaction });
const updateData = { ...req.body };
if (Array.isArray(updateData.snList)) {
updateData.currentStock = updateData.snList.length;
}
await consumable.update(updateData, { transaction });
await ConsumableLog.create({
consumableId: consumable.consumableId,
+1
View File
@@ -1798,6 +1798,7 @@ router.put('/:deviceId/to-idle', async (req, res) => {
await device.update({
isIdle: true,
status: 'idle',
idleDate: new Date(),
idleReason: idleReason || `从设备管理转入`
}, { transaction: t });
+2
View File
@@ -181,6 +181,7 @@ router.post('/from-device/:deviceId', async (req, res) => {
await device.update({
isIdle: true,
status: 'idle',
idleDate: new Date(),
idleReason: idleReason || `从设备管理转入`,
sourceType: 'rack'
@@ -230,6 +231,7 @@ router.post('/batch-from-devices', async (req, res) => {
await Device.update(
{
isIdle: true,
status: 'idle',
idleDate: new Date(),
idleReason: idleReason || `批量转入`
},
+1 -1
View File
@@ -53,7 +53,7 @@ router.get('/', async (req, res) => {
const rackIds = racks.map(r => r.rackId);
const devices = await Device.findAll({
where: { rackId: rackIds },
attributes: ['deviceId', 'rackId', 'name', 'powerConsumption']
attributes: ['deviceId', 'rackId', 'name', 'powerConsumption', 'height']
});
// 将设备信息关联到对应的机柜
+1 -1
View File
@@ -1,7 +1,7 @@
const Joi = require('joi');
const DEVICE_TYPES = ['server', 'switch', 'router', 'storage', 'other'];
const DEVICE_STATUS = ['running', 'maintenance', 'offline', 'fault'];
const DEVICE_STATUS = ['running', 'maintenance', 'offline', 'fault', 'idle'];
const createDeviceSchema = Joi.object({
name: Joi.string().required().max(100).messages({