feat: 修改了bug
This commit is contained in:
@@ -87,7 +87,7 @@ const defaultDeviceFields = [
|
|||||||
fieldName: 'powerConsumption',
|
fieldName: 'powerConsumption',
|
||||||
displayName: '功率(W)',
|
displayName: '功率(W)',
|
||||||
fieldType: 'number',
|
fieldType: 'number',
|
||||||
required: false,
|
required: true,
|
||||||
order: 9,
|
order: 9,
|
||||||
visible: true,
|
visible: true,
|
||||||
isSystem: true
|
isSystem: true
|
||||||
|
|||||||
@@ -1,45 +1,84 @@
|
|||||||
/**
|
|
||||||
* 请求参数验证中间件
|
|
||||||
* 使用Joi进行参数校验
|
|
||||||
*/
|
|
||||||
|
|
||||||
const validate = (schema, source = 'body') => {
|
const validate = (schema, source = 'body') => {
|
||||||
return (req, res, next) => {
|
return async (req, res, next) => {
|
||||||
const data = source === 'query' ? req.query : req.body;
|
const data = source === 'query' ? req.query : req.body;
|
||||||
|
|
||||||
const { error, value } = schema.validate(data, {
|
try {
|
||||||
abortEarly: false, // 返回所有错误
|
let result;
|
||||||
stripUnknown: true, // 移除未定义的字段
|
|
||||||
allowUnknown: source === 'query' // 查询参数允许未知字段
|
|
||||||
});
|
|
||||||
|
|
||||||
if (error) {
|
if (schema.validate && typeof schema.validate === 'function') {
|
||||||
const errorMessages = error.details.map(detail => ({
|
if (schema.validate.constructor.name === 'AsyncFunction' ||
|
||||||
field: detail.path.join('.'),
|
schema.validate.length === 1) {
|
||||||
message: detail.message
|
result = await schema.validate(data, {
|
||||||
}));
|
abortEarly: false,
|
||||||
|
stripUnknown: true,
|
||||||
|
allowUnknown: source === 'query'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
result = schema.validate(data, {
|
||||||
|
abortEarly: false,
|
||||||
|
stripUnknown: true,
|
||||||
|
allowUnknown: source === 'query'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (schema.validateAsync) {
|
||||||
|
result = await schema.validateAsync(data, {
|
||||||
|
abortEarly: false,
|
||||||
|
stripUnknown: true,
|
||||||
|
allowUnknown: source === 'query'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
result = schema.validate(data, {
|
||||||
|
abortEarly: false,
|
||||||
|
stripUnknown: true,
|
||||||
|
allowUnknown: source === 'query'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return res.status(400).json({
|
const { error, value } = result;
|
||||||
error: '参数验证失败',
|
|
||||||
details: errorMessages
|
if (error) {
|
||||||
|
const errorMessages = error.details.map(detail => ({
|
||||||
|
field: detail.path.join('.'),
|
||||||
|
message: detail.message
|
||||||
|
}));
|
||||||
|
|
||||||
|
return res.status(400).json({
|
||||||
|
error: '参数验证失败',
|
||||||
|
details: errorMessages
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (source === 'query') {
|
||||||
|
req.query = value;
|
||||||
|
} else {
|
||||||
|
req.body = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
} catch (error) {
|
||||||
|
if (error.details) {
|
||||||
|
const errorMessages = error.details.map(detail => ({
|
||||||
|
field: detail.path.join('.'),
|
||||||
|
message: detail.message
|
||||||
|
}));
|
||||||
|
|
||||||
|
return res.status(400).json({
|
||||||
|
error: '参数验证失败',
|
||||||
|
details: errorMessages
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error('验证中间件错误:', error);
|
||||||
|
return res.status(500).json({
|
||||||
|
error: '验证过程发生错误',
|
||||||
|
message: error.message
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 将验证后的值替换到请求对象
|
|
||||||
if (source === 'query') {
|
|
||||||
req.query = value;
|
|
||||||
} else {
|
|
||||||
req.body = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
next();
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// 验证查询参数
|
|
||||||
const validateQuery = (schema) => validate(schema, 'query');
|
const validateQuery = (schema) => validate(schema, 'query');
|
||||||
|
|
||||||
// 验证请求体
|
|
||||||
const validateBody = (schema) => validate(schema, 'body');
|
const validateBody = (schema) => validate(schema, 'body');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|||||||
+16
-14
@@ -11,24 +11,24 @@ const Device = sequelize.define('Device', {
|
|||||||
},
|
},
|
||||||
name: {
|
name: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false
|
allowNull: true
|
||||||
},
|
},
|
||||||
type: {
|
type: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false
|
allowNull: true
|
||||||
},
|
},
|
||||||
model: {
|
model: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false
|
allowNull: true
|
||||||
},
|
},
|
||||||
serialNumber: {
|
serialNumber: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false,
|
allowNull: true,
|
||||||
unique: true
|
unique: true
|
||||||
},
|
},
|
||||||
rackId: {
|
rackId: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false,
|
allowNull: true,
|
||||||
references: {
|
references: {
|
||||||
model: Rack,
|
model: Rack,
|
||||||
key: 'rackId'
|
key: 'rackId'
|
||||||
@@ -36,20 +36,21 @@ const Device = sequelize.define('Device', {
|
|||||||
},
|
},
|
||||||
position: {
|
position: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
allowNull: false // 设备在机柜中的位置(U数)
|
allowNull: true
|
||||||
},
|
},
|
||||||
height: {
|
height: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
allowNull: false,
|
allowNull: true,
|
||||||
defaultValue: 1 // 设备高度(U数)
|
defaultValue: 1
|
||||||
},
|
},
|
||||||
powerConsumption: {
|
powerConsumption: {
|
||||||
type: DataTypes.FLOAT,
|
type: DataTypes.FLOAT,
|
||||||
allowNull: false
|
allowNull: true,
|
||||||
|
defaultValue: 0
|
||||||
},
|
},
|
||||||
status: {
|
status: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
defaultValue: 'running'
|
defaultValue: 'offline'
|
||||||
},
|
},
|
||||||
purchaseDate: {
|
purchaseDate: {
|
||||||
type: DataTypes.DATE,
|
type: DataTypes.DATE,
|
||||||
@@ -60,15 +61,17 @@ const Device = sequelize.define('Device', {
|
|||||||
allowNull: true
|
allowNull: true
|
||||||
},
|
},
|
||||||
ipAddress: {
|
ipAddress: {
|
||||||
type: DataTypes.STRING
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true
|
||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
type: DataTypes.TEXT
|
type: DataTypes.TEXT,
|
||||||
|
allowNull: true
|
||||||
},
|
},
|
||||||
customFields: {
|
customFields: {
|
||||||
type: DataTypes.JSON,
|
type: DataTypes.JSON,
|
||||||
defaultValue: {},
|
defaultValue: {},
|
||||||
allowNull: false
|
allowNull: true
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
tableName: 'devices',
|
tableName: 'devices',
|
||||||
@@ -83,7 +86,6 @@ const Device = sequelize.define('Device', {
|
|||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
// 关联关系
|
|
||||||
Device.belongsTo(Rack, { foreignKey: 'rackId' });
|
Device.belongsTo(Rack, { foreignKey: 'rackId' });
|
||||||
Rack.hasMany(Device, { foreignKey: 'rackId' });
|
Rack.hasMany(Device, { foreignKey: 'rackId' });
|
||||||
|
|
||||||
|
|||||||
@@ -88,33 +88,29 @@ router.post('/config', async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const fieldConfigs = req.body;
|
const fieldConfigs = req.body;
|
||||||
|
|
||||||
// 验证输入
|
|
||||||
if (!Array.isArray(fieldConfigs)) {
|
if (!Array.isArray(fieldConfigs)) {
|
||||||
return res.status(400).json({ error: '输入必须是数组' });
|
return res.status(400).json({ error: '输入必须是数组' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 批量更新字段配置
|
|
||||||
const updatedFields = [];
|
const updatedFields = [];
|
||||||
for (const config of fieldConfigs) {
|
for (const config of fieldConfigs) {
|
||||||
// 检查字段是否存在
|
|
||||||
const existingField = await DeviceField.findOne({ where: { fieldName: config.fieldName } });
|
const existingField = await DeviceField.findOne({ where: { fieldName: config.fieldName } });
|
||||||
|
|
||||||
if (existingField) {
|
if (existingField) {
|
||||||
// 更新现有字段
|
|
||||||
await existingField.update({
|
await existingField.update({
|
||||||
visible: config.visible,
|
visible: config.visible !== undefined ? config.visible : existingField.visible,
|
||||||
displayName: config.displayName // 同时更新显示名称,以防变化
|
required: config.required !== undefined ? config.required : existingField.required,
|
||||||
|
displayName: config.displayName || existingField.displayName,
|
||||||
});
|
});
|
||||||
updatedFields.push(existingField);
|
updatedFields.push(existingField);
|
||||||
} else {
|
} else {
|
||||||
// 创建新字段
|
|
||||||
const newField = await DeviceField.create({
|
const newField = await DeviceField.create({
|
||||||
fieldName: config.fieldName,
|
fieldName: config.fieldName,
|
||||||
visible: config.visible,
|
visible: config.visible !== undefined ? config.visible : true,
|
||||||
displayName: config.displayName,
|
required: config.required !== undefined ? config.required : false,
|
||||||
fieldType: config.fieldType || 'text',
|
displayName: config.displayName || config.fieldName,
|
||||||
required: false, // 默认为非必填
|
fieldType: config.fieldType || 'text',
|
||||||
order: 0 // 默认顺序
|
order: config.order || 0,
|
||||||
});
|
});
|
||||||
updatedFields.push(newField);
|
updatedFields.push(newField);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ const initDefaultSettings = async () => {
|
|||||||
{ settingKey: 'timezone', settingValue: JSON.stringify('Asia/Shanghai'), settingType: 'string', category: 'general', description: '时区设置', isEditable: true },
|
{ settingKey: 'timezone', settingValue: JSON.stringify('Asia/Shanghai'), settingType: 'string', category: 'general', description: '时区设置', isEditable: true },
|
||||||
{ settingKey: 'date_format', settingValue: JSON.stringify('YYYY-MM-DD'), settingType: 'string', category: 'general', description: '日期格式', isEditable: true },
|
{ settingKey: 'date_format', settingValue: JSON.stringify('YYYY-MM-DD'), settingType: 'string', category: 'general', description: '日期格式', isEditable: true },
|
||||||
{ settingKey: 'session_timeout', settingValue: JSON.stringify(30), settingType: 'number', category: 'general', description: '登录有效期(分钟)', isEditable: true },
|
{ settingKey: 'session_timeout', settingValue: JSON.stringify(30), settingType: 'number', category: 'general', description: '登录有效期(分钟)', isEditable: true },
|
||||||
{ settingKey: 'idle_timeout', settingValue: JSON.stringify(10), settingType: 'number', category: 'general', description: '用户空闲超时时间(分钟)', isEditable: true },
|
{ settingKey: 'idle_timeout', settingValue: JSON.stringify(30), settingType: 'number', category: 'general', description: '用户空闲超时时间(分钟)', isEditable: true },
|
||||||
{ settingKey: 'idle_warning_time', settingValue: JSON.stringify(10), settingType: 'number', category: 'general', description: '空闲超时前警告时间(秒)', isEditable: false },
|
{ settingKey: 'idle_warning_time', settingValue: JSON.stringify(60), settingType: 'number', category: 'general', description: '空闲超时前警告时间(秒)', isEditable: false },
|
||||||
{ settingKey: 'max_login_attempts', settingValue: JSON.stringify(5), settingType: 'number', category: 'general', description: '最大登录尝试次数', isEditable: true },
|
{ settingKey: 'max_login_attempts', settingValue: JSON.stringify(5), settingType: 'number', category: 'general', description: '最大登录尝试次数', isEditable: true },
|
||||||
{ settingKey: 'maintenance_mode', settingValue: JSON.stringify(false), settingType: 'boolean', category: 'general', description: '维护模式', isEditable: true },
|
{ settingKey: 'maintenance_mode', settingValue: JSON.stringify(false), settingType: 'boolean', category: 'general', description: '维护模式', isEditable: true },
|
||||||
|
|
||||||
@@ -106,8 +106,8 @@ router.get('/idle-timeout', async (req, res) => {
|
|||||||
const timeoutSetting = await SystemSetting.findByPk('idle_timeout');
|
const timeoutSetting = await SystemSetting.findByPk('idle_timeout');
|
||||||
|
|
||||||
// 默认配置
|
// 默认配置
|
||||||
const defaultTimeout = 10; // 10分钟
|
const defaultTimeout = 30; // 30分钟
|
||||||
const fixedWarningTime = 10; // 固定10秒警告时间
|
const fixedWarningTime = 60; // 固定60秒警告时间
|
||||||
|
|
||||||
const timeout = timeoutSetting ? JSON.parse(timeoutSetting.settingValue) : defaultTimeout;
|
const timeout = timeoutSetting ? JSON.parse(timeoutSetting.settingValue) : defaultTimeout;
|
||||||
|
|
||||||
@@ -263,8 +263,8 @@ router.post('/reset/:key', async (req, res) => {
|
|||||||
timezone: 'Asia/Shanghai',
|
timezone: 'Asia/Shanghai',
|
||||||
date_format: 'YYYY-MM-DD',
|
date_format: 'YYYY-MM-DD',
|
||||||
session_timeout: 30,
|
session_timeout: 30,
|
||||||
idle_timeout: 10,
|
idle_timeout: 30,
|
||||||
idle_warning_time: 10,
|
idle_warning_time: 60,
|
||||||
max_login_attempts: 5,
|
max_login_attempts: 5,
|
||||||
maintenance_mode: false,
|
maintenance_mode: false,
|
||||||
frontend_port: 3000,
|
frontend_port: 3000,
|
||||||
|
|||||||
@@ -64,6 +64,21 @@ const migrations = [
|
|||||||
name: '耗材SN序列号字段',
|
name: '耗材SN序列号字段',
|
||||||
description: '为 consumables、consumable_records、consumable_logs 添加 snList 字段',
|
description: '为 consumables、consumable_records、consumable_logs 添加 snList 字段',
|
||||||
migrate: migrateSnList
|
migrate: migrateSnList
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '设备型号字段可空',
|
||||||
|
description: '将 devices 表 model 字段改为可空,支持非必填',
|
||||||
|
migrate: migrateDeviceModelField
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '设备字段配置同步',
|
||||||
|
description: '同步前后端字段必填配置',
|
||||||
|
migrate: migrateDeviceFieldsConfig
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '设备表字段可空',
|
||||||
|
description: '将设备表所有字段改为可空,由应用层验证控制',
|
||||||
|
migrate: migrateDeviceFieldsNullable
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -392,6 +407,149 @@ async function migrateSnList() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function migrateDeviceModelField() {
|
||||||
|
if (!(await tableExists('devices'))) {
|
||||||
|
console.log(' devices 表不存在,跳过');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dialect = sequelize.getDialect();
|
||||||
|
|
||||||
|
if (dialect === 'mysql') {
|
||||||
|
await sequelize.query(
|
||||||
|
'ALTER TABLE devices MODIFY COLUMN model VARCHAR(255) NULL'
|
||||||
|
);
|
||||||
|
console.log(' devices 表 model 字段已改为可空');
|
||||||
|
} else if (dialect === 'sqlite') {
|
||||||
|
const columns = await getTableColumns('devices');
|
||||||
|
if (columns.includes('model_old')) {
|
||||||
|
console.log(' model_old 字段已存在,跳过迁移');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await sequelize.query('ALTER TABLE devices RENAME COLUMN model TO model_old');
|
||||||
|
await sequelize.query('ALTER TABLE devices ADD COLUMN model VARCHAR(255)');
|
||||||
|
await sequelize.query('UPDATE devices SET model = model_old');
|
||||||
|
await sequelize.query('ALTER TABLE devices DROP COLUMN model_old');
|
||||||
|
console.log(' devices 表 model 字段已改为可空');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function migrateDeviceFieldsConfig() {
|
||||||
|
const DeviceField = require('../models/DeviceField');
|
||||||
|
|
||||||
|
const updates = [
|
||||||
|
{ fieldName: 'model', required: false },
|
||||||
|
{ fieldName: 'powerConsumption', required: true },
|
||||||
|
{ fieldName: 'purchaseDate', required: false },
|
||||||
|
{ fieldName: 'warrantyExpiry', required: false },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const update of updates) {
|
||||||
|
const field = await DeviceField.findOne({ where: { fieldName: update.fieldName } });
|
||||||
|
if (field && field.required !== update.required) {
|
||||||
|
await field.update(update);
|
||||||
|
console.log(` 更新字段 ${update.fieldName}: required=${update.required}`);
|
||||||
|
} else if (!field) {
|
||||||
|
console.log(` 字段 ${update.fieldName} 不存在,跳过`);
|
||||||
|
} else {
|
||||||
|
console.log(` 字段 ${update.fieldName} 配置已正确,跳过`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function migrateDeviceFieldsNullable() {
|
||||||
|
const dialect = sequelize.getDialect();
|
||||||
|
|
||||||
|
if (dialect === 'mysql') {
|
||||||
|
const alterCommands = [
|
||||||
|
"ALTER TABLE devices MODIFY COLUMN name VARCHAR(255) NULL",
|
||||||
|
"ALTER TABLE devices MODIFY COLUMN type VARCHAR(255) NULL",
|
||||||
|
"ALTER TABLE devices MODIFY COLUMN model VARCHAR(255) NULL",
|
||||||
|
"ALTER TABLE devices MODIFY COLUMN serialNumber VARCHAR(255) NULL",
|
||||||
|
"ALTER TABLE devices MODIFY COLUMN rackId VARCHAR(255) NULL",
|
||||||
|
"ALTER TABLE devices MODIFY COLUMN position INTEGER NULL",
|
||||||
|
"ALTER TABLE devices MODIFY COLUMN height INTEGER NULL",
|
||||||
|
"ALTER TABLE devices MODIFY COLUMN powerConsumption FLOAT NULL",
|
||||||
|
"ALTER TABLE devices MODIFY COLUMN customFields JSON NULL"
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const sql of alterCommands) {
|
||||||
|
try {
|
||||||
|
await sequelize.query(sql);
|
||||||
|
} catch (e) {
|
||||||
|
if (!e.message.includes('Unknown column')) {
|
||||||
|
console.log(` 警告: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log(' devices 表字段已改为可空');
|
||||||
|
|
||||||
|
} else if (dialect === 'sqlite') {
|
||||||
|
const columns = await getTableColumns('devices');
|
||||||
|
const hasNullableFlag = columns.includes('_nullable_migration_done');
|
||||||
|
|
||||||
|
if (hasNullableFlag) {
|
||||||
|
console.log(' 已完成可空迁移,跳过');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await sequelize.query('PRAGMA foreign_keys = OFF');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await sequelize.query('DROP TABLE IF EXISTS devices_new');
|
||||||
|
|
||||||
|
await sequelize.query(`
|
||||||
|
CREATE TABLE devices_new (
|
||||||
|
deviceId VARCHAR(255) PRIMARY KEY NOT NULL UNIQUE,
|
||||||
|
name VARCHAR(255),
|
||||||
|
type VARCHAR(255),
|
||||||
|
model VARCHAR(255),
|
||||||
|
serialNumber VARCHAR(255) UNIQUE,
|
||||||
|
rackId VARCHAR(255),
|
||||||
|
position INTEGER,
|
||||||
|
height INTEGER DEFAULT 1,
|
||||||
|
powerConsumption FLOAT DEFAULT 0,
|
||||||
|
status VARCHAR(255) DEFAULT 'offline',
|
||||||
|
purchaseDate DATETIME,
|
||||||
|
warrantyExpiry DATETIME,
|
||||||
|
ipAddress VARCHAR(255),
|
||||||
|
description TEXT,
|
||||||
|
customFields JSON DEFAULT '{}',
|
||||||
|
createdAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updatedAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
_nullable_migration_done INTEGER DEFAULT 1
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
await sequelize.query(`
|
||||||
|
INSERT INTO devices_new (
|
||||||
|
deviceId, name, type, model, serialNumber, rackId, position, height,
|
||||||
|
powerConsumption, status, purchaseDate, warrantyExpiry, ipAddress,
|
||||||
|
description, customFields, createdAt, updatedAt
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
deviceId, name, type, model, serialNumber, rackId, position, height,
|
||||||
|
powerConsumption, status, purchaseDate, warrantyExpiry, ipAddress,
|
||||||
|
description, customFields, createdAt, updatedAt
|
||||||
|
FROM devices
|
||||||
|
`);
|
||||||
|
|
||||||
|
await sequelize.query('DROP TABLE devices');
|
||||||
|
await sequelize.query('ALTER TABLE devices_new RENAME TO devices');
|
||||||
|
|
||||||
|
await sequelize.query('CREATE INDEX IF NOT EXISTS idx_devices_status ON devices(status)');
|
||||||
|
await sequelize.query('CREATE INDEX IF NOT EXISTS idx_devices_type ON devices(type)');
|
||||||
|
await sequelize.query('CREATE INDEX IF NOT EXISTS idx_devices_rackId ON devices(rackId)');
|
||||||
|
await sequelize.query('CREATE INDEX IF NOT EXISTS idx_devices_name ON devices(name)');
|
||||||
|
|
||||||
|
console.log(' devices 表字段已改为可空');
|
||||||
|
} finally {
|
||||||
|
await sequelize.query('PRAGMA foreign_keys = ON');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 执行迁移
|
// 执行迁移
|
||||||
runMigrations().catch(error => {
|
runMigrations().catch(error => {
|
||||||
console.error('迁移执行失败:', error);
|
console.error('迁移执行失败:', error);
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
const Joi = require('joi');
|
const Joi = require('joi');
|
||||||
|
const DeviceField = require('../models/DeviceField');
|
||||||
|
|
||||||
// 设备类型枚举
|
|
||||||
const DEVICE_TYPES = ['server', 'switch', 'router', 'storage', 'other'];
|
const DEVICE_TYPES = ['server', 'switch', 'router', 'storage', 'other'];
|
||||||
|
|
||||||
// 设备状态枚举
|
|
||||||
const DEVICE_STATUS = ['running', 'maintenance', 'offline', 'fault'];
|
const DEVICE_STATUS = ['running', 'maintenance', 'offline', 'fault'];
|
||||||
|
|
||||||
// 创建设备验证Schema
|
const baseFieldSchemas = {
|
||||||
const createDeviceSchema = Joi.object({
|
|
||||||
deviceId: Joi.string()
|
deviceId: Joi.string()
|
||||||
.max(50)
|
.max(50)
|
||||||
.pattern(/^[a-zA-Z0-9_-]+$/)
|
.pattern(/^[a-zA-Z0-9_-]+$/)
|
||||||
@@ -18,21 +15,17 @@ const createDeviceSchema = Joi.object({
|
|||||||
}),
|
}),
|
||||||
|
|
||||||
name: Joi.string()
|
name: Joi.string()
|
||||||
.required()
|
|
||||||
.max(100)
|
.max(100)
|
||||||
.messages({
|
.messages({
|
||||||
'string.empty': '设备名称不能为空',
|
'string.empty': '设备名称不能为空',
|
||||||
'string.max': '设备名称不能超过100个字符',
|
'string.max': '设备名称不能超过100个字符'
|
||||||
'any.required': '设备名称是必填字段'
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
type: Joi.string()
|
type: Joi.string()
|
||||||
.valid(...DEVICE_TYPES)
|
.valid(...DEVICE_TYPES)
|
||||||
.required()
|
|
||||||
.messages({
|
.messages({
|
||||||
'string.empty': '设备类型不能为空',
|
'string.empty': '设备类型不能为空',
|
||||||
'any.only': `设备类型必须是以下之一: ${DEVICE_TYPES.join(', ')}`,
|
'any.only': `设备类型必须是以下之一: ${DEVICE_TYPES.join(', ')}`
|
||||||
'any.required': '设备类型是必填字段'
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
model: Joi.string()
|
model: Joi.string()
|
||||||
@@ -44,138 +37,18 @@ const createDeviceSchema = Joi.object({
|
|||||||
|
|
||||||
serialNumber: Joi.string()
|
serialNumber: Joi.string()
|
||||||
.max(100)
|
.max(100)
|
||||||
.allow('', null)
|
|
||||||
.messages({
|
.messages({
|
||||||
|
'string.empty': '序列号不能为空',
|
||||||
'string.max': '序列号不能超过100个字符'
|
'string.max': '序列号不能超过100个字符'
|
||||||
}),
|
}),
|
||||||
|
|
||||||
rackId: Joi.string()
|
rackId: Joi.string()
|
||||||
.required()
|
|
||||||
.max(50)
|
.max(50)
|
||||||
.messages({
|
.messages({
|
||||||
'string.empty': '机柜ID不能为空',
|
'string.empty': '机柜ID不能为空',
|
||||||
'string.max': '机柜ID不能超过50个字符',
|
'string.max': '机柜ID不能超过50个字符'
|
||||||
'any.required': '机柜ID是必填字段'
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
position: Joi.number()
|
|
||||||
.integer()
|
|
||||||
.min(1)
|
|
||||||
.max(100)
|
|
||||||
.required()
|
|
||||||
.messages({
|
|
||||||
'number.base': '位置必须是数字',
|
|
||||||
'number.integer': '位置必须是整数',
|
|
||||||
'number.min': '位置不能小于1',
|
|
||||||
'number.max': '位置不能大于100',
|
|
||||||
'any.required': '位置是必填字段'
|
|
||||||
}),
|
|
||||||
|
|
||||||
height: Joi.number()
|
|
||||||
.integer()
|
|
||||||
.min(1)
|
|
||||||
.max(50)
|
|
||||||
.required()
|
|
||||||
.messages({
|
|
||||||
'number.base': '高度必须是数字',
|
|
||||||
'number.integer': '高度必须是整数',
|
|
||||||
'number.min': '高度不能小于1',
|
|
||||||
'number.max': '高度不能大于50',
|
|
||||||
'any.required': '高度是必填字段'
|
|
||||||
}),
|
|
||||||
|
|
||||||
powerConsumption: Joi.number()
|
|
||||||
.min(0)
|
|
||||||
.max(100000)
|
|
||||||
.default(0)
|
|
||||||
.messages({
|
|
||||||
'number.base': '功率必须是数字',
|
|
||||||
'number.min': '功率不能小于0',
|
|
||||||
'number.max': '功率不能超过100000'
|
|
||||||
}),
|
|
||||||
|
|
||||||
ipAddress: Joi.string()
|
|
||||||
.ip({ version: ['ipv4', 'ipv6'] })
|
|
||||||
.allow('', null)
|
|
||||||
.messages({
|
|
||||||
'string.ip': 'IP地址格式无效'
|
|
||||||
}),
|
|
||||||
|
|
||||||
status: Joi.string()
|
|
||||||
.valid(...DEVICE_STATUS)
|
|
||||||
.default('offline')
|
|
||||||
.messages({
|
|
||||||
'any.only': `状态必须是以下之一: ${DEVICE_STATUS.join(', ')}`
|
|
||||||
}),
|
|
||||||
|
|
||||||
purchaseDate: Joi.date()
|
|
||||||
.allow(null)
|
|
||||||
.messages({
|
|
||||||
'date.base': '购买日期格式无效'
|
|
||||||
}),
|
|
||||||
|
|
||||||
warrantyExpiry: Joi.date()
|
|
||||||
.allow(null)
|
|
||||||
.messages({
|
|
||||||
'date.base': '保修到期日期格式无效'
|
|
||||||
}),
|
|
||||||
|
|
||||||
description: Joi.string()
|
|
||||||
.max(500)
|
|
||||||
.allow('', null)
|
|
||||||
.messages({
|
|
||||||
'string.max': '描述不能超过500个字符'
|
|
||||||
}),
|
|
||||||
|
|
||||||
customFields: Joi.object()
|
|
||||||
.allow(null)
|
|
||||||
}).custom((value, helpers) => {
|
|
||||||
// 验证保修日期必须晚于购买日期
|
|
||||||
if (value.purchaseDate && value.warrantyExpiry) {
|
|
||||||
const purchase = new Date(value.purchaseDate);
|
|
||||||
const warranty = new Date(value.warrantyExpiry);
|
|
||||||
if (warranty <= purchase) {
|
|
||||||
return helpers.error('date.warrantyAfterPurchase');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}).messages({
|
|
||||||
'date.warrantyAfterPurchase': '保修到期日期必须晚于购买日期'
|
|
||||||
});
|
|
||||||
|
|
||||||
// 更新设备验证Schema(所有字段可选)
|
|
||||||
const updateDeviceSchema = Joi.object({
|
|
||||||
deviceId: Joi.string()
|
|
||||||
.max(50)
|
|
||||||
.pattern(/^[a-zA-Z0-9_-]+$/)
|
|
||||||
.messages({
|
|
||||||
'string.max': '设备ID不能超过50个字符',
|
|
||||||
'string.pattern.base': '设备ID只能包含字母、数字、下划线和横线'
|
|
||||||
}),
|
|
||||||
|
|
||||||
name: Joi.string()
|
|
||||||
.max(100)
|
|
||||||
.messages({
|
|
||||||
'string.max': '设备名称不能超过100个字符'
|
|
||||||
}),
|
|
||||||
|
|
||||||
type: Joi.string()
|
|
||||||
.valid(...DEVICE_TYPES)
|
|
||||||
.messages({
|
|
||||||
'any.only': `设备类型必须是以下之一: ${DEVICE_TYPES.join(', ')}`
|
|
||||||
}),
|
|
||||||
|
|
||||||
model: Joi.string()
|
|
||||||
.max(100)
|
|
||||||
.allow('', null),
|
|
||||||
|
|
||||||
serialNumber: Joi.string()
|
|
||||||
.max(100)
|
|
||||||
.allow('', null),
|
|
||||||
|
|
||||||
rackId: Joi.string()
|
|
||||||
.max(50),
|
|
||||||
|
|
||||||
position: Joi.number()
|
position: Joi.number()
|
||||||
.integer()
|
.integer()
|
||||||
.min(1)
|
.min(1)
|
||||||
@@ -216,27 +89,81 @@ const updateDeviceSchema = Joi.object({
|
|||||||
|
|
||||||
status: Joi.string()
|
status: Joi.string()
|
||||||
.valid(...DEVICE_STATUS)
|
.valid(...DEVICE_STATUS)
|
||||||
|
.default('offline')
|
||||||
.messages({
|
.messages({
|
||||||
'any.only': `状态必须是以下之一: ${DEVICE_STATUS.join(', ')}`
|
'any.only': `状态必须是以下之一: ${DEVICE_STATUS.join(', ')}`
|
||||||
}),
|
}),
|
||||||
|
|
||||||
purchaseDate: Joi.date()
|
purchaseDate: Joi.date()
|
||||||
.allow(null),
|
.allow(null)
|
||||||
|
.messages({
|
||||||
|
'date.base': '购买日期格式无效'
|
||||||
|
}),
|
||||||
|
|
||||||
warrantyExpiry: Joi.date()
|
warrantyExpiry: Joi.date()
|
||||||
.allow(null),
|
.allow(null)
|
||||||
|
.messages({
|
||||||
|
'date.base': '保修到期日期格式无效'
|
||||||
|
}),
|
||||||
|
|
||||||
description: Joi.string()
|
description: Joi.string()
|
||||||
.max(500)
|
.max(500)
|
||||||
.allow('', null),
|
.allow('', null)
|
||||||
|
.messages({
|
||||||
|
'string.max': '描述不能超过500个字符'
|
||||||
|
}),
|
||||||
|
|
||||||
customFields: Joi.object()
|
customFields: Joi.object().allow(null)
|
||||||
.allow(null)
|
};
|
||||||
}).min(1).messages({
|
|
||||||
'object.min': '至少需要提供一个字段进行更新'
|
async function buildDynamicSchema(isCreate = true) {
|
||||||
});
|
const fields = await DeviceField.findAll({
|
||||||
|
where: { isSystem: true },
|
||||||
|
order: [['order', 'ASC']]
|
||||||
|
});
|
||||||
|
|
||||||
|
const schemaObj = {};
|
||||||
|
|
||||||
|
fields.forEach(field => {
|
||||||
|
const baseSchema = baseFieldSchemas[field.fieldName];
|
||||||
|
if (baseSchema) {
|
||||||
|
let fieldSchema = baseSchema.clone();
|
||||||
|
|
||||||
|
if (field.required && isCreate) {
|
||||||
|
fieldSchema = fieldSchema.required();
|
||||||
|
}
|
||||||
|
|
||||||
|
schemaObj[field.fieldName] = fieldSchema;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
schemaObj.customFields = baseFieldSchemas.customFields;
|
||||||
|
|
||||||
|
return Joi.object(schemaObj).custom((value, helpers) => {
|
||||||
|
if (value.purchaseDate && value.warrantyExpiry) {
|
||||||
|
const purchase = new Date(value.purchaseDate);
|
||||||
|
const warranty = new Date(value.warrantyExpiry);
|
||||||
|
if (warranty <= purchase) {
|
||||||
|
return helpers.error('date.warrantyAfterPurchase');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}).messages({
|
||||||
|
'date.warrantyAfterPurchase': '保修到期日期必须晚于购买日期'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getCreateDeviceSchema() {
|
||||||
|
return buildDynamicSchema(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getUpdateDeviceSchema() {
|
||||||
|
const schema = await buildDynamicSchema(false);
|
||||||
|
return schema.min(1).messages({
|
||||||
|
'object.min': '至少需要提供一个字段进行更新'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 批量操作验证Schema
|
|
||||||
const batchDeviceIdsSchema = Joi.object({
|
const batchDeviceIdsSchema = Joi.object({
|
||||||
deviceIds: Joi.array()
|
deviceIds: Joi.array()
|
||||||
.items(Joi.string().required())
|
.items(Joi.string().required())
|
||||||
@@ -249,7 +176,6 @@ const batchDeviceIdsSchema = Joi.object({
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
// 批量状态变更验证Schema
|
|
||||||
const batchStatusSchema = Joi.object({
|
const batchStatusSchema = Joi.object({
|
||||||
deviceIds: Joi.array()
|
deviceIds: Joi.array()
|
||||||
.items(Joi.string().required())
|
.items(Joi.string().required())
|
||||||
@@ -269,7 +195,6 @@ const batchStatusSchema = Joi.object({
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
// 批量移动验证Schema
|
|
||||||
const batchMoveSchema = Joi.object({
|
const batchMoveSchema = Joi.object({
|
||||||
deviceIds: Joi.array()
|
deviceIds: Joi.array()
|
||||||
.items(Joi.string().required())
|
.items(Joi.string().required())
|
||||||
@@ -288,7 +213,6 @@ const batchMoveSchema = Joi.object({
|
|||||||
.allow(null)
|
.allow(null)
|
||||||
});
|
});
|
||||||
|
|
||||||
// 查询参数验证Schema
|
|
||||||
const queryDeviceSchema = Joi.object({
|
const queryDeviceSchema = Joi.object({
|
||||||
keyword: Joi.string()
|
keyword: Joi.string()
|
||||||
.max(100)
|
.max(100)
|
||||||
@@ -313,6 +237,20 @@ const queryDeviceSchema = Joi.object({
|
|||||||
.default(10)
|
.default(10)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const createDeviceSchema = {
|
||||||
|
validate: async (data, options = {}) => {
|
||||||
|
const schema = await getCreateDeviceSchema();
|
||||||
|
return schema.validate(data, options);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateDeviceSchema = {
|
||||||
|
validate: async (data, options = {}) => {
|
||||||
|
const schema = await getUpdateDeviceSchema();
|
||||||
|
return schema.validate(data, options);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
createDeviceSchema,
|
createDeviceSchema,
|
||||||
updateDeviceSchema,
|
updateDeviceSchema,
|
||||||
@@ -321,5 +259,7 @@ module.exports = {
|
|||||||
batchMoveSchema,
|
batchMoveSchema,
|
||||||
queryDeviceSchema,
|
queryDeviceSchema,
|
||||||
DEVICE_TYPES,
|
DEVICE_TYPES,
|
||||||
DEVICE_STATUS
|
DEVICE_STATUS,
|
||||||
|
getCreateDeviceSchema,
|
||||||
|
getUpdateDeviceSchema
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -133,8 +133,8 @@ const ProtectedRoute = ({ component: Component }) => (
|
|||||||
|
|
||||||
// 默认空闲超时配置
|
// 默认空闲超时配置
|
||||||
const DEFAULT_IDLE_CONFIG = {
|
const DEFAULT_IDLE_CONFIG = {
|
||||||
timeout: 10 * 60 * 1000, // 10分钟
|
timeout: 30 * 60 * 1000, // 30分钟
|
||||||
warningTime: 30 * 1000, // 30秒
|
warningTime: 60 * 1000, // 60秒
|
||||||
};
|
};
|
||||||
|
|
||||||
const AppLayout = ({ children }) => {
|
const AppLayout = ({ children }) => {
|
||||||
|
|||||||
@@ -202,7 +202,7 @@ const RackModel = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<LODManager
|
<LODManager
|
||||||
key={device.id}
|
key={device.deviceId || device.id}
|
||||||
device={device}
|
device={device}
|
||||||
uHeight={uHeight}
|
uHeight={uHeight}
|
||||||
rackDepth={depth}
|
rackDepth={depth}
|
||||||
@@ -216,7 +216,7 @@ const RackModel = ({
|
|||||||
uHeight={uHeight}
|
uHeight={uHeight}
|
||||||
rackDepth={depth}
|
rackDepth={depth}
|
||||||
position={[0, 0, 0]}
|
position={[0, 0, 0]}
|
||||||
isSelected={selectedDeviceId === device.id}
|
isSelected={selectedDeviceId === (device.deviceId || device.id)}
|
||||||
onClick={onDeviceClick}
|
onClick={onDeviceClick}
|
||||||
onPointerOver={onDeviceHover}
|
onPointerOver={onDeviceHover}
|
||||||
onPointerOut={onDeviceLeave}
|
onPointerOut={onDeviceLeave}
|
||||||
|
|||||||
@@ -229,6 +229,8 @@ function DeviceManagement() {
|
|||||||
const [devices, setDevices] = useState([]);
|
const [devices, setDevices] = useState([]);
|
||||||
const [allDevices, setAllDevices] = useState([]);
|
const [allDevices, setAllDevices] = useState([]);
|
||||||
const [racks, setRacks] = useState([]);
|
const [racks, setRacks] = useState([]);
|
||||||
|
const [rooms, setRooms] = useState([]);
|
||||||
|
const [selectedRoomId, setSelectedRoomId] = useState(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [searching, setSearching] = useState(false);
|
const [searching, setSearching] = useState(false);
|
||||||
const [modalVisible, setModalVisible] = useState(false);
|
const [modalVisible, setModalVisible] = useState(false);
|
||||||
@@ -361,9 +363,9 @@ function DeviceManagement() {
|
|||||||
fieldName: 'deviceId',
|
fieldName: 'deviceId',
|
||||||
displayName: '设备ID',
|
displayName: '设备ID',
|
||||||
fieldType: 'string',
|
fieldType: 'string',
|
||||||
required: true,
|
required: false,
|
||||||
order: 1,
|
order: 1,
|
||||||
visible: true,
|
visible: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldName: 'name',
|
fieldName: 'name',
|
||||||
@@ -392,7 +394,7 @@ function DeviceManagement() {
|
|||||||
fieldName: 'model',
|
fieldName: 'model',
|
||||||
displayName: '型号',
|
displayName: '型号',
|
||||||
fieldType: 'string',
|
fieldType: 'string',
|
||||||
required: true,
|
required: false,
|
||||||
order: 4,
|
order: 4,
|
||||||
visible: true,
|
visible: true,
|
||||||
},
|
},
|
||||||
@@ -454,7 +456,7 @@ function DeviceManagement() {
|
|||||||
fieldName: 'purchaseDate',
|
fieldName: 'purchaseDate',
|
||||||
displayName: '购买日期',
|
displayName: '购买日期',
|
||||||
fieldType: 'date',
|
fieldType: 'date',
|
||||||
required: true,
|
required: false,
|
||||||
order: 11,
|
order: 11,
|
||||||
visible: true,
|
visible: true,
|
||||||
},
|
},
|
||||||
@@ -462,7 +464,7 @@ function DeviceManagement() {
|
|||||||
fieldName: 'warrantyExpiry',
|
fieldName: 'warrantyExpiry',
|
||||||
displayName: '保修到期',
|
displayName: '保修到期',
|
||||||
fieldType: 'date',
|
fieldType: 'date',
|
||||||
required: true,
|
required: false,
|
||||||
order: 12,
|
order: 12,
|
||||||
visible: true,
|
visible: true,
|
||||||
},
|
},
|
||||||
@@ -487,8 +489,9 @@ function DeviceManagement() {
|
|||||||
// 获取所有机柜
|
// 获取所有机柜
|
||||||
const fetchRacks = async () => {
|
const fetchRacks = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get('/api/racks');
|
const response = await axios.get('/api/racks', {
|
||||||
// 现在API返回的格式是 { racks: [], total: number }
|
params: { pageSize: 1000 }
|
||||||
|
});
|
||||||
setRacks(response.data.racks || []);
|
setRacks(response.data.racks || []);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error('获取机柜列表失败');
|
message.error('获取机柜列表失败');
|
||||||
@@ -496,9 +499,21 @@ function DeviceManagement() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 获取所有机房
|
||||||
|
const fetchRooms = async () => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get('/api/rooms');
|
||||||
|
setRooms(response.data || []);
|
||||||
|
} catch (error) {
|
||||||
|
message.error('获取机房列表失败');
|
||||||
|
console.error('获取机房列表失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchDevices(1, pagination.pageSize);
|
fetchDevices(1, pagination.pageSize);
|
||||||
fetchRacks();
|
fetchRacks();
|
||||||
|
fetchRooms();
|
||||||
fetchDeviceFields();
|
fetchDeviceFields();
|
||||||
}, [fetchDevices]);
|
}, [fetchDevices]);
|
||||||
|
|
||||||
@@ -576,8 +591,17 @@ function DeviceManagement() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
form.setFieldsValue(cleanDeviceData);
|
form.setFieldsValue(cleanDeviceData);
|
||||||
|
|
||||||
|
// 编辑设备时,根据 rackId 找到对应的机房并设置 selectedRoomId
|
||||||
|
if (device.rackId) {
|
||||||
|
const rack = racks.find(r => r.rackId === device.rackId);
|
||||||
|
if (rack) {
|
||||||
|
setSelectedRoomId(rack.roomId);
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
|
setSelectedRoomId(null);
|
||||||
}
|
}
|
||||||
setModalVisible(true);
|
setModalVisible(true);
|
||||||
};
|
};
|
||||||
@@ -586,12 +610,12 @@ function DeviceManagement() {
|
|||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
setModalVisible(false);
|
setModalVisible(false);
|
||||||
setEditingDevice(null);
|
setEditingDevice(null);
|
||||||
|
setSelectedRoomId(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 提交表单
|
// 提交表单
|
||||||
const handleSubmit = async values => {
|
const handleSubmit = async values => {
|
||||||
try {
|
try {
|
||||||
// 定义设备模型的固定字段
|
|
||||||
const fixedFields = [
|
const fixedFields = [
|
||||||
'deviceId',
|
'deviceId',
|
||||||
'name',
|
'name',
|
||||||
@@ -607,25 +631,25 @@ function DeviceManagement() {
|
|||||||
'warrantyExpiry',
|
'warrantyExpiry',
|
||||||
'ipAddress',
|
'ipAddress',
|
||||||
'description',
|
'description',
|
||||||
|
'roomId',
|
||||||
];
|
];
|
||||||
|
|
||||||
// 构建最终的设备数据,包含固定字段和自定义字段
|
|
||||||
const deviceData = {
|
const deviceData = {
|
||||||
...values,
|
...values,
|
||||||
purchaseDate: values.purchaseDate ? values.purchaseDate.format('YYYY-MM-DD') : null,
|
purchaseDate: values.purchaseDate ? values.purchaseDate.format('YYYY-MM-DD') : null,
|
||||||
warrantyExpiry: values.warrantyExpiry ? values.warrantyExpiry.format('YYYY-MM-DD') : null,
|
warrantyExpiry: values.warrantyExpiry ? values.warrantyExpiry.format('YYYY-MM-DD') : null,
|
||||||
customFields: {}, // 用于存储自定义字段
|
customFields: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
// 分离固定字段和自定义字段
|
|
||||||
Object.keys(deviceData).forEach(key => {
|
Object.keys(deviceData).forEach(key => {
|
||||||
if (!fixedFields.includes(key) && key !== 'customFields') {
|
if (!fixedFields.includes(key) && key !== 'customFields') {
|
||||||
// 将非固定字段移动到customFields对象中
|
|
||||||
deviceData.customFields[key] = deviceData[key];
|
deviceData.customFields[key] = deviceData[key];
|
||||||
delete deviceData[key];
|
delete deviceData[key];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
delete deviceData.roomId;
|
||||||
|
|
||||||
if (editingDevice) {
|
if (editingDevice) {
|
||||||
// 更新设备
|
// 更新设备
|
||||||
await axios.put(`/api/devices/${editingDevice.deviceId}`, deviceData);
|
await axios.put(`/api/devices/${editingDevice.deviceId}`, deviceData);
|
||||||
@@ -640,7 +664,8 @@ function DeviceManagement() {
|
|||||||
fetchDevices();
|
fetchDevices();
|
||||||
setEditingDevice(null);
|
setEditingDevice(null);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error(editingDevice ? '设备更新失败' : '设备创建失败');
|
const errorMsg = error.response?.data?.error || error.message || '未知错误';
|
||||||
|
message.error(editingDevice ? `设备更新失败: ${errorMsg}` : `设备创建失败: ${errorMsg}`);
|
||||||
console.error(editingDevice ? '设备更新失败:' : '设备创建失败:', error);
|
console.error(editingDevice ? '设备更新失败:' : '设备创建失败:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1242,17 +1267,17 @@ function DeviceManagement() {
|
|||||||
// 保存字段配置
|
// 保存字段配置
|
||||||
const handleSaveFieldConfig = async values => {
|
const handleSaveFieldConfig = async values => {
|
||||||
try {
|
try {
|
||||||
// 更新设备字段配置的可见性
|
|
||||||
const updatedFields = deviceFields.map(field => ({
|
const updatedFields = deviceFields.map(field => ({
|
||||||
...field,
|
fieldId: field.fieldId,
|
||||||
visible: values[field.fieldName],
|
fieldName: field.fieldName,
|
||||||
|
displayName: field.displayName,
|
||||||
|
visible: values[`visible_${field.fieldName}`] ?? field.visible,
|
||||||
|
required: values[`required_${field.fieldName}`] ?? field.required,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// 保存到后端
|
const response = await axios.post('/api/deviceFields/config', updatedFields);
|
||||||
await axios.post('/api/deviceFields/config', updatedFields);
|
|
||||||
|
|
||||||
// 更新本地状态
|
setDeviceFields(response.data);
|
||||||
setDeviceFields(updatedFields);
|
|
||||||
message.success('字段配置保存成功');
|
message.success('字段配置保存成功');
|
||||||
setFieldConfigModalVisible(false);
|
setFieldConfigModalVisible(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -1616,10 +1641,18 @@ function DeviceManagement() {
|
|||||||
className="device-modal"
|
className="device-modal"
|
||||||
>
|
>
|
||||||
<Form form={form} layout="vertical" onFinish={handleSubmit}>
|
<Form form={form} layout="vertical" onFinish={handleSubmit}>
|
||||||
{deviceFields
|
{(() => {
|
||||||
.filter(field => field.fieldName !== 'deviceId')
|
const filteredFields = deviceFields.filter(
|
||||||
.map(field => {
|
field => field.fieldName !== 'deviceId' && field.fieldName !== 'rackId'
|
||||||
|
);
|
||||||
|
const formItems = [];
|
||||||
|
|
||||||
|
filteredFields.forEach((field, index) => {
|
||||||
let control = null;
|
let control = null;
|
||||||
|
const inputStyle = {
|
||||||
|
borderRadius: '8px',
|
||||||
|
transition: 'all 0.3s ease',
|
||||||
|
};
|
||||||
|
|
||||||
switch (field.fieldType) {
|
switch (field.fieldType) {
|
||||||
case 'text':
|
case 'text':
|
||||||
@@ -1627,7 +1660,8 @@ function DeviceManagement() {
|
|||||||
control = (
|
control = (
|
||||||
<Input
|
<Input
|
||||||
placeholder={`请输入${field.displayName}`}
|
placeholder={`请输入${field.displayName}`}
|
||||||
style={{ borderRadius: '8px' }}
|
style={inputStyle}
|
||||||
|
className="form-input-enhanced"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
@@ -1636,7 +1670,8 @@ function DeviceManagement() {
|
|||||||
<InputNumber
|
<InputNumber
|
||||||
placeholder={`请输入${field.displayName}`}
|
placeholder={`请输入${field.displayName}`}
|
||||||
min={0}
|
min={0}
|
||||||
style={{ width: '100%', borderRadius: '8px' }}
|
style={{ width: '100%', ...inputStyle }}
|
||||||
|
className="form-input-enhanced"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
@@ -1646,8 +1681,9 @@ function DeviceManagement() {
|
|||||||
case 'date':
|
case 'date':
|
||||||
control = (
|
control = (
|
||||||
<DatePicker
|
<DatePicker
|
||||||
style={{ width: '100%', borderRadius: '8px' }}
|
style={{ width: '100%', ...inputStyle }}
|
||||||
placeholder={`请选择${field.displayName}`}
|
placeholder={`请选择${field.displayName}`}
|
||||||
|
className="form-input-enhanced"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
@@ -1656,90 +1692,250 @@ function DeviceManagement() {
|
|||||||
<Input.TextArea
|
<Input.TextArea
|
||||||
placeholder={`请输入${field.displayName}`}
|
placeholder={`请输入${field.displayName}`}
|
||||||
rows={3}
|
rows={3}
|
||||||
style={{ borderRadius: '8px' }}
|
style={inputStyle}
|
||||||
|
className="form-input-enhanced"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case 'select':
|
case 'select':
|
||||||
if (field.fieldName === 'rackId') {
|
control = (
|
||||||
control = (
|
<Select
|
||||||
<Select
|
placeholder={`请选择${field.displayName}`}
|
||||||
placeholder={`请选择${field.displayName}`}
|
style={inputStyle}
|
||||||
style={{ borderRadius: '8px' }}
|
className="form-input-enhanced"
|
||||||
>
|
>
|
||||||
{racks.map(rack => (
|
{field.options &&
|
||||||
<Option key={rack.rackId} value={rack.rackId}>
|
field.options.map(option => (
|
||||||
{rack.name} ({rack.rackId})
|
<Option key={option.value} value={option.value}>
|
||||||
|
{option.label}
|
||||||
</Option>
|
</Option>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
control = (
|
|
||||||
<Select
|
|
||||||
placeholder={`请选择${field.displayName}`}
|
|
||||||
style={{ borderRadius: '8px' }}
|
|
||||||
>
|
|
||||||
{field.options &&
|
|
||||||
field.options.map(option => (
|
|
||||||
<Option key={option.value} value={option.value}>
|
|
||||||
{option.label}
|
|
||||||
</Option>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
control = (
|
control = (
|
||||||
<Input
|
<Input
|
||||||
placeholder={`请输入${field.displayName}`}
|
placeholder={`请输入${field.displayName}`}
|
||||||
style={{ borderRadius: '8px' }}
|
style={inputStyle}
|
||||||
|
className="form-input-enhanced"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
// 机房和机柜联动选择区域特殊处理
|
||||||
<Form.Item
|
if (field.fieldName === 'serialNumber') {
|
||||||
key={field.fieldName}
|
formItems.push(
|
||||||
name={field.fieldName}
|
<React.Fragment key={field.fieldName}>
|
||||||
label={field.displayName}
|
<Col span={12} key={`${field.fieldName}-col`}>
|
||||||
rules={
|
<Form.Item
|
||||||
field.required && field.fieldName !== 'deviceId'
|
name={field.fieldName}
|
||||||
? [{ required: true, message: `请输入${field.displayName}` }]
|
label={
|
||||||
: []
|
<span>
|
||||||
}
|
{field.displayName}
|
||||||
>
|
{field.required && (
|
||||||
{control}
|
<span style={{ color: '#ff4d4f', marginLeft: '4px' }}>*</span>
|
||||||
</Form.Item>
|
)}
|
||||||
);
|
</span>
|
||||||
})}
|
}
|
||||||
|
rules={
|
||||||
|
field.required
|
||||||
|
? [{ required: true, message: `请输入${field.displayName}` }]
|
||||||
|
: []
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{control}
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
{/* 机房机柜联动选择区域 - 特殊突出显示 */}
|
||||||
|
<Col span={24} key="room-rack-section">
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
background: 'linear-gradient(135deg, #f0f5ff 0%, #e6f7ff 100%)',
|
||||||
|
borderRadius: '12px',
|
||||||
|
padding: '20px',
|
||||||
|
marginBottom: '16px',
|
||||||
|
border: '2px solid #d6e4ff',
|
||||||
|
boxShadow: '0 2px 8px rgba(24, 144, 255, 0.1)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: '14px',
|
||||||
|
fontWeight: '600',
|
||||||
|
color: '#1890ff',
|
||||||
|
marginBottom: '16px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DatabaseOutlined style={{ marginRight: '8px' }} />
|
||||||
|
设备位置选择
|
||||||
|
</div>
|
||||||
|
<Row gutter={16}>
|
||||||
|
<Col span={12}>
|
||||||
|
<Form.Item
|
||||||
|
name="roomId"
|
||||||
|
label={
|
||||||
|
<span>
|
||||||
|
机房
|
||||||
|
<span style={{ color: '#ff4d4f', marginLeft: '4px' }}>*</span>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
rules={[{ required: true, message: '请选择机房' }]}
|
||||||
|
style={{ marginBottom: '0' }}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
placeholder="请选择机房"
|
||||||
|
style={{ borderRadius: '8px' }}
|
||||||
|
showSearch
|
||||||
|
optionFilterProp="children"
|
||||||
|
onChange={(value) => {
|
||||||
|
setSelectedRoomId(value);
|
||||||
|
form.setFieldValue('rackId', undefined);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{rooms.map(room => (
|
||||||
|
<Option key={room.roomId} value={room.roomId}>
|
||||||
|
{room.name}
|
||||||
|
</Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col span={12}>
|
||||||
|
<Form.Item
|
||||||
|
name="rackId"
|
||||||
|
label={
|
||||||
|
<span>
|
||||||
|
机柜
|
||||||
|
<span style={{ color: '#ff4d4f', marginLeft: '4px' }}>*</span>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
rules={[{ required: true, message: '请选择机柜' }]}
|
||||||
|
style={{ marginBottom: '0' }}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
placeholder={selectedRoomId ? '请选择机柜' : '请先选择机房'}
|
||||||
|
style={{ borderRadius: '8px' }}
|
||||||
|
disabled={!selectedRoomId}
|
||||||
|
showSearch
|
||||||
|
optionFilterProp="children"
|
||||||
|
>
|
||||||
|
{(selectedRoomId
|
||||||
|
? racks.filter(rack => rack.roomId === selectedRoomId)
|
||||||
|
: []
|
||||||
|
).map(rack => (
|
||||||
|
<Option key={rack.rackId} value={rack.rackId}>
|
||||||
|
{rack.name} ({rack.rackId})
|
||||||
|
</Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
} else if (field.fieldType === 'textarea') {
|
||||||
|
// textarea 占整行
|
||||||
|
formItems.push(
|
||||||
|
<Col span={24} key={field.fieldName}>
|
||||||
|
<Form.Item
|
||||||
|
name={field.fieldName}
|
||||||
|
label={
|
||||||
|
<span>
|
||||||
|
{field.displayName}
|
||||||
|
{field.required && (
|
||||||
|
<span style={{ color: '#ff4d4f', marginLeft: '4px' }}>*</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
rules={
|
||||||
|
field.required
|
||||||
|
? [{ required: true, message: `请输入${field.displayName}` }]
|
||||||
|
: []
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{control}
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// 其他字段两列布局
|
||||||
|
formItems.push(
|
||||||
|
<Col span={12} key={field.fieldName}>
|
||||||
|
<Form.Item
|
||||||
|
name={field.fieldName}
|
||||||
|
label={
|
||||||
|
<span>
|
||||||
|
{field.displayName}
|
||||||
|
{field.required && (
|
||||||
|
<span style={{ color: '#ff4d4f', marginLeft: '4px' }}>*</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
rules={
|
||||||
|
field.required
|
||||||
|
? [{ required: true, message: `请输入${field.displayName}` }]
|
||||||
|
: []
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{control}
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
<Form.Item style={{ textAlign: 'right', marginTop: '24px' }}>
|
return <Row gutter={16}>{formItems}</Row>;
|
||||||
<Space>
|
})()}
|
||||||
<Button onClick={handleCancel} style={secondaryActionStyle}>
|
|
||||||
取消
|
{/* 底部按钮区域 */}
|
||||||
</Button>
|
<div
|
||||||
<Button
|
style={{
|
||||||
type="primary"
|
display: 'flex',
|
||||||
htmlType="submit"
|
justifyContent: 'flex-end',
|
||||||
style={{
|
gap: '12px',
|
||||||
height: '40px',
|
marginTop: '32px',
|
||||||
borderRadius: designTokens.borderRadius.small,
|
paddingTop: '24px',
|
||||||
background: designTokens.colors.primary.gradient,
|
borderTop: '1px solid #f0f0f0',
|
||||||
border: 'none',
|
}}
|
||||||
color: '#ffffff',
|
>
|
||||||
boxShadow: designTokens.shadows.small,
|
<Button
|
||||||
fontWeight: '500',
|
onClick={handleCancel}
|
||||||
display: 'inline-flex',
|
style={{
|
||||||
alignItems: 'center',
|
height: '40px',
|
||||||
justifyContent: 'center',
|
borderRadius: '8px',
|
||||||
}}
|
padding: '0 24px',
|
||||||
>
|
fontWeight: '500',
|
||||||
确定
|
transition: 'all 0.3s ease',
|
||||||
</Button>
|
}}
|
||||||
</Space>
|
>
|
||||||
</Form.Item>
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
htmlType="submit"
|
||||||
|
style={{
|
||||||
|
height: '40px',
|
||||||
|
borderRadius: '8px',
|
||||||
|
background: designTokens.colors.primary.gradient,
|
||||||
|
border: 'none',
|
||||||
|
color: '#ffffff',
|
||||||
|
boxShadow: designTokens.shadows.small,
|
||||||
|
fontWeight: '500',
|
||||||
|
display: 'inline-flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
padding: '0 32px',
|
||||||
|
transition: 'all 0.3s ease',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
确定
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
@@ -1762,29 +1958,46 @@ function DeviceManagement() {
|
|||||||
<Form
|
<Form
|
||||||
layout="vertical"
|
layout="vertical"
|
||||||
onFinish={handleSaveFieldConfig}
|
onFinish={handleSaveFieldConfig}
|
||||||
initialValues={deviceFields.reduce(
|
initialValues={{
|
||||||
(acc, field) => ({
|
...deviceFields.reduce(
|
||||||
...acc,
|
(acc, field) => ({
|
||||||
[field.fieldName]: field.visible,
|
...acc,
|
||||||
}),
|
[`visible_${field.fieldName}`]: field.visible,
|
||||||
{}
|
[`required_${field.fieldName}`]: field.required,
|
||||||
)}
|
}),
|
||||||
|
{}
|
||||||
|
),
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ maxHeight: 400, overflowY: 'auto' }}>
|
<div style={{ maxHeight: 400, overflowY: 'auto' }}>
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '12px' }}>
|
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||||
{deviceFields
|
<thead>
|
||||||
.filter(field => field.fieldName !== 'deviceId')
|
<tr style={{ borderBottom: '1px solid #f0f0f0' }}>
|
||||||
.map(field => (
|
<th style={{ padding: '8px', textAlign: 'left', width: '40%' }}>字段名称</th>
|
||||||
<Form.Item
|
<th style={{ padding: '8px', textAlign: 'center', width: '30%' }}>可见</th>
|
||||||
key={field.fieldName}
|
<th style={{ padding: '8px', textAlign: 'center', width: '30%' }}>必填</th>
|
||||||
name={field.fieldName}
|
</tr>
|
||||||
valuePropName="checked"
|
</thead>
|
||||||
noStyle
|
<tbody>
|
||||||
>
|
{deviceFields
|
||||||
<Checkbox style={{ marginBottom: '8px' }}>{field.displayName}</Checkbox>
|
.filter(field => field.fieldName !== 'deviceId')
|
||||||
</Form.Item>
|
.map(field => (
|
||||||
))}
|
<tr key={field.fieldName} style={{ borderBottom: '1px solid #f0f0f0' }}>
|
||||||
</div>
|
<td style={{ padding: '8px' }}>{field.displayName}</td>
|
||||||
|
<td style={{ padding: '8px', textAlign: 'center' }}>
|
||||||
|
<Form.Item name={`visible_${field.fieldName}`} valuePropName="checked" noStyle>
|
||||||
|
<Switch size="small" />
|
||||||
|
</Form.Item>
|
||||||
|
</td>
|
||||||
|
<td style={{ padding: '8px', textAlign: 'center' }}>
|
||||||
|
<Form.Item name={`required_${field.fieldName}`} valuePropName="checked" noStyle>
|
||||||
|
<Switch size="small" />
|
||||||
|
</Form.Item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Form.Item style={{ textAlign: 'right', marginTop: '20px' }}>
|
<Form.Item style={{ textAlign: 'right', marginTop: '20px' }}>
|
||||||
|
|||||||
@@ -241,7 +241,7 @@ const Rack3DVisualization = () => {
|
|||||||
if (!rackId) return;
|
if (!rackId) return;
|
||||||
try {
|
try {
|
||||||
setLoadingDevices(true);
|
setLoadingDevices(true);
|
||||||
const response = await axios.get(`/api/devices?rackId=${rackId}`);
|
const response = await axios.get(`/api/devices?rackId=${rackId}&pageSize=100`);
|
||||||
|
|
||||||
let devicesData = [];
|
let devicesData = [];
|
||||||
if (Array.isArray(response.data)) {
|
if (Array.isArray(response.data)) {
|
||||||
|
|||||||
@@ -954,6 +954,37 @@ export const generateGlobalStyles = tokens => `
|
|||||||
background: inherit !important;
|
background: inherit !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 表单输入框增强样式 */
|
||||||
|
.form-input-enhanced:hover {
|
||||||
|
border-color: ${tokens.colors.primary.main} !important;
|
||||||
|
box-shadow: 0 2px 8px rgba(24, 144, 255, 0.1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input-enhanced:focus,
|
||||||
|
.form-input-enhanced.ant-input-focused,
|
||||||
|
.form-input-enhanced.ant-select-focused .ant-select-selector,
|
||||||
|
.form-input-enhanced.ant-input-number-focused {
|
||||||
|
border-color: ${tokens.colors.primary.main} !important;
|
||||||
|
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表单项标签增强 */
|
||||||
|
.device-modal .ant-form-item-label > label {
|
||||||
|
font-weight: 500;
|
||||||
|
color: ${tokens.colors.text.primary};
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表单项间距优化 */
|
||||||
|
.device-modal .ant-form-item {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 必填项标识动画 */
|
||||||
|
.device-modal .ant-form-item-label > label::after {
|
||||||
|
content: '';
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 768px) {
|
@media screen and (max-width: 768px) {
|
||||||
.device-table-wrapper .ant-table-tbody > tr > td {
|
.device-table-wrapper .ant-table-tbody > tr > td {
|
||||||
max-width: 150px !important;
|
max-width: 150px !important;
|
||||||
|
|||||||
Reference in New Issue
Block a user