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) => (
+
+ ))}
+
+
+ );
+};
+
+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)' } }}
+ >
+
+
+
+ {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.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()}
+
+
+
}
+ size="small"
+ onClick={onRefresh}
+ loading={isRefreshing}
+ style={{
+ background: designTokens.colors.primary.gradient,
+ border: 'none',
+ borderRadius: '8px',
+ boxShadow: '0 4px 12px rgba(24, 144, 255, 0.3)',
+ }}
+ >
+ 刷新数据
+
+
+
+
+ );
+};
+
+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"
+ >
+
+
+ );
+};
+
+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' },
+ }}
+ >
+
+
+
+
+
+
+
+
+
+ );
+};
+
+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)
+
+
+
+
+
+
+
+ }
+ block
+ style={{
+ height: '40px',
+ borderRadius: designTokens.borderRadius.small,
+ background: designTokens.colors.primary.gradient,
+ border: 'none',
+ color: '#ffffff',
+ boxShadow: designTokens.shadows.small,
+ fontWeight: '500',
+ display: 'inline-flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ }}
+ >
+ 选择CSV文件
+
+
+
+ ) : isImporting ? (
+
+
+
+
+
+
+
+ 正在导入设备数据
+
+
{importPhase}
+
+
+
+ ) : 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) => (
-
- ))}
-
-
- );
-};
-
-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.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)' } }}
- >
-
-
-
- {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()}
-
-
-
}
- size="small"
- onClick={handleRefresh}
- loading={isRefreshing}
- style={{
- background: designTokens.colors.primary.gradient,
- border: 'none',
- borderRadius: '8px',
- boxShadow: '0 4px 12px rgba(24, 144, 255, 0.3)',
- }}
- >
- 刷新数据
-
-
-
- ),
- [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}
+
-
- {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' },
- }}
- >
-
-
-
-
-
-
-
-
-
-
-
-
- 导入设备
-
- }
- 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)
-
-
-
-
-
-
-
- }
- block
- style={{
- height: '40px',
- borderRadius: designTokens.borderRadius.small,
- background: designTokens.colors.primary.gradient,
- border: 'none',
- color: '#ffffff',
- boxShadow: designTokens.shadows.small,
- fontWeight: '500',
- display: 'inline-flex',
- alignItems: 'center',
- justifyContent: 'center',
- }}
- >
- 选择CSV文件
-
-
-
- ) : isImporting ? (
-
-
-
-
-
-
-
- 正在导入设备数据
-
-
{importPhase}
-
-
-
- ) : 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);
-}