feat: 添加空闲设备状态支持并优化耗材导入功能
refactor(设备管理): 重构状态筛选和高级搜索界面 perf(机柜管理): 优化U位计算方式为基于设备高度 fix(耗材管理): 修复导入时库存计算不准确的问题 style(耗材统计): 调整表格样式和实时刷新功能 docs: 更新设备状态映射包含空闲状态
This commit is contained in:
@@ -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}`);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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 || `批量转入`
|
||||
},
|
||||
|
||||
@@ -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,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({
|
||||
|
||||
Reference in New Issue
Block a user