From a2f0032bad001102580d839a8043106bb991a35d Mon Sep 17 00:00:00 2001 From: zhang1106 <849185023@qq.com> Date: Tue, 20 Jan 2026 14:31:20 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E5=BA=93=E7=B4=A2=E5=BC=95=E4=BC=98=E5=8C=96=E6=9F=A5=E8=AF=A2?= =?UTF-8?q?=E6=80=A7=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit refactor(前端): 使用useMemo和useCallback优化性能 perf(后端): 优化统计查询性能 style: 统一前端样式定义 build: 添加创建索引脚本 --- .trae/rules/project_rules.md | 10 + backend/create_indexes.js | 165 ++++++++++++ backend/models/Consumable.js | 7 +- backend/models/ConsumableLog.js | 8 +- backend/models/ConsumableRecord.js | 7 +- backend/models/Device.js | 10 +- backend/models/LoginHistory.js | 8 +- backend/models/OperationLog.js | 9 +- backend/models/Rack.js | 7 +- backend/models/Room.js | 6 +- backend/models/User.js | 7 +- backend/package-lock.json | 49 ++++ backend/package.json | 5 +- backend/routes/consumableRecords.js | 64 ++--- backend/routes/consumables.js | 51 ++-- backend/routes/tickets.js | 117 ++++---- backend/routes/users.js | 38 ++- frontend/src/App.jsx | 265 +++++-------------- frontend/src/pages/Dashboard.jsx | 182 ++++++++----- frontend/src/pages/DeviceFieldManagement.jsx | 222 ++++++++-------- frontend/src/pages/DeviceManagement.jsx | 173 ++---------- frontend/src/pages/LoginHistory.jsx | 24 +- frontend/src/pages/RackManagement.jsx | 12 +- frontend/src/pages/TicketManagement.jsx | 6 +- frontend/src/pages/UserManagement.jsx | 52 ++-- 25 files changed, 803 insertions(+), 701 deletions(-) create mode 100644 .trae/rules/project_rules.md create mode 100644 backend/create_indexes.js diff --git a/.trae/rules/project_rules.md b/.trae/rules/project_rules.md new file mode 100644 index 0000000..0d98718 --- /dev/null +++ b/.trae/rules/project_rules.md @@ -0,0 +1,10 @@ +### 项目专属规则 +项目初始化:React+TS 优先用 Vite 创建(npm create vite@latest),默认配置 ESLint+Prettier,锁定包管理器为 pnpm。 +目录规范:src 下分 api/(接口)、components/(通用组件)、pages/(页面)、assets/(静态资源)、utils/(工具函数),禁止多层嵌套。 +版本管理:用 nvm 锁定 Node.js LTS 版本(如 v20.10.0),package.json 标注依赖版本(如 react@18.2.0),提交前执行 pnpm lint 校验。分支管理:主分支 main/develop,功能分支 feature/xxx,修复分支 fix/xxx,合并前必须通过单元测试(vitest 覆盖率≥80%)。 +静态资源:public 目录仅存无需打包的静态文件,src/assets 资源 import 引入,大文件(>1MB)用懒加载+CDN 备选方案。 +构建部署:本地构建 npm run build,生产环境加 --mode production,输出目录 dist 适配 Nginx 部署,配置 gzip 压缩。 + 文档要求:项目根目录必须有 README.md(含启动/部署步骤)、CHANGELOG.md(版本迭代记录),接口文档放 docs/api 目录。 +性能监控:接入 web-vitals 监测首屏加载/交互延迟,打包后用 bundle-analyzer 分析体积,按需拆分 chunk。 +环境配置:区分 .env.development/.env.production,敏感配置通过环境变量注入,禁止硬编码密钥。 +迭代规范:需求拆分按「原子化功能」,单分支迭代周期≤7天,提测前完成自测+代码评审,标注改动影响范围。 \ No newline at end of file diff --git a/backend/create_indexes.js b/backend/create_indexes.js new file mode 100644 index 0000000..0f9fe08 --- /dev/null +++ b/backend/create_indexes.js @@ -0,0 +1,165 @@ +const { sequelize } = require('./db'); + +const createIndexes = async () => { + const queryInterface = sequelize.getQueryInterface(); + + console.log('开始创建数据库索引...'); + + try { + // Device 表索引 + console.log('创建 devices 表索引...'); + await queryInterface.addIndex('devices', ['status']); + await queryInterface.addIndex('devices', ['type']); + await queryInterface.addIndex('devices', ['rackId']); + await queryInterface.addIndex('devices', ['createdAt']); + await queryInterface.addIndex('devices', ['status', 'type']); + await queryInterface.addIndex('devices', ['name']); + console.log(' ✓ devices 表索引创建完成'); + + // User 表索引 + console.log('创建 users 表索引...'); + await queryInterface.addIndex('users', ['status']); + await queryInterface.addIndex('users', ['username']); + await queryInterface.addIndex('users', ['email']); + console.log(' ✓ users 表索引创建完成'); + + // Consumable 表索引 + console.log('创建 consumables 表索引...'); + await queryInterface.addIndex('consumables', ['category']); + await queryInterface.addIndex('consumables', ['status']); + await queryInterface.addIndex('consumables', ['category', 'status']); + console.log(' ✓ consumables 表索引创建完成'); + + // ConsumableRecord 表索引 + console.log('创建 consumable_records 表索引...'); + await queryInterface.addIndex('consumable_records', ['consumableId']); + await queryInterface.addIndex('consumable_records', ['type']); + await queryInterface.addIndex('consumable_records', ['createdAt']); + console.log(' ✓ consumable_records 表索引创建完成'); + + // ConsumableLog 表索引 + console.log('创建 consumable_logs 表索引...'); + await queryInterface.addIndex('consumable_logs', ['consumableId']); + await queryInterface.addIndex('consumable_logs', ['operationType']); + await queryInterface.addIndex('consumable_logs', ['createdAt']); + await queryInterface.addIndex('consumable_logs', ['consumableId', 'createdAt']); + console.log(' ✓ consumable_logs 表索引创建完成'); + + // OperationLog 表索引 + console.log('创建 operation_logs 表索引...'); + await queryInterface.addIndex('operation_logs', ['userId']); + await queryInterface.addIndex('operation_logs', ['action']); + await queryInterface.addIndex('operation_logs', ['module']); + await queryInterface.addIndex('operation_logs', ['createdAt']); + await queryInterface.addIndex('operation_logs', ['userId', 'createdAt']); + console.log(' ✓ operation_logs 表索引创建完成'); + + // LoginHistory 表索引 + console.log('创建 login_histories 表索引...'); + await queryInterface.addIndex('login_histories', ['userId']); + await queryInterface.addIndex('login_histories', ['loginTime']); + await queryInterface.addIndex('login_histories', ['loginType']); + await queryInterface.addIndex('login_histories', ['userId', 'loginTime']); + console.log(' ✓ login_histories 表索引创建完成'); + + // Rack 表索引 + console.log('创建 racks 表索引...'); + await queryInterface.addIndex('racks', ['roomId']); + await queryInterface.addIndex('racks', ['status']); + await queryInterface.addIndex('racks', ['roomId', 'status']); + console.log(' ✓ racks 表索引创建完成'); + + // Room 表索引 + console.log('创建 rooms 表索引...'); + await queryInterface.addIndex('rooms', ['status']); + await queryInterface.addIndex('rooms', ['name']); + console.log(' ✓ rooms 表索引创建完成'); + + console.log('\n✅ 所有索引创建完成!'); + + } catch (error) { + console.error('创建索引失败:', error.message); + throw error; + } +}; + +const checkIndexes = async () => { + const queryInterface = sequelize.getQueryInterface(); + const tables = [ + 'devices', 'users', 'consumables', 'consumable_records', + 'consumable_logs', 'operation_logs', 'login_histories', + 'racks', 'rooms' + ]; + + console.log('\n检查现有索引...'); + for (const table of tables) { + try { + const indexes = await queryInterface.showIndex(table); + console.log(`\n${table} 表索引:`); + indexes.forEach(idx => { + console.log(` - ${idx.name}: [${idx.fields.join(', ')}]`); + }); + } catch (error) { + console.log(` ${table} 表检查失败: ${error.message}`); + } + } +}; + +const dropIndexes = async () => { + const queryInterface = sequelize.getQueryInterface(); + + console.log('开始删除自定义索引...'); + + try { + const indexDefinitions = [ + { table: 'devices', indexes: ['status', 'type', 'rackId', 'createdAt', 'status_type', 'name'] }, + { table: 'users', indexes: ['status', 'username', 'email'] }, + { table: 'consumables', indexes: ['category', 'status', 'category_status'] }, + { table: 'consumable_records', indexes: ['consumableId', 'type', 'createdAt'] }, + { table: 'consumable_logs', indexes: ['consumableId', 'operationType', 'createdAt', 'consumableId_createdAt'] }, + { table: 'operation_logs', indexes: ['userId', 'action', 'module', 'createdAt', 'userId_createdAt'] }, + { table: 'login_histories', indexes: ['userId', 'loginTime', 'loginType', 'userId_loginTime'] }, + { table: 'racks', indexes: ['roomId', 'status', 'roomId_status'] }, + { table: 'rooms', indexes: ['status', 'name'] } + ]; + + for (const def of indexDefinitions) { + console.log(`处理 ${def.table} 表...`); + const existingIndexes = await queryInterface.showIndex(def.table); + for (const existing of existingIndexes) { + if (def.indexes.includes(existing.name)) { + await queryInterface.removeIndex(def.table, existing.name); + console.log(` ✓ 删除索引: ${existing.name}`); + } + } + } + + console.log('\n✅ 索引删除完成!'); + } catch (error) { + console.error('删除索引失败:', error.message); + throw error; + } +}; + +module.exports = { createIndexes, checkIndexes, dropIndexes }; + +if (require.main === module) { + const command = process.argv[2] || 'create'; + + sequelize.authenticate() + .then(async () => { + console.log('数据库连接成功\n'); + if (command === 'check') { + await checkIndexes(); + } else if (command === 'drop') { + await dropIndexes(); + } else { + await createIndexes(); + } + await sequelize.close(); + }) + .catch(err => { + console.error('数据库连接失败:', err.message); + process.exit(1); + }); +} diff --git a/backend/models/Consumable.js b/backend/models/Consumable.js index ca69bdf..cd9f727 100644 --- a/backend/models/Consumable.js +++ b/backend/models/Consumable.js @@ -57,7 +57,12 @@ const Consumable = sequelize.define('Consumable', { } }, { tableName: 'consumables', - timestamps: true + timestamps: true, + indexes: [ + { fields: ['category'] }, + { fields: ['status'] }, + { fields: ['category', 'status'] } + ] }); module.exports = Consumable; diff --git a/backend/models/ConsumableLog.js b/backend/models/ConsumableLog.js index 7e49d9b..c470f29 100644 --- a/backend/models/ConsumableLog.js +++ b/backend/models/ConsumableLog.js @@ -57,7 +57,13 @@ const ConsumableLog = sequelize.define('ConsumableLog', { }, { tableName: 'consumable_logs', timestamps: true, - comment: '耗材操作日志表' + comment: '耗材操作日志表', + indexes: [ + { fields: ['consumableId'] }, + { fields: ['operationType'] }, + { fields: ['createdAt'] }, + { fields: ['consumableId', 'createdAt'] } + ] }); ConsumableLog.belongsTo(Consumable, { diff --git a/backend/models/ConsumableRecord.js b/backend/models/ConsumableRecord.js index d485b89..f454a53 100644 --- a/backend/models/ConsumableRecord.js +++ b/backend/models/ConsumableRecord.js @@ -49,7 +49,12 @@ const ConsumableRecord = sequelize.define('ConsumableRecord', { } }, { tableName: 'consumable_records', - timestamps: true + timestamps: true, + indexes: [ + { fields: ['consumableId'] }, + { fields: ['type'] }, + { fields: ['createdAt'] } + ] }); ConsumableRecord.belongsTo(Consumable, { diff --git a/backend/models/Device.js b/backend/models/Device.js index 425489d..ecdaa1c 100644 --- a/backend/models/Device.js +++ b/backend/models/Device.js @@ -72,7 +72,15 @@ const Device = sequelize.define('Device', { } }, { tableName: 'devices', - timestamps: true + timestamps: true, + indexes: [ + { fields: ['status'] }, + { fields: ['type'] }, + { fields: ['rackId'] }, + { fields: ['createdAt'] }, + { fields: ['status', 'type'] }, + { fields: ['name'] } + ] }); // 关联关系 diff --git a/backend/models/LoginHistory.js b/backend/models/LoginHistory.js index 63166fd..11ec7de 100644 --- a/backend/models/LoginHistory.js +++ b/backend/models/LoginHistory.js @@ -51,7 +51,13 @@ const LoginHistory = sequelize.define('LoginHistory', { tableName: 'login_histories', timestamps: true, createdAt: 'loginTime', - updatedAt: false + updatedAt: false, + indexes: [ + { fields: ['userId'] }, + { fields: ['loginTime'] }, + { fields: ['loginType'] }, + { fields: ['userId', 'loginTime'] } + ] }); module.exports = LoginHistory; diff --git a/backend/models/OperationLog.js b/backend/models/OperationLog.js index d217adc..9f28d80 100644 --- a/backend/models/OperationLog.js +++ b/backend/models/OperationLog.js @@ -76,7 +76,14 @@ const OperationLog = sequelize.define('OperationLog', { tableName: 'operation_logs', timestamps: true, createdAt: 'operateTime', - updatedAt: false + updatedAt: false, + indexes: [ + { fields: ['userId'] }, + { fields: ['action'] }, + { fields: ['module'] }, + { fields: ['createdAt'] }, + { fields: ['userId', 'createdAt'] } + ] }); module.exports = OperationLog; diff --git a/backend/models/Rack.js b/backend/models/Rack.js index 07dffe2..499fba2 100644 --- a/backend/models/Rack.js +++ b/backend/models/Rack.js @@ -40,7 +40,12 @@ const Rack = sequelize.define('Rack', { } }, { tableName: 'racks', - timestamps: true + timestamps: true, + indexes: [ + { fields: ['roomId'] }, + { fields: ['status'] }, + { fields: ['roomId', 'status'] } + ] }); // 关联关系 diff --git a/backend/models/Room.js b/backend/models/Room.js index 9f343fa..5f38978 100644 --- a/backend/models/Room.js +++ b/backend/models/Room.js @@ -33,7 +33,11 @@ const Room = sequelize.define('Room', { } }, { tableName: 'rooms', - timestamps: true + timestamps: true, + indexes: [ + { fields: ['status'] }, + { fields: ['name'] } + ] }); module.exports = Room; \ No newline at end of file diff --git a/backend/models/User.js b/backend/models/User.js index ac28830..c960569 100644 --- a/backend/models/User.js +++ b/backend/models/User.js @@ -57,7 +57,12 @@ const User = sequelize.define('User', { } }, { tableName: 'users', - timestamps: true + timestamps: true, + indexes: [ + { fields: ['status'] }, + { fields: ['username'] }, + { fields: ['email'] } + ] }); module.exports = User; diff --git a/backend/package-lock.json b/backend/package-lock.json index bb83662..68d668f 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "dependencies": { "bcryptjs": "^3.0.3", + "compression": "^1.7.5", "cors": "^2.8.5", "csv-parser": "^3.2.0", "csv-writer": "^1.6.0", @@ -2646,6 +2647,45 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -5771,6 +5811,15 @@ "node": ">= 0.8" } }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", diff --git a/backend/package.json b/backend/package.json index c1c88f9..ed5d0bc 100644 --- a/backend/package.json +++ b/backend/package.json @@ -4,7 +4,10 @@ "description": "IDC设备管理系统后端", "scripts": { "start": "node server.js", - "dev": "nodemon server.js" + "dev": "nodemon server.js", + "create-indexes": "node create_indexes.js", + "check-indexes": "node create_indexes.js check", + "drop-indexes": "node create_indexes.js drop" }, "dependencies": { "bcryptjs": "^3.0.3", diff --git a/backend/routes/consumableRecords.js b/backend/routes/consumableRecords.js index 77e68a8..b082702 100644 --- a/backend/routes/consumableRecords.js +++ b/backend/routes/consumableRecords.js @@ -124,40 +124,42 @@ router.post('/', async (req, res) => { router.get('/statistics', async (req, res) => { try { const { startDate, endDate } = req.query; - + const dateWhere = {}; if (startDate && endDate) { dateWhere.createdAt = { [Op.between]: [new Date(startDate), new Date(endDate)] }; } - - const inCount = await ConsumableRecord.count({ - where: { ...dateWhere, type: 'in' } - }); - - const outCount = await ConsumableRecord.count({ - where: { ...dateWhere, type: 'out' } - }); - - const inQuantity = await ConsumableRecord.sum('quantity', { - where: { ...dateWhere, type: 'in' } - }) || 0; - - const outQuantity = await ConsumableRecord.sum('quantity', { - where: { ...dateWhere, type: 'out' } - }) || 0; - - const byType = await ConsumableRecord.findAll({ + + const records = await ConsumableRecord.findAll({ where: dateWhere, - attributes: [ - 'type', - [sequelize.fn('SUM', sequelize.col('quantity')), 'totalQuantity'], - [sequelize.fn('COUNT', '*'), 'count'] - ], - group: ['type'] + attributes: ['type', 'quantity'] }); - + + let inCount = 0; + let outCount = 0; + let inQuantity = 0; + let outQuantity = 0; + const typeMap = {}; + + records.forEach(item => { + const qty = parseFloat(item.quantity) || 0; + if (item.type === 'in') { + inCount++; + inQuantity += qty; + } else if (item.type === 'out') { + outCount++; + outQuantity += qty; + } + + if (!typeMap[item.type]) { + typeMap[item.type] = { count: 0, totalQuantity: 0 }; + } + typeMap[item.type].count++; + typeMap[item.type].totalQuantity += qty; + }); + const recentRecords = await ConsumableRecord.findAll({ where: dateWhere, include: [ @@ -166,17 +168,17 @@ router.get('/statistics', async (req, res) => { order: [['createdAt', 'DESC']], limit: 10 }); - + res.json({ inCount, outCount, inQuantity, outQuantity, netQuantity: inQuantity - outQuantity, - byType: byType.map(item => ({ - type: item.type, - totalQuantity: item.dataValues.totalQuantity, - count: item.dataValues.count + byType: Object.entries(typeMap).map(([type, data]) => ({ + type, + totalQuantity: data.totalQuantity, + count: data.count })), recentRecords }); diff --git a/backend/routes/consumables.js b/backend/routes/consumables.js index 4512a4c..6ec5376 100644 --- a/backend/routes/consumables.js +++ b/backend/routes/consumables.js @@ -179,34 +179,43 @@ router.get('/low-stock', async (req, res) => { router.get('/statistics/summary', async (req, res) => { try { - const total = await Consumable.count(); - const lowStock = await Consumable.count({ - where: sequelize.where(sequelize.col('currentStock'), { - [Op.lte]: sequelize.col('minStock') - }) - }); - const consumables = await Consumable.findAll({ - attributes: ['currentStock', 'unitPrice'] + attributes: ['currentStock', 'unitPrice', 'category'] }); - - const totalValue = consumables.reduce((sum, item) => { - return sum + (parseFloat(item.currentStock) || 0) * (parseFloat(item.unitPrice) || 0); - }, 0); - - const byCategory = await Consumable.findAll({ - attributes: ['category', [sequelize.fn('COUNT', '*'), 'count']], - group: ['category'] + + let total = consumables.length; + let lowStock = 0; + let totalValue = 0; + const categoryMap = {}; + + consumables.forEach(item => { + const currentStock = parseFloat(item.currentStock) || 0; + const unitPrice = parseFloat(item.unitPrice) || 0; + + totalValue += currentStock * unitPrice; + + if (currentStock <= (parseFloat(item.minStock) || 0)) { + lowStock++; + } + + if (item.category) { + if (!categoryMap[item.category]) { + categoryMap[item.category] = 0; + } + categoryMap[item.category]++; + } }); - + + const byCategory = Object.entries(categoryMap).map(([category, count]) => ({ + category, + count + })); + res.json({ total, lowStock, totalValue: totalValue.toFixed(2), - byCategory: byCategory.map(item => ({ - category: item.category, - count: item.dataValues.count - })) + byCategory }); } catch (error) { res.status(500).json({ error: error.message }); diff --git a/backend/routes/tickets.js b/backend/routes/tickets.js index 98819d4..fe46e97 100644 --- a/backend/routes/tickets.js +++ b/backend/routes/tickets.js @@ -19,48 +19,60 @@ router.get('/stats', async (req, res) => { if (endDate) where.createdAt[Op.lte] = new Date(endDate + ' 23:59:59'); } - const total = await Ticket.count({ where }); + const Sequelize = require('sequelize'); - const statusStats = await Ticket.findAll({ - where, - attributes: [ - 'status', - [require('sequelize').fn('COUNT', '*'), 'count'] - ], - group: ['status'] - }); - - const priorityStats = await Ticket.findAll({ - where, - attributes: [ - 'priority', - [require('sequelize').fn('COUNT', '*'), 'count'] - ], - group: ['priority'] - }); - - const categoryStats = await Ticket.findAll({ - where, - attributes: [ - 'faultCategory', - [require('sequelize').fn('COUNT', '*'), 'count'] - ], - group: ['faultCategory'] - }); - - const monthlyStats = await Ticket.findAll({ - where, - attributes: [ - [dbDialect === 'mysql' - ? require('sequelize').fn('DATE_FORMAT', require('sequelize').col('createdAt'), '%Y-%m') - : require('sequelize').fn('strftime', '%Y-%m', require('sequelize').col('createdAt')), - 'month'], - [require('sequelize').fn('COUNT', '*'), 'count'] - ], - group: ['month'], - order: [['month', 'DESC']], - limit: 12 - }); + const [total, statusStats, priorityStats, categoryStats, monthlyStats, deviceStats, dailyStats] = await Promise.all([ + Ticket.count({ where }), + Ticket.findAll({ + where, + attributes: ['status', [Sequelize.fn('COUNT', '*'), 'count']], + group: ['status'] + }), + Ticket.findAll({ + where, + attributes: ['priority', [Sequelize.fn('COUNT', '*'), 'count']], + group: ['priority'] + }), + Ticket.findAll({ + where, + attributes: ['faultCategory', [Sequelize.fn('COUNT', '*'), 'count']], + group: ['faultCategory'] + }), + Ticket.findAll({ + where, + attributes: [ + [dbDialect === 'mysql' + ? Sequelize.fn('DATE_FORMAT', Sequelize.col('createdAt'), '%Y-%m') + : Sequelize.fn('strftime', '%Y-%m', Sequelize.col('createdAt')), + 'month'], + [Sequelize.fn('COUNT', '*'), 'count'] + ], + group: ['month'], + order: [['month', 'DESC']], + limit: 12 + }), + Ticket.findAll({ + where, + attributes: [ + 'deviceId', + 'deviceName', + [Sequelize.fn('COUNT', '*'), 'count'], + [Sequelize.fn('MAX', Sequelize.col('createdAt')), 'lastFaultTime'] + ], + group: ['deviceId', 'deviceName'], + order: [[Sequelize.fn('COUNT', '*'), 'DESC']], + limit: 10 + }), + Ticket.findAll({ + where, + attributes: [ + [Sequelize.fn('DATE', Sequelize.col('createdAt')), 'date'], + [Sequelize.fn('COUNT', '*'), 'created'] + ], + group: ['date'], + order: [['date', 'ASC']] + }) + ]); const statusData = statusStats.map(s => s.dataValues); const pending = statusData.find(s => s.status === 'pending')?.count || 0; @@ -88,19 +100,6 @@ router.get('/stats', async (req, res) => { avgTime: 0 })); - const deviceStats = await Ticket.findAll({ - where, - attributes: [ - 'deviceId', - 'deviceName', - [require('sequelize').fn('COUNT', '*'), 'count'], - [require('sequelize').fn('MAX', require('sequelize').col('createdAt')), 'lastFaultTime'] - ], - group: ['deviceId', 'deviceName'], - order: [[require('sequelize').fn('COUNT', '*'), 'DESC']], - limit: 10 - }); - const byDevice = deviceStats.map(d => ({ deviceId: d.deviceId, deviceName: d.deviceName, @@ -109,16 +108,6 @@ router.get('/stats', async (req, res) => { deviceType: '' })); - const dailyStats = await Ticket.findAll({ - where, - attributes: [ - [require('sequelize').fn('DATE', require('sequelize').col('createdAt')), 'date'], - [require('sequelize').fn('COUNT', '*'), 'created'] - ], - group: ['date'], - order: [['date', 'ASC']] - }); - const trend = dailyStats.map(d => ({ date: d.dataValues.date, created: d.dataValues.created, diff --git a/backend/routes/users.js b/backend/routes/users.js index 5fe5e44..5958cac 100644 --- a/backend/routes/users.js +++ b/backend/routes/users.js @@ -51,20 +51,38 @@ router.get('/', authMiddleware, async (req, res) => { order: [['createdAt', 'DESC']] }); - for (const user of users) { - const userRoles = await UserRole.findAll({ + if (users.length > 0) { + const userIds = users.map(u => u.userId); + const allUserRoles = await UserRole.findAll({ include: [{ model: Role, - where: { status: 'active' } + where: { status: 'active' }, + attributes: ['roleId', 'roleName', 'roleCode'] }], - where: { UserId: user.userId } + where: { + UserId: { [Op.in]: userIds } + } + }); + + const userRolesMap = {}; + allUserRoles.forEach(ur => { + if (!userRolesMap[ur.UserId]) { + userRolesMap[ur.UserId] = []; + } + userRolesMap[ur.UserId].push({ + roleId: ur.Role.roleId, + roleName: ur.Role.roleName, + roleCode: ur.Role.roleCode + }); + }); + + users.forEach(user => { + user.dataValues.roles = userRolesMap[user.userId] || []; + }); + } else { + users.forEach(user => { + user.dataValues.roles = []; }); - - user.dataValues.roles = userRoles.map(ur => ({ - roleId: ur.Role.roleId, - roleName: ur.Role.roleName, - roleCode: ur.Role.roleCode - })); } res.json({ diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index a2d9297..168fbcb 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -27,6 +27,36 @@ const SystemSettings = lazy(() => import('./pages/SystemSettings')); const { Header, Content, Sider } = Layout; +const PageLoading = () => ( +
+ + 正在加载页面... +
+); + +const AuthLoading = () => ( +
+ + 正在加载认证状态... +
+); + const designTokens = { colors: { primary: { @@ -81,50 +111,22 @@ const PrivateRoute = ({ children }) => { const location = useLocation(); if (!initialized) { - return ( -
- - 正在加载认证状态... -
- ); + return ; } if (!token) { return ; } - return ( - - - 正在加载页面... - - } - > - - {children} - - - ); + return {children}; }; +const ProtectedRoute = ({ component: Component }) => ( + + + +); + const AppLayout = ({ children }) => { const [collapsed, setCollapsed] = useState(false); const [activeKey, setActiveKey] = useState('dashboard'); @@ -464,173 +466,30 @@ const AppLayout = ({ children }) => { function App() { return ( - - - - 正在加载登录页面... - - } - > - - - } /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - } /> - + }> + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + ); } diff --git a/frontend/src/pages/Dashboard.jsx b/frontend/src/pages/Dashboard.jsx index edf330b..1a042a9 100644 --- a/frontend/src/pages/Dashboard.jsx +++ b/frontend/src/pages/Dashboard.jsx @@ -134,7 +134,65 @@ const subtitleStyle = { margin: '0' }; -const statCardStyle = (color) => ({ +const progressCardStyle = { + borderRadius: designTokens.borderRadius.large, + border: 'none', + boxShadow: designTokens.shadows.medium, + background: '#fff', + height: '100%', + animation: 'fadeInUp 0.6s ease-out 0.3s backwards' +}; + +const chartContainerStyle = { + padding: '20px', + borderRadius: designTokens.borderRadius.medium, + background: 'linear-gradient(135deg, #fafafa 0%, #f5f5f5 100%)', + border: '1px solid #f0f0f0' +}; + +const PIE_CHART_COLORS = { + success: designTokens.colors.success.main, + warning: designTokens.colors.warning.main, + error: designTokens.colors.error.main, + primary: designTokens.colors.primary.main +}; + +const pieChartStyle = { + width: '180px', + height: '180px', + borderRadius: '50%', + background: `conic-gradient( + ${PIE_CHART_COLORS.success} 0deg 216deg, + ${PIE_CHART_COLORS.warning} 216deg 288deg, + ${PIE_CHART_COLORS.error} 288deg 324deg, + ${PIE_CHART_COLORS.primary} 324deg 360deg + )`, + position: 'relative', + display: 'flex', + alignItems: 'center', + justifyContent: 'center' +}; + +const pieChartInner = { + width: '120px', + height: '120px', + borderRadius: '50%', + background: '#fff', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + 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, @@ -145,9 +203,9 @@ const statCardStyle = (color) => ({ cursor: 'pointer', height: '100%', animation: 'fadeInUp 0.6s ease-out backwards' -}); +}; -const statIconContainer = (color) => ({ +const STAT_ICON_CONTAINER_BASE = { position: 'absolute', top: '20px', right: '20px', @@ -157,19 +215,68 @@ const statIconContainer = (color) => ({ display: 'flex', alignItems: 'center', justifyContent: 'center', - background: `linear-gradient(135deg, ${color}20 0%, ${color}10 100%)`, fontSize: '32px', transition: `all ${designTokens.transitions.normal}` -}); +}; -const topBorderStyle = (color) => ({ +const TOP_BORDER_BASE = { position: 'absolute', top: 0, left: 0, right: 0, height: '4px', - background: `linear-gradient(90deg, ${color}, ${color}80)`, borderRadius: `${designTokens.borderRadius.large} ${designTokens.borderRadius.large} 0 0` +}; + +const NAV_BUTTON_BASE = { + 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 NAV_ICON_CONTAINER_BASE = { + width: '60px', + height: '60px', + borderRadius: designTokens.borderRadius.medium, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + fontSize: '28px', + transition: `all ${designTokens.transitions.normal}` +}; + +const createStatCardStyle = (color) => ({ + ...STAT_CARD_BASE_STYLE, + borderTop: `4px solid ${color}` +}); + +const createStatIconContainer = (color) => ({ + ...STAT_ICON_CONTAINER_BASE, + background: `linear-gradient(135deg, ${color}20 0%, ${color}10 100%)` +}); + +const createTopBorderStyle = (color) => ({ + ...TOP_BORDER_BASE, + background: `linear-gradient(90deg, ${color}, ${color}80)` +}); + +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 = { @@ -245,57 +352,6 @@ const quickStatItemStyle = { boxShadow: designTokens.shadows.small }; -const progressCardStyle = { - borderRadius: designTokens.borderRadius.large, - border: 'none', - boxShadow: designTokens.shadows.medium, - background: '#fff', - height: '100%', - animation: 'fadeInUp 0.6s ease-out 0.3s backwards' -}; - -const chartContainerStyle = { - padding: '20px', - borderRadius: designTokens.borderRadius.medium, - background: 'linear-gradient(135deg, #fafafa 0%, #f5f5f5 100%)', - border: '1px solid #f0f0f0' -}; - -const pieChartStyle = { - width: '180px', - height: '180px', - borderRadius: '50%', - background: `conic-gradient( - ${designTokens.colors.success.main} 0deg 216deg, - ${designTokens.colors.warning.main} 216deg 288deg, - ${designTokens.colors.error.main} 288deg 324deg, - ${designTokens.colors.primary.main} 324deg 360deg - )`, - position: 'relative', - display: 'flex', - alignItems: 'center', - justifyContent: 'center' -}; - -const pieChartInner = { - width: '120px', - height: '120px', - borderRadius: '50%', - background: '#fff', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - flexDirection: 'column' -}; - -const trendItemStyle = { - display: 'flex', - alignItems: 'center', - justifyContent: 'space-between', - padding: '12px 0', - borderBottom: '1px solid #f0f0f0' -}; - const AnimatedCounter = ({ value, duration = 1500 }) => { const [displayValue, setDisplayValue] = useState(0); const animationRef = useRef(null); @@ -680,7 +736,7 @@ function Dashboard() { const { icon: Icon, color, statKey, title, trend, tagColor, customStatus, xs, sm, lg, xl, delay } = config; const colProps = { xs, sm, lg, xl }; const cardStyle = { - ...statCardStyle(color), + ...createStatCardStyle(color), ...(hoveredCard === statKey ? { transform: 'translateY(-6px)', boxShadow: designTokens.shadows.hover } : {}), animationDelay: `${delay * 0.1}s` }; @@ -693,8 +749,8 @@ function Dashboard() { onMouseLeave={() => setHoveredCard(null)} >
-
-
+
+
handleNavHover(e, true, `nav-${key}`)} onMouseLeave={(e) => handleNavHover(e, false, `nav-${key}`)} > -
+
{text} diff --git a/frontend/src/pages/DeviceFieldManagement.jsx b/frontend/src/pages/DeviceFieldManagement.jsx index eae337c..d26f96c 100644 --- a/frontend/src/pages/DeviceFieldManagement.jsx +++ b/frontend/src/pages/DeviceFieldManagement.jsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useMemo } from 'react'; import { Table, Button, Modal, Form, Input, Select, message, Card, Space, InputNumber, Switch, Tag, Statistic } from 'antd'; import { PlusOutlined, EditOutlined, DeleteOutlined, AppstoreOutlined, FontSizeOutlined, NumberOutlined, CheckCircleOutlined, CalendarOutlined, FileTextOutlined } from '@ant-design/icons'; import axios from 'axios'; @@ -126,6 +126,92 @@ const tableStyle = { background: designTokens.colors.background.primary }; +const titleIconStyle = { + color: designTokens.colors.primary.main +}; + +const modalTitleStyle = { + fontWeight: '600' +}; + +const formLabelStyle = { + fontWeight: '500' +}; + +const tableCellStyle = { + fontWeight: '500', + color: designTokens.colors.text.primary +}; + +const typeTagStyle = { + border: 'none', + borderRadius: designTokens.borderRadius.small, + fontWeight: '500' +}; + +const orderBadgeStyle = { + background: designTokens.colors.background.tertiary, + padding: '2px 8px', + borderRadius: designTokens.borderRadius.small, + fontSize: '12px', + fontWeight: '500' +}; + +const editButtonStyle = { + color: designTokens.colors.primary.main, + height: '28px', + padding: '0 8px' +}; + +const deleteButtonStyle = { + height: '28px', + padding: '0 8px' +}; + +const formRowStyle = { + display: 'flex', + gap: designTokens.spacing.md +}; + +const formItemFlexStyle = { + flex: 1 +}; + +const textAreaStyle = { + fontFamily: 'monospace' +}; + +const modalBodyStyle = { + padding: designTokens.spacing.lg +}; + +const formActionsStyle = { + marginBottom: 0, + textAlign: 'right' +}; + +const modalStyle = { + borderRadius: designTokens.borderRadius.large +}; + +const FIELD_TYPE_MAP = { + string: { text: '文本', color: designTokens.colors.fieldType.string }, + number: { text: '数字', color: designTokens.colors.fieldType.number }, + boolean: { text: '布尔值', color: designTokens.colors.fieldType.boolean }, + select: { text: '下拉选择', color: designTokens.colors.fieldType.select }, + date: { text: '日期', color: designTokens.colors.fieldType.date }, + textarea: { text: '多行文本', color: designTokens.colors.fieldType.textarea } +}; + +const FIELD_TYPE_OPTIONS = [ + { value: 'string', label: '文本' }, + { value: 'number', label: '数字' }, + { value: 'boolean', label: '布尔值' }, + { value: 'select', label: '下拉选择' }, + { value: 'date', label: '日期' }, + { value: 'textarea', label: '多行文本' } +]; + function DeviceFieldManagement() { const [fields, setFields] = useState([]); const [loading, setLoading] = useState(true); @@ -225,17 +311,13 @@ function DeviceFieldManagement() { return iconMap[type] || ; }; - const columns = [ + const columns = useMemo(() => [ { title: '字段名称', dataIndex: 'fieldName', key: 'fieldName', width: 150, - render: (text) => ( - - {text} - - ) + render: (text) => {text} }, { title: '显示名称', @@ -249,25 +331,9 @@ function DeviceFieldManagement() { key: 'fieldType', width: 110, render: (type) => { - const typeMap = { - string: { text: '文本', color: designTokens.colors.fieldType.string }, - number: { text: '数字', color: designTokens.colors.fieldType.number }, - boolean: { text: '布尔值', color: designTokens.colors.fieldType.boolean }, - select: { text: '下拉选择', color: designTokens.colors.fieldType.select }, - date: { text: '日期', color: designTokens.colors.fieldType.date }, - textarea: { text: '多行文本', color: designTokens.colors.fieldType.textarea } - }; - const config = typeMap[type] || { text: type, color: designTokens.colors.text.tertiary }; + const config = FIELD_TYPE_MAP[type] || { text: type, color: designTokens.colors.text.tertiary }; return ( - + {getFieldTypeIcon(type)} {config.text} @@ -282,7 +348,7 @@ function DeviceFieldManagement() { render: (required) => ( {required ? '是' : '否'} @@ -296,7 +362,7 @@ function DeviceFieldManagement() { render: (visible) => ( {visible ? '是' : '否'} @@ -307,17 +373,7 @@ function DeviceFieldManagement() { dataIndex: 'order', key: 'order', width: 80, - render: (order) => ( - - {order} - - ) + render: (order) => {order} }, { title: '操作', @@ -326,48 +382,25 @@ function DeviceFieldManagement() { fixed: 'right', render: (_, record) => ( - - ), }, - ]; + ], []); return (
- + 设备字段管理
-
@@ -390,30 +423,18 @@ function DeviceFieldManagement() {
- {editingField ? '编辑字段' : '添加字段'} - - } + title={{editingField ? '编辑字段' : '添加字段'}} open={modalVisible} onCancel={handleCancel} footer={null} width={600} - styles={{ - body: { padding: designTokens.spacing.lg } - }} - style={{ - borderRadius: designTokens.borderRadius.large - }} + styles={{ body: modalBodyStyle }} + style={modalStyle} > -
+ 字段名称} + label={字段名称} rules={[{ required: true, message: '请输入字段名称' }]} > @@ -421,7 +442,7 @@ function DeviceFieldManagement() { 显示名称} + label={显示名称} rules={[{ required: true, message: '请输入显示名称' }]} > @@ -429,34 +450,31 @@ function DeviceFieldManagement() { 字段类型} + label={字段类型} rules={[{ required: true, message: '请选择字段类型' }]} > -
+
必填} + label={必填} valuePropName="checked" - style={{ flex: 1 }} + style={formItemFlexStyle} > 可见} + label={可见} valuePropName="checked" - style={{ flex: 1 }} + style={formItemFlexStyle} > @@ -464,7 +482,7 @@ function DeviceFieldManagement() { 显示顺序} + label={显示顺序} rules={[{ required: true, message: '请输入显示顺序' }]} > @@ -472,17 +490,13 @@ function DeviceFieldManagement() { 选项配置(JSON格式)} + label={选项配置(JSON格式)} tooltip="格式示例:[{value: 'option1', label: '选项1'}],仅下拉选择类型需要配置" > - + - + diff --git a/frontend/src/pages/DeviceManagement.jsx b/frontend/src/pages/DeviceManagement.jsx index 8a8237a..073e6ed 100644 --- a/frontend/src/pages/DeviceManagement.jsx +++ b/frontend/src/pages/DeviceManagement.jsx @@ -312,174 +312,51 @@ function DeviceManagement() { // 列宽状态 const [columnWidths, setColumnWidths] = useState({}); - // 缓存用于搜索的数据(避免重复处理) - const devicesCacheRef = useRef({ - timestamp: 0, - data: null, - TTL: 5 * 60 * 1000 // 缓存5分钟 - }); - // 防抖搜索关键词 const debouncedKeyword = useDebounce(keyword, 300); - // 预计算所有设备的搜索索引(提升搜索性能) - const searchIndexRef = useRef(new Map()); + // 使用 useMemo 缓存筛选后的设备数据(现在直接使用 allDevices,因为后端已经处理了筛选) + const filteredDevicesMemo = useMemo(() => { + return allDevices; + }, [allDevices]); - // 构建设备搜索索引 - const buildSearchIndex = useCallback((devices) => { - const index = new Map(); - devices.forEach((device, idx) => { - const searchableValues = []; - - // 收集所有基本类型字段值 - Object.entries(device).forEach(([key, value]) => { - if (value === null || value === undefined) return; - if (typeof value === 'object') { - // 收集嵌套对象值 - if (device.Rack?.name) searchableValues.push(String(device.Rack.name).toLowerCase()); - if (device.Rack?.Room?.name) searchableValues.push(String(device.Rack.Room.name).toLowerCase()); - // 收集自定义字段值 - if (device.customFields && typeof device.customFields === 'object') { - Object.values(device.customFields).forEach(cfValue => { - if (cfValue !== null && cfValue !== undefined && typeof cfValue !== 'object') { - searchableValues.push(String(cfValue).toLowerCase()); - } - }); - } - } else { - searchableValues.push(String(value).toLowerCase()); - } - }); - - index.set(idx, searchableValues); - }); - return index; - }, []); - - // 优化的全字段搜索函数 - const searchDevices = useCallback((devices, keyword) => { - if (!keyword || !keyword.trim()) { - return devices; - } - - const searchTerm = keyword.toLowerCase().trim(); - - return devices.filter((device, idx) => { - // 使用预计算的搜索索引 - let searchableValues = searchIndexRef.current.get(idx); - - if (!searchableValues) { - // 如果没有预计算索引,当场计算并缓存 - searchableValues = []; - Object.entries(device).forEach(([key, value]) => { - if (value === null || value === undefined) return; - if (typeof value === 'object') { - if (device.Rack?.name) searchableValues.push(String(device.Rack.name).toLowerCase()); - if (device.Rack?.Room?.name) searchableValues.push(String(device.Rack.Room.name).toLowerCase()); - if (device.customFields && typeof device.customFields === 'object') { - Object.values(device.customFields).forEach(cfValue => { - if (cfValue !== null && cfValue !== undefined && typeof cfValue !== 'object') { - searchableValues.push(String(cfValue).toLowerCase()); - } - }); - } - } else { - searchableValues.push(String(value).toLowerCase()); - } - }); - searchIndexRef.current.set(idx, searchableValues); - } - - return searchableValues.some(value => value.includes(searchTerm)); - }); - }, []); - - // 获取所有设备数据(不分页,用于本地搜索)- 使用缓存 - const fetchAllDevices = useCallback(async (forceRefresh = false) => { - const now = Date.now(); - const cache = devicesCacheRef.current; - - // 检查缓存是否有效 - if (!forceRefresh && cache.data && (now - cache.timestamp) < cache.TTL) { - return cache.data; - } - + // 获取所有设备(支持搜索、筛选和分页) + const fetchDevices = useCallback(async (page = 1, pageSize = 10, forceRefresh = false) => { try { - const response = await axios.get('/api/devices', { - params: { page: 1, pageSize: 99999 } - }); - const { devices } = response.data; + setLoading(true); - // 将customFields中的字段值映射为设备对象的直接属性 + // 使用后端分页加载 + const params = { + page, + pageSize, + keyword: debouncedKeyword || undefined, + status: status !== 'all' ? status : undefined, + type: type !== 'all' ? type : undefined + }; + + const response = await axios.get('/api/devices', { params }); + const { devices, 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; }); - // 更新缓存 - cache.data = processedDevices; - cache.timestamp = now; - - // 预计算搜索索引 - searchIndexRef.current = buildSearchIndex(processedDevices); - - return processedDevices; - } catch (error) { - console.error('获取所有设备数据失败:', error); - return cache.data || []; - } - }, [buildSearchIndex]); - - // 使用 useMemo 缓存筛选后的设备数据 - const filteredDevicesMemo = useMemo(() => { - if (!allDevices.length) return []; - - let result = allDevices; - - // 状态筛选 - if (status && status !== 'all') { - result = result.filter(device => device.status === status); - } - - // 类型筛选 - if (type && type !== 'all') { - result = result.filter(device => device.type === type); - } - - // 关键词搜索(使用防抖后的关键词) - if (debouncedKeyword && debouncedKeyword.trim()) { - result = searchDevices(result, debouncedKeyword); - } - - return result; - }, [allDevices, status, type, debouncedKeyword, searchDevices]); - - // 获取所有设备(支持搜索、筛选和分页)- 使用缓存和useCallback - const fetchDevices = useCallback(async (page = 1, pageSize = 10, forceRefresh = false) => { - try { - setLoading(true); - - // 先获取所有设备数据(使用缓存,批量删除后强制刷新) - const allData = await fetchAllDevices(forceRefresh); - setAllDevices(allData); - - // 更新分页信息(筛选后的数据会通过useMemo自动更新) - setPagination(prev => ({ ...prev, current: page, pageSize, total: filteredDevicesMemo.length })); + setAllDevices(processedDevices); + setPagination(prev => ({ ...prev, current: page, pageSize, total })); } catch (error) { message.error('获取设备列表失败'); console.error('获取设备列表失败:', error); } finally { setLoading(false); } - }, [fetchAllDevices, filteredDevicesMemo.length]); + }, [debouncedKeyword, status, type]); // 获取设备字段配置 const fetchDeviceFields = async () => { @@ -533,10 +410,10 @@ function DeviceManagement() { }; useEffect(() => { - fetchDevices(); + fetchDevices(1, pagination.pageSize); fetchRacks(); fetchDeviceFields(); - }, []); + }, [fetchDevices]); // 同步当前页设备数据 useEffect(() => { diff --git a/frontend/src/pages/LoginHistory.jsx b/frontend/src/pages/LoginHistory.jsx index 249c969..438078f 100644 --- a/frontend/src/pages/LoginHistory.jsx +++ b/frontend/src/pages/LoginHistory.jsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { Card, Table, Tag, Space, Button, DatePicker, Select, message, Popconfirm, Typography, Descriptions } from 'antd'; import { ReloadOutlined, DeleteOutlined, EyeOutlined, SafetyCertificateOutlined } from '@ant-design/icons'; import { loginHistoryAPI } from '../api'; @@ -17,7 +17,7 @@ const LoginHistory = () => { fetchHistories(); }, [pagination.current, filters]); - const fetchHistories = async () => { + const fetchHistories = useCallback(async () => { setLoading(true); try { const params = { @@ -35,14 +35,14 @@ const LoginHistory = () => { } finally { setLoading(false); } - }; + }, [pagination.current, pagination.pageSize, filters]); - const handleFilterChange = (key, value) => { + const handleFilterChange = useCallback((key, value) => { setFilters(prev => ({ ...prev, [key]: value })); setPagination(prev => ({ ...prev, current: 1 })); - }; + }, []); - const handleDateChange = (dates) => { + const handleDateChange = useCallback((dates) => { if (dates) { setFilters(prev => ({ ...prev, @@ -53,9 +53,9 @@ const LoginHistory = () => { setFilters(prev => ({ ...prev, startDate: undefined, endDate: undefined })); } setPagination(prev => ({ ...prev, current: 1 })); - }; + }, []); - const handleClear = async () => { + const handleClear = useCallback(async () => { try { const response = await loginHistoryAPI.clear({ days: 30 }); if (response.success) { @@ -65,9 +65,9 @@ const LoginHistory = () => { } catch (error) { message.error('清理失败'); } - }; + }, [fetchHistories]); - const columns = [ + const tableColumns = useMemo(() => [ { title: '用户名', dataIndex: 'username', @@ -128,7 +128,7 @@ const LoginHistory = () => { return browser; } } - ]; + ], []); const pageHeaderStyle = { marginBottom: '24px', @@ -172,7 +172,7 @@ const LoginHistory = () => { ({ +const statCardStyle = { background: 'rgba(255, 255, 255, 0.15)', borderRadius: designTokens.borderRadius.medium, padding: '16px', border: '1px solid rgba(255, 255, 255, 0.2)', backdropFilter: 'blur(10px)' -}); +}; const cardStyle = { borderRadius: designTokens.borderRadius.large, @@ -654,19 +654,19 @@ function RackManagement() {

-
+
总机柜
{stats.total}
-
+
在用机柜
{stats.active}
-
+
设备总数
{stats.totalDevices}
-
+
总功率
{(stats.totalPower / 1000).toFixed(1)}kW
diff --git a/frontend/src/pages/TicketManagement.jsx b/frontend/src/pages/TicketManagement.jsx index 68e0815..4b31c6a 100644 --- a/frontend/src/pages/TicketManagement.jsx +++ b/frontend/src/pages/TicketManagement.jsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useCallback } from 'react'; +import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { Table, Button, Modal, Form, Input, Select, DatePicker, message, Card, Space, Tag, Dropdown, Menu, Tabs, Timeline, Descriptions, Checkbox, Popover, InputNumber, Switch } from 'antd'; import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined, EyeOutlined, MoreOutlined, UserOutlined, ToolOutlined, CheckCircleOutlined, SyncOutlined, ClockCircleOutlined, CloseCircleOutlined, SettingOutlined } from '@ant-design/icons'; import axios from 'axios'; @@ -233,7 +233,7 @@ function TicketManagement() { ); }, [devices]); - const getTableColumns = useCallback(() => { + const tableColumns = useMemo(() => { const baseColumns = [ { title: '工单编号', dataIndex: 'ticketId', key: 'ticketId', width: 150, fixed: 'left' }, { title: '标题', dataIndex: 'title', key: 'title', width: 200, ellipsis: true }, @@ -609,7 +609,7 @@ function TicketManagement() {
{ fetchRoles(); }, [pagination.current]); - const fetchUsers = async () => { + const fetchUsers = useCallback(async () => { setLoading(true); try { const response = await userAPI.list({ @@ -42,9 +42,9 @@ const UserManagement = () => { } finally { setLoading(false); } - }; + }, [pagination.current, pagination.pageSize]); - const fetchRoles = async () => { + const fetchRoles = useCallback(async () => { try { const response = await roleAPI.all(); if (response.success) { @@ -56,15 +56,15 @@ const UserManagement = () => { console.error('获取角色列表失败:', error); message.error('获取角色列表失败,请检查网络连接'); } - }; + }, []); - const handleAdd = () => { + const handleAdd = useCallback(() => { setEditingUser(null); form.resetFields(); setModalVisible(true); - }; + }, []); - const handleEdit = (user) => { + const handleEdit = useCallback((user) => { setEditingUser(user); form.setFieldsValue({ username: user.username, @@ -75,20 +75,20 @@ const UserManagement = () => { roleIds: user.roles?.map(r => r.roleId) || [] }); setModalVisible(true); - }; + }, []); - const handleResetPassword = (user) => { + const handleResetPassword = useCallback((user) => { setPasswordUser(user); passwordForm.resetFields(); setPasswordModalVisible(true); - }; + }, []); - const handleAvatarClick = (user) => { + const handleAvatarClick = useCallback((user) => { setAvatarUser(user); setAvatarModalVisible(true); - }; + }, []); - const handleAvatarUpload = async (e) => { + const handleAvatarUpload = useCallback(async (e) => { const file = e.target.files[0]; if (!file) return; @@ -120,9 +120,9 @@ const UserManagement = () => { fileInputRef.current.value = ''; } } - }; + }, [avatarUser, fetchUsers]); - const handleAvatarDelete = async () => { + const handleAvatarDelete = useCallback(async () => { try { const response = await userAPI.deleteAvatar(avatarUser.userId); if (response.success) { @@ -135,9 +135,9 @@ const UserManagement = () => { } catch (error) { message.error('删除失败'); } - }; + }, [avatarUser, fetchUsers]); - const handleDelete = async (userId) => { + const handleDelete = useCallback(async (userId) => { try { const response = await userAPI.delete(userId); if (response.success) { @@ -149,9 +149,9 @@ const UserManagement = () => { } catch (error) { message.error('删除失败'); } - }; + }, [fetchUsers]); - const handleSubmit = async (values) => { + const handleSubmit = useCallback(async (values) => { try { let response; if (editingUser) { @@ -170,9 +170,9 @@ const UserManagement = () => { } catch (error) { message.error('操作失败'); } - }; + }, [editingUser, fetchUsers]); - const handleResetPasswordSubmit = async (values) => { + const handleResetPasswordSubmit = useCallback(async (values) => { try { const response = await userAPI.resetPassword(passwordUser.userId, values); if (response.success) { @@ -184,7 +184,7 @@ const UserManagement = () => { } catch (error) { message.error('重置失败'); } - }; + }, [passwordUser]); const getStatusColor = (status) => { const colors = { @@ -209,7 +209,7 @@ const UserManagement = () => { return user.avatar; }; - const columns = [ + const tableColumns = useMemo(() => [ { title: '头像', key: 'avatar', @@ -317,7 +317,7 @@ const UserManagement = () => { ) } - ]; + ], [handleAvatarClick, handleEdit, handleResetPassword, handleDelete]); const pageHeaderStyle = { marginBottom: '24px', @@ -426,7 +426,7 @@ const UserManagement = () => {