refactor: 移除调试日志并优化代码结构

feat(components): 新增设备管理相关组件和仪表盘组件
feat(hooks): 添加自定义hooks用于API调用和数据管理
style: 优化滚动条样式和模态框布局
chore: 清理无用脚本和调试文件
docs: 更新组件导出文件
This commit is contained in:
zhang1106
2026-03-10 14:34:39 +08:00
parent 98f705de8b
commit 196dc4ae41
43 changed files with 3082 additions and 3736 deletions
+5 -1
View File
@@ -40,4 +40,8 @@ lerna-debug.log*
# Temporary files
*.tmp
*.temp
*.temp
# Backup files (generated by maintenance scripts)
backend/backups/
*.backup.json
-33
View File
@@ -1,33 +0,0 @@
const { sequelize } = require('./db');
const User = require('./models/User');
const bcrypt = require('bcryptjs');
async function main() {
try {
await sequelize.authenticate();
console.log('数据库连接成功!\n');
const users = await User.findAll({
attributes: ['userId', 'username', 'password', 'email', 'status', 'createdAt']
});
console.log('用户列表:');
console.log('============');
for (const user of users) {
console.log('用户名:', user.username);
console.log('密码(加密后):', user.password);
console.log('状态:', user.status);
console.log('创建时间:', user.createdAt);
console.log('------------');
}
console.log('\n' + users.length + ' 个用户');
await sequelize.close();
} catch (error) {
console.error('错误:', error.message);
}
}
main();
-99
View File
@@ -1,99 +0,0 @@
const { sequelize } = require('./db');
async function fixMaxStock() {
try {
console.log('开始修复 maxStock 字段...');
// 检查当前表结构
const [results] = await sequelize.query(
"PRAGMA table_info(consumables);"
);
console.log('当前表结构:');
results.forEach(col => {
console.log(` ${col.name}: ${col.type} ${col.notnull ? 'NOT NULL' : 'NULL'} default=${col.dflt_value}`);
});
const maxStockCol = results.find(c => c.name === 'maxStock');
if (maxStockCol) {
console.log('\n当前 maxStock 字段:', maxStockCol);
// SQLite 不支持直接修改列,需要创建新表
console.log('\n需要重建表结构...');
// 1. 创建新表
await sequelize.query(`
CREATE TABLE consumables_new (
consumableId VARCHAR(255) PRIMARY KEY NOT NULL UNIQUE,
name VARCHAR(255) NOT NULL,
category VARCHAR(255) NOT NULL,
unit VARCHAR(255) NOT NULL DEFAULT '个',
currentStock INTEGER NOT NULL DEFAULT 0,
minStock INTEGER NOT NULL DEFAULT 10,
maxStock INTEGER NOT NULL DEFAULT 0,
unitPrice DECIMAL(10,2) NOT NULL DEFAULT 0,
supplier VARCHAR(255),
location VARCHAR(255),
description TEXT,
status VARCHAR(255) DEFAULT 'active',
version INTEGER NOT NULL DEFAULT 0,
createdAt DATETIME,
updatedAt DATETIME
)
`);
// 2. 复制数据(将 null 转换为 0)
await sequelize.query(`
INSERT INTO consumables_new
SELECT
consumableId,
name,
category,
unit,
currentStock,
minStock,
COALESCE(maxStock, 0) as maxStock,
unitPrice,
supplier,
location,
description,
status,
version,
createdAt,
updatedAt
FROM consumables
`);
// 3. 删除旧表
await sequelize.query('DROP TABLE consumables');
// 4. 重命名新表
await sequelize.query('ALTER TABLE consumables_new RENAME TO consumables');
// 5. 创建索引
await sequelize.query('CREATE INDEX consumables_category ON consumables(category)');
await sequelize.query('CREATE INDEX consumables_status ON consumables(status)');
await sequelize.query('CREATE INDEX consumables_category_status ON consumables(category, status)');
await sequelize.query('CREATE INDEX consumables_updatedAt ON consumables(updatedAt)');
console.log('\n表结构修复完成!');
// 验证新表结构
const [newResults] = await sequelize.query(
"PRAGMA table_info(consumables);"
);
console.log('\n新表结构:');
newResults.forEach(col => {
console.log(` ${col.name}: ${col.type} ${col.notnull ? 'NOT NULL' : 'NULL'} default=${col.dflt_value}`);
});
}
console.log('\n修复完成!');
process.exit(0);
} catch (error) {
console.error('修复失败:', error);
process.exit(1);
}
}
fixMaxStock();
-125
View File
@@ -1,125 +0,0 @@
const bcrypt = require('bcryptjs');
const { sequelize } = require('./db');
const User = require('./models/User');
const Role = require('./models/Role');
const UserRole = require('./models/UserRole');
async function fixAdminUser() {
try {
await sequelize.authenticate();
console.log('数据库连接成功!\n');
const adminUser = await User.findOne({
where: { username: 'admin' }
});
if (!adminUser) {
console.log('未找到 admin 用户');
return;
}
console.log('当前 admin 用户信息:');
console.log('用户名:', adminUser.username);
console.log('邮箱:', adminUser.email);
console.log('状态:', adminUser.status);
console.log('');
const userRoles = await UserRole.findAll({
where: { UserId: adminUser.userId },
include: [{ model: Role }]
});
console.log('当前用户角色:');
if (userRoles.length === 0) {
console.log(' - 无角色分配');
} else {
userRoles.forEach(ur => {
console.log(` - ${ur.Role.roleName} (${ur.Role.roleCode})`);
});
}
console.log('');
let adminRole = await Role.findOne({
where: { roleCode: 'admin' }
});
if (!adminRole) {
console.log('正在创建管理员角色...');
adminRole = await Role.create({
roleId: 'role_admin',
roleName: '管理员',
roleCode: 'admin',
description: '系统管理员,拥有所有权限',
status: 'active',
permissions: []
});
console.log('管理员角色创建成功!');
} else {
console.log('管理员角色已存在');
}
const existingUserRole = await UserRole.findOne({
where: {
UserId: adminUser.userId,
RoleId: adminRole.roleId
}
});
if (!existingUserRole) {
console.log('正在为 admin 用户分配管理员角色...');
await UserRole.create({
UserId: adminUser.userId,
RoleId: adminRole.roleId
});
console.log('角色分配成功!');
} else {
console.log('管理员角色已分配给该用户');
}
console.log('');
const newPassword = 'admin123';
const salt = await bcrypt.genSalt(10);
const hashedPassword = await bcrypt.hash(newPassword, salt);
await adminUser.update({
password: hashedPassword
});
console.log('密码已重置为: admin123');
console.log('');
const updatedUser = await User.findOne({
where: { username: 'admin' },
include: [{ model: Role }]
});
console.log('修复后的 admin 用户信息:');
console.log('用户名:', updatedUser.username);
console.log('邮箱:', updatedUser.email);
console.log('状态:', updatedUser.status);
console.log('角色:',
updatedUser.Roles && updatedUser.Roles.length > 0
? updatedUser.Roles.map(r => r.roleName).join(', ')
: '无角色'
);
console.log('');
const testResult = bcrypt.compareSync(newPassword, updatedUser.password);
console.log('密码验证测试:');
console.log(` 输入密码: "${newPassword}"`);
console.log(` 验证结果: ${testResult ? '✓ 成功' : '✗ 失败'}`);
console.log('\n========================================');
console.log('修复完成!现在可以使用以下信息登录:');
console.log(' 用户名: admin');
console.log(' 密码: admin123');
console.log('========================================');
} catch (error) {
console.error('修复过程中发生错误:', error.message);
} finally {
await sequelize.close();
}
}
fixAdminUser();
-133
View File
@@ -1,133 +0,0 @@
const { sequelize } = require('./db');
const Role = require('./models/Role');
const User = require('./models/User');
const UserRole = require('./models/UserRole');
async function fixForeignKeys() {
try {
console.log('开始修复外键约束...');
await sequelize.query('PRAGMA foreign_keys = OFF');
await sequelize.query('DROP TABLE IF EXISTS user_roles');
await sequelize.query('DROP TABLE IF EXISTS users');
await sequelize.query('DROP TABLE IF EXISTS roles');
console.log('已删除现有表,正在重新创建...');
await sequelize.query(`
CREATE TABLE IF NOT EXISTS roles (
roleId TEXT PRIMARY KEY,
roleName TEXT NOT NULL,
roleCode TEXT NOT NULL UNIQUE,
description TEXT,
permissions TEXT DEFAULT '[]',
sort INTEGER DEFAULT 0,
status TEXT DEFAULT 'active',
createdAt DATETIME,
updatedAt DATETIME
)
`);
await sequelize.query(`
CREATE TABLE IF NOT EXISTS users (
userId TEXT PRIMARY KEY,
username TEXT NOT NULL UNIQUE,
password TEXT NOT NULL,
email TEXT,
phone TEXT,
realName TEXT,
avatar TEXT,
status TEXT DEFAULT 'active',
lastLoginTime DATETIME,
lastLoginIp TEXT,
loginCount INTEGER DEFAULT 0,
remark TEXT,
createdAt DATETIME,
updatedAt DATETIME
)
`);
await sequelize.query(`
CREATE TABLE IF NOT EXISTS user_roles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
UserId TEXT NOT NULL,
RoleId TEXT NOT NULL,
createdAt DATETIME,
updatedAt DATETIME,
FOREIGN KEY (UserId) REFERENCES users(userId) ON DELETE CASCADE,
FOREIGN KEY (RoleId) REFERENCES roles(roleId) ON DELETE CASCADE,
UNIQUE(UserId, RoleId)
)
`);
await sequelize.query('PRAGMA foreign_keys = ON');
console.log('外键约束修复完成!');
console.log('正在重新初始化角色数据...');
const bcrypt = require('bcryptjs');
const SALT_ROUNDS = 10;
await Role.bulkCreate([
{
roleId: 'role_admin',
roleName: '管理员',
roleCode: 'admin',
description: '系统管理员,拥有所有权限',
permissions: '[]',
sort: 1,
status: 'active'
},
{
roleId: 'role_operator',
roleName: '操作员',
roleCode: 'operator',
description: '设备操作员,可进行设备操作',
permissions: '[]',
sort: 2,
status: 'active'
},
{
roleId: 'role_viewer',
roleName: '访客',
roleCode: 'viewer',
description: '只读权限,只能查看数据',
permissions: '[]',
sort: 3,
status: 'active'
}
]);
console.log('角色数据初始化完成!');
const hashedPassword = await bcrypt.hash('admin123', SALT_ROUNDS);
const adminUser = await User.create({
userId: 'user_admin',
username: 'admin',
password: hashedPassword,
email: 'admin@example.com',
realName: '系统管理员',
status: 'active'
});
await UserRole.create({
UserId: adminUser.userId,
RoleId: 'role_admin'
});
console.log('管理员账户创建完成!');
console.log('用户名: admin');
console.log('密码: admin123');
console.log('\n修复完成!请重启后端服务。');
} catch (error) {
console.error('修复外键约束失败:', error);
} finally {
await sequelize.close();
}
}
fixForeignKeys();
-35
View File
@@ -1,35 +0,0 @@
const sqlite3 = require('sqlite3').verbose();
const path = require('path');
const dbPath = path.join(__dirname, 'idc_management.db');
const db = new sqlite3.Database(dbPath, (err) => {
if (err) {
console.error('无法打开数据库:', err);
process.exit(1);
}
});
console.log('开始修复数据库表结构...');
db.serialize(() => {
db.run(`ALTER TABLE consumables ADD COLUMN location TEXT;`, function(err) {
if (err) {
if (err.message.includes('duplicate column name: location')) {
console.log('location 列已存在,无需添加');
} else {
console.error('添加 location 列失败:', err.message);
}
} else {
console.log('成功添加 location 列到 consumables 表');
}
db.close((closeErr) => {
if (closeErr) {
console.error('关闭数据库失败:', closeErr);
} else {
console.log('数据库修复完成');
}
});
});
});
-58
View File
@@ -1,58 +0,0 @@
const sqlite3 = require('sqlite3').verbose();
const path = require('path');
const dbPath = process.env.DB_PATH || path.join(__dirname, 'idc_management.db');
const db = new sqlite3.Database(dbPath, (err) => {
if (err) {
console.error('无法连接到数据库:', err);
process.exit(1);
}
console.log('已连接到数据库:', dbPath);
});
const columnsToAdd = [
{ table: 'pending_devices', column: 'height', type: 'INTEGER', defaultValue: 1 },
{ table: 'pending_devices', column: 'powerConsumption', type: 'FLOAT', defaultValue: 0 },
{ table: 'pending_devices', column: 'brand', type: 'VARCHAR(255)', defaultValue: null },
{ table: 'pending_devices', column: 'purchaseDate', type: 'DATE', defaultValue: null },
{ table: 'pending_devices', column: 'warrantyExpiry', type: 'DATE', defaultValue: null },
];
db.serialize(() => {
columnsToAdd.forEach(({ table, column, type, defaultValue }) => {
db.all(`PRAGMA table_info(${table})`, (err, rows) => {
if (err) {
console.error(`获取表 ${table} 信息失败:`, err);
return;
}
const columnExists = rows && rows.some(row => row.name === column);
if (!columnExists) {
const defaultClause = defaultValue !== null ? ` DEFAULT ${typeof defaultValue === 'string' ? `'${defaultValue}'` : defaultValue}` : '';
const sql = `ALTER TABLE ${table} ADD COLUMN ${column} ${type}${defaultClause}`;
db.run(sql, (err) => {
if (err) {
console.error(`添加列 ${table}.${column} 失败:`, err.message);
} else {
console.log(`成功添加列 ${table}.${column}`);
}
});
} else {
console.log(`${table}.${column} 已存在,跳过`);
}
});
});
});
setTimeout(() => {
db.close((err) => {
if (err) {
console.error('关闭数据库失败:', err);
} else {
console.log('数据库迁移完成,连接已关闭');
}
});
}, 2000);
-51
View File
@@ -1,51 +0,0 @@
const { sequelize } = require('./db');
const FaultCategory = require('./models/FaultCategory');
async function recreateFaultCategoryTable() {
try {
console.log('开始重建故障分类表...');
await sequelize.authenticate();
console.log('数据库连接成功');
// 删除旧表并创建新表
await sequelize.query('DROP TABLE IF EXISTS fault_categories');
console.log('旧表已删除');
await FaultCategory.sync();
console.log('新表已创建');
// 初始化默认分类
const defaultCategories = [
{ name: '系统故障', description: '操作系统、应用程序等系统软件的故障问题', priority: 1, defaultPriority: 'high', expectedDuration: 120, isSystem: true, isActive: true },
{ name: '硬件故障', description: '物理设备、服务器、存储等硬件设备的故障问题', priority: 2, defaultPriority: 'high', expectedDuration: 180, isSystem: true, isActive: true },
{ name: '网络故障', description: '网络连接、交换机、路由器等网络相关故障', priority: 3, defaultPriority: 'high', expectedDuration: 120, isSystem: true, isActive: true },
{ name: '软件故障', description: '应用程序错误、软件兼容性等问题', priority: 4, defaultPriority: 'medium', expectedDuration: 90, isSystem: true, isActive: true },
{ name: '安全事件', description: '安全漏洞、入侵检测、权限异常等安全问题', priority: 5, defaultPriority: 'urgent', expectedDuration: 60, isSystem: true, isActive: true },
{ name: '性能问题', description: '系统响应慢、资源利用率高等性能问题', priority: 6, defaultPriority: 'medium', expectedDuration: 120, isSystem: true, isActive: true },
{ name: '配置变更', description: '系统配置、软件配置等变更需求', priority: 7, defaultPriority: 'low', expectedDuration: 60, isSystem: true, isActive: true },
{ name: '例行维护', description: '定期维护、巡检、更新等计划性工作', priority: 8, defaultPriority: 'low', expectedDuration: 120, isSystem: true, isActive: true },
{ name: '数据问题', description: '数据错误、数据丢失、数据同步等数据相关问题', priority: 9, defaultPriority: 'high', expectedDuration: 150, isSystem: true, isActive: true },
{ name: '其他问题', description: '无法归类的其他问题', priority: 99, defaultPriority: 'medium', expectedDuration: 90, isSystem: true, isActive: true }
];
for (const cat of defaultCategories) {
const categoryId = `CAT${Date.now().toString(36).toUpperCase()}${Math.random().toString(36).substr(2, 4).toUpperCase()}`;
await FaultCategory.create({
categoryId,
...cat,
solutions: [],
metadata: {}
});
}
console.log('默认分类已初始化');
console.log('故障分类表重建完成!');
process.exit(0);
} catch (error) {
console.error('操作失败:', error);
process.exit(1);
}
}
recreateFaultCategoryTable();
-2
View File
@@ -55,12 +55,10 @@ router.get('/', async (req, res) => {
router.post('/', async (req, res) => {
const transaction = await sequelize.transaction();
try {
console.log('接收到的数据:', JSON.stringify(req.body, null, 2));
const consumableData = {
...req.body,
consumableId: req.body.consumableId || `CON${Date.now()}`
};
console.log('处理后的数据:', JSON.stringify(consumableData, null, 2));
const consumable = await Consumable.create(consumableData, { transaction });
await ConsumableLog.create({
-5
View File
@@ -81,8 +81,6 @@ router.get('/plans', async (req, res) => {
router.get('/plans/:planId', async (req, res) => {
try {
console.log('=== GET /plans/:planId ===', req.params.planId);
const plan = await InventoryPlan.findByPk(req.params.planId, {
include: [
{ model: require('../models/User'), as: 'Creator', attributes: ['userId', 'username', 'realName'] }
@@ -90,7 +88,6 @@ router.get('/plans/:planId', async (req, res) => {
});
if (!plan) {
console.log('Plan not found');
return res.status(404).json({ error: '盘点计划不存在' });
}
@@ -102,10 +99,8 @@ router.get('/plans/:planId', async (req, res) => {
order: [['createdAt', 'ASC']]
});
console.log('Found tasks:', tasks.length);
res.json({ plan, tasks });
} catch (error) {
console.error('Error:', error);
res.status(500).json({ error: error.message });
}
});
+123 -132
View File
@@ -13,136 +13,131 @@ app.use(express.json());
app.use(fileUpload({ limits: { fileSize: FILE_UPLOAD.MAX_FILE_SIZE } }));
app.use('/temp', express.static('temp'));
// 数据库连接已从db.js导入
async function syncDatabase() {
await sequelize.authenticate();
console.log('数据库连接成功');
// 测试数据库连接并同步表结构
sequelize.authenticate()
.then(() => {
console.log('数据库连接成功');
// 自动同步表结构,不再强制删除表
return sequelize.sync({
force: false, // 改为false,避免每次启动都删除表结构
alter: false // SQLite不支持复杂的表结构修改
});
})
.then(() => {
console.log('数据库表结构同步完成');
// 同步完成后初始化设备字段
return require('./initDeviceFields')();
})
.then(() => {
// 初始化工单字段
return require('./initTicketFields')();
})
.then(() => {
// 初始化工单模型关联
return require('./models/ticketIndex').initializeModels();
})
.then(() => {
// 同步系统设置模型
const SystemSetting = require('./models/SystemSetting');
return SystemSetting.sync();
})
.then(() => {
// 同步耗材模型
const Consumable = require('./models/Consumable');
const ConsumableLog = require('./models/ConsumableLog');
const ConsumableCategory = require('./models/ConsumableCategory');
const ConsumableRecord = require('./models/ConsumableRecord');
const ConsumableLogArchive = require('./models/ConsumableLogArchive');
return Promise.all([
Consumable.sync({ alter: true }),
ConsumableLog.sync(),
ConsumableCategory.sync(),
ConsumableRecord.sync(),
ConsumableLogArchive.sync()
]);
})
.then(() => {
console.log('耗材模型同步完成');
})
.then(() => {
// 同步盘点模型
const InventoryPlan = require('./models/InventoryPlan');
const InventoryTask = require('./models/InventoryTask');
const InventoryRecord = require('./models/InventoryRecord');
return Promise.all([
InventoryPlan.sync(),
InventoryTask.sync(),
InventoryRecord.sync()
]);
})
.then(() => {
console.log('盘点模型同步完成');
})
.then(() => {
// 同步耗材相关模型
const Consumable = require('./models/Consumable');
const ConsumableLog = require('./models/ConsumableLog');
const ConsumableCategory = require('./models/ConsumableCategory');
const ConsumableRecord = require('./models/ConsumableRecord');
const ConsumableLogArchive = require('./models/ConsumableLogArchive');
return Promise.all([
Consumable.sync({ alter: true }),
ConsumableLog.sync(),
ConsumableCategory.sync(),
ConsumableRecord.sync(),
ConsumableLogArchive.sync()
]);
})
.then(() => {
console.log('耗材模型同步完成');
})
.then(() => {
// 初始化系统设置默认值(关键:确保部署时数据正确初始化)
console.log('开始初始化系统设置默认值...');
const { initDefaultSettings } = require('./routes/systemSettings');
return initDefaultSettings();
})
.then(() => {
console.log('系统设置初始化完成');
})
.then(() => {
// 初始化故障分类数据
console.log('开始初始化故障分类...');
const FaultCategory = require('./models/FaultCategory');
const defaultCategories = [
{ name: '系统故障', description: '操作系统、应用程序等系统软件的故障问题', priority: 1, defaultPriority: 'high' },
{ name: '硬件故障', description: '物理设备、服务器、存储等硬件设备的故障问题', priority: 2, defaultPriority: 'high' },
{ name: '网络故障', description: '网络连接、交换机、路由器等网络相关故障', priority: 3, defaultPriority: 'high' },
{ name: '软件故障', description: '应用程序错误、软件兼容性等问题', priority: 4, defaultPriority: 'medium' },
{ name: '安全事件', description: '安全漏洞、入侵检测、权限异常等安全问题', priority: 5, defaultPriority: 'urgent' },
{ name: '性能问题', description: '系统响应慢、资源利用率高等性能问题', priority: 6, defaultPriority: 'medium' },
{ name: '配置变更', description: '系统配置、软件配置等变更需求', priority: 7, defaultPriority: 'low' },
{ name: '例行维护', description: '定期维护、巡检、更新等计划性工作', priority: 8, defaultPriority: 'low' },
{ name: '数据问题', description: '数据错误、数据丢失、数据同步等数据相关问题', priority: 9, defaultPriority: 'high' },
{ name: '其他问题', description: '无法归类的其他问题', priority: 99, defaultPriority: 'medium' }
];
await sequelize.sync({
force: false,
alter: false
});
console.log('数据库表结构同步完成');
}
return Promise.all(
defaultCategories.map(async (cat) => {
const existing = await FaultCategory.findOne({ where: { name: cat.name } });
if (!existing) {
const categoryId = `CAT${Date.now().toString(36).toUpperCase()}${Math.random().toString(36).substr(2, 4).toUpperCase()}`;
await FaultCategory.create({
categoryId,
...cat,
expectedDuration: 120,
solutions: [],
isSystem: true,
isActive: true
});
console.log(`创建故障分类: ${cat.name}`);
}
})
);
})
.then(() => {
console.log('故障分类初始化完成');
})
.catch(err => console.error('数据库操作失败:', err));
async function initDeviceFields() {
await require('./initDeviceFields')();
console.log('设备字段初始化完成');
}
async function initTicketFields() {
await require('./initTicketFields')();
console.log('工单字段初始化完成');
}
async function initTicketModels() {
await require('./models/ticketIndex').initializeModels();
console.log('工单模型关联初始化完成');
}
async function syncSystemSettings() {
const SystemSetting = require('./models/SystemSetting');
await SystemSetting.sync();
console.log('系统设置模型同步完成');
}
async function syncConsumableModels() {
const Consumable = require('./models/Consumable');
const ConsumableLog = require('./models/ConsumableLog');
const ConsumableCategory = require('./models/ConsumableCategory');
const ConsumableRecord = require('./models/ConsumableRecord');
const ConsumableLogArchive = require('./models/ConsumableLogArchive');
await Promise.all([
Consumable.sync({ alter: true }),
ConsumableLog.sync(),
ConsumableCategory.sync(),
ConsumableRecord.sync(),
ConsumableLogArchive.sync()
]);
console.log('耗材模型同步完成');
}
async function syncInventoryModels() {
const InventoryPlan = require('./models/InventoryPlan');
const InventoryTask = require('./models/InventoryTask');
const InventoryRecord = require('./models/InventoryRecord');
await Promise.all([
InventoryPlan.sync(),
InventoryTask.sync(),
InventoryRecord.sync()
]);
console.log('盘点模型同步完成');
}
async function initDefaultSystemSettings() {
console.log('开始初始化系统设置默认值...');
const { initDefaultSettings } = require('./routes/systemSettings');
await initDefaultSettings();
console.log('系统设置初始化完成');
}
async function initFaultCategories() {
console.log('开始初始化故障分类...');
const FaultCategory = require('./models/FaultCategory');
const defaultCategories = [
{ name: '系统故障', description: '操作系统、应用程序等系统软件的故障问题', priority: 1, defaultPriority: 'high' },
{ name: '硬件故障', description: '物理设备、服务器、存储等硬件设备的故障问题', priority: 2, defaultPriority: 'high' },
{ name: '网络故障', description: '网络连接、交换机、路由器等网络相关故障', priority: 3, defaultPriority: 'high' },
{ name: '软件故障', description: '应用程序错误、软件兼容性等问题', priority: 4, defaultPriority: 'medium' },
{ name: '安全事件', description: '安全漏洞、入侵检测、权限异常等安全问题', priority: 5, defaultPriority: 'urgent' },
{ name: '性能问题', description: '系统响应慢、资源利用率高等性能问题', priority: 6, defaultPriority: 'medium' },
{ name: '配置变更', description: '系统配置、软件配置等变更需求', priority: 7, defaultPriority: 'low' },
{ name: '例行维护', description: '定期维护、巡检、更新等计划性工作', priority: 8, defaultPriority: 'low' },
{ name: '数据问题', description: '数据错误、数据丢失、数据同步等数据相关问题', priority: 9, defaultPriority: 'high' },
{ name: '其他问题', description: '无法归类的其他问题', priority: 99, defaultPriority: 'medium' }
];
for (const cat of defaultCategories) {
const existing = await FaultCategory.findOne({ where: { name: cat.name } });
if (!existing) {
const categoryId = `CAT${Date.now().toString(36).toUpperCase()}${Math.random().toString(36).substr(2, 4).toUpperCase()}`;
await FaultCategory.create({
categoryId,
...cat,
expectedDuration: 120,
solutions: [],
isSystem: true,
isActive: true
});
console.log(`创建故障分类: ${cat.name}`);
}
}
console.log('故障分类初始化完成');
}
async function initializeApp() {
try {
await syncDatabase();
await initDeviceFields();
await initTicketFields();
await initTicketModels();
await syncSystemSettings();
await syncConsumableModels();
await syncInventoryModels();
await initDefaultSystemSettings();
await initFaultCategories();
console.log('所有初始化完成,服务器准备就绪');
} catch (error) {
console.error('初始化失败:', error);
process.exit(1);
}
}
initializeApp();
// 导入路由
const deviceRoutes = require('./routes/devices');
const rackRoutes = require('./routes/racks');
const roomRoutes = require('./routes/rooms');
@@ -163,7 +158,6 @@ const devicePortRoutes = require('./routes/devicePorts');
const networkCardRoutes = require('./routes/networkCards');
const inventoryRoutes = require('./routes/inventory');
// 使用路由
app.use('/api/devices', deviceRoutes);
app.use('/api/racks', rackRoutes);
app.use('/api/rooms', roomRoutes);
@@ -184,15 +178,12 @@ app.use('/api/device-ports', devicePortRoutes);
app.use('/api/network-cards', networkCardRoutes);
app.use('/api/inventory', inventoryRoutes);
// 静态文件服务
app.use('/uploads', express.static('uploads'));
// 健康检查
app.get('/health', (req, res) => {
res.json({ status: 'ok', message: 'IDC设备管理系统后端服务正常运行' });
});
// 启动服务器
app.listen(PORT, () => {
console.log(`服务器运行在 http://localhost:${PORT}`);
});
});
-17
View File
@@ -1,17 +0,0 @@
const fs = require('fs');
const content = fs.readFileSync('e:/IDC/jigui/frontend/src/pages/DeviceManagement.jsx', 'utf8');
const startMarker = '// 可调整列宽的表头组件';
const endMarker = '// 防抖 Hook';
const startIndex = content.indexOf(startMarker);
const endIndex = content.indexOf(endMarker, startIndex);
console.log('startMarker index:', startIndex);
console.log('endMarker index:', endIndex);
if (startIndex >= 0 && endIndex >= 0) {
console.log('Content to remove:');
console.log(content.substring(startIndex, endIndex + endMarker.length).substring(0, 500));
}
+40 -163
View File
@@ -50,6 +50,7 @@ import { ConfigProvider, useConfig } from './context/ConfigContext';
import { Scene3DProvider } from './context/Scene3DContext';
import { useDesignTokens } from './hooks/useDesignTokens';
import useIdleTimeout from './hooks/useIdleTimeout';
import { SWRConfig, swrConfig } from './hooks/useSWR';
import axios from 'axios';
import { Spin } from 'antd';
@@ -561,55 +562,50 @@ const AppLayout = ({ children }) => {
);
};
const routeConfig = [
{ path: '/', component: Dashboard },
{ path: '/devices', component: DeviceManagement },
{ path: '/racks', component: RackManagement },
{ path: '/rooms', component: RoomManagement },
{ path: '/fields', component: DeviceFieldManagement },
{ path: '/consumables', component: ConsumableManagement },
{ path: '/consumables-categories', component: CategoryManagement },
{ path: '/consumables-stats', component: ConsumableStatistics },
{ path: '/consumables-logs', component: ConsumableLogs },
{ path: '/users', component: UserManagement },
{ path: '/tickets', component: TicketManagement },
{ path: '/ticket-categories', component: TicketCategoryManagement },
{ path: '/ticket-statistics', component: TicketStatistics },
{ path: '/ticket-fields', component: TicketFieldManagement },
{ path: '/settings', component: SystemSettings },
{ path: '/cables', component: CableManagement },
{ path: '/inventory', component: InventoryManagement },
{ path: '/inventory/execution', component: InventoryTaskExecution },
{ path: '/pending-devices', component: PendingDeviceManagement },
{ path: '/ports', component: PortManagement },
];
const ThemeConfig = () => {
const designTokens = useDesignTokens();
return (
<AntdConfigProvider theme={{ token: designTokens }}>
<Router>
<Suspense fallback={<PageLoading />}>
<Routes>
<SWRConfig value={swrConfig}>
<Router>
<Suspense fallback={<PageLoading />}>
<Routes>
<Route path="/login" element={<Login />} />
<Route
path="/"
element={
<PrivateRoute>
<Dashboard />
</PrivateRoute>
}
/>
<Route
path="/devices"
element={
<PrivateRoute>
<DeviceManagement />
</PrivateRoute>
}
/>
<Route
path="/racks"
element={
<PrivateRoute>
<RackManagement />
</PrivateRoute>
}
/>
<Route
path="/rooms"
element={
<PrivateRoute>
<RoomManagement />
</PrivateRoute>
}
/>
<Route
path="/fields"
element={
<PrivateRoute>
<DeviceFieldManagement />
</PrivateRoute>
}
/>
{routeConfig.map(({ path, component: Component }) => (
<Route
key={path}
path={path}
element={
<PrivateRoute>
<Component />
</PrivateRoute>
}
/>
))}
<Route
path="/visualization-3d"
element={
@@ -620,130 +616,11 @@ const ThemeConfig = () => {
</PrivateRoute>
}
/>
<Route
path="/consumables"
element={
<PrivateRoute>
<ConsumableManagement />
</PrivateRoute>
}
/>
<Route
path="/consumables-categories"
element={
<PrivateRoute>
<CategoryManagement />
</PrivateRoute>
}
/>
<Route
path="/consumables-stats"
element={
<PrivateRoute>
<ConsumableStatistics />
</PrivateRoute>
}
/>
<Route
path="/consumables-logs"
element={
<PrivateRoute>
<ConsumableLogs />
</PrivateRoute>
}
/>
<Route
path="/users"
element={
<PrivateRoute>
<UserManagement />
</PrivateRoute>
}
/>
<Route
path="/tickets"
element={
<PrivateRoute>
<TicketManagement />
</PrivateRoute>
}
/>
<Route
path="/ticket-categories"
element={
<PrivateRoute>
<TicketCategoryManagement />
</PrivateRoute>
}
/>
<Route
path="/ticket-statistics"
element={
<PrivateRoute>
<TicketStatistics />
</PrivateRoute>
}
/>
<Route
path="/ticket-fields"
element={
<PrivateRoute>
<TicketFieldManagement />
</PrivateRoute>
}
/>
<Route
path="/settings"
element={
<PrivateRoute>
<SystemSettings />
</PrivateRoute>
}
/>
<Route
path="/cables"
element={
<PrivateRoute>
<CableManagement />
</PrivateRoute>
}
/>
<Route
path="/inventory"
element={
<PrivateRoute>
<InventoryManagement />
</PrivateRoute>
}
/>
<Route
path="/inventory/execution"
element={
<PrivateRoute>
<InventoryTaskExecution />
</PrivateRoute>
}
/>
<Route
path="/pending-devices"
element={
<PrivateRoute>
<PendingDeviceManagement />
</PrivateRoute>
}
/>
<Route
path="/ports"
element={
<PrivateRoute>
<PortManagement />
</PrivateRoute>
}
/>
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</Suspense>
</Router>
</SWRConfig>
</AntdConfigProvider>
);
};
-5
View File
@@ -45,13 +45,8 @@ api.interceptors.response.use(
if (status === 401) {
const currentPath = window.location.pathname;
console.log('[API] 401 error, current path:', currentPath);
if (!currentPath.startsWith('/login')) {
const savedToken = localStorage.getItem('token');
if (savedToken) {
console.log('[API] Token exists but got 401, might be expired');
}
localStorage.removeItem('token');
localStorage.removeItem('user');
window.location.href = '/login';
@@ -116,7 +116,6 @@ const CableCreateModal = ({ visible, onClose, onSuccess, sourceDevice }) => {
targetDeviceId: values.sourceDeviceId,
targetPort: values.sourcePort,
};
console.log('Swapped source/target to ensure Switch is Source');
}
await axios.post('/api/cables', payload);
@@ -0,0 +1,38 @@
import React, { useState, useEffect, useRef } from 'react';
const AnimatedCounter = ({ value, duration = 1500 }) => {
const [displayValue, setDisplayValue] = useState(0);
const animationRef = useRef(null);
const startTimeRef = useRef(null);
useEffect(() => {
const animate = (currentTime) => {
if (!startTimeRef.current) {
startTimeRef.current = currentTime;
}
const elapsed = currentTime - startTimeRef.current;
const progress = Math.min(elapsed / duration, 1);
const easeOutQuart = 1 - Math.pow(1 - progress, 4);
const currentValue = Math.floor(easeOutQuart * value);
setDisplayValue(currentValue);
if (progress < 1) {
animationRef.current = requestAnimationFrame(animate);
}
};
animationRef.current = requestAnimationFrame(animate);
return () => {
if (animationRef.current) {
cancelAnimationFrame(animationRef.current);
}
};
}, [value, duration]);
return <span>{displayValue}</span>;
};
export default React.memo(AnimatedCounter);
@@ -0,0 +1,61 @@
import React from 'react';
import { designTokens } from '../../config/theme';
const CircularProgress = ({ percentage, size = 120, strokeWidth = 10, color, label }) => {
const circumference = 2 * Math.PI * ((size - strokeWidth) / 2);
const offset = circumference - (percentage / 100) * circumference;
return (
<div style={{ position: 'relative', width: size, height: size }}>
<svg width={size} height={size} style={{ transform: 'rotate(-90deg)' }}>
<circle
cx={size / 2}
cy={size / 2}
r={(size - strokeWidth) / 2}
fill="none"
stroke="#f0f0f0"
strokeWidth={strokeWidth}
/>
<circle
cx={size / 2}
cy={size / 2}
r={(size - strokeWidth) / 2}
fill="none"
stroke={color}
strokeWidth={strokeWidth}
strokeDasharray={circumference}
strokeDashoffset={offset}
strokeLinecap="round"
style={{
transition: 'stroke-dashoffset 1s ease-out',
filter: `drop-shadow(0 0 6px ${color}40)`,
}}
/>
</svg>
<div
style={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
textAlign: 'center',
}}
>
<div
style={{
fontSize: '1.5rem',
fontWeight: '700',
color: designTokens.colors.text.primary,
}}
>
{percentage}%
</div>
<div style={{ fontSize: '0.75rem', color: designTokens.colors.text.secondary }}>
{label}
</div>
</div>
</div>
);
};
export default React.memo(CircularProgress);
@@ -0,0 +1,53 @@
import React from 'react';
import { designTokens } from '../../config/theme';
const DeviceTrendChart = ({ data }) => {
const maxValue = Math.max(...data.map((d) => d.value));
const chartHeight = 120;
return (
<div style={{ marginTop: '16px' }}>
<div
style={{
display: 'flex',
alignItems: 'flex-end',
justifyContent: 'space-between',
height: chartHeight,
gap: '8px',
padding: '0 8px',
}}
>
{data.map((item, index) => (
<div
key={index}
style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center' }}
>
<div
style={{
width: '100%',
maxWidth: '40px',
height: `${(item.value / maxValue) * chartHeight}px`,
borderRadius: '4px 4px 0 0',
background: `linear-gradient(180deg, ${item.color} 0%, ${item.color}80 100%)`,
transition: `height ${designTokens.transitions.slow}`,
boxShadow: `0 -2px 8px ${item.color}30`,
}}
/>
<span
style={{
fontSize: '0.7rem',
color: designTokens.colors.text.tertiary,
marginTop: '4px',
whiteSpace: 'nowrap',
}}
>
{item.label}
</span>
</div>
))}
</div>
</div>
);
};
export default React.memo(DeviceTrendChart);
@@ -0,0 +1,139 @@
import React from 'react';
import { useNavigate } from 'react-router-dom';
import {
CloudServerOutlined,
DatabaseOutlined,
WarningOutlined,
BarChartOutlined,
AppstoreOutlined,
SettingOutlined,
} from '@ant-design/icons';
import { designTokens } from '../../config/theme';
const NAV_BUTTONS_DATA = [
{
key: 'devices',
icon: CloudServerOutlined,
text: '设备管理',
path: '/devices',
color: designTokens.colors.primary.main,
},
{
key: 'racks',
icon: DatabaseOutlined,
text: '资源规划',
path: '/racks',
color: '#722ed1',
},
{
key: 'faults',
icon: WarningOutlined,
text: '故障监控',
path: '/faults',
color: designTokens.colors.warning.main,
},
{
key: 'tickets',
icon: BarChartOutlined,
text: '工单管理',
path: '/tickets',
color: '#13c2c2',
},
{
key: 'consumables',
icon: AppstoreOutlined,
text: '耗材管理',
path: '/consumables',
color: '#fa8c16',
},
{
key: 'settings',
icon: SettingOutlined,
text: '系统配置',
path: '/settings',
color: designTokens.colors.success.main,
},
];
const createNavButtonStyle = (color, isHovered) => ({
height: 'auto',
padding: 'clamp(16px, 4vw, 24px) clamp(12px, 3vw, 20px)',
borderRadius: designTokens.borderRadius.medium,
border: `2px solid ${isHovered ? color : '#f0f0f0'}`,
background: '#fff',
transition: `all ${designTokens.transitions.normal}`,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: 'clamp(8px, 2vw, 12px)',
cursor: 'pointer',
boxShadow: isHovered ? designTokens.shadows.large : designTokens.shadows.small,
transform: isHovered ? 'translateY(-4px)' : 'none',
minWidth: 0,
});
const createNavIconContainer = (color) => ({
width: 'clamp(44px, 10vw, 60px)',
height: 'clamp(44px, 10vw, 60px)',
borderRadius: designTokens.borderRadius.medium,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: `linear-gradient(135deg, ${color}20 0%, ${color}10 100%)`,
fontSize: 'clamp(20px, 5vw, 28px)',
transition: `all ${designTokens.transitions.normal}`,
flexShrink: 0,
});
const navTextStyle = {
fontSize: 'clamp(0.75rem, 2vw, 0.9rem)',
fontWeight: '600',
color: designTokens.colors.text.primary,
textAlign: 'center',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
maxWidth: '100%',
};
const NavigationGrid = ({ hoveredCard, onHover }) => {
const navigate = useNavigate();
return (
<div
className="nav-grid"
style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(100px, 1fr))',
gap: 'clamp(8px, 2vw, 16px)',
marginBottom: '24px',
}}
>
{NAV_BUTTONS_DATA.map(({ key, icon: Icon, text, color }) => {
const isHovered = hoveredCard === `nav-${key}`;
return (
<div
key={key}
className="nav-button"
style={{
...createNavButtonStyle(color, isHovered),
animationDelay: `${NAV_BUTTONS_DATA.findIndex((b) => b.key === key) * 0.1}s`,
}}
onMouseEnter={() => onHover(`nav-${key}`)}
onMouseLeave={() => onHover(null)}
onClick={() => navigate(`/${key}`)}
>
<div className="nav-icon" style={createNavIconContainer(color)}>
<Icon style={{ color, fontSize: 'clamp(20px, 5vw, 28px)' }} />
</div>
<span className="nav-text" style={navTextStyle}>
{text}
</span>
</div>
);
})}
</div>
);
};
export default React.memo(NavigationGrid);
@@ -0,0 +1,60 @@
import React from 'react';
import { Typography } from 'antd';
import { designTokens } from '../../config/theme';
const { Text } = Typography;
const PowerGauge = ({ value, maxValue }) => {
const percentage = Math.min((value / maxValue) * 100, 100);
const getColor = () => {
if (percentage >= 80) return designTokens.colors.error.main;
if (percentage >= 60) return designTokens.colors.warning.main;
return designTokens.colors.success.main;
};
return (
<div style={{ marginTop: '12px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '8px' }}>
<Text style={{ fontSize: '0.85rem', color: designTokens.colors.text.secondary }}>
功率使用率
</Text>
<Text style={{ fontSize: '0.85rem', fontWeight: '600', color: getColor() }}>
{percentage.toFixed(1)}%
</Text>
</div>
<div
style={{
height: '8px',
borderRadius: '4px',
background: '#f0f0f0',
overflow: 'hidden',
}}
>
<div
style={{
height: '100%',
borderRadius: '4px',
background: `linear-gradient(90deg, ${getColor()}, ${getColor()}80)`,
width: `${percentage}%`,
transition: 'width 0.8s ease-out',
boxShadow: `0 0 8px ${getColor()}40`,
}}
/>
</div>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
marginTop: '4px',
fontSize: '0.75rem',
color: designTokens.colors.text.tertiary,
}}
>
<span>{value}W</span>
<span>{maxValue}W</span>
</div>
</div>
);
};
export default React.memo(PowerGauge);
@@ -0,0 +1,89 @@
import React from 'react';
import { Card, Typography } from 'antd';
import { LineChartOutlined, SafetyOutlined, ThunderboltOutlined } from '@ant-design/icons';
import { designTokens } from '../../config/theme';
const { Text } = Typography;
const quickStatItemStyle = {
background: 'linear-gradient(135deg, #fff 0%, #fafafa 100%)',
borderRadius: designTokens.borderRadius.medium,
padding: '20px',
display: 'flex',
alignItems: 'center',
gap: '16px',
border: '1px solid #f0f0f0',
boxShadow: designTokens.shadows.small,
};
const QuickStats = ({ onlineRate, powerUsage }) => {
const quickStats = [
{
icon: LineChartOutlined,
label: '在线率',
value: `${onlineRate}%`,
color: designTokens.colors.success.main,
},
{
icon: SafetyOutlined,
label: '安全等级',
value: 'A级',
color: designTokens.colors.primary.main,
},
{
icon: ThunderboltOutlined,
label: '功率使用',
value: `${powerUsage}W`,
color: designTokens.colors.warning.main,
},
];
return (
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))',
gap: '16px',
marginBottom: '0',
animation: 'fadeInUp 0.6s ease-out 0.5s backwards',
}}
>
{quickStats.map((stat, index) => (
<div key={index} style={quickStatItemStyle}>
<div
style={{
width: '48px',
height: '48px',
borderRadius: designTokens.borderRadius.medium,
background: `linear-gradient(135deg, ${stat.color}20 0%, ${stat.color}10 100%)`,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '24px',
color: stat.color,
boxShadow: `0 4px 12px ${stat.color}20`,
}}
>
<stat.icon />
</div>
<div>
<Text style={{ color: designTokens.colors.text.secondary, fontSize: '0.85rem' }}>
{stat.label}
</Text>
<div
style={{
fontSize: '1.2rem',
fontWeight: '700',
color: designTokens.colors.text.primary,
}}
>
{stat.value}
</div>
</div>
</div>
))}
</div>
);
};
export default React.memo(QuickStats);
@@ -0,0 +1,176 @@
import React from 'react';
import { Card, Col, Tag, Spin } from 'antd';
import { ArrowUpOutlined, ArrowDownOutlined } from '@ant-design/icons';
import { designTokens } from '../../config/theme';
import AnimatedCounter from './AnimatedCounter';
const createStatCardStyle = (color) => ({
borderRadius: designTokens.borderRadius.large,
border: 'none',
boxShadow: designTokens.shadows.medium,
background: '#fff',
transition: `all ${designTokens.transitions.normal}`,
overflow: 'hidden',
cursor: 'pointer',
height: '100%',
animation: 'fadeInUp 0.6s ease-out backwards',
borderLeft: `4px solid ${color}`,
});
const createStatIconContainer = (color) => ({
width: 'clamp(40px, 8vw, 64px)',
height: 'clamp(40px, 8vw, 64px)',
borderRadius: designTokens.borderRadius.medium,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 'clamp(20px, 4vw, 32px)',
transition: `all ${designTokens.transitions.normal}`,
flexShrink: 0,
background: `linear-gradient(135deg, ${color}20 0%, ${color}10 100%)`,
});
const StatCard = ({
config,
stats,
loading,
animatedKey,
hoveredCard,
onHover,
}) => {
const {
icon: Icon,
color,
statKey,
title,
trend,
tagColor,
customStatus,
xs,
sm,
lg,
xl,
delay,
} = config;
const colProps = { xs, sm, lg, xl };
const cardStyle = {
...createStatCardStyle(color),
...(hoveredCard === statKey
? { transform: 'translateY(-6px)', boxShadow: designTokens.shadows.xl }
: {}),
animationDelay: `${delay * 0.1}s`,
};
return (
<Col key={statKey} {...colProps}>
<Card
style={cardStyle}
onMouseEnter={() => onHover(statKey)}
onMouseLeave={() => onHover(null)}
styles={{ body: { padding: 'clamp(16px, 3vw, 24px)' } }}
>
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
gap: '8px',
}}
>
<span
style={{
fontSize: 'clamp(0.75rem, 2vw, 0.9rem)',
fontWeight: '600',
color: designTokens.colors.text.secondary,
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
flex: 1,
}}
>
{title}
</span>
<div style={createStatIconContainer(color)}>
<Icon style={{ color }} />
</div>
</div>
<div
className="stat-value"
style={{
fontSize: 'clamp(1.6rem, 4vw, 2.2rem)',
fontWeight: '700',
color: color,
lineHeight: 1,
whiteSpace: 'nowrap',
}}
>
{loading ? (
<Spin size="small" />
) : (
<AnimatedCounter key={`${animatedKey}-${statKey}`} value={stats[statKey]} />
)}
</div>
{customStatus ? (
<div
style={{
display: 'flex',
alignItems: 'center',
fontSize: 'clamp(0.7rem, 1.8vw, 0.85rem)',
color: designTokens.colors.success.main,
whiteSpace: 'nowrap',
}}
>
<span
style={{
width: '6px',
height: '6px',
background: designTokens.colors.success.main,
borderRadius: '50%',
marginRight: '6px',
boxShadow: '0 0 6px rgba(82, 196, 26, 0.5)',
flexShrink: 0,
}}
/>
<span>{statKey === 'totalRacks' ? '正常运行中' : '全部在线'}</span>
</div>
) : (
<div
style={{
display: 'flex',
alignItems: 'center',
fontSize: 'clamp(0.7rem, 1.8vw, 0.875rem)',
fontWeight: '500',
color: trend > 0 ? designTokens.colors.success.main : designTokens.colors.error.main,
flexWrap: 'wrap',
gap: '4px',
}}
>
{trend > 0 ? (
<ArrowUpOutlined style={{ fontSize: '0.75rem' }} />
) : (
<ArrowDownOutlined style={{ fontSize: '0.75rem' }} />
)}
<span>{Math.abs(trend)}%</span>
<Tag
color={tagColor}
style={{
fontSize: 'clamp(0.6rem, 1.5vw, 0.75rem)',
borderRadius: '4px',
margin: 0,
padding: '0 4px',
lineHeight: '1.4',
}}
>
环比
</Tag>
</div>
)}
</div>
</Card>
</Col>
);
};
export default React.memo(StatCard);
@@ -0,0 +1,57 @@
import React from 'react';
import { Typography } from 'antd';
import { designTokens } from '../../config/theme';
const { Text } = Typography;
const StatusLegend = () => {
const legends = [
{ color: designTokens.colors.success.main, label: '运行中', percent: 60 },
{ color: designTokens.colors.warning.main, label: '维护中', percent: 20 },
{ color: designTokens.colors.error.main, label: '故障', percent: 10 },
{ color: designTokens.colors.primary.main, label: '离线', percent: 10 },
];
return (
<div style={{ marginTop: '16px' }}>
{legends.map((item, index) => (
<div
key={index}
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '8px 0',
borderBottom: index < legends.length - 1 ? '1px solid #f5f5f5' : 'none',
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<div
style={{
width: '12px',
height: '12px',
borderRadius: '3px',
background: item.color,
boxShadow: `0 0 6px ${item.color}40`,
}}
/>
<Text style={{ fontSize: '0.85rem', color: designTokens.colors.text.secondary }}>
{item.label}
</Text>
</div>
<Text
style={{
fontSize: '0.85rem',
fontWeight: '600',
color: designTokens.colors.text.primary,
}}
>
{item.percent}%
</Text>
</div>
))}
</div>
);
};
export default React.memo(StatusLegend);
@@ -0,0 +1,69 @@
import React from 'react';
import { Button } from 'antd';
import { ReloadOutlined } from '@ant-design/icons';
import { designTokens } from '../../config/theme';
const systemInfoStyle = {
background: 'linear-gradient(135deg, #f0f7ff 0%, #e6f7ff 100%)',
borderRadius: designTokens.borderRadius.medium,
padding: '20px',
border: '1px solid #91d5ff',
};
const SystemInfo = ({ onRefresh, isRefreshing }) => {
return (
<div style={{ animation: 'fadeInUp 0.6s ease-out 0.5s backwards' }}>
<div style={systemInfoStyle}>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
flexWrap: 'wrap',
gap: '16px',
}}
>
<div>
<p
style={{
margin: '0',
fontSize: '0.9rem',
color: designTokens.colors.text.primary,
fontWeight: '600',
}}
>
<strong>系统版本</strong> v1.0.0
</p>
<p
style={{
margin: '4px 0 0 0',
fontSize: '0.85rem',
color: designTokens.colors.text.secondary,
}}
>
<strong>最后更新</strong>
{new Date().toLocaleDateString()}
</p>
</div>
<Button
type="primary"
icon={<ReloadOutlined spin={isRefreshing} />}
size="small"
onClick={onRefresh}
loading={isRefreshing}
style={{
background: designTokens.colors.primary.gradient,
border: 'none',
borderRadius: '8px',
boxShadow: '0 4px 12px rgba(24, 144, 255, 0.3)',
}}
>
刷新数据
</Button>
</div>
</div>
</div>
);
};
export default React.memo(SystemInfo);
@@ -0,0 +1,9 @@
export { default as AnimatedCounter } from './AnimatedCounter';
export { default as CircularProgress } from './CircularProgress';
export { default as PowerGauge } from './PowerGauge';
export { default as DeviceTrendChart } from './DeviceTrendChart';
export { default as StatusLegend } from './StatusLegend';
export { default as StatCard } from './StatCard';
export { default as NavigationGrid } from './NavigationGrid';
export { default as QuickStats } from './QuickStats';
export { default as SystemInfo } from './SystemInfo';
@@ -0,0 +1,110 @@
import React from 'react';
import { Modal, Form, Select, Button } from 'antd';
import { ReloadOutlined } from '@ant-design/icons';
import { designTokens } from '../../config/theme';
const { Option } = Select;
const modalHeaderStyle = {
display: 'flex',
alignItems: 'center',
gap: '8px',
fontSize: '18px',
fontWeight: 600,
};
const secondaryActionStyle = {
height: '40px',
borderRadius: designTokens.borderRadius.small,
border: `1px solid ${designTokens.colors.border.light}`,
fontWeight: '500',
};
const BatchStatusModal = ({
visible,
selectedCount,
loading,
onSubmit,
onCancel,
}) => {
const [form] = Form.useForm();
const handleSubmit = async () => {
try {
const values = await form.validateFields();
await onSubmit(values.status);
} catch (error) {
if (!error.errorFields) {
console.error('批量状态变更失败:', error);
}
}
};
return (
<Modal
title={
<div style={{ ...modalHeaderStyle, paddingRight: '32px' }}>
<ReloadOutlined style={{ color: '#52c41a' }} />
批量状态变更
</div>
}
open={visible}
onCancel={onCancel}
footer={[
<Button key="cancel" onClick={onCancel} style={secondaryActionStyle}>
取消
</Button>,
<Button
key="submit"
type="primary"
loading={loading}
onClick={handleSubmit}
style={{
height: '40px',
borderRadius: designTokens.borderRadius.small,
background: designTokens.colors.primary.gradient,
border: 'none',
color: '#ffffff',
boxShadow: designTokens.shadows.small,
fontWeight: '500',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
确定
</Button>,
]}
destroyOnHidden
styles={{
header: {
borderBottom: '1px solid #f0f0f0',
padding: '16px 24px',
position: 'relative',
},
body: { padding: '24px' },
}}
>
<Form form={form} layout="vertical">
<Form.Item
name="status"
label="选择新状态"
rules={[{ required: true, message: '请选择设备状态' }]}
>
<Select placeholder="请选择设备状态" style={{ width: '100%' }}>
<Option value="running">运行中</Option>
<Option value="maintenance">维护中</Option>
<Option value="offline">离线</Option>
<Option value="fault">故障</Option>
</Select>
</Form.Item>
<div style={{ color: '#666', fontSize: '13px' }}>
已选择{' '}
<span style={{ color: '#1890ff', fontWeight: 600 }}>{selectedCount}</span> 个设备
</div>
</Form>
</Modal>
);
};
export default React.memo(BatchStatusModal);
@@ -0,0 +1,256 @@
import React from 'react';
import { Modal, Button, Card, Row, Col, Tag } from 'antd';
import { AppstoreOutlined } from '@ant-design/icons';
import { designTokens } from '../../config/theme';
import { getStatusConfig, getTypeLabel, getDeviceTypeIcon } from '../../utils/deviceUtils.jsx';
const modalHeaderStyle = {
display: 'flex',
alignItems: 'center',
gap: '8px',
fontSize: '18px',
fontWeight: 600,
};
const secondaryActionStyle = {
height: '40px',
borderRadius: designTokens.borderRadius.small,
border: `1px solid ${designTokens.colors.border.light}`,
fontWeight: '500',
};
const DeviceDetailModal = ({
visible,
device,
deviceFields,
onClose,
onEdit,
onViewTickets,
onCreateTicket,
}) => {
if (!device) return null;
return (
<Modal
title={
<div style={{ ...modalHeaderStyle, paddingRight: '32px' }}>
<AppstoreOutlined style={{ color: '#667eea' }} />
设备详情
</div>
}
open={visible}
onCancel={onClose}
footer={[
<Button key="close" onClick={onClose} style={secondaryActionStyle}>
关闭
</Button>,
<Button key="viewTickets" onClick={() => onViewTickets(device)} style={secondaryActionStyle}>
查看工单
</Button>,
<Button key="createTicket" onClick={() => onCreateTicket(device)} style={secondaryActionStyle}>
创建工单
</Button>,
<Button
key="edit"
type="primary"
onClick={() => {
onClose();
onEdit(device);
}}
style={{
height: '40px',
borderRadius: designTokens.borderRadius.small,
background: designTokens.colors.primary.gradient,
border: 'none',
color: '#ffffff',
boxShadow: designTokens.shadows.small,
fontWeight: '500',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
编辑
</Button>,
]}
width={700}
destroyOnHidden
styles={{
header: {
borderBottom: '1px solid #f0f0f0',
padding: '16px 24px',
position: 'relative',
},
body: { padding: '0', overflow: 'auto' },
}}
>
<div>
<div
style={{
padding: '24px',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
color: '#fff',
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
<div
style={{
width: '64px',
height: '64px',
borderRadius: '12px',
backgroundColor: 'rgba(255,255,255,0.2)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
{getDeviceTypeIcon(device.type)}
</div>
<div style={{ flex: 1 }}>
<div style={{ fontSize: '24px', fontWeight: 600, marginBottom: '8px' }}>
{device.name}
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '16px', opacity: 0.9 }}>
<span>{getTypeLabel(device.type)}</span>
<span>|</span>
<span>{device.deviceId}</span>
<span>|</span>
<Tag
color={device.status ? getStatusConfig(device.status).badgeColor : 'default'}
style={{ margin: 0 }}
>
{device.status ? getStatusConfig(device.status).text : '-'}
</Tag>
</div>
</div>
</div>
</div>
<div style={{ padding: '20px 24px' }}>
<Card
size="small"
title={<span style={{ fontWeight: 600 }}>基本信息</span>}
style={{ marginBottom: '16px', borderRadius: '8px' }}
>
<Row gutter={[24, 16]}>
<Col span={8}>
<div style={{ color: '#666', fontSize: '12px', marginBottom: '4px' }}>设备型号</div>
<div style={{ fontWeight: 500 }}>{device.model || '-'}</div>
</Col>
<Col span={8}>
<div style={{ color: '#666', fontSize: '12px', marginBottom: '4px' }}>序列号</div>
<div style={{ fontWeight: 500 }}>{device.serialNumber || '-'}</div>
</Col>
<Col span={8}>
<div style={{ color: '#666', fontSize: '12px', marginBottom: '4px' }}>IP地址</div>
<div style={{ fontWeight: 500 }}>{device.ipAddress || '-'}</div>
</Col>
<Col span={8}>
<div style={{ color: '#666', fontSize: '12px', marginBottom: '4px' }}>所在机房</div>
<div style={{ fontWeight: 500 }}>{device.Rack?.Room?.name || '-'}</div>
</Col>
<Col span={8}>
<div style={{ color: '#666', fontSize: '12px', marginBottom: '4px' }}>所在机柜</div>
<div style={{ fontWeight: 500 }}>{device.Rack?.name || '-'}</div>
</Col>
<Col span={8}>
<div style={{ color: '#666', fontSize: '12px', marginBottom: '4px' }}>位置(U)</div>
<div style={{ fontWeight: 500 }}>U{device.position || '-'}</div>
</Col>
<Col span={8}>
<div style={{ color: '#666', fontSize: '12px', marginBottom: '4px' }}>高度</div>
<div style={{ fontWeight: 500 }}>{device.height ? `${device.height}U` : '-'}</div>
</Col>
<Col span={8}>
<div style={{ color: '#666', fontSize: '12px', marginBottom: '4px' }}>功率</div>
<div style={{ fontWeight: 500 }}>{device.power ? `${device.power}W` : '-'}</div>
</Col>
<Col span={8}>
<div style={{ color: '#666', fontSize: '12px', marginBottom: '4px' }}>状态</div>
<div
style={{
fontWeight: 500,
color: device.status ? getStatusConfig(device.status).color : '#666',
}}
>
{device.status ? getStatusConfig(device.status).text : '-'}
</div>
</Col>
</Row>
</Card>
<Card
size="small"
title={<span style={{ fontWeight: 600 }}>维保信息</span>}
style={{ marginBottom: '16px', borderRadius: '8px' }}
>
<Row gutter={[24, 16]}>
<Col span={12}>
<div style={{ color: '#666', fontSize: '12px', marginBottom: '4px' }}>购买日期</div>
<div style={{ fontWeight: 500 }}>
{device.purchaseDate
? new Date(device.purchaseDate).toLocaleDateString('zh-CN')
: '-'}
</div>
</Col>
<Col span={12}>
<div style={{ color: '#666', fontSize: '12px', marginBottom: '4px' }}>保修到期</div>
<div
style={{
fontWeight:
device.warrantyExpiry && new Date(device.warrantyExpiry) < new Date()
? 600
: 500,
color:
device.warrantyExpiry && new Date(device.warrantyExpiry) < new Date()
? '#d93025'
: '#333',
}}
>
{device.warrantyExpiry
? new Date(device.warrantyExpiry).toLocaleDateString('zh-CN')
: '-'}
</div>
</Col>
</Row>
</Card>
{device.description && (
<Card
size="small"
title={<span style={{ fontWeight: 600 }}>描述</span>}
style={{ marginBottom: '16px', borderRadius: '8px' }}
>
<div style={{ whiteSpace: 'pre-wrap', lineHeight: 1.6 }}>{device.description}</div>
</Card>
)}
{device.customFields && Object.keys(device.customFields).length > 0 && (
<Card
size="small"
title={<span style={{ fontWeight: 600 }}>自定义字段</span>}
style={{ borderRadius: '8px' }}
>
<Row gutter={[24, 16]}>
{Object.entries(device.customFields).map(([key, value]) => {
const fieldConfig = deviceFields.find((f) => f.fieldName === key);
const displayName = fieldConfig?.displayName || key;
return (
<Col span={8} key={key}>
<div style={{ color: '#666', fontSize: '12px', marginBottom: '4px' }}>
{displayName}
</div>
<div style={{ fontWeight: 500 }}>{String(value)}</div>
</Col>
);
})}
</Row>
</Card>
)}
</div>
</div>
</Modal>
);
};
export default React.memo(DeviceDetailModal);
@@ -0,0 +1,361 @@
import React, { useState, useEffect } from 'react';
import { Modal, Form, Input, Select, InputNumber, DatePicker, Switch, Row, Col, Button, Space } from 'antd';
import { PlusOutlined, EditOutlined, DatabaseOutlined } from '@ant-design/icons';
import dayjs from 'dayjs';
import { designTokens } from '../../config/theme';
import { getFormInitialValues, prepareDeviceFormData } from '../../utils/deviceUtils.jsx';
const { Option } = Select;
const modalHeaderStyle = {
display: 'flex',
alignItems: 'center',
gap: '8px',
fontSize: '18px',
fontWeight: 600,
};
const inputStyle = {
borderRadius: '8px',
transition: 'all 0.3s ease',
};
const DeviceFormModal = ({
visible,
editingDevice,
deviceFields,
racks,
rooms,
onCancel,
onSubmit,
}) => {
const [form] = Form.useForm();
const [selectedRoomId, setSelectedRoomId] = useState(null);
useEffect(() => {
if (visible) {
if (editingDevice) {
const initialValues = getFormInitialValues(editingDevice, racks);
if (initialValues.purchaseDate) {
initialValues.purchaseDate = dayjs(initialValues.purchaseDate);
}
if (initialValues.warrantyExpiry) {
initialValues.warrantyExpiry = dayjs(initialValues.warrantyExpiry);
}
form.setFieldsValue(initialValues);
if (editingDevice.rackId) {
const rack = racks.find((r) => r.rackId === editingDevice.rackId);
if (rack) {
setSelectedRoomId(rack.roomId);
}
}
} else {
form.resetFields();
setSelectedRoomId(null);
}
}
}, [visible, editingDevice, racks, form]);
const handleSubmit = (values) => {
const deviceData = prepareDeviceFormData(values, !!editingDevice);
onSubmit(deviceData);
};
const handleRoomChange = (value) => {
setSelectedRoomId(value);
form.setFieldValue('rackId', undefined);
};
const renderFieldControl = (field) => {
switch (field.fieldType) {
case 'number':
return (
<InputNumber
placeholder={`请输入${field.displayName}`}
min={0}
style={{ width: '100%', ...inputStyle }}
className="form-input-enhanced"
/>
);
case 'boolean':
return <Switch />;
case 'date':
return (
<DatePicker
style={{ width: '100%', ...inputStyle }}
placeholder={`请选择${field.displayName}`}
className="form-input-enhanced"
/>
);
case 'textarea':
return (
<Input.TextArea
placeholder={`请输入${field.displayName}`}
rows={3}
style={inputStyle}
className="form-input-enhanced"
/>
);
case 'select':
return (
<Select
placeholder={`请选择${field.displayName}`}
style={inputStyle}
className="form-input-enhanced"
>
{field.options &&
field.options.map((option) => (
<Option key={option.value} value={option.value}>
{option.label}
</Option>
))}
</Select>
);
default:
return (
<Input
placeholder={`请输入${field.displayName}`}
style={inputStyle}
className="form-input-enhanced"
/>
);
}
};
const filteredFields = deviceFields.filter(
(field) => field.fieldName !== 'deviceId' && field.fieldName !== 'rackId'
);
const formItems = [];
filteredFields.forEach((field) => {
if (field.fieldName === 'serialNumber') {
formItems.push(
<React.Fragment key={field.fieldName}>
<Col span={12} key={`${field.fieldName}-col`}>
<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}` }] : []
}
>
{renderFieldControl(field)}
</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={handleRoomChange}
>
{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') {
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}` }] : []
}
>
{renderFieldControl(field)}
</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}` }] : []
}
>
{renderFieldControl(field)}
</Form.Item>
</Col>
);
}
});
return (
<Modal
title={
<div style={{ ...modalHeaderStyle, paddingRight: '32px' }}>
{editingDevice ? (
<EditOutlined style={{ color: '#667eea' }} />
) : (
<PlusOutlined style={{ color: '#667eea' }} />
)}
{editingDevice ? '编辑设备' : '添加设备'}
</div>
}
open={visible}
onCancel={onCancel}
footer={null}
width={700}
style={{ borderRadius: '16px' }}
styles={{
header: {
borderBottom: '1px solid #f0f0f0',
padding: '16px 24px',
position: 'relative',
},
body: { padding: '24px' },
}}
className="device-modal"
>
<Form form={form} layout="vertical" onFinish={handleSubmit}>
<Row gutter={16}>{formItems}</Row>
<div
style={{
display: 'flex',
justifyContent: 'flex-end',
gap: '12px',
marginTop: '32px',
paddingTop: '24px',
borderTop: '1px solid #f0f0f0',
}}
>
<Button
onClick={onCancel}
style={{
height: '40px',
borderRadius: '8px',
padding: '0 24px',
fontWeight: '500',
transition: 'all 0.3s ease',
}}
>
取消
</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>
</Modal>
);
};
export default React.memo(DeviceFormModal);
@@ -0,0 +1,179 @@
import React, { useState, useMemo } from 'react';
import { Modal, Form, Select, Checkbox, Button, message } from 'antd';
import { ExportOutlined } from '@ant-design/icons';
import { designTokens } from '../../config/theme';
const { Option } = Select;
const modalHeaderStyle = {
display: 'flex',
alignItems: 'center',
gap: '8px',
fontSize: '18px',
fontWeight: 600,
};
const ExportModal = ({
visible,
deviceFields,
selectedDevices,
currentPageDevices,
allDevices,
onExport,
onCancel,
}) => {
const [exportFormat, setExportFormat] = useState('csv');
const [exportScope, setExportScope] = useState('selected');
const [exportFields, setExportFields] = useState([]);
const [exportLoading, setExportLoading] = useState(false);
const visibleFields = useMemo(() => {
return deviceFields.filter((f) => f.visible && f.fieldName !== 'rackId');
}, [deviceFields]);
React.useEffect(() => {
if (visible) {
setExportFields(visibleFields.map((f) => f.fieldName));
}
}, [visible, visibleFields]);
const handleExport = async () => {
if (exportFields.length === 0) {
message.warning('请至少选择一个导出字段');
return;
}
setExportLoading(true);
try {
await onExport({
format: exportFormat,
scope: exportScope,
fields: exportFields,
});
onCancel();
} finally {
setExportLoading(false);
}
};
const getScopeLabel = () => {
switch (exportScope) {
case 'selected':
return `选择的行 (${selectedDevices.length} 个)`;
case 'currentPage':
return `当前页 (${currentPageDevices.length} 个)`;
case 'all':
return `全部设备 (${allDevices.length} 个)`;
default:
return '';
}
};
return (
<Modal
title={
<div style={{ ...modalHeaderStyle, paddingRight: '32px' }}>
<ExportOutlined style={{ color: '#fa8c16' }} />
导出设备数据
</div>
}
open={visible}
onCancel={onCancel}
footer={[
<Button
key="cancel"
onClick={onCancel}
style={{
height: '40px',
borderRadius: designTokens.borderRadius.small,
border: `1px solid ${designTokens.colors.border.light}`,
}}
>
取消
</Button>,
<Button
key="submit"
type="primary"
loading={exportLoading}
onClick={handleExport}
style={{
height: '40px',
borderRadius: designTokens.borderRadius.small,
background: designTokens.colors.primary.gradient,
border: 'none',
color: '#ffffff',
boxShadow: designTokens.shadows.small,
fontWeight: '500',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
导出
</Button>,
]}
destroyOnHidden
styles={{
header: {
borderBottom: '1px solid #f0f0f0',
padding: '16px 24px',
position: 'relative',
},
body: { padding: '24px' },
}}
width={600}
>
<Form layout="vertical">
<Form.Item label="导出格式">
<Select value={exportFormat} onChange={setExportFormat} style={{ width: '100%' }}>
<Option value="csv">CSV 格式</Option>
<Option value="json">JSON 格式</Option>
</Select>
</Form.Item>
<Form.Item label="导出范围">
<Select value={exportScope} onChange={setExportScope} style={{ width: '100%' }}>
<Option value="selected">{getScopeLabel()}</Option>
<Option value="currentPage">当前页 ({currentPageDevices.length} )</Option>
<Option value="all">全部设备 ({allDevices.length} )</Option>
</Select>
</Form.Item>
<Form.Item label="选择导出字段">
<div
style={{
maxHeight: '300px',
overflow: 'auto',
border: '1px solid #f0f0f0',
borderRadius: '8px',
padding: '12px',
}}
>
{visibleFields.map((field) => (
<div key={field.fieldName} style={{ marginBottom: '8px' }}>
<Checkbox
checked={exportFields.includes(field.fieldName)}
onChange={(e) => {
if (e.target.checked) {
setExportFields([...exportFields, field.fieldName]);
} else {
setExportFields(exportFields.filter((f) => f !== field.fieldName));
}
}}
>
{field.displayName}
</Checkbox>
</div>
))}
</div>
</Form.Item>
<div style={{ color: '#666', fontSize: '13px' }}>
已选择{' '}
<span style={{ color: '#1890ff', fontWeight: 600 }}>{selectedDevices.length}</span> 个设备
将导出{' '}
<span style={{ color: '#52c41a', fontWeight: 600 }}>{exportFields.length}</span> 个字段
</div>
</Form>
</Modal>
);
};
export default React.memo(ExportModal);
@@ -0,0 +1,150 @@
import React from 'react';
import { Modal, Form, Switch, Button, Space, message } from 'antd';
import { SettingOutlined } from '@ant-design/icons';
import { designTokens } from '../../config/theme';
const modalHeaderStyle = {
display: 'flex',
alignItems: 'center',
gap: '8px',
fontSize: '18px',
fontWeight: 600,
};
const secondaryActionStyle = {
height: '40px',
borderRadius: designTokens.borderRadius.small,
border: `1px solid ${designTokens.colors.border.light}`,
fontWeight: '500',
};
const FieldConfigModal = ({
visible,
deviceFields,
defaultDeviceFields,
onSave,
onReset,
onCancel,
}) => {
const [form] = Form.useForm();
const getInitialValues = () => {
return deviceFields.reduce(
(acc, field) => ({
...acc,
[`visible_${field.fieldName}`]: field.visible,
[`required_${field.fieldName}`]: field.required,
}),
{}
);
};
const handleSubmit = async (values) => {
const updatedFields = deviceFields.map((field) => ({
fieldId: field.fieldId,
fieldName: field.fieldName,
displayName: field.displayName,
visible: values[`visible_${field.fieldName}`] ?? field.visible,
required: values[`required_${field.fieldName}`] ?? field.required,
}));
await onSave(updatedFields);
};
const handleReset = () => {
onReset(defaultDeviceFields);
message.success('字段配置已重置为默认值');
};
const filteredFields = deviceFields.filter((field) => field.fieldName !== 'deviceId');
return (
<Modal
title={
<div style={{ ...modalHeaderStyle, paddingRight: '32px' }}>
<SettingOutlined style={{ color: '#667eea' }} />
字段配置
</div>
}
open={visible}
onCancel={onCancel}
footer={null}
width={600}
styles={{
header: {
borderBottom: '1px solid #f0f0f0',
padding: '16px 24px',
position: 'relative',
},
body: { padding: '24px' },
}}
>
<Form
form={form}
layout="vertical"
onFinish={handleSubmit}
initialValues={getInitialValues()}
>
<div style={{ maxHeight: 400, overflowY: 'auto' }}>
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr style={{ borderBottom: '1px solid #f0f0f0' }}>
<th style={{ padding: '8px', textAlign: 'left', width: '40%' }}>字段名称</th>
<th style={{ padding: '8px', textAlign: 'center', width: '30%' }}>可见</th>
<th style={{ padding: '8px', textAlign: 'center', width: '30%' }}>必填</th>
</tr>
</thead>
<tbody>
{filteredFields.map((field) => (
<tr key={field.fieldName} style={{ borderBottom: '1px solid #f0f0f0' }}>
<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>
<Form.Item style={{ textAlign: 'right', marginTop: '20px' }}>
<Space>
<Button onClick={onCancel} style={secondaryActionStyle}>
取消
</Button>
<Button onClick={handleReset} style={secondaryActionStyle}>
重置默认
</Button>
<Button
type="primary"
htmlType="submit"
style={{
height: '40px',
borderRadius: designTokens.borderRadius.small,
background: designTokens.colors.primary.gradient,
border: 'none',
color: '#ffffff',
boxShadow: designTokens.shadows.small,
fontWeight: '500',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
保存
</Button>
</Space>
</Form.Item>
</Form>
</Modal>
);
};
export default React.memo(FieldConfigModal);
@@ -0,0 +1,385 @@
import React, { useState } from 'react';
import { Modal, Upload, Button, Progress, message } from 'antd';
import { UploadOutlined, DownloadOutlined } from '@ant-design/icons';
import { designTokens } from '../../config/theme';
const modalHeaderStyle = {
display: 'flex',
alignItems: 'center',
gap: '8px',
fontSize: '18px',
fontWeight: 600,
};
const ImportModal = ({
visible,
deviceFields,
onImport,
onCancel,
}) => {
const [isImporting, setIsImporting] = useState(false);
const [importProgress, setImportProgress] = useState(0);
const [importPhase, setImportPhase] = useState('');
const [importResult, setImportResult] = useState(null);
const handleImport = async (file) => {
try {
setIsImporting(true);
setImportProgress(0);
setImportPhase('正在上传文件...');
setImportResult(null);
await onImport(file, {
onProgress: (progress, phase) => {
setImportProgress(progress);
setImportPhase(phase);
},
onSuccess: (result) => {
setImportResult(result);
setImportProgress(100);
setImportPhase('导入完成');
setIsImporting(false);
},
onError: (error) => {
setImportResult({
success: false,
statistics: {
total: 0,
success: 0,
failed: 1,
errors: [{ row: 0, error: error.message || '导入失败' }],
},
});
setIsImporting(false);
},
});
} catch (error) {
setIsImporting(false);
setImportProgress(0);
message.error('导入失败');
}
return false;
};
const handleClose = () => {
setImportProgress(0);
setImportPhase('');
setImportResult(null);
setIsImporting(false);
onCancel();
};
const requiredFields = deviceFields.filter((f) => f.visible && f.required);
const optionalFields = deviceFields.filter((f) => f.visible && !f.required);
return (
<Modal
title={
<div style={{ ...modalHeaderStyle, paddingRight: '32px' }}>
<UploadOutlined style={{ color: '#667eea' }} />
导入设备
</div>
}
open={visible}
onCancel={handleClose}
footer={null}
width={650}
destroyOnHidden
styles={{
header: {
borderBottom: '1px solid #f0f0f0',
padding: '16px 24px',
position: 'relative',
},
body: { padding: '24px' },
}}
>
{!isImporting && !importResult ? (
<div>
<p style={{ color: '#666', marginBottom: '8px' }}>请上传CSV格式的设备数据文件</p>
<p style={{ color: '#999', fontSize: '12px', marginBottom: '20px' }}>
支持的编码格式GBK
</p>
<div
style={{
marginBottom: '20px',
padding: '16px',
background: 'linear-gradient(180deg, #fafafa 0%, #ffffff 100%)',
borderRadius: '12px',
border: '1px solid #f0f0f0',
}}
>
<p style={{ fontWeight: '600', marginBottom: '8px', color: '#333' }}>
CSV文件格式要求
</p>
<div style={{ maxHeight: '200px', overflowY: 'auto' }}>
{requiredFields.length > 0 && (
<div style={{ marginBottom: '8px' }}>
<span style={{ color: '#d93025', fontWeight: '500' }}>必填字段</span>
<span style={{ color: '#666', fontSize: '13px' }}>
{requiredFields.map((f) => f.displayName).join('、')}
</span>
</div>
)}
{optionalFields.length > 0 && (
<div style={{ marginBottom: '8px' }}>
<span style={{ color: '#666', fontWeight: '500' }}>可选字段</span>
<span style={{ color: '#666', fontSize: '13px' }}>
{optionalFields.map((f) => f.displayName).join('、')}
</span>
</div>
)}
<ul
style={{
paddingLeft: '20px',
marginBottom: '10px',
color: '#666',
fontSize: '13px',
marginTop: '12px',
}}
>
<li>
设备类型server(服务器)switch(交换机)router(路由器)storage(存储设备)other(其他)
</li>
<li>状态值running(运行中)maintenance(维护中)offline(离线)fault(故障)</li>
<li>日期格式YYYY-MM-DD (例如2023-01-01)</li>
</ul>
</div>
</div>
<div style={{ marginBottom: '20px' }}>
<a href="/api/devices/import-template" download="设备导入模板.csv">
<Button
icon={<DownloadOutlined />}
style={{
height: '36px',
borderRadius: designTokens.borderRadius.small,
border: `1px solid ${designTokens.colors.border.light}`,
}}
>
下载导入模板
</Button>
</a>
<span style={{ color: '#999', fontSize: '12px', marginLeft: '10px' }}>
包含示例数据的CSV模板文件根据当前字段配置生成
</span>
</div>
<Upload
name="csvFile"
accept=".csv"
showUploadList={false}
beforeUpload={handleImport}
maxCount={1}
>
<Button
type="primary"
icon={<UploadOutlined />}
block
style={{
height: '40px',
borderRadius: designTokens.borderRadius.small,
background: designTokens.colors.primary.gradient,
border: 'none',
color: '#ffffff',
boxShadow: designTokens.shadows.small,
fontWeight: '500',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
选择CSV文件
</Button>
</Upload>
</div>
) : isImporting ? (
<div>
<div style={{ display: 'flex', alignItems: 'center', marginBottom: '16px' }}>
<div
style={{
width: '48px',
height: '48px',
borderRadius: '50%',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
marginRight: '16px',
color: '#fff',
fontSize: '20px',
}}
>
<UploadOutlined spin />
</div>
<div>
<p
style={{
margin: '0 0 4px 0',
fontWeight: '600',
color: '#333',
fontSize: '16px',
}}
>
正在导入设备数据
</p>
<p style={{ margin: 0, color: '#667eea', fontSize: '14px' }}>{importPhase}</p>
</div>
</div>
<Progress
percent={importProgress}
status="active"
strokeColor={{ '0%': '#667eea', '100%': '#764ba2' }}
format={() => `${importProgress}%`}
/>
</div>
) : importResult?.statistics ? (
<div>
<p style={{ marginBottom: '10px', fontWeight: '600' }}>导入完成</p>
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)',
gap: '12px',
marginBottom: '16px',
}}
>
<div
style={{
padding: '12px',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
borderRadius: '8px',
color: '#fff',
textAlign: 'center',
}}
>
<div style={{ fontSize: '24px', fontWeight: '700' }}>
{importResult.statistics.total || 0}
</div>
<div style={{ fontSize: '12px', opacity: 0.9 }}>总记录数</div>
</div>
<div
style={{
padding: '12px',
background: 'linear-gradient(135deg, #52c41a 0%, #389e0d 100%)',
borderRadius: '8px',
color: '#fff',
textAlign: 'center',
}}
>
<div style={{ fontSize: '24px', fontWeight: '700' }}>
{importResult.statistics.success || 0}
</div>
<div style={{ fontSize: '12px', opacity: 0.9 }}>成功</div>
</div>
<div
style={{
padding: '12px',
background: 'linear-gradient(135deg, #ff4d4f 0%, #cf1322 100%)',
borderRadius: '8px',
color: '#fff',
textAlign: 'center',
}}
>
<div style={{ fontSize: '24px', fontWeight: '700' }}>
{importResult.statistics.failed || 0}
</div>
<div style={{ fontSize: '12px', opacity: 0.9 }}>失败</div>
</div>
</div>
{importResult.statistics?.errors?.length > 0 && (
<div
style={{
marginTop: '20px',
maxHeight: 400,
overflowY: 'auto',
border: '1px solid #ffcccc',
borderRadius: '8px',
padding: '12px',
backgroundColor: '#fff7f7',
}}
>
<h4 style={{ color: '#d93025', marginBottom: '12px', fontWeight: '600' }}>
失败记录详情
</h4>
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '13px' }}>
<thead>
<tr style={{ backgroundColor: '#ffeeee' }}>
<th
style={{
border: '1px solid #ffcccc',
padding: '8px',
textAlign: 'left',
width: '80px',
}}
>
行号
</th>
<th
style={{
border: '1px solid #ffcccc',
padding: '8px',
textAlign: 'left',
}}
>
失败原因
</th>
</tr>
</thead>
<tbody>
{importResult.statistics.errors.map((item, index) => (
<tr key={index} style={{ borderBottom: '1px solid #ffcccc' }}>
<td
style={{
border: '1px solid #ffcccc',
padding: '8px',
fontWeight: 'bold',
}}
>
{item.row || index + 1}
</td>
<td
style={{
border: '1px solid #ffcccc',
padding: '8px',
color: '#d93025',
}}
>
{item.error || '未知错误'}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
<Button
type="primary"
onClick={handleClose}
style={{
marginTop: '20px',
height: '40px',
borderRadius: designTokens.borderRadius.small,
background: designTokens.colors.primary.gradient,
border: 'none',
color: '#ffffff',
boxShadow: designTokens.shadows.small,
fontWeight: '500',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
确定
</Button>
</div>
) : null}
</Modal>
);
};
export default React.memo(ImportModal);
@@ -0,0 +1,72 @@
import React from 'react';
const resizableTitleStyles = {
container: {
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
width: '100%',
paddingRight: '8px',
},
text: {
flex: 1,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
},
resizeHandle: {
width: '10px',
height: '100%',
cursor: 'col-resize',
position: 'absolute',
right: 0,
top: 0,
bottom: 0,
backgroundColor: 'transparent',
transition: 'background-color 0.2s',
},
};
const ResizableTitle = (props) => {
const { children, onResize, width, ...restProps } = props;
const handleMouseDown = (e) => {
if (!onResize) return;
e.preventDefault();
e.stopPropagation();
const th = e.currentTarget.closest('th');
if (!th) return;
const startWidth = th.offsetWidth;
const startX = e.clientX;
const handleMouseMove = (moveEvent) => {
const diff = moveEvent.clientX - startX;
const newWidth = Math.max(50, startWidth + diff);
onResize(newWidth);
};
const handleMouseUp = () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
};
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
};
return (
<th {...restProps} style={{ position: 'relative' }}>
<div style={resizableTitleStyles.container}>
<span style={resizableTitleStyles.text}>{children}</span>
{onResize && (
<div onMouseDown={handleMouseDown} style={resizableTitleStyles.resizeHandle} />
)}
</div>
</th>
);
};
export default React.memo(ResizableTitle);
+7
View File
@@ -0,0 +1,7 @@
export { default as ResizableTitle } from './ResizableTitle';
export { default as DeviceDetailModal } from './DeviceDetailModal';
export { default as DeviceFormModal } from './DeviceFormModal';
export { default as ImportModal } from './ImportModal';
export { default as ExportModal } from './ExportModal';
export { default as FieldConfigModal } from './FieldConfigModal';
export { default as BatchStatusModal } from './BatchStatusModal';
+123
View File
@@ -0,0 +1,123 @@
import { useSWR, useFetch, useFetchList } from './useSWR';
export const useDevices = (params = {}) => {
const queryString = new URLSearchParams(
Object.entries(params).filter(([_, v]) => v !== undefined && v !== '')
).toString();
const key = queryString ? `/devices?${queryString}` : '/devices';
return useFetchList(key);
};
export const useDevice = deviceId => {
return useFetch(deviceId ? `/devices/${deviceId}` : null);
};
export const useRacks = (params = {}) => {
const queryString = new URLSearchParams(
Object.entries(params).filter(([_, v]) => v !== undefined && v !== '')
).toString();
const key = queryString ? `/racks?${queryString}` : '/racks';
return useFetchList(key);
};
export const useRack = rackId => {
return useFetch(rackId ? `/racks/${rackId}` : null);
};
export const useRooms = (params = {}) => {
const queryString = new URLSearchParams(
Object.entries(params).filter(([_, v]) => v !== undefined && v !== '')
).toString();
const key = queryString ? `/rooms?${queryString}` : '/rooms';
return useFetchList(key);
};
export const useRoom = roomId => {
return useFetch(roomId ? `/rooms/${roomId}` : null);
};
export const useUsers = (params = {}) => {
const queryString = new URLSearchParams(
Object.entries(params).filter(([_, v]) => v !== undefined && v !== '')
).toString();
const key = queryString ? `/users?${queryString}` : '/users';
return useFetchList(key);
};
export const useUser = userId => {
return useFetch(userId ? `/users/${userId}` : null);
};
export const useTickets = (params = {}) => {
const queryString = new URLSearchParams(
Object.entries(params).filter(([_, v]) => v !== undefined && v !== '')
).toString();
const key = queryString ? `/tickets?${queryString}` : '/tickets';
return useFetchList(key);
};
export const useTicket = ticketId => {
return useFetch(ticketId ? `/tickets/${ticketId}` : null);
};
export const useDeviceFields = () => {
return useFetchList('/device-fields');
};
export const useConsumables = (params = {}) => {
const queryString = new URLSearchParams(
Object.entries(params).filter(([_, v]) => v !== undefined && v !== '')
).toString();
const key = queryString ? `/consumables?${queryString}` : '/consumables';
return useFetchList(key);
};
export const useConsumableCategories = () => {
return useFetchList('/consumable-categories');
};
export const useCables = (params = {}) => {
const queryString = new URLSearchParams(
Object.entries(params).filter(([_, v]) => v !== undefined && v !== '')
).toString();
const key = queryString ? `/cables?${queryString}` : '/cables';
return useFetchList(key);
};
export const usePorts = (params = {}) => {
const queryString = new URLSearchParams(
Object.entries(params).filter(([_, v]) => v !== undefined && v !== '')
).toString();
const key = queryString ? `/device-ports?${queryString}` : '/device-ports';
return useFetchList(key);
};
export const useInventoryPlans = (params = {}) => {
const queryString = new URLSearchParams(
Object.entries(params).filter(([_, v]) => v !== undefined && v !== '')
).toString();
const key = queryString ? `/inventory/plans?${queryString}` : '/inventory/plans';
return useFetchList(key);
};
export const useSystemSettings = () => {
return useFetch('/system-settings');
};
export const useDashboardStats = () => {
return useFetch('/devices/stats');
};
export const useMutate = () => {
const { mutate } = useSWR();
return mutate;
};
+53
View File
@@ -0,0 +1,53 @@
import useSWR, { SWRConfig } from 'swr';
import api from '../api';
const fetcher = url => api.get(url).then(res => res);
export const swrConfig = {
fetcher,
revalidateOnFocus: false,
revalidateOnReconnect: true,
shouldRetryOnError: false,
dedupingInterval: 5000,
errorRetryCount: 2,
};
export const useSWRConfig = () => {
return {
mutate: useSWR().mutate,
};
};
export const useFetch = (key, options = {}) => {
const { data, error, isLoading, isValidating, mutate } = useSWR(key, options);
return {
data,
error,
isLoading,
isValidating,
mutate,
isError: !!error,
};
};
export const useFetchList = (key, options = {}) => {
const { data, error, isLoading, mutate } = useSWR(key, {
...options,
onSuccess: data => {
if (options.onSuccess) {
options.onSuccess(data);
}
},
});
return {
list: data?.list || data?.devices || data?.racks || data?.rooms || data || [],
total: data?.total || data?.count || 0,
error,
isLoading,
mutate,
};
};
export { SWRConfig, useSWR };
+23 -1
View File
@@ -592,7 +592,29 @@ body {
.ant-modal-body {
padding: 24px !important;
overflow: hidden !important;
}
.custom-scroll-container {
overflow-y: auto !important;
overflow-x: hidden !important;
}
.custom-scroll-container::-webkit-scrollbar {
width: 8px;
}
.custom-scroll-container::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
.custom-scroll-container::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 4px;
}
.custom-scroll-container::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
.ant-modal-header {
-1
View File
@@ -536,7 +536,6 @@ function CableManagement() {
if (errors.length > 0) {
message.warning(`发现 ${errors.length} 条数据错误,已跳过`);
console.log('导入错误:', errors);
}
return validatedData;
+10 -1
View File
@@ -1256,8 +1256,16 @@ function ConsumableManagement() {
footer={null}
width={900}
style={{ top: 20 }}
bodyStyle={{ padding: '24px', maxHeight: 'calc(100vh - 200px)', overflowY: 'auto' }}
>
<div
style={{
maxHeight: '65vh',
overflowY: 'auto',
padding: '24px',
paddingRight: '16px',
}}
className="custom-scroll-container"
>
<Form form={form} layout="vertical" onFinish={handleSubmit}>
{/* 基本信息 */}
<div style={{
@@ -1778,6 +1786,7 @@ function ConsumableManagement() {
</Button>
</div>
</Form>
</div>
</Modal>
{/* 导入耗材弹窗 */}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-1
View File
@@ -509,7 +509,6 @@ function PortManagement() {
if (errors.length > 0) {
message.warning(`发现 ${errors.length} 条数据错误,已跳过`);
console.log('导入错误:', errors);
}
return validatedData;
+150
View File
@@ -0,0 +1,150 @@
import React from 'react';
import {
CloudServerOutlined,
SafetyOutlined,
DatabaseOutlined,
AppstoreOutlined,
SwapOutlined,
} from '@ant-design/icons';
export const STATUS_MAP = {
running: { text: '运行中', color: '#52c41a', badgeColor: 'green' },
maintenance: { text: '维护中', color: '#faad14', badgeColor: 'orange' },
offline: { text: '离线', color: '#8c8c8c', badgeColor: 'default' },
fault: { text: '故障', color: '#ff4d4f', badgeColor: 'red' },
};
export const TYPE_MAP = {
server: '服务器',
switch: '交换机',
router: '路由器',
storage: '存储设备',
other: '其他设备',
};
export const getStatusConfig = (status) => {
return STATUS_MAP[status] || { text: status, color: 'black', badgeColor: 'default' };
};
export const getTypeLabel = (type) => {
return TYPE_MAP[type] || type;
};
export const getDeviceTypeIcon = (type) => {
const iconMap = {
server: <CloudServerOutlined style={{ color: '#1890ff' }} />,
switch: <SwapOutlined style={{ color: '#52c41a' }} />,
router: <SafetyOutlined style={{ color: '#faad14' }} />,
storage: <DatabaseOutlined style={{ color: '#722ed1' }} />,
other: <AppstoreOutlined style={{ color: '#8c8c8c' }} />,
};
return iconMap[type] || <AppstoreOutlined style={{ color: '#8c8c8c' }} />;
};
export const formatDate = (date, fieldName) => {
if (!date) return '';
const dateObj = new Date(date);
const formattedDate = dateObj.toLocaleDateString('zh-CN');
if (fieldName === 'warrantyExpiry') {
const today = new Date();
today.setHours(0, 0, 0, 0);
dateObj.setHours(0, 0, 0, 0);
if (dateObj < today) {
return <span style={{ color: '#d93025', fontWeight: 'bold' }}>{formattedDate}</span>;
}
}
return formattedDate;
};
export const FIXED_FIELDS = [
'deviceId',
'name',
'type',
'model',
'serialNumber',
'rackId',
'position',
'height',
'powerConsumption',
'status',
'purchaseDate',
'warrantyExpiry',
'ipAddress',
'description',
];
export const SYSTEM_FIELDS = ['createdAt', 'updatedAt', 'Rack', 'Room', 'customFields'];
export const processDeviceData = (device) => {
const deviceWithFields = { ...device };
if (device.customFields && typeof device.customFields === 'object') {
Object.entries(device.customFields).forEach(([fieldName, value]) => {
deviceWithFields[fieldName] = value;
});
}
return deviceWithFields;
};
export const prepareDeviceFormData = (values, isEditing) => {
const deviceData = {
...values,
purchaseDate: values.purchaseDate ? values.purchaseDate.format('YYYY-MM-DD') : null,
warrantyExpiry: values.warrantyExpiry ? values.warrantyExpiry.format('YYYY-MM-DD') : null,
customFields: {},
};
Object.keys(deviceData).forEach((key) => {
if (!FIXED_FIELDS.includes(key) && key !== 'customFields' && key !== 'roomId') {
deviceData.customFields[key] = deviceData[key];
delete deviceData[key];
}
});
delete deviceData.roomId;
return deviceData;
};
export const getFormInitialValues = (device, racks) => {
if (!device) return {};
const deviceData = { ...device };
const cleanDeviceData = {};
FIXED_FIELDS.forEach((field) => {
if (deviceData[field] !== undefined) {
cleanDeviceData[field] = deviceData[field];
}
});
Object.entries(deviceData).forEach(([key, value]) => {
if (
!FIXED_FIELDS.includes(key) &&
!SYSTEM_FIELDS.includes(key) &&
key !== 'deviceId' &&
typeof value !== 'object' &&
value !== null
) {
cleanDeviceData[key] = value;
}
});
if (deviceData.customFields && typeof deviceData.customFields === 'object') {
Object.entries(deviceData.customFields).forEach(([key, value]) => {
cleanDeviceData[key] = value;
});
}
if (device.rackId) {
const rack = racks.find((r) => r.rackId === device.rackId);
if (rack) {
cleanDeviceData.roomId = rack.roomId;
}
}
return cleanDeviceData;
};
-19
View File
@@ -1,19 +0,0 @@
const fs = require('fs');
const content = fs.readFileSync('e:/IDC/jigui/frontend/src/pages/DeviceManagement.jsx', 'utf8');
const startMarker = '];\n\n// 可调整列宽的表头组件';
const endMarker = '\n\n// 防抖 Hook';
const startIndex = content.indexOf(startMarker);
const endIndex = content.indexOf(endMarker, startIndex);
if (startIndex >= 0 && endIndex >= 0) {
const newContent = content.substring(0, startIndex + 3) + '\n\n// 防抖 Hook' + content.substring(endIndex + endMarker.length);
fs.writeFileSync('e:/IDC/jigui/frontend/src/pages/DeviceManagement.jsx', newContent, 'utf8');
console.log('Removed ResizeableTitle component');
} else {
console.log('Could not find markers');
console.log('startMarker found:', startIndex >= 0);
console.log('endMarker found:', endIndex >= 0);
}