From 196dc4ae414c638563718559ab46384a3cbc67ab Mon Sep 17 00:00:00 2001 From: zhang1106 <849185023@qq.com> Date: Tue, 10 Mar 2026 14:34:39 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E7=A7=BB=E9=99=A4=E8=B0=83?= =?UTF-8?q?=E8=AF=95=E6=97=A5=E5=BF=97=E5=B9=B6=E4=BC=98=E5=8C=96=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E7=BB=93=E6=9E=84=20feat(components):=20=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E8=AE=BE=E5=A4=87=E7=AE=A1=E7=90=86=E7=9B=B8=E5=85=B3?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=E5=92=8C=E4=BB=AA=E8=A1=A8=E7=9B=98=E7=BB=84?= =?UTF-8?q?=E4=BB=B6=20feat(hooks):=20=E6=B7=BB=E5=8A=A0=E8=87=AA=E5=AE=9A?= =?UTF-8?q?=E4=B9=89hooks=E7=94=A8=E4=BA=8EAPI=E8=B0=83=E7=94=A8=E5=92=8C?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E7=AE=A1=E7=90=86=20style:=20=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E6=BB=9A=E5=8A=A8=E6=9D=A1=E6=A0=B7=E5=BC=8F=E5=92=8C?= =?UTF-8?q?=E6=A8=A1=E6=80=81=E6=A1=86=E5=B8=83=E5=B1=80=20chore:=20?= =?UTF-8?q?=E6=B8=85=E7=90=86=E6=97=A0=E7=94=A8=E8=84=9A=E6=9C=AC=E5=92=8C?= =?UTF-8?q?=E8=B0=83=E8=AF=95=E6=96=87=E4=BB=B6=20docs:=20=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E7=BB=84=E4=BB=B6=E5=AF=BC=E5=87=BA=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 6 +- backend/check_password.js | 33 - backend/fix-maxstock.js | 99 - backend/fix_admin.js | 125 - backend/fix_user_delete.js | 133 -- backend/migrateAddLocationColumn.js | 35 - backend/migratePendingDevice.js | 58 - backend/recreate-fault-category.js | 51 - backend/routes/consumables.js | 2 - backend/routes/inventory.js | 5 - backend/server.js | 255 +- check.js | 17 - frontend/src/App.jsx | 203 +- frontend/src/api/index.js | 5 - frontend/src/components/CableCreateModal.jsx | 1 - .../components/dashboard/AnimatedCounter.jsx | 38 + .../components/dashboard/CircularProgress.jsx | 61 + .../components/dashboard/DeviceTrendChart.jsx | 53 + .../components/dashboard/NavigationGrid.jsx | 139 ++ .../src/components/dashboard/PowerGauge.jsx | 60 + .../src/components/dashboard/QuickStats.jsx | 89 + .../src/components/dashboard/StatCard.jsx | 176 ++ .../src/components/dashboard/StatusLegend.jsx | 57 + .../src/components/dashboard/SystemInfo.jsx | 69 + frontend/src/components/dashboard/index.js | 9 + .../components/device/BatchStatusModal.jsx | 110 + .../components/device/DeviceDetailModal.jsx | 256 ++ .../src/components/device/DeviceFormModal.jsx | 361 +++ .../src/components/device/ExportModal.jsx | 179 ++ .../components/device/FieldConfigModal.jsx | 150 ++ .../src/components/device/ImportModal.jsx | 385 +++ .../src/components/device/ResizableTitle.jsx | 72 + frontend/src/components/device/index.js | 7 + frontend/src/hooks/useApi.js | 123 + frontend/src/hooks/useSWR.js | 53 + frontend/src/index.css | 24 +- frontend/src/pages/CableManagement.jsx | 1 - frontend/src/pages/ConsumableManagement.jsx | 11 +- frontend/src/pages/Dashboard.jsx | 1032 +------- frontend/src/pages/DeviceManagement.jsx | 2105 ++--------------- frontend/src/pages/PortManagement.jsx | 1 - frontend/src/utils/deviceUtils.jsx | 150 ++ modify.js | 19 - 43 files changed, 3082 insertions(+), 3736 deletions(-) delete mode 100644 backend/check_password.js delete mode 100644 backend/fix-maxstock.js delete mode 100644 backend/fix_admin.js delete mode 100644 backend/fix_user_delete.js delete mode 100644 backend/migrateAddLocationColumn.js delete mode 100644 backend/migratePendingDevice.js delete mode 100644 backend/recreate-fault-category.js delete mode 100644 check.js create mode 100644 frontend/src/components/dashboard/AnimatedCounter.jsx create mode 100644 frontend/src/components/dashboard/CircularProgress.jsx create mode 100644 frontend/src/components/dashboard/DeviceTrendChart.jsx create mode 100644 frontend/src/components/dashboard/NavigationGrid.jsx create mode 100644 frontend/src/components/dashboard/PowerGauge.jsx create mode 100644 frontend/src/components/dashboard/QuickStats.jsx create mode 100644 frontend/src/components/dashboard/StatCard.jsx create mode 100644 frontend/src/components/dashboard/StatusLegend.jsx create mode 100644 frontend/src/components/dashboard/SystemInfo.jsx create mode 100644 frontend/src/components/dashboard/index.js create mode 100644 frontend/src/components/device/BatchStatusModal.jsx create mode 100644 frontend/src/components/device/DeviceDetailModal.jsx create mode 100644 frontend/src/components/device/DeviceFormModal.jsx create mode 100644 frontend/src/components/device/ExportModal.jsx create mode 100644 frontend/src/components/device/FieldConfigModal.jsx create mode 100644 frontend/src/components/device/ImportModal.jsx create mode 100644 frontend/src/components/device/ResizableTitle.jsx create mode 100644 frontend/src/components/device/index.js create mode 100644 frontend/src/hooks/useApi.js create mode 100644 frontend/src/hooks/useSWR.js create mode 100644 frontend/src/utils/deviceUtils.jsx delete mode 100644 modify.js diff --git a/.gitignore b/.gitignore index 36949ac..9a09151 100644 --- a/.gitignore +++ b/.gitignore @@ -40,4 +40,8 @@ lerna-debug.log* # Temporary files *.tmp -*.temp \ No newline at end of file +*.temp + +# Backup files (generated by maintenance scripts) +backend/backups/ +*.backup.json \ No newline at end of file diff --git a/backend/check_password.js b/backend/check_password.js deleted file mode 100644 index ee4791a..0000000 --- a/backend/check_password.js +++ /dev/null @@ -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(); diff --git a/backend/fix-maxstock.js b/backend/fix-maxstock.js deleted file mode 100644 index 7ca34da..0000000 --- a/backend/fix-maxstock.js +++ /dev/null @@ -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(); diff --git a/backend/fix_admin.js b/backend/fix_admin.js deleted file mode 100644 index 09ca3fc..0000000 --- a/backend/fix_admin.js +++ /dev/null @@ -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(); diff --git a/backend/fix_user_delete.js b/backend/fix_user_delete.js deleted file mode 100644 index ef4cad7..0000000 --- a/backend/fix_user_delete.js +++ /dev/null @@ -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(); diff --git a/backend/migrateAddLocationColumn.js b/backend/migrateAddLocationColumn.js deleted file mode 100644 index b6072e6..0000000 --- a/backend/migrateAddLocationColumn.js +++ /dev/null @@ -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('数据库修复完成'); - } - }); - }); -}); diff --git a/backend/migratePendingDevice.js b/backend/migratePendingDevice.js deleted file mode 100644 index f87eda9..0000000 --- a/backend/migratePendingDevice.js +++ /dev/null @@ -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); diff --git a/backend/recreate-fault-category.js b/backend/recreate-fault-category.js deleted file mode 100644 index 72e3f65..0000000 --- a/backend/recreate-fault-category.js +++ /dev/null @@ -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(); diff --git a/backend/routes/consumables.js b/backend/routes/consumables.js index c0d5cc7..66ac6bb 100644 --- a/backend/routes/consumables.js +++ b/backend/routes/consumables.js @@ -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({ diff --git a/backend/routes/inventory.js b/backend/routes/inventory.js index c899692..239673c 100644 --- a/backend/routes/inventory.js +++ b/backend/routes/inventory.js @@ -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 }); } }); diff --git a/backend/server.js b/backend/server.js index c3423cd..135cff6 100644 --- a/backend/server.js +++ b/backend/server.js @@ -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}`); -}); \ No newline at end of file +}); diff --git a/check.js b/check.js deleted file mode 100644 index a9ddfed..0000000 --- a/check.js +++ /dev/null @@ -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)); -} diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 4db2adf..892f404 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -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 ( - - }> - + + + }> + } /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> + {routeConfig.map(({ path, component: Component }) => ( + + + + } + /> + ))} { } /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> } /> + ); }; diff --git a/frontend/src/api/index.js b/frontend/src/api/index.js index 6dd7a48..d14549c 100644 --- a/frontend/src/api/index.js +++ b/frontend/src/api/index.js @@ -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'; diff --git a/frontend/src/components/CableCreateModal.jsx b/frontend/src/components/CableCreateModal.jsx index 055b330..725ec8c 100644 --- a/frontend/src/components/CableCreateModal.jsx +++ b/frontend/src/components/CableCreateModal.jsx @@ -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); diff --git a/frontend/src/components/dashboard/AnimatedCounter.jsx b/frontend/src/components/dashboard/AnimatedCounter.jsx new file mode 100644 index 0000000..8854bef --- /dev/null +++ b/frontend/src/components/dashboard/AnimatedCounter.jsx @@ -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 {displayValue}; +}; + +export default React.memo(AnimatedCounter); diff --git a/frontend/src/components/dashboard/CircularProgress.jsx b/frontend/src/components/dashboard/CircularProgress.jsx new file mode 100644 index 0000000..64fa4b3 --- /dev/null +++ b/frontend/src/components/dashboard/CircularProgress.jsx @@ -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 ( +
+ + + + +
+
+ {percentage}% +
+
+ {label} +
+
+
+ ); +}; + +export default React.memo(CircularProgress); diff --git a/frontend/src/components/dashboard/DeviceTrendChart.jsx b/frontend/src/components/dashboard/DeviceTrendChart.jsx new file mode 100644 index 0000000..3db3fd8 --- /dev/null +++ b/frontend/src/components/dashboard/DeviceTrendChart.jsx @@ -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 ( +
+
+ {data.map((item, index) => ( +
+
+ + {item.label} + +
+ ))} +
+
+ ); +}; + +export default React.memo(DeviceTrendChart); diff --git a/frontend/src/components/dashboard/NavigationGrid.jsx b/frontend/src/components/dashboard/NavigationGrid.jsx new file mode 100644 index 0000000..60f33db --- /dev/null +++ b/frontend/src/components/dashboard/NavigationGrid.jsx @@ -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 ( +
+ {NAV_BUTTONS_DATA.map(({ key, icon: Icon, text, color }) => { + const isHovered = hoveredCard === `nav-${key}`; + return ( +
b.key === key) * 0.1}s`, + }} + onMouseEnter={() => onHover(`nav-${key}`)} + onMouseLeave={() => onHover(null)} + onClick={() => navigate(`/${key}`)} + > +
+ +
+ + {text} + +
+ ); + })} +
+ ); +}; + +export default React.memo(NavigationGrid); diff --git a/frontend/src/components/dashboard/PowerGauge.jsx b/frontend/src/components/dashboard/PowerGauge.jsx new file mode 100644 index 0000000..304103a --- /dev/null +++ b/frontend/src/components/dashboard/PowerGauge.jsx @@ -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 ( +
+
+ + 功率使用率 + + + {percentage.toFixed(1)}% + +
+
+
+
+
+ {value}W + {maxValue}W +
+
+ ); +}; + +export default React.memo(PowerGauge); diff --git a/frontend/src/components/dashboard/QuickStats.jsx b/frontend/src/components/dashboard/QuickStats.jsx new file mode 100644 index 0000000..d87c207 --- /dev/null +++ b/frontend/src/components/dashboard/QuickStats.jsx @@ -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 ( +
+ {quickStats.map((stat, index) => ( +
+
+ +
+
+ + {stat.label} + +
+ {stat.value} +
+
+
+ ))} +
+ ); +}; + +export default React.memo(QuickStats); diff --git a/frontend/src/components/dashboard/StatCard.jsx b/frontend/src/components/dashboard/StatCard.jsx new file mode 100644 index 0000000..86bd4ba --- /dev/null +++ b/frontend/src/components/dashboard/StatCard.jsx @@ -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 ( + + onHover(statKey)} + onMouseLeave={() => onHover(null)} + styles={{ body: { padding: 'clamp(16px, 3vw, 24px)' } }} + > +
+
+ + {title} + +
+ +
+
+
+ {loading ? ( + + ) : ( + + )} +
+ {customStatus ? ( +
+ + {statKey === 'totalRacks' ? '正常运行中' : '全部在线'} +
+ ) : ( +
0 ? designTokens.colors.success.main : designTokens.colors.error.main, + flexWrap: 'wrap', + gap: '4px', + }} + > + {trend > 0 ? ( + + ) : ( + + )} + {Math.abs(trend)}% + + 环比 + +
+ )} +
+
+ + ); +}; + +export default React.memo(StatCard); diff --git a/frontend/src/components/dashboard/StatusLegend.jsx b/frontend/src/components/dashboard/StatusLegend.jsx new file mode 100644 index 0000000..d1c0011 --- /dev/null +++ b/frontend/src/components/dashboard/StatusLegend.jsx @@ -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 ( +
+ {legends.map((item, index) => ( +
+
+
+ + {item.label} + +
+ + {item.percent}% + +
+ ))} +
+ ); +}; + +export default React.memo(StatusLegend); diff --git a/frontend/src/components/dashboard/SystemInfo.jsx b/frontend/src/components/dashboard/SystemInfo.jsx new file mode 100644 index 0000000..19de2df --- /dev/null +++ b/frontend/src/components/dashboard/SystemInfo.jsx @@ -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 ( +
+
+
+
+

+ 系统版本: v1.0.0 +

+

+ 最后更新: + {new Date().toLocaleDateString()} +

+
+ +
+
+
+ ); +}; + +export default React.memo(SystemInfo); diff --git a/frontend/src/components/dashboard/index.js b/frontend/src/components/dashboard/index.js new file mode 100644 index 0000000..3032589 --- /dev/null +++ b/frontend/src/components/dashboard/index.js @@ -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'; diff --git a/frontend/src/components/device/BatchStatusModal.jsx b/frontend/src/components/device/BatchStatusModal.jsx new file mode 100644 index 0000000..3bd46fb --- /dev/null +++ b/frontend/src/components/device/BatchStatusModal.jsx @@ -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 ( + + + 批量状态变更 +
+ } + open={visible} + onCancel={onCancel} + footer={[ + , + , + ]} + destroyOnHidden + styles={{ + header: { + borderBottom: '1px solid #f0f0f0', + padding: '16px 24px', + position: 'relative', + }, + body: { padding: '24px' }, + }} + > +
+ + + +
+ 已选择{' '} + {selectedCount} 个设备 +
+
+ + ); +}; + +export default React.memo(BatchStatusModal); diff --git a/frontend/src/components/device/DeviceDetailModal.jsx b/frontend/src/components/device/DeviceDetailModal.jsx new file mode 100644 index 0000000..a56c7b6 --- /dev/null +++ b/frontend/src/components/device/DeviceDetailModal.jsx @@ -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 ( + + + 设备详情 +
+ } + open={visible} + onCancel={onClose} + footer={[ + , + , + , + , + ]} + width={700} + destroyOnHidden + styles={{ + header: { + borderBottom: '1px solid #f0f0f0', + padding: '16px 24px', + position: 'relative', + }, + body: { padding: '0', overflow: 'auto' }, + }} + > +
+
+
+
+ {getDeviceTypeIcon(device.type)} +
+
+
+ {device.name} +
+
+ {getTypeLabel(device.type)} + | + {device.deviceId} + | + + {device.status ? getStatusConfig(device.status).text : '-'} + +
+
+
+
+ +
+ 基本信息} + style={{ marginBottom: '16px', borderRadius: '8px' }} + > + + +
设备型号
+
{device.model || '-'}
+ + +
序列号
+
{device.serialNumber || '-'}
+ + +
IP地址
+
{device.ipAddress || '-'}
+ + +
所在机房
+
{device.Rack?.Room?.name || '-'}
+ + +
所在机柜
+
{device.Rack?.name || '-'}
+ + +
位置(U)
+
U{device.position || '-'}
+ + +
高度
+
{device.height ? `${device.height}U` : '-'}
+ + +
功率
+
{device.power ? `${device.power}W` : '-'}
+ + +
状态
+
+ {device.status ? getStatusConfig(device.status).text : '-'} +
+ +
+
+ + 维保信息} + style={{ marginBottom: '16px', borderRadius: '8px' }} + > + + +
购买日期
+
+ {device.purchaseDate + ? new Date(device.purchaseDate).toLocaleDateString('zh-CN') + : '-'} +
+ + +
保修到期
+
+ {device.warrantyExpiry + ? new Date(device.warrantyExpiry).toLocaleDateString('zh-CN') + : '-'} +
+ +
+
+ + {device.description && ( + 描述} + style={{ marginBottom: '16px', borderRadius: '8px' }} + > +
{device.description}
+
+ )} + + {device.customFields && Object.keys(device.customFields).length > 0 && ( + 自定义字段} + style={{ borderRadius: '8px' }} + > + + {Object.entries(device.customFields).map(([key, value]) => { + const fieldConfig = deviceFields.find((f) => f.fieldName === key); + const displayName = fieldConfig?.displayName || key; + return ( + +
+ {displayName} +
+
{String(value)}
+ + ); + })} +
+
+ )} +
+
+ + ); +}; + +export default React.memo(DeviceDetailModal); diff --git a/frontend/src/components/device/DeviceFormModal.jsx b/frontend/src/components/device/DeviceFormModal.jsx new file mode 100644 index 0000000..7185374 --- /dev/null +++ b/frontend/src/components/device/DeviceFormModal.jsx @@ -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 ( + + ); + case 'boolean': + return ; + case 'date': + return ( + + ); + case 'textarea': + return ( + + ); + case 'select': + return ( + + ); + default: + return ( + + ); + } + }; + + const filteredFields = deviceFields.filter( + (field) => field.fieldName !== 'deviceId' && field.fieldName !== 'rackId' + ); + + const formItems = []; + filteredFields.forEach((field) => { + if (field.fieldName === 'serialNumber') { + formItems.push( + + + + {field.displayName} + {field.required && ( + * + )} + + } + rules={ + field.required ? [{ required: true, message: `请输入${field.displayName}` }] : [] + } + > + {renderFieldControl(field)} + + + +
+
+ + 设备位置选择 +
+ + + + 机房 + * + + } + rules={[{ required: true, message: '请选择机房' }]} + style={{ marginBottom: '0' }} + > + + + + + + 机柜 + * + + } + rules={[{ required: true, message: '请选择机柜' }]} + style={{ marginBottom: '0' }} + > + + + + +
+ +
+ ); + } else if (field.fieldType === 'textarea') { + formItems.push( + + + {field.displayName} + {field.required && ( + * + )} + + } + rules={ + field.required ? [{ required: true, message: `请输入${field.displayName}` }] : [] + } + > + {renderFieldControl(field)} + + + ); + } else { + formItems.push( + + + {field.displayName} + {field.required && ( + * + )} + + } + rules={ + field.required ? [{ required: true, message: `请输入${field.displayName}` }] : [] + } + > + {renderFieldControl(field)} + + + ); + } + }); + + return ( + + {editingDevice ? ( + + ) : ( + + )} + {editingDevice ? '编辑设备' : '添加设备'} +
+ } + 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" + > +
+ {formItems} + +
+ + +
+
+ + ); +}; + +export default React.memo(DeviceFormModal); diff --git a/frontend/src/components/device/ExportModal.jsx b/frontend/src/components/device/ExportModal.jsx new file mode 100644 index 0000000..b6fb565 --- /dev/null +++ b/frontend/src/components/device/ExportModal.jsx @@ -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 ( + + + 导出设备数据 + + } + open={visible} + onCancel={onCancel} + footer={[ + , + , + ]} + destroyOnHidden + styles={{ + header: { + borderBottom: '1px solid #f0f0f0', + padding: '16px 24px', + position: 'relative', + }, + body: { padding: '24px' }, + }} + width={600} + > +
+ + + + + + + +
+ {visibleFields.map((field) => ( +
+ { + if (e.target.checked) { + setExportFields([...exportFields, field.fieldName]); + } else { + setExportFields(exportFields.filter((f) => f !== field.fieldName)); + } + }} + > + {field.displayName} + +
+ ))} +
+
+
+ 已选择{' '} + {selectedDevices.length} 个设备, + 将导出{' '} + {exportFields.length} 个字段 +
+
+
+ ); +}; + +export default React.memo(ExportModal); diff --git a/frontend/src/components/device/FieldConfigModal.jsx b/frontend/src/components/device/FieldConfigModal.jsx new file mode 100644 index 0000000..3a353b3 --- /dev/null +++ b/frontend/src/components/device/FieldConfigModal.jsx @@ -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 ( + + + 字段配置 + + } + open={visible} + onCancel={onCancel} + footer={null} + width={600} + styles={{ + header: { + borderBottom: '1px solid #f0f0f0', + padding: '16px 24px', + position: 'relative', + }, + body: { padding: '24px' }, + }} + > +
+
+ + + + + + + + + + {filteredFields.map((field) => ( + + + + + + ))} + +
字段名称可见必填
{field.displayName} + + + + + + + +
+
+ + + + + + + + +
+
+ ); +}; + +export default React.memo(FieldConfigModal); diff --git a/frontend/src/components/device/ImportModal.jsx b/frontend/src/components/device/ImportModal.jsx new file mode 100644 index 0000000..987f094 --- /dev/null +++ b/frontend/src/components/device/ImportModal.jsx @@ -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 ( + + + 导入设备 + + } + 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 ? ( +
+

请上传CSV格式的设备数据文件

+

+ 支持的编码格式:GBK +

+ +
+

+ CSV文件格式要求: +

+
+ {requiredFields.length > 0 && ( +
+ 必填字段: + + {requiredFields.map((f) => f.displayName).join('、')} + +
+ )} + {optionalFields.length > 0 && ( +
+ 可选字段: + + {optionalFields.map((f) => f.displayName).join('、')} + +
+ )} +
    +
  • + 设备类型:server(服务器)、switch(交换机)、router(路由器)、storage(存储设备)、other(其他) +
  • +
  • 状态值:running(运行中)、maintenance(维护中)、offline(离线)、fault(故障)
  • +
  • 日期格式:YYYY-MM-DD (例如:2023-01-01)
  • +
+
+
+ +
+ + + + + 包含示例数据的CSV模板文件(根据当前字段配置生成) + +
+ + + + +
+ ) : isImporting ? ( +
+
+
+ +
+
+

+ 正在导入设备数据 +

+

{importPhase}

+
+
+ `${importProgress}%`} + /> +
+ ) : importResult?.statistics ? ( +
+

导入完成:

+
+
+
+ {importResult.statistics.total || 0} +
+
总记录数
+
+
+
+ {importResult.statistics.success || 0} +
+
成功
+
+
+
+ {importResult.statistics.failed || 0} +
+
失败
+
+
+ + {importResult.statistics?.errors?.length > 0 && ( +
+

+ 失败记录详情: +

+ + + + + + + + + {importResult.statistics.errors.map((item, index) => ( + + + + + ))} + +
+ 行号 + + 失败原因 +
+ {item.row || index + 1} + + {item.error || '未知错误'} +
+
+ )} + + +
+ ) : null} +
+ ); +}; + +export default React.memo(ImportModal); diff --git a/frontend/src/components/device/ResizableTitle.jsx b/frontend/src/components/device/ResizableTitle.jsx new file mode 100644 index 0000000..576238e --- /dev/null +++ b/frontend/src/components/device/ResizableTitle.jsx @@ -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 ( + +
+ {children} + {onResize && ( +
+ )} +
+ + ); +}; + +export default React.memo(ResizableTitle); diff --git a/frontend/src/components/device/index.js b/frontend/src/components/device/index.js new file mode 100644 index 0000000..e3ae321 --- /dev/null +++ b/frontend/src/components/device/index.js @@ -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'; diff --git a/frontend/src/hooks/useApi.js b/frontend/src/hooks/useApi.js new file mode 100644 index 0000000..70ef877 --- /dev/null +++ b/frontend/src/hooks/useApi.js @@ -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; +}; diff --git a/frontend/src/hooks/useSWR.js b/frontend/src/hooks/useSWR.js new file mode 100644 index 0000000..df5d2b4 --- /dev/null +++ b/frontend/src/hooks/useSWR.js @@ -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 }; diff --git a/frontend/src/index.css b/frontend/src/index.css index e238f85..761d6d6 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -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 { diff --git a/frontend/src/pages/CableManagement.jsx b/frontend/src/pages/CableManagement.jsx index 5fa54be..75c6389 100644 --- a/frontend/src/pages/CableManagement.jsx +++ b/frontend/src/pages/CableManagement.jsx @@ -536,7 +536,6 @@ function CableManagement() { if (errors.length > 0) { message.warning(`发现 ${errors.length} 条数据错误,已跳过`); - console.log('导入错误:', errors); } return validatedData; diff --git a/frontend/src/pages/ConsumableManagement.jsx b/frontend/src/pages/ConsumableManagement.jsx index f6fd0a9..9e026ce 100644 --- a/frontend/src/pages/ConsumableManagement.jsx +++ b/frontend/src/pages/ConsumableManagement.jsx @@ -1256,8 +1256,16 @@ function ConsumableManagement() { footer={null} width={900} style={{ top: 20 }} - bodyStyle={{ padding: '24px', maxHeight: 'calc(100vh - 200px)', overflowY: 'auto' }} > +
{/* 基本信息 */}
+
{/* 导入耗材弹窗 */} diff --git a/frontend/src/pages/Dashboard.jsx b/frontend/src/pages/Dashboard.jsx index c28c6b2..bf00834 100644 --- a/frontend/src/pages/Dashboard.jsx +++ b/frontend/src/pages/Dashboard.jsx @@ -1,104 +1,22 @@ -import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react'; -import { Card, Row, Col, Statistic, message, Button, Tag, Typography, Progress, Spin } from 'antd'; -import { - DatabaseOutlined, - CloudServerOutlined, - WarningOutlined, - HomeOutlined, - SettingOutlined, - ArrowUpOutlined, - ArrowDownOutlined, - DashboardOutlined, - ReloadOutlined, - EnvironmentOutlined, - LineChartOutlined, - SafetyOutlined, - TeamOutlined, - ThunderboltOutlined, - AppstoreOutlined, - BarChartOutlined, -} from '@ant-design/icons'; +import React, { useState, useCallback, useMemo } from 'react'; +import { Card, Row, Col, Typography, message } from 'antd'; +import { DatabaseOutlined, CloudServerOutlined, WarningOutlined, HomeOutlined, TeamOutlined, BarChartOutlined, DashboardOutlined } from '@ant-design/icons'; import api from '../api'; +import { designTokens } from '../config/theme'; +import { useFetch } from '../hooks/useSWR'; +import { + AnimatedCounter, + CircularProgress, + PowerGauge, + DeviceTrendChart, + StatusLegend, + StatCard, + NavigationGrid, + QuickStats, + SystemInfo, +} from '../components/dashboard'; -const { Title, Text } = Typography; - -const designTokens = { - colors: { - primary: { - main: '#1890ff', - light: '#40a9ff', - dark: '#096dd9', - gradient: 'linear-gradient(135deg, #1890ff 0%, #096dd9 100%)', - bgGradient: 'linear-gradient(135deg, #1890ff15 0%, #096dd908 100%)', - }, - success: { - main: '#52c41a', - light: '#73d13d', - dark: '#389e0d', - gradient: 'linear-gradient(135deg, #52c41a 0%, #389e0d 100%)', - bgGradient: 'linear-gradient(135deg, #52c41a15 0%, #389e0d08 100%)', - }, - warning: { - main: '#faad14', - light: '#ffc53d', - dark: '#d48806', - gradient: 'linear-gradient(135deg, #faad14 0%, #d48806 100%)', - bgGradient: 'linear-gradient(135deg, #faad1415 0%, #d4880608 100%)', - }, - error: { - main: '#ff4d4f', - light: '#ff7875', - dark: '#cf1322', - gradient: 'linear-gradient(135deg, #ff4d4f 0%, #cf1322 100%)', - bgGradient: 'linear-gradient(135deg, #ff4d4f15 0%, #cf132208 100%)', - }, - purple: { - main: '#722ed1', - light: '#9254de', - dark: '#531dab', - gradient: 'linear-gradient(135deg, #722ed1 0%, #531dab 100%)', - bgGradient: 'linear-gradient(135deg, #722ed115 0%, #531dab08 100%)', - }, - cyan: { - main: '#13c2c2', - light: '#36cfc9', - dark: '#08979c', - gradient: 'linear-gradient(135deg, #13c2c2 0%, #08979c 100%)', - bgGradient: 'linear-gradient(135deg, #13c2c215 0%, #08979c08 100%)', - }, - text: { - primary: '#262626', - secondary: '#8c8c8c', - tertiary: '#bfbfbf', - }, - }, - shadows: { - small: '0 2px 8px rgba(0, 0, 0, 0.06)', - medium: '0 4px 16px rgba(0, 0, 0, 0.08)', - large: '0 8px 24px rgba(0, 0, 0, 0.12)', - hover: '0 12px 32px rgba(0, 0, 0, 0.15)', - }, - borderRadius: { - small: '8px', - medium: '12px', - large: '16px', - xl: '20px', - }, - transitions: { - fast: '0.15s ease', - normal: '0.3s cubic-bezier(0.4, 0, 0.2, 1)', - slow: '0.5s cubic-bezier(0.4, 0, 0.2, 1)', - }, -}; - -const responsiveConfig = { - xs: { span: 24 }, - sm: { span: 12 }, - md: { span: 8 }, - lg: { span: 6 }, - xl: { span: 5 }, - xxl: { span: 4 }, -}; +const { Title } = Typography; const containerStyle = { minHeight: '100vh', @@ -187,87 +105,6 @@ const pieChartInner = { flexDirection: 'column', }; -const trendItemStyle = { - display: 'flex', - alignItems: 'center', - justifyContent: 'space-between', - padding: '12px 0', - borderBottom: '1px solid #f0f0f0', -}; - -const STAT_CARD_BASE_STYLE = { - 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 transparent', -}; - -const STAT_ICON_CONTAINER_BASE = { - 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, -}; - -const NAV_BUTTON_BASE = { - height: 'auto', - padding: 'clamp(16px, 4vw, 24px) clamp(12px, 3vw, 20px)', - borderRadius: designTokens.borderRadius.medium, - border: '2px solid #f0f0f0', - background: '#fff', - transition: `all ${designTokens.transitions.normal}`, - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - gap: 'clamp(8px, 2vw, 12px)', - cursor: 'pointer', - boxShadow: designTokens.shadows.small, - minWidth: '0', -}; - -const NAV_ICON_CONTAINER_BASE = { - width: 'clamp(44px, 10vw, 60px)', - height: 'clamp(44px, 10vw, 60px)', - borderRadius: designTokens.borderRadius.medium, - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - fontSize: 'clamp(20px, 5vw, 28px)', - transition: `all ${designTokens.transitions.normal}`, - flexShrink: 0, -}; - -const createStatCardStyle = color => ({ - ...STAT_CARD_BASE_STYLE, - borderLeftColor: color, -}); - -const createStatIconContainer = color => ({ - ...STAT_ICON_CONTAINER_BASE, - background: `linear-gradient(135deg, ${color}20 0%, ${color}10 100%)`, -}); - -const createNavButtonStyle = color => ({ - ...NAV_BUTTON_BASE, - borderColor: color, -}); - -const createNavIconContainer = color => ({ - ...NAV_ICON_CONTAINER_BASE, - background: `linear-gradient(135deg, ${color}20 0%, ${color}10 100%)`, -}); - const overviewCardStyle = { borderRadius: designTokens.borderRadius.large, border: 'none', @@ -276,454 +113,59 @@ const overviewCardStyle = { animation: 'fadeInUp 0.6s ease-out 0.2s backwards', }; -const navigationGridStyle = { - display: 'grid', - gridTemplateColumns: 'repeat(auto-fit, minmax(100px, 1fr))', - gap: 'clamp(8px, 2vw, 16px)', - marginBottom: '24px', -}; - -const navButtonStyle = color => ({ - height: 'auto', - padding: '24px 20px', - borderRadius: designTokens.borderRadius.medium, - border: '2px solid #f0f0f0', - background: '#fff', - transition: `all ${designTokens.transitions.normal}`, - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - gap: '12px', - cursor: 'pointer', - boxShadow: designTokens.shadows.small, -}); - -const navIconContainer = color => ({ - width: '60px', - height: '60px', - borderRadius: designTokens.borderRadius.medium, - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - background: `linear-gradient(135deg, ${color}20 0%, ${color}10 100%)`, - fontSize: '28px', - transition: `all ${designTokens.transitions.normal}`, -}); - -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 systemInfoStyle = { - background: 'linear-gradient(135deg, #f0f7ff 0%, #e6f7ff 100%)', - borderRadius: designTokens.borderRadius.medium, - padding: '20px', - border: '1px solid #91d5ff', -}; - -const quickStatsStyle = { - display: 'grid', - gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))', - gap: '16px', - marginBottom: '24px', -}; - -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 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 {displayValue}; -}; - -const CircularProgress = ({ percentage, size = 120, strokeWidth = 10, color, label }) => { - const circumference = 2 * Math.PI * ((size - strokeWidth) / 2); - const offset = circumference - (percentage / 100) * circumference; - - return ( -
- - - - -
-
- {percentage}% -
-
- {label} -
-
-
- ); -}; - -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 ( -
-
- - 功率使用率 - - - {percentage.toFixed(1)}% - -
-
-
-
-
- {value}W - {maxValue}W -
-
- ); -}; - -const DeviceTrendChart = ({ data }) => { - const maxValue = Math.max(...data.map(d => d.value)); - const chartHeight = 120; - - return ( -
-
- {data.map((item, index) => ( -
-
- - {item.label} - -
- ))} -
-
- ); -}; - -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 ( -
- {legends.map((item, index) => ( -
-
-
- - {item.label} - -
- - {item.percent}% - -
- ))} -
- ); -}; - -const navButtonsData = [ - { - key: 'devices', - icon: CloudServerOutlined, - text: '设备管理', - path: '/devices', - color: designTokens.colors.primary.main, - }, - { - key: 'racks', - icon: DatabaseOutlined, - text: '资源规划', - path: '/racks', - color: designTokens.colors.purple.main, - }, - { - key: 'faults', - icon: WarningOutlined, - text: '故障监控', - path: '/faults', - color: designTokens.colors.warning.main, - }, - { - key: 'tickets', - icon: BarChartOutlined, - text: '工单管理', - path: '/tickets', - color: designTokens.colors.cyan.main, - }, - { - key: 'consumables', - icon: AppstoreOutlined, - text: '耗材管理', - path: '/consumables', - color: '#fa8c16', - }, - { - key: 'settings', - icon: SettingOutlined, - text: '系统配置', - path: '/settings', - color: designTokens.colors.success.main, - }, -]; - function Dashboard() { - const [stats, setStats] = useState({ - totalDevices: 0, - totalRacks: 0, - totalRooms: 0, - faultDevices: 0, - deviceGrowth: 2.5, - faultTrend: -12.3, - onlineRate: 98.5, - powerUsage: 0, - totalUsers: 0, - activeTickets: 0, - }); - const [loading, setLoading] = useState(true); const [hoveredCard, setHoveredCard] = useState(null); - const [isRefreshing, setIsRefreshing] = useState(false); const [animatedKey, setAnimatedKey] = useState(0); - const fetchStats = useCallback(async () => { - try { - setIsRefreshing(true); + const { data: devicesData, isLoading: devicesLoading, mutate: mutateDevices } = useFetch( + '/devices?pageSize=1' + ); + const { data: racksData, isLoading: racksLoading } = useFetch('/racks?pageSize=1'); + const { data: roomsData, isLoading: roomsLoading } = useFetch('/rooms'); + const { data: usersData, isLoading: usersLoading } = useFetch('/users?pageSize=1'); + const { data: ticketsData, isLoading: ticketsLoading } = useFetch('/tickets?pageSize=1&status=open'); + const { data: faultData, isLoading: faultLoading } = useFetch('/devices?status=fault&pageSize=1'); - const [devicesRes, racksRes, roomsRes, usersRes, ticketsRes] = await Promise.all([ - api.get('/devices', { params: { pageSize: 1 } }), - api.get('/racks', { params: { pageSize: 1 } }), - api.get('/rooms'), - api.get('/users', { params: { pageSize: 1 } }), - api.get('/tickets', { params: { pageSize: 1, status: 'open' } }), - ]); + const loading = devicesLoading || racksLoading || roomsLoading || usersLoading || ticketsLoading; + const isRefreshing = devicesLoading && devicesData; - const totalDevices = devicesRes.total || 0; - const totalRacks = racksRes.total || 0; - const rooms = roomsRes || []; - const totalRooms = rooms.length; - const totalUsers = usersRes.total || 0; - const activeTickets = ticketsRes.total || 0; + const stats = useMemo(() => { + const totalDevices = devicesData?.total || 0; + const totalRacks = racksData?.total || 0; + const rooms = roomsData || []; + const totalRooms = Array.isArray(rooms) ? rooms.length : 0; + const totalUsers = usersData?.total || 0; + const activeTickets = ticketsData?.total || 0; + const faultDevices = faultData?.total || 0; - let faultDevices = 0; - if (totalDevices > 0) { - try { - const faultRes = await api.get('/devices', { - params: { status: 'fault', pageSize: 1 }, - }); - faultDevices = faultRes.total || 0; - } catch (error) { - message.warning('获取故障设备数失败,使用默认值'); - console.error('获取故障设备数失败:', error); - faultDevices = 0; - } - } + return { + totalDevices, + totalRacks, + totalRooms, + totalUsers, + activeTickets, + faultDevices, + deviceGrowth: 2.5, + faultTrend: -12.3, + onlineRate: + totalDevices > 0 + ? (((totalDevices - faultDevices) / totalDevices) * 100).toFixed(1) + : 100, + powerUsage: Math.floor(Math.random() * 5000) + 2000, + }; + }, [devicesData, racksData, roomsData, usersData, ticketsData, faultData]); - setStats({ - totalDevices, - totalRacks, - totalRooms, - totalUsers, - activeTickets, - faultDevices, - deviceGrowth: 2.5, - faultTrend: -12.3, - onlineRate: - totalDevices > 0 - ? (((totalDevices - faultDevices) / totalDevices) * 100).toFixed(1) - : 100, - powerUsage: Math.floor(Math.random() * 5000) + 2000, - }); + const handleRefresh = useCallback(async () => { + setAnimatedKey((prev) => prev + 1); + await Promise.all([ + mutateDevices(), + ]); + }, [mutateDevices]); - setAnimatedKey(prev => prev + 1); - } catch (error) { - message.error(`获取统计数据失败: ${error}`); - console.error('获取统计数据失败:', error); - } finally { - setLoading(false); - setIsRefreshing(false); - } + const handleHover = useCallback((key) => { + setHoveredCard(key); }, []); - useEffect(() => { - fetchStats(); - }, [fetchStats]); - - const handleNavHover = (e, isEnter, buttonKey) => { - if (isEnter) { - setHoveredCard(buttonKey); - } else { - setHoveredCard(null); - } - }; - - const handleRefresh = useCallback(() => { - fetchStats(); - }, [fetchStats]); - const statCards = useMemo( () => [ { @@ -749,7 +191,7 @@ function Dashboard() { lg: 6, xl: 4, icon: DatabaseOutlined, - color: designTokens.colors.purple.main, + color: '#722ed1', statKey: 'totalRacks', title: '总机柜数', trend: 0, @@ -796,7 +238,7 @@ function Dashboard() { lg: 6, xl: 4, icon: TeamOutlined, - color: designTokens.colors.cyan.main, + color: '#13c2c2', statKey: 'totalUsers', title: '用户总数', trend: 5.2, @@ -822,225 +264,6 @@ function Dashboard() { [stats.deviceGrowth, stats.faultTrend] ); - const renderStatCard = useCallback( - config => { - 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.hover } - : {}), - animationDelay: `${delay * 0.1}s`, - }; - - return ( - - setHoveredCard(statKey)} - onMouseLeave={() => setHoveredCard(null)} - styles={{ body: { padding: 'clamp(16px, 3vw, 24px)' } }} - > -
-
- - {title} - -
- -
-
-
- {loading ? ( - - ) : ( - - )} -
- {customStatus ? ( - statKey === 'totalRacks' ? ( -
- - 正常运行中 -
- ) : ( -
- - 全部在线 -
- ) - ) : ( -
0 ? designTokens.colors.success.main : designTokens.colors.error.main, - flexWrap: 'wrap', - gap: '4px', - }} - > - {trend > 0 ? ( - - ) : ( - - )} - {Math.abs(trend)}% - - 环比 - -
- )} -
-
- - ); - }, - [stats, loading, hoveredCard, animatedKey] - ); - - const navButtons = useMemo( - () => - navButtonsData.map(({ key, icon: Icon, text, color }) => ( -
b.key === key) * 0.1}s`, - }} - onMouseEnter={e => handleNavHover(e, true, `nav-${key}`)} - onMouseLeave={e => handleNavHover(e, false, `nav-${key}`)} - > -
- -
- - {text} - -
- )), - [hoveredCard] - ); - - const quickStats = useMemo( - () => [ - { - icon: LineChartOutlined, - label: '在线率', - value: `${stats.onlineRate}%`, - color: designTokens.colors.success.main, - }, - { - icon: SafetyOutlined, - label: '安全等级', - value: 'A级', - color: designTokens.colors.primary.main, - }, - { - icon: ThunderboltOutlined, - label: '功率使用', - value: `${stats.powerUsage}W`, - color: designTokens.colors.warning.main, - }, - ], - [stats.onlineRate, stats.powerUsage] - ); - const deviceTrendData = useMemo( () => [ { label: '周一', value: 45, color: designTokens.colors.primary.main }, @@ -1054,61 +277,6 @@ function Dashboard() { [] ); - const systemInfo = useMemo( - () => ( -
-
-
-

- 系统版本: v1.0.0 -

-

- 最后更新: - {new Date().toLocaleDateString()} -

-
- -
-
- ), - [handleRefresh, isRefreshing] - ); - const styles = ` @keyframes fadeInDown { from { @@ -1147,7 +315,17 @@ function Dashboard() {
- {statCards.map(renderStatCard)} + {statCards.map((config) => ( + + ))} @@ -1235,9 +413,9 @@ function Dashboard() { gap: '16px', }} > - + 周一 至 周日 设备变化趋势 - +
@@ -1282,71 +460,15 @@ function Dashboard() {

-
- {quickStats.map((stat, index) => ( -
-
- -
-
- - {stat.label} - -
- {stat.value} -
-
-
- ))} -
+ -
- {systemInfo} -
+ -
- {navButtons} -
+ diff --git a/frontend/src/pages/DeviceManagement.jsx b/frontend/src/pages/DeviceManagement.jsx index 2c5b222..1b92341 100644 --- a/frontend/src/pages/DeviceManagement.jsx +++ b/frontend/src/pages/DeviceManagement.jsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react'; +import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { Table, Button, @@ -6,17 +6,9 @@ import { Form, Input, Select, - DatePicker, message, Card, Space, - InputNumber, - Switch, - Upload, - Progress, - Checkbox, - Spin, - Dropdown, Tooltip, Tag, Row, @@ -28,38 +20,18 @@ import { DeleteOutlined, SearchOutlined, UploadOutlined, - DownloadOutlined, - SettingOutlined, - UndoOutlined, - CloudServerOutlined, - SafetyOutlined, - DatabaseOutlined, - AppstoreOutlined, - MoreOutlined, - ReloadOutlined, ExportOutlined, - FileExcelOutlined, - SwapOutlined, + SettingOutlined, + CloudServerOutlined, + ReloadOutlined, } from '@ant-design/icons'; import axios from 'axios'; -import dayjs from 'dayjs'; import { useNavigate, useSearchParams } from 'react-router-dom'; import { designTokens } from '../config/theme'; import { PAGINATION_CONFIG, DEBOUNCE_DELAY, - TABLE_SCROLL_CONFIG, DEFAULT_DEVICE_FIELDS, - BASE_FIELD_NAMES, - SYSTEM_FIELDS, - FIXED_FIELDS, - IMPORT_CONFIG, - EXPORT_CONFIG, - MODAL_CONFIG, - DEVICE_TYPE_OPTIONS, - DEVICE_STATUS_OPTIONS, - COLUMN_WIDTH_CONFIG, - EMPTY_STATE_CONFIG, STATUS_MAP, TYPE_MAP, } from '../constants/deviceManagementConstants'; @@ -72,177 +44,51 @@ import { titleTextStyle, pageTitleStyle, pageSubtitleStyle, - primaryActionStyle, secondaryActionStyle, - statsRowStyle, - statCardStyle, - statValueStyle, - statLabelStyle, - statCardRunningStyle, - statCardMaintenanceStyle, - statCardFaultStyle, cardStyle, filterCardStyle, modalHeaderStyle, - tableStyles, - searchInputStyle, - selectStyle, - refreshButtonStyle, - searchButtonStyle, - resetButtonStyle, - importModalStyles, - detailModalStyles, - exportModalStyles, - generateGlobalStyles, + tableContainerStyle, emptyStateStyle, emptyStateIconStyle, - tableContainerStyle, - resizableTitleStyles, + generateGlobalStyles, } from '../styles/deviceManagementStyles'; +import { + ResizableTitle, + DeviceDetailModal, + DeviceFormModal, + ImportModal, + ExportModal, + FieldConfigModal, + BatchStatusModal, +} from '../components/device'; +import { useDebounce } from '../hooks/useDebounce'; +import { + getDeviceTypeIcon, + getStatusConfig, + processDeviceData, +} from '../utils/deviceUtils.jsx'; const { Option } = Select; -const { RangePicker } = DatePicker; -// 防抖 Hook -function useDebounce(value, delay = DEBOUNCE_DELAY) { - const [debouncedValue, setDebouncedValue] = useState(value); - - useEffect(() => { - const handler = setTimeout(() => { - setDebouncedValue(value); - }, delay); - - return () => { - clearTimeout(handler); - }; - }, [value, delay]); - - return debouncedValue; -} - -// 工具函数提取到组件外部,避免每次渲染重复创建 -const getStatusConfig = status => { - const statusMap = { - running: { text: '运行中', color: 'green' }, - maintenance: { text: '维护中', color: 'orange' }, - offline: { text: '离线', color: 'gray' }, - fault: { text: '故障', color: 'red' }, - }; - return statusMap[status] || { text: status, color: 'black' }; -}; - -const getTypeLabel = type => { - const typeMap = { - server: '服务器', - switch: '交换机', - router: '路由器', - storage: '存储设备', - other: '其他设备', - }; - return typeMap[type] || type; -}; - -const getDeviceTypeIcon = type => { - const iconMap = { - server: , - switch: , - router: , - storage: , - other: , - }; - return iconMap[type] || ; -}; - -// 格式化日期 -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 {formattedDate}; - } - } - - return formattedDate; -}; - -// 使用从常量文件导入的默认设备字段配置 -const defaultDeviceFields = DEFAULT_DEVICE_FIELDS; - -// 简单的可调整列宽的表头组件 -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 ( - -
- - {children} - - {onResize && ( -
- )} -
- - ); -}; +const DEFAULT_DEVICE_FIELDS_LOCAL = DEFAULT_DEVICE_FIELDS; function DeviceManagement() { const navigate = useNavigate(); const [searchParams, setSearchParams] = useSearchParams(); + const [devices, setDevices] = useState([]); const [allDevices, setAllDevices] = useState([]); const [racks, setRacks] = useState([]); const [rooms, setRooms] = useState([]); - const [selectedRoomId, setSelectedRoomId] = useState(null); const [loading, setLoading] = useState(true); const [searching, setSearching] = useState(false); - const [modalVisible, setModalVisible] = useState(false); - const [editingDevice, setEditingDevice] = useState(null); - const [form] = Form.useForm(); - // 搜索和筛选状态 + const [keyword, setKeyword] = useState(''); const [status, setStatus] = useState('all'); const [type, setType] = useState('all'); const [searchForm] = Form.useForm(); - // 分页状态 - 使用常量配置 + const [pagination, setPagination] = useState({ current: 1, pageSize: PAGINATION_CONFIG.defaultPageSize, @@ -252,60 +98,35 @@ function DeviceManagement() { showTotal: PAGINATION_CONFIG.showTotal, }); - // 设备字段配置 const [deviceFields, setDeviceFields] = useState([]); const [loadingFields, setLoadingFields] = useState(true); - // 导入状态 + const [importModalVisible, setImportModalVisible] = useState(false); - const [importProgress, setImportProgress] = useState(0); - const [importPhase, setImportPhase] = useState(''); - const [isImporting, setIsImporting] = useState(false); - const [importResult, setImportResult] = useState(null); - const [selectedDevices, setSelectedDevices] = useState([]); const [detailModalVisible, setDetailModalVisible] = useState(false); const [selectedDevice, setSelectedDevice] = useState(null); - // 自定义字段状态 - const [customFieldName, setCustomFieldName] = useState(''); - const [customFieldValue, setCustomFieldValue] = useState(''); + const [modalVisible, setModalVisible] = useState(false); + const [editingDevice, setEditingDevice] = useState(null); - // 字段配置模态框 const [fieldConfigModalVisible, setFieldConfigModalVisible] = useState(false); - // 批量状态变更模态框 const [batchStatusModalVisible, setBatchStatusModalVisible] = useState(false); const [batchStatusLoading, setBatchStatusLoading] = useState(false); - const [batchStatusForm] = Form.useForm(); - // 导出选项模态框 const [exportModalVisible, setExportModalVisible] = useState(false); - const [exportFormat, setExportFormat] = useState('csv'); - const [exportScope, setExportScope] = useState('selected'); - const [exportFields, setExportFields] = useState([]); - const [exportLoading, setExportLoading] = useState(false); const [currentPageDevices, setCurrentPageDevices] = useState([]); - // 全选状态 + const [selectedDevices, setSelectedDevices] = useState([]); const [selectAll, setSelectAll] = useState(false); - - // 列宽状态 const [columnWidths, setColumnWidths] = useState({}); - // 防抖搜索关键词 - 使用常量配置的延迟时间 const debouncedKeyword = useDebounce(keyword, DEBOUNCE_DELAY); - // 使用 useMemo 缓存筛选后的设备数据(现在直接使用 allDevices,因为后端已经处理了筛选) - const filteredDevicesMemo = useMemo(() => { - return allDevices; - }, [allDevices]); - - // 获取所有设备(支持搜索、筛选和分页) const fetchDevices = useCallback( async (page = 1, pageSize = 10, forceRefresh = false) => { try { setLoading(true); - // 使用后端分页加载 const params = { page, pageSize, @@ -315,21 +136,12 @@ function DeviceManagement() { }; const response = await axios.get('/api/devices', { params }); - const { devices, total } = response.data; + const { devices: deviceList, total } = response.data; - // 处理设备数据,展开自定义字段 - const processedDevices = devices.map(device => { - const deviceWithFields = { ...device }; - if (device.customFields && typeof device.customFields === 'object') { - Object.entries(device.customFields).forEach(([fieldName, value]) => { - deviceWithFields[fieldName] = value; - }); - } - return deviceWithFields; - }); + const processedDevices = deviceList.map(processDeviceData); setAllDevices(processedDevices); - setPagination(prev => ({ ...prev, current: page, pageSize, total })); + setPagination((prev) => ({ ...prev, current: page, pageSize, total })); } catch (error) { message.error('获取设备列表失败'); console.error('获取设备列表失败:', error); @@ -340,158 +152,25 @@ function DeviceManagement() { [debouncedKeyword, status, type] ); - // 获取设备字段配置 const fetchDeviceFields = async () => { try { setLoadingFields(true); const response = await axios.get('/api/deviceFields'); - // 按顺序排序字段 const sortedFields = response.data.sort((a, b) => a.order - b.order); setDeviceFields(sortedFields); } catch (error) { message.error('获取字段配置失败'); console.error('获取字段配置失败:', error); - // 如果获取失败,使用默认字段配置 - setDeviceFields(defaultDeviceFields); + setDeviceFields(DEFAULT_DEVICE_FIELDS_LOCAL); } finally { setLoadingFields(false); } }; - // 默认设备字段配置 - const defaultDeviceFields = [ - { - fieldName: 'deviceId', - displayName: '设备ID', - fieldType: 'string', - required: false, - order: 1, - visible: false, - }, - { - fieldName: 'name', - displayName: '设备名称', - fieldType: 'string', - required: true, - order: 2, - visible: true, - }, - { - fieldName: 'type', - displayName: '设备类型', - fieldType: 'select', - required: true, - order: 3, - visible: true, - options: [ - { value: 'server', label: '服务器' }, - { value: 'switch', label: '交换机' }, - { value: 'router', label: '路由器' }, - { value: 'storage', label: '存储设备' }, - { value: 'other', label: '其他设备' }, - ], - }, - { - fieldName: 'model', - displayName: '型号', - fieldType: 'string', - required: false, - order: 4, - visible: true, - }, - { - fieldName: 'serialNumber', - displayName: '序列号', - fieldType: 'string', - required: true, - order: 5, - visible: true, - }, - { - fieldName: 'rackId', - displayName: '所在机柜', - fieldType: 'select', - required: true, - order: 6, - visible: true, - }, - { - fieldName: 'position', - displayName: '位置(U)', - fieldType: 'number', - required: true, - order: 7, - visible: true, - }, - { - fieldName: 'height', - displayName: '高度(U)', - fieldType: 'number', - required: true, - order: 8, - visible: true, - }, - { - fieldName: 'powerConsumption', - displayName: '功率(W)', - fieldType: 'number', - required: true, - order: 9, - visible: true, - }, - { - fieldName: 'status', - displayName: '状态', - fieldType: 'select', - required: true, - order: 10, - visible: true, - options: [ - { value: 'running', label: '运行中' }, - { value: 'maintenance', label: '维护中' }, - { value: 'offline', label: '离线' }, - { value: 'fault', label: '故障' }, - ], - }, - { - fieldName: 'purchaseDate', - displayName: '购买日期', - fieldType: 'date', - required: false, - order: 11, - visible: true, - }, - { - fieldName: 'warrantyExpiry', - displayName: '保修到期', - fieldType: 'date', - required: false, - order: 12, - visible: true, - }, - { - fieldName: 'ipAddress', - displayName: 'IP地址', - fieldType: 'string', - required: false, - order: 13, - visible: true, - }, - { - fieldName: 'description', - displayName: '描述', - fieldType: 'textarea', - required: false, - order: 14, - visible: true, - }, - ]; - - // 获取所有机柜 const fetchRacks = async () => { try { const response = await axios.get('/api/racks', { - params: { pageSize: 1000 } + params: { pageSize: 1000 }, }); setRacks(response.data.racks || []); } catch (error) { @@ -500,7 +179,6 @@ function DeviceManagement() { } }; - // 获取所有机房 const fetchRooms = async () => { try { const response = await axios.get('/api/rooms'); @@ -518,7 +196,6 @@ function DeviceManagement() { fetchDeviceFields(); }, [fetchDevices]); - // 处理URL参数中的设备ID,自动打开设备详情 useEffect(() => { const deviceIdFromUrl = searchParams.get('deviceId'); if (deviceIdFromUrl) { @@ -534,7 +211,7 @@ function DeviceManagement() { console.error('获取设备详情失败:', error); const errorStatus = error.response?.status; const errorMessage = error.response?.data?.error; - + if (errorStatus === 404 || errorMessage === '设备不存在') { message.warning('该设备已被删除,无法查看详情'); } else { @@ -547,145 +224,33 @@ function DeviceManagement() { } }, [searchParams, setSearchParams]); - // 同步当前页设备数据 useEffect(() => { - if (filteredDevicesMemo.length > 0) { + if (allDevices.length > 0) { const start = (pagination.current - 1) * pagination.pageSize; const end = start + pagination.pageSize; - const currentPageData = filteredDevicesMemo.slice(start, end); + const currentPageData = allDevices.slice(start, end); setCurrentPageDevices(currentPageData); } else { setCurrentPageDevices([]); } - }, [filteredDevicesMemo, pagination.current, pagination.pageSize]); + }, [allDevices, pagination.current, pagination.pageSize]); - // 打开模态框 const showModal = (device = null) => { setEditingDevice(device); - if (device) { - // 转换日期字段为dayjs格式 - const deviceData = { ...device }; - if (deviceData.purchaseDate) deviceData.purchaseDate = dayjs(deviceData.purchaseDate); - if (deviceData.warrantyExpiry) deviceData.warrantyExpiry = dayjs(deviceData.warrantyExpiry); - - // 定义设备模型的固定字段 - const fixedFields = [ - 'deviceId', - 'name', - 'type', - 'model', - 'serialNumber', - 'rackId', - 'position', - 'height', - 'powerConsumption', - 'status', - 'purchaseDate', - 'warrantyExpiry', - 'ipAddress', - 'description', - ]; - - // 定义需要排除的系统字段 - const systemFields = ['createdAt', 'updatedAt', 'Rack', 'Room', 'customFields']; - - // 创建一个干净的设备数据对象 - const cleanDeviceData = {}; - - // 复制固定字段 - fixedFields.forEach(field => { - if (deviceData[field] !== undefined) { - cleanDeviceData[field] = deviceData[field]; - } - }); - - // 将自定义字段直接添加到表单数据中(不在customFields对象内) - Object.entries(deviceData).forEach(([key, value]) => { - // 排除固定字段、系统字段和非基本类型的值 - if ( - !fixedFields.includes(key) && - !systemFields.includes(key) && - key !== 'deviceId' && - typeof value !== 'object' && - value !== null - ) { - cleanDeviceData[key] = value; - } - }); - - // 如果有原始customFields对象,将其字段也添加到表单数据中 - if (deviceData.customFields && typeof deviceData.customFields === 'object') { - Object.entries(deviceData.customFields).forEach(([key, value]) => { - cleanDeviceData[key] = value; - }); - } - - form.setFieldsValue(cleanDeviceData); - - // 编辑设备时,根据 rackId 找到对应的机房并设置 selectedRoomId - if (device.rackId) { - const rack = racks.find(r => r.rackId === device.rackId); - if (rack) { - setSelectedRoomId(rack.roomId); - } - } - } else { - form.resetFields(); - setSelectedRoomId(null); - } setModalVisible(true); }; - // 关闭模态框 const handleCancel = () => { setModalVisible(false); setEditingDevice(null); - setSelectedRoomId(null); }; - // 提交表单 - const handleSubmit = async values => { + const handleSubmit = async (deviceData) => { try { - const fixedFields = [ - 'deviceId', - 'name', - 'type', - 'model', - 'serialNumber', - 'rackId', - 'position', - 'height', - 'powerConsumption', - 'status', - 'purchaseDate', - 'warrantyExpiry', - 'ipAddress', - 'description', - 'roomId', - ]; - - 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 (!fixedFields.includes(key) && key !== 'customFields') { - deviceData.customFields[key] = deviceData[key]; - delete deviceData[key]; - } - }); - - delete deviceData.roomId; - if (editingDevice) { - // 更新设备 await axios.put(`/api/devices/${editingDevice.deviceId}`, deviceData); message.success('设备更新成功'); } else { - // 创建设备 await axios.post('/api/devices', deviceData); message.success('设备创建成功'); } @@ -700,22 +265,18 @@ function DeviceManagement() { } }; - // 搜索处理函数 - const handleSearch = values => { + const handleSearch = (values) => { setSearching(true); setKeyword(values.keyword || ''); setStatus(values.status || 'all'); setType(values.type || 'all'); - // 筛选后的数据会通过useMemo自动更新,这里只需更新分页 - setPagination(prev => ({ ...prev, current: 1 })); + setPagination((prev) => ({ ...prev, current: 1 })); - // 短暂延迟后移除搜索状态,提供视觉反馈 setTimeout(() => setSearching(false), 300); }; - // 重置筛选条件 const handleReset = () => { setSearching(true); @@ -724,23 +285,20 @@ function DeviceManagement() { setType('all'); searchForm.resetFields(); - // 短暂延迟后移除搜索状态 setTimeout(() => setSearching(false), 300); }; - // 表格分页变化处理 - const handleTableChange = newPagination => { + const handleTableChange = (newPagination) => { setPagination(newPagination); const start = (newPagination.current - 1) * newPagination.pageSize; const end = start + newPagination.pageSize; - const currentPageData = filteredDevicesMemo.slice(start, end); + const currentPageData = allDevices.slice(start, end); setCurrentPageDevices(currentPageData); fetchDevices(newPagination.current, newPagination.pageSize); }; - // 批量删除设备 const handleBatchDelete = async () => { Modal.confirm({ title: '批量删除确认', @@ -765,7 +323,6 @@ function DeviceManagement() { }); }; - // 一键删除所有设备 const handleDeleteAll = async () => { Modal.confirm({ title: '危险操作确认', @@ -789,8 +346,7 @@ function DeviceManagement() { }); }; - // 删除设备 - const handleDelete = async deviceId => { + const handleDelete = async (deviceId) => { Modal.confirm({ title: '确认删除', content: '确定要删除这个设备吗?', @@ -810,41 +366,37 @@ function DeviceManagement() { }); }; - // 显示设备详情 - const handleShowDetail = device => { + const handleShowDetail = (device) => { setSelectedDevice(device); setDetailModalVisible(true); }; - // 查看设备关联的工单 - const handleViewDeviceTickets = device => { - navigate(`/tickets?deviceId=${device.deviceId}&deviceName=${encodeURIComponent(device.name)}&serialNumber=${encodeURIComponent(device.serialNumber || '')}&view=true`); + const handleViewDeviceTickets = (device) => { + navigate( + `/tickets?deviceId=${device.deviceId}&deviceName=${encodeURIComponent(device.name)}&serialNumber=${encodeURIComponent(device.serialNumber || '')}&view=true` + ); }; - // 为设备创建工单 - const handleCreateTicketForDevice = device => { - navigate(`/tickets?deviceId=${device.deviceId}&deviceName=${encodeURIComponent(device.name)}&serialNumber=${encodeURIComponent(device.serialNumber || '')}&create=true`); + const handleCreateTicketForDevice = (device) => { + navigate( + `/tickets?deviceId=${device.deviceId}&deviceName=${encodeURIComponent(device.name)}&serialNumber=${encodeURIComponent(device.serialNumber || '')}&create=true` + ); }; - // 打开批量状态变更模态框 const showBatchStatusModal = () => { if (selectedDevices.length === 0) { message.warning('请先选择要操作的设备'); return; } - batchStatusForm.resetFields(); setBatchStatusModalVisible(true); }; - // 执行批量状态变更 - const handleBatchStatusChange = async () => { + const handleBatchStatusChange = async (newStatus) => { + setBatchStatusLoading(true); try { - const values = await batchStatusForm.validateFields(); - setBatchStatusLoading(true); - const response = await axios.put('/api/devices/batch-status', { deviceIds: selectedDevices, - status: values.status, + status: newStatus, }); message.success(response.data.message || '批量状态变更成功'); @@ -853,9 +405,6 @@ function DeviceManagement() { setSelectAll(false); fetchDevices(); } catch (error) { - if (error.errorFields) { - return; - } message.error('批量状态变更失败'); console.error('批量状态变更失败:', error); } finally { @@ -863,113 +412,61 @@ function DeviceManagement() { } }; - // 打开导出选项模态框 const showExportModal = () => { if (selectedDevices.length === 0) { message.warning('请先选择要导出的设备'); return; } - setExportFormat('csv'); - setExportFields( - deviceFields.filter(f => f.visible && f.fieldName !== 'rackId').map(f => f.fieldName) - ); setExportModalVisible(true); }; - // 执行增强导出 - const handleEnhancedExport = async () => { + const handleEnhancedExport = async ({ format, scope, fields }) => { + const fieldLabels = {}; + deviceFields.forEach((field) => { + fieldLabels[field.fieldName] = field.displayName; + }); + + let deviceIds = []; + if (scope === 'selected') { + deviceIds = selectedDevices; + } else if (scope === 'currentPage') { + deviceIds = allDevices.map((device) => device.deviceId); + } else if (scope === 'all') { + deviceIds = allDevices.map((device) => device.deviceId); + } + + if (deviceIds.length === 0) { + message.warning('没有可导出的设备'); + return; + } + + const params = new URLSearchParams(); + deviceIds.forEach((id) => params.append('deviceIds', id)); + params.append('format', format); + params.append('fields', JSON.stringify(fields)); + params.append('fieldLabels', JSON.stringify(fieldLabels)); + + const response = await axios.get(`/api/devices/enhanced-export?${params.toString()}`, { + responseType: 'blob', + }); + + const contentType = format === 'csv' ? 'text/csv; charset=gbk' : 'application/json'; + const blob = new Blob([response.data], { type: contentType }); + const url = window.URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = `devices_export_${new Date().toISOString().split('T')[0]}.${format}`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + window.URL.revokeObjectURL(url); + + message.success(`成功导出 ${deviceIds.length} 个设备`); + }; + + const handleImport = async (file, callbacks) => { try { - setExportLoading(true); - - const fieldLabels = {}; - deviceFields.forEach(field => { - fieldLabels[field.fieldName] = field.displayName; - }); - - let deviceIds = []; - if (exportScope === 'selected') { - deviceIds = selectedDevices; - } else if (exportScope === 'currentPage') { - deviceIds = filteredDevicesMemo.map(device => device.deviceId); - } else if (exportScope === 'all') { - deviceIds = allDevices.map(device => device.deviceId); - } - - if (deviceIds.length === 0) { - message.warning('没有可导出的设备'); - setExportLoading(false); - return; - } - - const params = new URLSearchParams(); - deviceIds.forEach(id => params.append('deviceIds', id)); - params.append('format', exportFormat); - params.append('fields', JSON.stringify(exportFields)); - params.append('fieldLabels', JSON.stringify(fieldLabels)); - - const response = await axios.get(`/api/devices/enhanced-export?${params.toString()}`, { - responseType: 'blob', - }); - - const contentType = exportFormat === 'csv' ? 'text/csv; charset=gbk' : 'application/json'; - const blob = new Blob([response.data], { type: contentType }); - const url = window.URL.createObjectURL(blob); - const link = document.createElement('a'); - link.href = url; - link.download = `devices_export_${new Date().toISOString().split('T')[0]}.${exportFormat}`; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - window.URL.revokeObjectURL(url); - - message.success(`成功导出 ${deviceIds.length} 个设备`); - setExportModalVisible(false); - } catch (error) { - message.error('导出失败'); - console.error('增强导出失败:', error); - } finally { - setExportLoading(false); - } - }; - - // 切换选择全部设备 - const handleSelectAll = () => { - if (selectAll) { - setSelectedDevices([]); - setSelectAll(false); - } else { - const allIds = filteredDevicesMemo.map(device => device.deviceId); - setSelectedDevices(allIds); - setSelectAll(true); - } - }; - - // 处理选择变化 - const handleSelectionChange = selectedRowKeys => { - setSelectedDevices(selectedRowKeys); - setSelectAll( - selectedRowKeys.length === filteredDevicesMemo.length && filteredDevicesMemo.length > 0 - ); - }; - - // 全选复选框的处理函数 - const handleSelectAllCheckbox = e => { - const checked = e.target.checked; - if (checked) { - const allIds = filteredDevicesMemo.map(device => device.deviceId); - setSelectedDevices(allIds); - setSelectAll(true); - } else { - setSelectedDevices([]); - setSelectAll(false); - } - }; - const handleImport = async file => { - try { - setIsImporting(true); - setImportProgress(0); - setImportPhase('正在上传文件...'); - setImportResult(null); + callbacks.onProgress(0, '正在上传文件...'); const formData = new FormData(); formData.append('csvFile', file); @@ -978,30 +475,23 @@ function DeviceManagement() { headers: { 'Content-Type': 'multipart/form-data', }, - onUploadProgress: progressEvent => { + onUploadProgress: (progressEvent) => { const progress = Math.round((progressEvent.loaded * 50) / progressEvent.total); - setImportProgress(Math.min(progress, 50)); - setImportPhase('正在上传文件...'); + callbacks.onProgress(Math.min(progress, 50), '正在上传文件...'); }, }); - setImportProgress(60); - setImportPhase('正在处理数据...'); + callbacks.onProgress(60, '正在处理数据...'); setTimeout(() => { - setImportProgress(80); - setImportPhase('正在验证数据...'); + callbacks.onProgress(80, '正在验证数据...'); }, 200); setTimeout(() => { - setImportProgress(90); - setImportPhase('正在保存数据...'); + callbacks.onProgress(90, '正在保存数据...'); }, 400); - setImportResult(response.data); - setImportProgress(100); - setImportPhase('导入完成'); - setIsImporting(false); + callbacks.onSuccess(response.data); const { success, failed } = response.data.statistics; if (failed > 0) { @@ -1014,9 +504,6 @@ function DeviceManagement() { fetchDevices(); }, 1000); } catch (error) { - setIsImporting(false); - setImportProgress(0); - let errorMessage = '导入失败'; let errorDetails = []; @@ -1030,7 +517,7 @@ function DeviceManagement() { Array.isArray(data.errors) && data.errors.length > 0 ) { - errorDetails = data.errors.map(err => ({ + errorDetails = data.errors.map((err) => ({ row: err.row || 0, error: err.error || err.message || '服务器内部错误', })); @@ -1048,69 +535,51 @@ function DeviceManagement() { errorMessage = error.message; } - setImportResult({ - success: false, - statistics: { - total: 0, - success: 0, - failed: errorDetails.length > 0 ? errorDetails.length : 1, - errors: errorDetails.length > 0 ? errorDetails : [{ row: 0, error: errorMessage }], - message: errorMessage, - }, + callbacks.onError({ + message: errorMessage, + errors: errorDetails, }); message.error(errorMessage); console.error('导入设备失败:', error); } - - return false; }; - // 使用从常量导入的映射,避免每次渲染重新创建 - - // 获取设备类型图标 - const getDeviceTypeIcon = type => { - const iconMap = { - server: , - switch: , - router: , - storage: , - other: , - }; - return iconMap[type] || ; + const handleSaveFieldConfig = async (updatedFields) => { + try { + const response = await axios.post('/api/deviceFields/config', updatedFields); + setDeviceFields(response.data); + message.success('字段配置保存成功'); + setFieldConfigModalVisible(false); + } catch (error) { + message.error('字段配置保存失败'); + console.error('保存字段配置失败:', error); + } }; - // 处理列宽变化 - const handleColumnResize = (key, width) => { - setColumnWidths(prev => ({ ...prev, [key]: width })); + const handleResetFieldConfig = (defaultFields) => { + setDeviceFields(defaultFields); }; - // 重置列宽到默认值 - const resetColumnWidths = () => { - setColumnWidths({}); - message.success('列宽已重置为默认值'); + const handleSelectionChange = (selectedRowKeys) => { + setSelectedDevices(selectedRowKeys); + setSelectAll(selectedRowKeys.length === allDevices.length && allDevices.length > 0); }; - // 处理表头单元格拖拽 - 自定义实现 - const handleHeaderCellResize = key => column => ({ + const handleHeaderCellResize = (key) => (column) => ({ width: column.width, - onResize: width => { - setColumnWidths(prev => ({ ...prev, [key]: width })); + onResize: (width) => { + setColumnWidths((prev) => ({ ...prev, [key]: width })); }, }); - // 动态生成表格列配置 - const columns = React.useMemo(() => { + const columns = useMemo(() => { const generatedColumns = []; - // 根据字段配置动态生成列,只显示visible为true的字段 - deviceFields.forEach(field => { - // 只处理可见字段 + deviceFields.forEach((field) => { if (!field.visible) return; - // 特殊处理机柜字段 if (field.fieldName === 'rackId') { - // 添加机房信息列 generatedColumns.push({ title: '所在机房', dataIndex: ['Rack', 'Room', 'name'], @@ -1118,7 +587,6 @@ function DeviceManagement() { width: columnWidths.roomName || 120, onHeaderCell: handleHeaderCellResize('roomName'), }); - // 添加机柜信息列 generatedColumns.push({ title: field.displayName, dataIndex: ['Rack', 'name'], @@ -1126,38 +594,32 @@ function DeviceManagement() { width: columnWidths[field.fieldName] || 120, onHeaderCell: handleHeaderCellResize(field.fieldName), }); - } - // 特殊处理设备类型 - else if (field.fieldName === 'type') { + } else if (field.fieldName === 'type') { generatedColumns.push({ title: field.displayName, dataIndex: field.fieldName, key: field.fieldName, width: columnWidths[field.fieldName] || 100, onHeaderCell: handleHeaderCellResize(field.fieldName), - render: type => { - return ( - - {getDeviceTypeIcon(type)} - {TYPE_MAP[type]} - - ); - }, + render: (type) => ( + + {getDeviceTypeIcon(type)} + {TYPE_MAP[type]} + + ), }); - } - // 特殊处理状态字段 - else if (field.fieldName === 'status') { + } else if (field.fieldName === 'status') { generatedColumns.push({ title: field.displayName, dataIndex: field.fieldName, key: field.fieldName, width: columnWidths[field.fieldName] || 100, onHeaderCell: handleHeaderCellResize(field.fieldName), - render: status => { + render: (status) => { if (Array.isArray(status)) { return ( - {status.map(s => ( + {status.map((s) => ( {STATUS_MAP[s]?.text || s} @@ -1172,29 +634,24 @@ function DeviceManagement() { ); }, }); - } - // 特殊处理日期字段 - else if (field.fieldType === 'date') { + } else if (field.fieldType === 'date') { generatedColumns.push({ title: field.displayName, dataIndex: field.fieldName, key: field.fieldName, width: columnWidths[field.fieldName] || 120, onHeaderCell: handleHeaderCellResize(field.fieldName), - render: date => { + render: (date) => { if (!date) return ''; const dateObj = new Date(date); const formattedDate = dateObj.toLocaleDateString('zh-CN'); - // 检查是否是保修到期字段 if (field.fieldName === 'warrantyExpiry') { const today = new Date(); - // 设置时间为同一天的00:00:00,确保只比较日期部分 today.setHours(0, 0, 0, 0); dateObj.setHours(0, 0, 0, 0); - // 如果保修日期已过期,显示为红色 if (dateObj < today) { return ( {formattedDate} @@ -1205,10 +662,7 @@ function DeviceManagement() { return formattedDate; }, }); - } - // 普通字段 - else { - // 为不同类型的字段设置不同的默认宽度 + } else { let defaultWidth = 120; if (field.fieldName === 'deviceId' || field.fieldName === 'name') { defaultWidth = 150; @@ -1220,7 +674,6 @@ function DeviceManagement() { defaultWidth = 180; } - // 为设备名称和ID列添加点击查看详情功能 const columnConfig = { title: field.displayName, dataIndex: field.fieldName, @@ -1232,7 +685,6 @@ function DeviceManagement() { ellipsis: field.fieldType !== 'textarea', }; - // 设备名称和ID列添加点击效果 if (field.fieldName === 'deviceId' || field.fieldName === 'name') { columnConfig.render = (value, record) => ( (e.target.style.textDecoration = 'underline')} - onMouseLeave={e => (e.target.style.textDecoration = 'none')} + onMouseEnter={(e) => (e.target.style.textDecoration = 'underline')} + onMouseLeave={(e) => (e.target.style.textDecoration = 'none')} > {value || '-'} @@ -1258,7 +710,6 @@ function DeviceManagement() { } }); - // 添加操作列 generatedColumns.push({ title: '操作', key: 'action', @@ -1294,37 +745,6 @@ function DeviceManagement() { return generatedColumns; }, [deviceFields, columnWidths]); - // 保存字段配置 - const handleSaveFieldConfig = async values => { - try { - 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, - })); - - const response = await axios.post('/api/deviceFields/config', updatedFields); - - setDeviceFields(response.data); - message.success('字段配置保存成功'); - setFieldConfigModalVisible(false); - } catch (error) { - message.error('字段配置保存失败'); - console.error('保存字段配置失败:', error); - } - }; - - // 重置字段配置为默认值 - const handleResetFieldConfig = () => { - const defaultFields = defaultDeviceFields; - setDeviceFields(defaultFields); - message.success('字段配置已重置为默认值'); - }; - - // 样式已从外部样式文件导入,无需在组件内定义 - return (
@@ -1462,7 +882,7 @@ function DeviceManagement() { transition: `all ${designTokens.transitions.fast}`, }} value={keyword} - onChange={e => setKeyword(e.target.value)} + onChange={(e) => setKeyword(e.target.value)} /> @@ -1471,7 +891,6 @@ function DeviceManagement() { value={status} onChange={setStatus} style={{ width: '140px', borderRadius: designTokens.borderRadius.medium }} - styles={{ popup: { root: { borderRadius: designTokens.borderRadius.medium } } }} > @@ -1486,7 +905,6 @@ function DeviceManagement() { value={type} onChange={setType} style={{ width: '140px', borderRadius: designTokens.borderRadius.medium }} - styles={{ popup: { root: { borderRadius: designTokens.borderRadius.medium } } }} > @@ -1544,7 +962,7 @@ function DeviceManagement() { - {!loading && filteredDevicesMemo.length === 0 && !searching && ( + {!loading && allDevices.length === 0 && !searching && (
暂无设备数据

)} - {filteredDevicesMemo.length > 0 && ( + {allDevices.length > 0 && (
`共 ${total} 条记录`, + showTotal: (total) => `共 ${total} 条记录`, style: { marginTop: '16px' }, }} onChange={handleTableChange} @@ -1591,7 +1009,7 @@ function DeviceManagement() { }} style={{ width: '100%', maxWidth: '100%' }} size="middle" - showHeader={filteredDevicesMemo.length > 0} + showHeader={allDevices.length > 0} rowSelection={{ selectedRowKeys: selectedDevices, onChange: handleSelectionChange, @@ -1604,7 +1022,7 @@ function DeviceManagement() { key: 'all', text: '全选', onSelect: () => { - const allIds = filteredDevicesMemo.map(device => device.deviceId); + const allIds = allDevices.map((device) => device.deviceId); setSelectedDevices(allIds); setSelectAll(true); }, @@ -1613,10 +1031,10 @@ function DeviceManagement() { key: 'invert', text: '反选', onSelect: () => { - const visibleIds = filteredDevicesMemo.map(device => device.deviceId); - const newSelected = visibleIds.filter(id => !selectedDevices.includes(id)); + const visibleIds = allDevices.map((device) => device.deviceId); + const newSelected = visibleIds.filter((id) => !selectedDevices.includes(id)); setSelectedDevices(newSelected); - setSelectAll(newSelected.length === filteredDevicesMemo.length); + setSelectAll(newSelected.length === allDevices.length); }, }, { @@ -1629,11 +1047,11 @@ function DeviceManagement() { }, ], }} - onRow={record => ({ + onRow={(record) => ({ onClick: () => handleSelectionChange( selectedDevices.includes(record.deviceId) - ? selectedDevices.filter(id => id !== record.deviceId) + ? selectedDevices.filter((id) => id !== record.deviceId) : [...selectedDevices, record.deviceId] ), })} @@ -1648,1171 +1066,62 @@ function DeviceManagement() { )} - - {editingDevice ? ( - - ) : ( - - )} - {editingDevice ? '编辑设备' : '添加设备'} - - } - open={modalVisible} + -
- {(() => { - const filteredFields = deviceFields.filter( - field => field.fieldName !== 'deviceId' && field.fieldName !== 'rackId' - ); - const formItems = []; + onSubmit={handleSubmit} + /> - filteredFields.forEach((field, index) => { - let control = null; - const inputStyle = { - borderRadius: '8px', - transition: 'all 0.3s ease', - }; - - switch (field.fieldType) { - case 'text': - case 'string': - control = ( - - ); - break; - case 'number': - control = ( - - ); - break; - case 'boolean': - control = ; - break; - case 'date': - control = ( - - ); - break; - case 'textarea': - control = ( - - ); - break; - case 'select': - control = ( - - ); - break; - default: - control = ( - - ); - } - - // 机房和机柜联动选择区域特殊处理 - if (field.fieldName === 'serialNumber') { - formItems.push( - -
- - {field.displayName} - {field.required && ( - * - )} - - } - rules={ - field.required - ? [{ required: true, message: `请输入${field.displayName}` }] - : [] - } - > - {control} - - - {/* 机房机柜联动选择区域 - 特殊突出显示 */} - -
-
- - 设备位置选择 -
- -
- - 机房 - * - - } - rules={[{ required: true, message: '请选择机房' }]} - style={{ marginBottom: '0' }} - > - - - - - - 机柜 - * - - } - rules={[{ required: true, message: '请选择机柜' }]} - style={{ marginBottom: '0' }} - > - - - - - - - - ); - } else if (field.fieldType === 'textarea') { - // textarea 占整行 - formItems.push( - - - {field.displayName} - {field.required && ( - * - )} - - } - rules={ - field.required - ? [{ required: true, message: `请输入${field.displayName}` }] - : [] - } - > - {control} - - - ); - } else { - // 其他字段两列布局 - formItems.push( - - - {field.displayName} - {field.required && ( - * - )} - - } - rules={ - field.required - ? [{ required: true, message: `请输入${field.displayName}` }] - : [] - } - > - {control} - - - ); - } - }); - - return {formItems}; - })()} - - {/* 底部按钮区域 */} -
- - -
- - - - - - 字段配置 - - } - open={fieldConfigModalVisible} - onCancel={() => setFieldConfigModalVisible(false)} - footer={null} - width={600} - styles={{ - header: { - borderBottom: '1px solid #f0f0f0', - padding: '16px 24px', - position: 'relative', - }, - body: { padding: '24px' }, - }} - > -
({ - ...acc, - [`visible_${field.fieldName}`]: field.visible, - [`required_${field.fieldName}`]: field.required, - }), - {} - ), - }} - > -
-
- - - - - - - - - {deviceFields - .filter(field => field.fieldName !== 'deviceId') - .map(field => ( - - - - - - ))} - -
字段名称可见必填
{field.displayName} - - - - - - - -
-
- - - - - - - - - - - - - - 导入设备 -
- } - open={importModalVisible} - onCancel={() => { - setImportModalVisible(false); - setImportProgress(0); - setImportPhase(''); - setImportResult(null); - setIsImporting(false); - }} - footer={null} - width={650} - destroyOnHidden - styles={{ - header: { - borderBottom: '1px solid #f0f0f0', - padding: '16px 24px', - position: 'relative', - }, - body: { padding: '24px' }, - }} - > - {!isImporting && !importResult ? ( -
-

请上传CSV格式的设备数据文件

-

- 支持的编码格式:GBK -

- -
-

- CSV文件格式要求: -

-
- {(() => { - // 获取必填字段 - const requiredFields = deviceFields.filter(f => f.visible && f.required); - // 获取可选字段 - const optionalFields = deviceFields.filter(f => f.visible && !f.required); - - return ( - <> - {requiredFields.length > 0 && ( -
- 必填字段: - - {requiredFields.map(f => f.displayName).join('、')} - -
- )} - {optionalFields.length > 0 && ( -
- 可选字段: - - {optionalFields.map(f => f.displayName).join('、')} - -
- )} - - ); - })()} -
    -
  • - 设备类型:server(服务器)、switch(交换机)、router(路由器)、storage(存储设备)、other(其他) -
  • -
  • 状态值:running(运行中)、maintenance(维护中)、offline(离线)、fault(故障)
  • -
  • 日期格式:YYYY-MM-DD (例如:2023-01-01)
  • -
-
-
- -
- - - - - 包含示例数据的CSV模板文件(根据当前字段配置生成) - -
- - - - -
- ) : isImporting ? ( -
-
-
- -
-
-

- 正在导入设备数据 -

-

{importPhase}

-
-
- `${importProgress}%`} - /> -
- ) : importResult?.statistics ? ( -
-

导入完成:

-
-
-
- {importResult.statistics.total || 0} -
-
总记录数
-
-
-
- {importResult.statistics.success || 0} -
-
成功
-
-
-
- {importResult.statistics.failed || 0} -
-
失败
-
-
- {(() => { - const errors = importResult.statistics?.errors; - const hasErrors = Array.isArray(errors) && errors.length > 0; - const hasFailedRecords = importResult.statistics?.failed > 0; - - return ( - (hasErrors || hasFailedRecords) && ( -
-

- 失败记录详情: -

- - {hasErrors ? ( - - - - - - - - - {errors.map((item, index) => ( - - - - - ))} - -
- 行号 - - 失败原因 -
- {item.row || index + 1} - - {item.error || '未知错误'} -
- ) : ( -
- 检测到 {importResult.statistics.failed} 条导入失败记录,但未提供详细错误信息 -
- )} -
- ) - ); - })()} - -
- ) : null} - - - - - 设备详情 -
- } - open={detailModalVisible} - onCancel={() => { + { setDetailModalVisible(false); setSelectedDevice(null); }} - footer={[ - , - , - , - , - ]} - width={700} - destroyOnHidden - styles={{ - header: { - borderBottom: '1px solid #f0f0f0', - padding: '16px 24px', - position: 'relative', - }, - body: { padding: '0', overflow: 'auto' }, - }} - > - {selectedDevice && ( -
- {/* 头部信息区域 */} -
-
-
- {getDeviceTypeIcon(selectedDevice.type)} -
-
-
- {selectedDevice.name} -
-
- {getTypeLabel(selectedDevice.type)} - | - {selectedDevice.deviceId} - | - - {selectedDevice.status ? getStatusConfig(selectedDevice.status).text : '-'} - -
-
-
-
+ onEdit={showModal} + onViewTickets={handleViewDeviceTickets} + onCreateTicket={handleCreateTicketForDevice} + /> - {/* 内容区域 */} -
- {/* 基本信息卡片 */} - 基本信息} - style={{ marginBottom: '16px', borderRadius: '8px' }} - > - - -
设备型号
-
{selectedDevice.model || '-'}
- - -
序列号
-
{selectedDevice.serialNumber || '-'}
- - -
IP地址
-
{selectedDevice.ipAddress || '-'}
- - -
所在机房
-
{selectedDevice.Rack?.Room?.name || '-'}
- - -
所在机柜
-
{selectedDevice.Rack?.name || '-'}
- - -
位置(U)
-
U{selectedDevice.position || '-'}
- - -
高度
-
{selectedDevice.height ? `${selectedDevice.height}U` : '-'}
- - -
功率
-
{selectedDevice.power ? `${selectedDevice.power}W` : '-'}
- - -
状态
-
- {selectedDevice.status ? getStatusConfig(selectedDevice.status).text : '-'} -
- -
-
+ setImportModalVisible(false)} + /> - {/* 维保信息卡片 */} - 维保信息} - style={{ marginBottom: '16px', borderRadius: '8px' }} - > - - -
购买日期
-
- {selectedDevice.purchaseDate - ? new Date(selectedDevice.purchaseDate).toLocaleDateString('zh-CN') - : '-'} -
- - -
保修到期
-
- {selectedDevice.warrantyExpiry - ? new Date(selectedDevice.warrantyExpiry).toLocaleDateString('zh-CN') - : '-'} -
- -
-
- - {/* 描述信息 */} - {selectedDevice.description && ( - 描述} - style={{ marginBottom: '16px', borderRadius: '8px' }} - > -
- {selectedDevice.description} -
-
- )} - - {/* 自定义字段卡片 */} - {selectedDevice.customFields && Object.keys(selectedDevice.customFields).length > 0 && ( - 自定义字段} - style={{ borderRadius: '8px' }} - > - - {Object.entries(selectedDevice.customFields).map(([key, value]) => { - // 从 deviceFields 中查找对应的中文显示名称 - const fieldConfig = deviceFields.find(f => f.fieldName === key); - const displayName = fieldConfig?.displayName || key; - return ( - -
{displayName}
-
{String(value)}
- - ); - })} -
-
- )} -
-
- )} - - - - - 批量状态变更 - - } - open={batchStatusModalVisible} - onCancel={() => setBatchStatusModalVisible(false)} - footer={[ - , - , - ]} - destroyOnHidden - styles={{ - header: { - borderBottom: '1px solid #f0f0f0', - padding: '16px 24px', - position: 'relative', - }, - body: { padding: '24px' }, - }} - > -
- - - -
- 已选择{' '} - {selectedDevices.length}{' '} - 个设备 -
-
-
- - - - 导出设备数据 - - } - open={exportModalVisible} + setExportModalVisible(false)} - footer={[ - , - , - ]} - destroyOnHidden - styles={{ - header: { - borderBottom: '1px solid #f0f0f0', - padding: '16px 24px', - position: 'relative', - }, - body: { padding: '24px' }, - }} - width={600} - > -
- - - - - - - -
- {deviceFields - .filter(f => f.visible && f.fieldName !== 'rackId') - .map(field => ( -
- { - if (e.target.checked) { - setExportFields([...exportFields, field.fieldName]); - } else { - setExportFields(exportFields.filter(f => f !== field.fieldName)); - } - }} - > - {field.displayName} - -
- ))} -
-
-
- 已选择{' '} - {selectedDevices.length}{' '} - 个设备, 将导出{' '} - {exportFields.length} 个字段 -
-
-
+ /> + + setFieldConfigModalVisible(false)} + /> + + setBatchStatusModalVisible(false)} + /> ); } diff --git a/frontend/src/pages/PortManagement.jsx b/frontend/src/pages/PortManagement.jsx index 5cd907b..7ad4571 100644 --- a/frontend/src/pages/PortManagement.jsx +++ b/frontend/src/pages/PortManagement.jsx @@ -509,7 +509,6 @@ function PortManagement() { if (errors.length > 0) { message.warning(`发现 ${errors.length} 条数据错误,已跳过`); - console.log('导入错误:', errors); } return validatedData; diff --git a/frontend/src/utils/deviceUtils.jsx b/frontend/src/utils/deviceUtils.jsx new file mode 100644 index 0000000..ae17ac0 --- /dev/null +++ b/frontend/src/utils/deviceUtils.jsx @@ -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: , + switch: , + router: , + storage: , + other: , + }; + return iconMap[type] || ; +}; + +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 {formattedDate}; + } + } + + 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; +}; diff --git a/modify.js b/modify.js deleted file mode 100644 index 618b07c..0000000 --- a/modify.js +++ /dev/null @@ -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); -}