diff --git a/backend/.env.example b/backend/.env.example index 6278a36..dc545e9 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -56,4 +56,70 @@ JWT_SECRET=your-strong-secret-key-minimum-32-characters-change-in-production # Token 过期时间(格式:数字+单位,如 24h, 2h, 30m) # 建议:开发环境 24h,生产环境 2h 或更短 -TOKEN_EXPIRY=24h \ No newline at end of file +TOKEN_EXPIRY=24h + +# 密码加密强度(bcrypt salt rounds) +# 默认值:10,范围:4-12,值越大越安全但计算越慢 +SALT_ROUNDS=10 + +# 登录失败锁定阈值 +# 连续登录失败超过此次数后锁定账户 +MAX_LOGIN_ATTEMPTS=5 + +# 账户锁定时间(分钟) +# 登录失败锁定后的解锁等待时间 +LOCK_TIME_MINUTES=30 + +# 密码最小长度 +PASSWORD_MIN_LENGTH=6 + +# 用户名长度限制 +USERNAME_MIN_LENGTH=3 +USERNAME_MAX_LENGTH=50 + +# ============================================== +# API 配置 +# ============================================== + +# API 请求超时时间(毫秒) +API_TIMEOUT=30000 + +# 数据库查询超时时间(毫秒) +DB_QUERY_TIMEOUT=30000 + +# ============================================== +# 分页配置 +# ============================================== + +# 默认每页条数 +DEFAULT_PAGE_SIZE=10 + +# 最大每页条数 +MAX_PAGE_SIZE=1000 + +# ============================================== +# 文件上传配置 +# ============================================== + +# 最大文件上传大小(MB) +MAX_FILE_SIZE_MB=50 + +# 最大头像上传大小(MB) +MAX_AVATAR_SIZE_MB=5 + +# ============================================== +# 重试配置 +# ============================================== + +# 最大重试次数 +MAX_RETRIES=3 + +# 重试延迟(毫秒) +RETRY_DELAY=1000 + +# ============================================== +# 前端配置 +# ============================================== + +# 前端默认端口 +FRONTEND_PORT=3000 \ No newline at end of file diff --git a/backend/config/constants.js b/backend/config/constants.js new file mode 100644 index 0000000..26cdd1b --- /dev/null +++ b/backend/config/constants.js @@ -0,0 +1,39 @@ +/** + * 通用配置常量 + * 集中管理分页、文件上传、重试等配置 + */ + +module.exports = { + PAGINATION: { + DEFAULT_PAGE_SIZE: parseInt(process.env.DEFAULT_PAGE_SIZE, 10) || 10, + MAX_PAGE_SIZE: parseInt(process.env.MAX_PAGE_SIZE, 10) || 1000, + PAGE_SIZE_OPTIONS: [10, 20, 30, 50, 100], + }, + + FILE_UPLOAD: { + MAX_FILE_SIZE: (parseInt(process.env.MAX_FILE_SIZE_MB, 10) || 50) * 1024 * 1024, + MAX_AVATAR_SIZE: (parseInt(process.env.MAX_AVATAR_SIZE_MB, 10) || 5) * 1024 * 1024, + ALLOWED_IMAGE_TYPES: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'], + ALLOWED_DOC_TYPES: [ + 'application/pdf', + 'application/msword', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'application/vnd.ms-excel', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + ], + }, + + RETRY: { + MAX_RETRIES: parseInt(process.env.MAX_RETRIES, 10) || 3, + RETRY_DELAY: parseInt(process.env.RETRY_DELAY, 10) || 1000, + }, + + TIMEOUT: { + API_TIMEOUT: parseInt(process.env.API_TIMEOUT, 10) || 30000, + DB_QUERY_TIMEOUT: parseInt(process.env.DB_QUERY_TIMEOUT, 10) || 30000, + }, + + FRONTEND: { + DEFAULT_PORT: parseInt(process.env.FRONTEND_PORT, 10) || 3000, + }, +}; diff --git a/backend/config/index.js b/backend/config/index.js new file mode 100644 index 0000000..92217f5 --- /dev/null +++ b/backend/config/index.js @@ -0,0 +1,13 @@ +/** + * 配置统一导出 + */ + +const security = require('./security'); +const constants = require('./constants'); + +module.exports = { + ...security, + ...constants, + security, + constants, +}; diff --git a/backend/config/security.js b/backend/config/security.js new file mode 100644 index 0000000..a7b92d4 --- /dev/null +++ b/backend/config/security.js @@ -0,0 +1,19 @@ +/** + * 安全相关配置常量 + * 集中管理密码加密、登录限制等安全配置 + */ + +module.exports = { + SALT_ROUNDS: parseInt(process.env.SALT_ROUNDS, 10) || 10, + + MAX_LOGIN_ATTEMPTS: parseInt(process.env.MAX_LOGIN_ATTEMPTS, 10) || 5, + + LOCK_TIME: (parseInt(process.env.LOCK_TIME_MINUTES, 10) || 30) * 60 * 1000, + + TOKEN_EXPIRY: process.env.TOKEN_EXPIRY || '24h', + + PASSWORD_MIN_LENGTH: parseInt(process.env.PASSWORD_MIN_LENGTH, 10) || 6, + + USERNAME_MIN_LENGTH: parseInt(process.env.USERNAME_MIN_LENGTH, 10) || 3, + USERNAME_MAX_LENGTH: parseInt(process.env.USERNAME_MAX_LENGTH, 10) || 50, +}; diff --git a/backend/routes/auth.js b/backend/routes/auth.js index 113a15a..fa5ef38 100644 --- a/backend/routes/auth.js +++ b/backend/routes/auth.js @@ -4,11 +4,10 @@ const User = require('../models/User'); const Role = require('../models/Role'); const UserRole = require('../models/UserRole'); const { generateToken, authMiddleware } = require('../middleware/auth'); +const { SALT_ROUNDS, MAX_LOGIN_ATTEMPTS, PASSWORD_MIN_LENGTH, USERNAME_MIN_LENGTH, USERNAME_MAX_LENGTH } = require('../config'); const router = express.Router(); -const SALT_ROUNDS = 10; - const generateId = () => { return 'user_' + Date.now().toString(36) + Math.random().toString(36).substr(2, 9); }; @@ -24,17 +23,17 @@ router.post('/register', async (req, res) => { }); } - if (username.length < 3 || username.length > 20) { + if (username.length < USERNAME_MIN_LENGTH || username.length > USERNAME_MAX_LENGTH) { return res.status(400).json({ success: false, - message: '用户名长度必须在3-20个字符之间' + message: `用户名长度必须在${USERNAME_MIN_LENGTH}-${USERNAME_MAX_LENGTH}个字符之间` }); } - if (password.length < 6) { + if (password.length < PASSWORD_MIN_LENGTH) { return res.status(400).json({ success: false, - message: '密码长度不能少于6个字符' + message: `密码长度不能少于${PASSWORD_MIN_LENGTH}个字符` }); } @@ -175,7 +174,7 @@ router.post('/login', async (req, res) => { const isPasswordValid = await bcrypt.compare(password, user.password); if (!isPasswordValid) { user.loginCount = (user.loginCount || 0) + 1; - if (user.loginCount >= 5) { + if (user.loginCount >= MAX_LOGIN_ATTEMPTS) { user.status = 'locked'; } await user.save(); @@ -309,10 +308,10 @@ router.put('/password', authMiddleware, async (req, res) => { }); } - if (newPassword.length < 6) { + if (newPassword.length < PASSWORD_MIN_LENGTH) { return res.status(400).json({ success: false, - message: '新密码长度不能少于6个字符' + message: `新密码长度不能少于${PASSWORD_MIN_LENGTH}个字符` }); } diff --git a/backend/routes/consumables.js b/backend/routes/consumables.js index 3e55dd5..9765cf6 100644 --- a/backend/routes/consumables.js +++ b/backend/routes/consumables.js @@ -7,10 +7,11 @@ const Consumable = require('../models/Consumable'); const ConsumableRecord = require('../models/ConsumableRecord'); const ConsumableLog = require('../models/ConsumableLog'); const ConsumableLogArchive = require('../models/ConsumableLogArchive'); +const { PAGINATION, RETRY } = require('../config'); router.get('/', async (req, res) => { try { - const { keyword, category, status, page = 1, pageSize = 10 } = req.query; + const { keyword, category, status, page = 1, pageSize = PAGINATION.DEFAULT_PAGE_SIZE } = req.query; const offset = (page - 1) * pageSize; const where = {}; @@ -281,10 +282,9 @@ router.get('/inout/records', async (req, res) => { }); router.post('/quick-inout', async (req, res) => { - const MAX_RETRIES = 3; let attempt = 0; - while (attempt < MAX_RETRIES) { + while (attempt < RETRY.MAX_RETRIES) { const transaction = await sequelize.transaction(); try { const { consumableId, type, quantity, operator, reason, notes, snList } = req.body; @@ -348,7 +348,7 @@ router.post('/quick-inout', async (req, res) => { if (affectedRows === 0) { await transaction.rollback(); attempt++; - if (attempt >= MAX_RETRIES) { + if (attempt >= RETRY.MAX_RETRIES) { return res.status(409).json({ error: '并发冲突,请稍后重试' }); } continue; @@ -397,7 +397,7 @@ router.post('/quick-inout', async (req, res) => { return; } catch (error) { await transaction.rollback(); - if (attempt >= MAX_RETRIES - 1) { + if (attempt >= RETRY.MAX_RETRIES - 1) { return res.status(500).json({ error: error.message }); } attempt++; @@ -406,10 +406,9 @@ router.post('/quick-inout', async (req, res) => { }); router.post('/inout', async (req, res) => { - const MAX_RETRIES = 3; let attempt = 0; - while (attempt < MAX_RETRIES) { + while (attempt < RETRY.MAX_RETRIES) { const transaction = await sequelize.transaction(); try { const { consumableId, type, quantity, operator, reason, recipient, notes, snList } = req.body; @@ -470,7 +469,7 @@ router.post('/inout', async (req, res) => { if (affectedRows === 0) { await transaction.rollback(); attempt++; - if (attempt >= MAX_RETRIES) { + if (attempt >= RETRY.MAX_RETRIES) { return res.status(409).json({ error: '并发冲突,请稍后重试' }); } continue; @@ -520,7 +519,7 @@ router.post('/inout', async (req, res) => { return; } catch (error) { await transaction.rollback(); - if (attempt >= MAX_RETRIES - 1) { + if (attempt >= RETRY.MAX_RETRIES - 1) { return res.status(500).json({ error: error.message }); } attempt++; @@ -529,10 +528,9 @@ router.post('/inout', async (req, res) => { }); router.post('/adjust', async (req, res) => { - const MAX_RETRIES = 3; let attempt = 0; - while (attempt < MAX_RETRIES) { + while (attempt < RETRY.MAX_RETRIES) { const transaction = await sequelize.transaction(); try { const { consumableId, adjustType, quantity, operator, reason, notes } = req.body; @@ -582,7 +580,7 @@ router.post('/adjust', async (req, res) => { if (affectedRows === 0) { await transaction.rollback(); attempt++; - if (attempt >= MAX_RETRIES) { + if (attempt >= RETRY.MAX_RETRIES) { return res.status(409).json({ error: '并发冲突,请稍后重试' }); } continue; @@ -619,7 +617,7 @@ router.post('/adjust', async (req, res) => { return; } catch (error) { await transaction.rollback(); - if (attempt >= MAX_RETRIES - 1) { + if (attempt >= RETRY.MAX_RETRIES - 1) { return res.status(500).json({ error: error.message }); } attempt++; diff --git a/backend/routes/inventory.js b/backend/routes/inventory.js index 3055695..9f8247d 100644 --- a/backend/routes/inventory.js +++ b/backend/routes/inventory.js @@ -10,6 +10,7 @@ const Rack = require('../models/Rack'); const Room = require('../models/Room'); const User = require('../models/User'); const { authMiddleware, authorize } = require('../middleware/auth'); +const { PAGINATION } = require('../config'); InventoryTask.belongsTo(InventoryPlan, { foreignKey: 'planId', as: 'Plan' }); InventoryPlan.hasMany(InventoryTask, { foreignKey: 'planId', as: 'Tasks' }); @@ -41,7 +42,7 @@ router.use(authMiddleware); router.get('/plans', async (req, res) => { try { - const { status, page = 1, pageSize = 10, keyword } = req.query; + const { status, page = 1, pageSize = PAGINATION.DEFAULT_PAGE_SIZE, keyword } = req.query; const offset = (page - 1) * pageSize; const where = {}; diff --git a/backend/routes/systemSettings.js b/backend/routes/systemSettings.js index f8957b5..ef46aab 100644 --- a/backend/routes/systemSettings.js +++ b/backend/routes/systemSettings.js @@ -4,6 +4,7 @@ const fs = require('fs'); const path = require('path'); const { Op } = require('sequelize'); const SystemSetting = require('../models/SystemSetting'); +const { FRONTEND } = require('../config'); // 初始化默认系统设置 const initDefaultSettings = async () => { @@ -267,7 +268,7 @@ router.post('/reset/:key', async (req, res) => { idle_warning_time: 60, max_login_attempts: 5, maintenance_mode: false, - frontend_port: 3000, + frontend_port: FRONTEND.DEFAULT_PORT, primary_color: '#667eea', secondary_color: '#764ba2', compact_mode: false, @@ -563,7 +564,7 @@ router.get('/system/info', async (req, res) => { router.get('/frontend/port', async (req, res) => { try { const portSetting = await SystemSetting.findByPk('frontend_port'); - const port = portSetting ? JSON.parse(portSetting.settingValue) : 3000; + const port = portSetting ? JSON.parse(portSetting.settingValue) : FRONTEND.DEFAULT_PORT; res.json({ port }); } catch (error) { res.status(500).json({ error: error.message }); @@ -577,7 +578,7 @@ router.post('/frontend/port/sync', async (req, res) => { const path = require('path'); const portSetting = await SystemSetting.findByPk('frontend_port'); - const port = portSetting ? JSON.parse(portSetting.settingValue) : 3000; + const port = portSetting ? JSON.parse(portSetting.settingValue) : FRONTEND.DEFAULT_PORT; // 写入前端配置文件 const frontendDir = path.join(__dirname, '../../frontend'); diff --git a/backend/routes/users.js b/backend/routes/users.js index 032c070..2b6cd34 100644 --- a/backend/routes/users.js +++ b/backend/routes/users.js @@ -6,11 +6,10 @@ const User = require('../models/User'); const Role = require('../models/Role'); const UserRole = require('../models/UserRole'); const { authMiddleware } = require('../middleware/auth'); +const { SALT_ROUNDS, PASSWORD_MIN_LENGTH, FILE_UPLOAD, PAGINATION } = require('../config'); const router = express.Router(); -const SALT_ROUNDS = 10; - const generateId = () => { return 'user_' + Date.now().toString(36) + Math.random().toString(36).substr(2, 9); }; @@ -37,9 +36,9 @@ const { Op } = require('sequelize'); router.get('/', authMiddleware, async (req, res) => { try { - const { page = 1, pageSize = 10, username, status, realName } = req.query; + const { page = 1, pageSize = PAGINATION.DEFAULT_PAGE_SIZE, username, status, realName } = req.query; const offset = (parseInt(page) - 1) * parseInt(pageSize); - const limit = parseInt(pageSize); + const limit = Math.min(parseInt(pageSize), PAGINATION.MAX_PAGE_SIZE); const where = getWhereClause({ username, status, realName }); @@ -256,7 +255,7 @@ router.put('/:userId', authMiddleware, async (req, res) => { if (status !== undefined) user.status = status; if (remark !== undefined) user.remark = remark; - if (newPassword && newPassword.length >= 6) { + if (newPassword && newPassword.length >= PASSWORD_MIN_LENGTH) { user.password = await bcrypt.hash(newPassword, SALT_ROUNDS); } @@ -303,10 +302,10 @@ router.put('/:userId/password', authMiddleware, async (req, res) => { }); } - if (!newPassword || newPassword.length < 6) { + if (!newPassword || newPassword.length < PASSWORD_MIN_LENGTH) { return res.status(400).json({ success: false, - message: '密码长度不能少于6个字符' + message: `密码长度不能少于${PASSWORD_MIN_LENGTH}个字符` }); } @@ -388,10 +387,10 @@ router.post('/:userId/avatar', authMiddleware, async (req, res) => { }); } - if (avatarFile.size > 5 * 1024 * 1024) { + if (avatarFile.size > FILE_UPLOAD.MAX_AVATAR_SIZE) { return res.status(400).json({ success: false, - message: '图片大小不能超过 5MB' + message: `图片大小不能超过 ${FILE_UPLOAD.MAX_AVATAR_SIZE / 1024 / 1024}MB` }); } diff --git a/backend/server.js b/backend/server.js index 9a834d6..d2c19ff 100644 --- a/backend/server.js +++ b/backend/server.js @@ -3,15 +3,14 @@ const express = require('express'); const cors = require('cors'); const fileUpload = require('express-fileupload'); const { sequelize } = require('./db'); +const { FILE_UPLOAD } = require('./config'); -// 创建Express应用 const app = express(); const PORT = process.env.PORT || 8000; -// 中间件 app.use(cors()); app.use(express.json()); -app.use(fileUpload({ limits: { fileSize: 50 * 1024 * 1024 } })); +app.use(fileUpload({ limits: { fileSize: FILE_UPLOAD.MAX_FILE_SIZE } })); app.use('/temp', express.static('temp')); // 数据库连接已从db.js导入 diff --git a/frontend/.env.example b/frontend/.env.example index 8d5a4fb..9da5d1b 100644 --- a/frontend/.env.example +++ b/frontend/.env.example @@ -1,4 +1,18 @@ -# 用户空闲超时配置(毫秒) +# ============================================== +# API 配置 +# ============================================== + +# API 请求超时时间(毫秒) +VITE_API_TIMEOUT=30000 + +# API 基础路径 +VITE_API_BASE_URL=/api + +# ============================================== +# 用户空闲超时配置 +# ============================================== + +# 用户空闲超时时间(毫秒) # 默认30分钟 = 30 * 60 * 1000 = 1800000 VITE_IDLE_TIMEOUT=1800000 diff --git a/frontend/src/api/index.js b/frontend/src/api/index.js index 4ee3db5..6dd7a48 100644 --- a/frontend/src/api/index.js +++ b/frontend/src/api/index.js @@ -1,10 +1,9 @@ import axios from 'axios'; - -const API_BASE_URL = '/api'; +import { API_CONFIG } from '../config/api'; const api = axios.create({ - baseURL: API_BASE_URL, - timeout: 30000, + baseURL: API_CONFIG.baseURL, + timeout: API_CONFIG.timeout, headers: { 'Content-Type': 'application/json', }, diff --git a/frontend/src/config/api.js b/frontend/src/config/api.js new file mode 100644 index 0000000..ca21c47 --- /dev/null +++ b/frontend/src/config/api.js @@ -0,0 +1,25 @@ +/** + * API 相关配置 + * 集中管理 API 超时、分页、防抖等配置 + */ + +export const API_CONFIG = { + timeout: parseInt(import.meta.env.VITE_API_TIMEOUT, 10) || 30000, + + baseURL: import.meta.env.VITE_API_BASE_URL || '/api', + + pagination: { + defaultPageSize: 10, + maxPageSize: 1000, + pageSizeOptions: [10, 20, 30, 50, 100], + }, + + debounceDelay: 300, + + retry: { + maxRetries: 3, + retryDelay: 1000, + }, +}; + +export default API_CONFIG; diff --git a/frontend/src/config/theme.js b/frontend/src/config/theme.js index 0c6387a..6fe9151 100644 --- a/frontend/src/config/theme.js +++ b/frontend/src/config/theme.js @@ -89,6 +89,26 @@ export const designTokens = { lg: '24px', xl: '32px', }, + typography: { + xs: '12px', + sm: '13px', + base: '14px', + md: '16px', + lg: '18px', + xl: '20px', + '2xl': '24px', + '3xl': '32px', + '4xl': '40px', + }, + zIndex: { + dropdown: 1000, + sticky: 1020, + fixed: 1030, + modalBackdrop: 1040, + modal: 1050, + popover: 1060, + tooltip: 1070, + }, }; export default designTokens; diff --git a/frontend/src/hooks/useDebounce.js b/frontend/src/hooks/useDebounce.js index 74e201b..d8d9ee6 100644 --- a/frontend/src/hooks/useDebounce.js +++ b/frontend/src/hooks/useDebounce.js @@ -1,4 +1,5 @@ import { useState, useEffect } from 'react'; +import { API_CONFIG } from '../config/api'; /** * 防抖 Hook @@ -15,7 +16,7 @@ import { useState, useEffect } from 'react'; * fetchData(debouncedKeyword); * }, [debouncedKeyword]); */ -export function useDebounce(value, delay = 300) { +export function useDebounce(value, delay = API_CONFIG.debounceDelay) { const [debouncedValue, setDebouncedValue] = useState(value); useEffect(() => { @@ -44,7 +45,7 @@ export function useDebounce(value, delay = 300) { * * debouncedSearch(e.target.value)} /> */ -export function useDebouncedCallback(callback, delay = 300) { +export function useDebouncedCallback(callback, delay = API_CONFIG.debounceDelay) { const [timeoutId, setTimeoutId] = useState(null); const debouncedFn = (...args) => { diff --git a/frontend/src/pages/DeviceManagement.jsx b/frontend/src/pages/DeviceManagement.jsx index 57c1be1..9f1aa0e 100644 --- a/frontend/src/pages/DeviceManagement.jsx +++ b/frontend/src/pages/DeviceManagement.jsx @@ -1620,7 +1620,7 @@ function DeviceManagement() { +
{editingDevice ? ( ) : ( @@ -1635,7 +1635,11 @@ function DeviceManagement() { width={700} style={{ borderRadius: '16px' }} styles={{ - header: { borderBottom: '1px solid #f0f0f0', padding: '16px 50px 16px 24px' }, + header: { + borderBottom: '1px solid #f0f0f0', + padding: '16px 24px', + position: 'relative', + }, body: { padding: '24px' }, }} className="device-modal" @@ -1941,7 +1945,7 @@ function DeviceManagement() { +
字段配置
@@ -1951,7 +1955,11 @@ function DeviceManagement() { footer={null} width={600} styles={{ - header: { borderBottom: '1px solid #f0f0f0', padding: '16px 24px' }, + header: { + borderBottom: '1px solid #f0f0f0', + padding: '16px 24px', + position: 'relative', + }, body: { padding: '24px' }, }} > @@ -2036,7 +2044,7 @@ function DeviceManagement() { +
导入设备
@@ -2053,7 +2061,11 @@ function DeviceManagement() { width={650} destroyOnHidden styles={{ - header: { borderBottom: '1px solid #f0f0f0', padding: '16px 24px' }, + header: { + borderBottom: '1px solid #f0f0f0', + padding: '16px 24px', + position: 'relative', + }, body: { padding: '24px' }, }} > @@ -2369,7 +2381,7 @@ function DeviceManagement() { +
设备详情
@@ -2434,7 +2446,11 @@ function DeviceManagement() { width={700} destroyOnHidden styles={{ - header: { borderBottom: '1px solid #f0f0f0', padding: '16px 24px' }, + header: { + borderBottom: '1px solid #f0f0f0', + padding: '16px 24px', + position: 'relative', + }, body: { padding: '0', overflow: 'auto' }, }} > @@ -2599,7 +2615,7 @@ function DeviceManagement() { +
批量状态变更
@@ -2637,7 +2653,11 @@ function DeviceManagement() { ]} destroyOnHidden styles={{ - header: { borderBottom: '1px solid #f0f0f0', padding: '16px 24px' }, + header: { + borderBottom: '1px solid #f0f0f0', + padding: '16px 24px', + position: 'relative', + }, body: { padding: '24px' }, }} > @@ -2664,7 +2684,7 @@ function DeviceManagement() { +
导出设备数据
@@ -2702,7 +2722,11 @@ function DeviceManagement() { ]} destroyOnHidden styles={{ - header: { borderBottom: '1px solid #f0f0f0', padding: '16px 24px' }, + header: { + borderBottom: '1px solid #f0f0f0', + padding: '16px 24px', + position: 'relative', + }, body: { padding: '24px' }, }} width={600} diff --git a/frontend/src/pages/RackManagement.jsx b/frontend/src/pages/RackManagement.jsx index 66fbcd8..43de7d8 100644 --- a/frontend/src/pages/RackManagement.jsx +++ b/frontend/src/pages/RackManagement.jsx @@ -755,6 +755,23 @@ function RackManagement() { return (
+
+
@@ -1231,7 +1255,7 @@ function RackManagement() { +
diff --git a/frontend/src/pages/RoomManagement.jsx b/frontend/src/pages/RoomManagement.jsx index 036b06b..51db977 100644 --- a/frontend/src/pages/RoomManagement.jsx +++ b/frontend/src/pages/RoomManagement.jsx @@ -591,6 +591,23 @@ function RoomManagement() { return (
+
+
diff --git a/frontend/src/styles/deviceManagementStyles.js b/frontend/src/styles/deviceManagementStyles.js index 2b2f29c..a56d789 100644 --- a/frontend/src/styles/deviceManagementStyles.js +++ b/frontend/src/styles/deviceManagementStyles.js @@ -815,6 +815,24 @@ export const inputPlaceholders = { // CSS-in-JS 样式字符串生成函数 export const generateGlobalStyles = tokens => ` + /* Modal 关闭按钮通用修复 */ + .ant-modal-close { + top: 16px !important; + right: 16px !important; + width: 32px !important; + height: 32px !important; + line-height: 32px !important; + } + + .ant-modal-close-x { + width: 32px !important; + height: 32px !important; + line-height: 32px !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; + } + .device-modal .ant-modal-close { top: 16px; right: 24px;