refactor: 集中管理配置常量并优化样式一致性
This commit is contained in:
+67
-1
@@ -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
|
||||
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
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* 配置统一导出
|
||||
*/
|
||||
|
||||
const security = require('./security');
|
||||
const constants = require('./constants');
|
||||
|
||||
module.exports = {
|
||||
...security,
|
||||
...constants,
|
||||
security,
|
||||
constants,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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}个字符`
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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++;
|
||||
|
||||
@@ -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 = {};
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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`
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+2
-3
@@ -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导入
|
||||
|
||||
+15
-1
@@ -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
|
||||
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
*
|
||||
* <Input onChange={e => 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) => {
|
||||
|
||||
@@ -1620,7 +1620,7 @@ function DeviceManagement() {
|
||||
|
||||
<Modal
|
||||
title={
|
||||
<div style={modalHeaderStyle}>
|
||||
<div style={{ ...modalHeaderStyle, paddingRight: '32px' }}>
|
||||
{editingDevice ? (
|
||||
<EditOutlined style={{ color: '#667eea' }} />
|
||||
) : (
|
||||
@@ -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() {
|
||||
|
||||
<Modal
|
||||
title={
|
||||
<div style={modalHeaderStyle}>
|
||||
<div style={{ ...modalHeaderStyle, paddingRight: '32px' }}>
|
||||
<SettingOutlined style={{ color: '#667eea' }} />
|
||||
字段配置
|
||||
</div>
|
||||
@@ -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() {
|
||||
|
||||
<Modal
|
||||
title={
|
||||
<div style={modalHeaderStyle}>
|
||||
<div style={{ ...modalHeaderStyle, paddingRight: '32px' }}>
|
||||
<UploadOutlined style={{ color: '#667eea' }} />
|
||||
导入设备
|
||||
</div>
|
||||
@@ -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() {
|
||||
|
||||
<Modal
|
||||
title={
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', fontSize: '18px', fontWeight: 600 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', fontSize: '18px', fontWeight: 600, paddingRight: '32px' }}>
|
||||
<AppstoreOutlined style={{ color: '#667eea' }} />
|
||||
设备详情
|
||||
</div>
|
||||
@@ -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() {
|
||||
|
||||
<Modal
|
||||
title={
|
||||
<div style={modalHeaderStyle}>
|
||||
<div style={{ ...modalHeaderStyle, paddingRight: '32px' }}>
|
||||
<ReloadOutlined style={{ color: '#52c41a' }} />
|
||||
批量状态变更
|
||||
</div>
|
||||
@@ -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() {
|
||||
|
||||
<Modal
|
||||
title={
|
||||
<div style={modalHeaderStyle}>
|
||||
<div style={{ ...modalHeaderStyle, paddingRight: '32px' }}>
|
||||
<ExportOutlined style={{ color: '#fa8c16' }} />
|
||||
导出设备数据
|
||||
</div>
|
||||
@@ -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}
|
||||
|
||||
@@ -755,6 +755,23 @@ function RackManagement() {
|
||||
|
||||
return (
|
||||
<div style={containerStyle}>
|
||||
<style>{`
|
||||
.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;
|
||||
}
|
||||
`}</style>
|
||||
<div style={headerStyle}>
|
||||
<div
|
||||
style={{
|
||||
@@ -969,7 +986,7 @@ function RackManagement() {
|
||||
|
||||
<Modal
|
||||
title={
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', paddingRight: '32px' }}>
|
||||
<div
|
||||
style={{
|
||||
width: '4px',
|
||||
@@ -988,9 +1005,16 @@ function RackManagement() {
|
||||
destroyOnHidden
|
||||
styles={{
|
||||
body: { padding: '24px' },
|
||||
header: { borderBottom: '1px solid #f0f0f0', padding: '16px 50px 16px 24px' },
|
||||
header: {
|
||||
borderBottom: '1px solid #f0f0f0',
|
||||
padding: '16px 24px',
|
||||
position: 'relative',
|
||||
},
|
||||
}}
|
||||
style={{ borderRadius: '16px' }}
|
||||
classNames={{
|
||||
header: 'modal-header-fix',
|
||||
}}
|
||||
>
|
||||
<Form form={form} layout="vertical" onFinish={handleSubmit}>
|
||||
<Row gutter={16}>
|
||||
@@ -1231,7 +1255,7 @@ function RackManagement() {
|
||||
|
||||
<Modal
|
||||
title={
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', paddingRight: '32px' }}>
|
||||
<div
|
||||
style={{
|
||||
width: '4px',
|
||||
@@ -1256,7 +1280,11 @@ function RackManagement() {
|
||||
destroyOnHidden
|
||||
styles={{
|
||||
body: { padding: '24px' },
|
||||
header: { borderBottom: '1px solid #f0f0f0', padding: '16px 24px' },
|
||||
header: {
|
||||
borderBottom: '1px solid #f0f0f0',
|
||||
padding: '16px 24px',
|
||||
position: 'relative',
|
||||
},
|
||||
}}
|
||||
style={{ borderRadius: '16px' }}
|
||||
>
|
||||
|
||||
@@ -591,6 +591,23 @@ function RoomManagement() {
|
||||
|
||||
return (
|
||||
<div style={containerStyle}>
|
||||
<style>{`
|
||||
.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;
|
||||
}
|
||||
`}</style>
|
||||
<div style={headerStyle}>
|
||||
<div
|
||||
style={{
|
||||
@@ -765,7 +782,7 @@ function RoomManagement() {
|
||||
|
||||
<Modal
|
||||
title={
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', paddingRight: '32px' }}>
|
||||
<div
|
||||
style={{
|
||||
width: '4px',
|
||||
@@ -784,7 +801,11 @@ function RoomManagement() {
|
||||
destroyOnHidden
|
||||
styles={{
|
||||
body: { padding: '24px' },
|
||||
header: { borderBottom: '1px solid #f0f0f0', padding: '16px 24px' },
|
||||
header: {
|
||||
borderBottom: '1px solid #f0f0f0',
|
||||
padding: '16px 24px',
|
||||
position: 'relative',
|
||||
},
|
||||
}}
|
||||
style={{ borderRadius: '16px' }}
|
||||
>
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user