feat(设备管理): 使用函数替代映射表获取设备和状态信息
This commit is contained in:
@@ -13,7 +13,9 @@ const initDefaultSettings = async () => {
|
||||
{ settingKey: 'site_logo', settingValue: JSON.stringify(''), settingType: 'string', category: 'general', description: '网站Logo URL', isEditable: true },
|
||||
{ settingKey: 'timezone', settingValue: JSON.stringify('Asia/Shanghai'), settingType: 'string', category: 'general', description: '时区设置', isEditable: true },
|
||||
{ settingKey: 'date_format', settingValue: JSON.stringify('YYYY-MM-DD'), settingType: 'string', category: 'general', description: '日期格式', isEditable: true },
|
||||
{ settingKey: 'session_timeout', settingValue: JSON.stringify(30), settingType: 'number', category: 'general', description: '会话超时时间(分钟)', isEditable: true },
|
||||
{ settingKey: 'session_timeout', settingValue: JSON.stringify(30), settingType: 'number', category: 'general', description: '登录有效期(分钟)', isEditable: true },
|
||||
{ settingKey: 'idle_timeout', settingValue: JSON.stringify(10), settingType: 'number', category: 'general', description: '用户空闲超时时间(分钟)', isEditable: true },
|
||||
{ settingKey: 'idle_warning_time', settingValue: JSON.stringify(10), settingType: 'number', category: 'general', description: '空闲超时前警告时间(秒)', isEditable: false },
|
||||
{ settingKey: 'max_login_attempts', settingValue: JSON.stringify(5), settingType: 'number', category: 'general', description: '最大登录尝试次数', isEditable: true },
|
||||
{ settingKey: 'maintenance_mode', settingValue: JSON.stringify(false), settingType: 'boolean', category: 'general', description: '维护模式', isEditable: true },
|
||||
|
||||
@@ -97,6 +99,29 @@ router.get('/', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// 获取用户空闲超时配置(公开接口,供前端使用)
|
||||
// 注意:此路由必须在 /:key 之前定义,否则会被当作 key 参数处理
|
||||
router.get('/idle-timeout', async (req, res) => {
|
||||
try {
|
||||
const timeoutSetting = await SystemSetting.findByPk('idle_timeout');
|
||||
|
||||
// 默认配置
|
||||
const defaultTimeout = 10; // 10分钟
|
||||
const fixedWarningTime = 10; // 固定10秒警告时间
|
||||
|
||||
const timeout = timeoutSetting ? JSON.parse(timeoutSetting.settingValue) : defaultTimeout;
|
||||
|
||||
res.json({
|
||||
timeout: timeout * 60 * 1000, // 转换为毫秒
|
||||
warningTime: fixedWarningTime * 1000, // 固定10秒(转换为毫秒)
|
||||
timeoutMinutes: timeout,
|
||||
warningTimeSeconds: fixedWarningTime
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 获取单个设置
|
||||
router.get('/:key', async (req, res) => {
|
||||
try {
|
||||
@@ -238,6 +263,8 @@ router.post('/reset/:key', async (req, res) => {
|
||||
timezone: 'Asia/Shanghai',
|
||||
date_format: 'YYYY-MM-DD',
|
||||
session_timeout: 30,
|
||||
idle_timeout: 10,
|
||||
idle_warning_time: 10,
|
||||
max_login_attempts: 5,
|
||||
maintenance_mode: false,
|
||||
frontend_port: 3000,
|
||||
|
||||
@@ -17,7 +17,9 @@ const defaultSettings = [
|
||||
{ settingKey: 'site_logo', settingValue: JSON.stringify(''), settingType: 'string', category: 'general', description: '网站Logo URL', isEditable: true },
|
||||
{ settingKey: 'timezone', settingValue: JSON.stringify('Asia/Shanghai'), settingType: 'string', category: 'general', description: '时区设置', isEditable: true },
|
||||
{ settingKey: 'date_format', settingValue: JSON.stringify('YYYY-MM-DD'), settingType: 'string', category: 'general', description: '日期格式', isEditable: true },
|
||||
{ settingKey: 'session_timeout', settingValue: JSON.stringify(30), settingType: 'number', category: 'general', description: '会话超时时间(分钟)', isEditable: true },
|
||||
{ settingKey: 'session_timeout', settingValue: JSON.stringify(30), settingType: 'number', category: 'general', description: '登录有效期(分钟)', isEditable: true },
|
||||
{ settingKey: 'idle_timeout', settingValue: JSON.stringify(10), settingType: 'number', category: 'general', description: '用户空闲超时时间(分钟)', isEditable: true },
|
||||
{ settingKey: 'idle_warning_time', settingValue: JSON.stringify(10), settingType: 'number', category: 'general', description: '空闲超时前警告时间(秒)', isEditable: false },
|
||||
{ settingKey: 'max_login_attempts', settingValue: JSON.stringify(5), settingType: 'number', category: 'general', description: '最大登录尝试次数', isEditable: true },
|
||||
{ settingKey: 'maintenance_mode', settingValue: JSON.stringify(false), settingType: 'boolean', category: 'general', description: '维护模式', isEditable: true },
|
||||
|
||||
|
||||
@@ -53,6 +53,44 @@ sequelize.authenticate()
|
||||
.then(() => {
|
||||
console.log('系统设置初始化完成');
|
||||
})
|
||||
.then(() => {
|
||||
// 初始化故障分类数据
|
||||
console.log('开始初始化故障分类...');
|
||||
const FaultCategory = require('./models/FaultCategory');
|
||||
const defaultCategories = [
|
||||
{ name: '系统故障', description: '操作系统、应用程序等系统软件的故障问题', priority: 1, defaultPriority: 'high' },
|
||||
{ name: '硬件故障', description: '物理设备、服务器、存储等硬件设备的故障问题', priority: 2, defaultPriority: 'high' },
|
||||
{ name: '网络故障', description: '网络连接、交换机、路由器等网络相关故障', priority: 3, defaultPriority: 'high' },
|
||||
{ name: '软件故障', description: '应用程序错误、软件兼容性等问题', priority: 4, defaultPriority: 'medium' },
|
||||
{ name: '安全事件', description: '安全漏洞、入侵检测、权限异常等安全问题', priority: 5, defaultPriority: 'urgent' },
|
||||
{ name: '性能问题', description: '系统响应慢、资源利用率高等性能问题', priority: 6, defaultPriority: 'medium' },
|
||||
{ name: '配置变更', description: '系统配置、软件配置等变更需求', priority: 7, defaultPriority: 'low' },
|
||||
{ name: '例行维护', description: '定期维护、巡检、更新等计划性工作', priority: 8, defaultPriority: 'low' },
|
||||
{ name: '数据问题', description: '数据错误、数据丢失、数据同步等数据相关问题', priority: 9, defaultPriority: 'high' },
|
||||
{ name: '其他问题', description: '无法归类的其他问题', priority: 99, defaultPriority: 'medium' }
|
||||
];
|
||||
|
||||
return Promise.all(
|
||||
defaultCategories.map(async (cat) => {
|
||||
const existing = await FaultCategory.findOne({ where: { name: cat.name } });
|
||||
if (!existing) {
|
||||
const categoryId = `CAT${Date.now().toString(36).toUpperCase()}${Math.random().toString(36).substr(2, 4).toUpperCase()}`;
|
||||
await FaultCategory.create({
|
||||
categoryId,
|
||||
...cat,
|
||||
expectedDuration: 120,
|
||||
solutions: [],
|
||||
isSystem: true,
|
||||
isActive: true
|
||||
});
|
||||
console.log(`创建故障分类: ${cat.name}`);
|
||||
}
|
||||
})
|
||||
);
|
||||
})
|
||||
.then(() => {
|
||||
console.log('故障分类初始化完成');
|
||||
})
|
||||
.catch(err => console.error('数据库操作失败:', err));
|
||||
|
||||
// 导入路由
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# 用户空闲超时配置(毫秒)
|
||||
# 默认30分钟 = 30 * 60 * 1000 = 1800000
|
||||
VITE_IDLE_TIMEOUT=1800000
|
||||
|
||||
# 超时前警告时间(毫秒)
|
||||
# 默认1分钟 = 60 * 1000 = 60000
|
||||
VITE_IDLE_WARNING_TIME=60000
|
||||
+35
-1
@@ -1,4 +1,4 @@
|
||||
import React, { useState, Suspense, lazy } from 'react';
|
||||
import React, { useState, useEffect, Suspense, lazy } from 'react';
|
||||
import {
|
||||
Layout,
|
||||
Menu,
|
||||
@@ -48,6 +48,8 @@ import { useAuth } from './context/AuthContext';
|
||||
import { ConfigProvider, useConfig } from './context/ConfigContext';
|
||||
import { Scene3DProvider } from './context/Scene3DContext';
|
||||
import { useDesignTokens } from './hooks/useDesignTokens';
|
||||
import useIdleTimeout from './hooks/useIdleTimeout';
|
||||
import axios from 'axios';
|
||||
import { Spin } from 'antd';
|
||||
|
||||
const Dashboard = lazy(() => import('./pages/Dashboard'));
|
||||
@@ -127,15 +129,47 @@ const ProtectedRoute = ({ component: Component }) => (
|
||||
</PrivateRoute>
|
||||
);
|
||||
|
||||
// 默认空闲超时配置
|
||||
const DEFAULT_IDLE_CONFIG = {
|
||||
timeout: 10 * 60 * 1000, // 10分钟
|
||||
warningTime: 30 * 1000, // 30秒
|
||||
};
|
||||
|
||||
const AppLayout = ({ children }) => {
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const [activeKey, setActiveKey] = useState('dashboard');
|
||||
const [idleConfig, setIdleConfig] = useState(DEFAULT_IDLE_CONFIG);
|
||||
const { user, logout } = useAuth();
|
||||
const { config } = useConfig();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const designTokens = useDesignTokens();
|
||||
|
||||
// 获取空闲超时配置
|
||||
useEffect(() => {
|
||||
const fetchIdleConfig = async () => {
|
||||
try {
|
||||
const response = await axios.get('/api/system-settings/idle-timeout');
|
||||
setIdleConfig({
|
||||
timeout: response.data.timeout,
|
||||
warningTime: response.data.warningTime,
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn('获取空闲超时配置失败,使用默认配置:', error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchIdleConfig();
|
||||
}, []);
|
||||
|
||||
// 启用空闲超时检测
|
||||
useIdleTimeout({
|
||||
timeout: idleConfig.timeout,
|
||||
warningTime: idleConfig.warningTime,
|
||||
onLogout: logout,
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
const handleLogout = () => {
|
||||
logout();
|
||||
message.success('已退出登录');
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
import { useEffect, useRef, useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { message, Modal } from 'antd';
|
||||
|
||||
/**
|
||||
* 用户空闲超时检测 Hook
|
||||
* @param {Object} options - 配置选项
|
||||
* @param {number} options.timeout - 超时时间(毫秒),默认30分钟
|
||||
* @param {number} options.warningTime - 警告提示时间(毫秒),默认超时前1分钟
|
||||
* @param {Function} options.onLogout - 登出回调函数
|
||||
* @param {boolean} options.enabled - 是否启用,默认true
|
||||
*/
|
||||
const useIdleTimeout = ({
|
||||
timeout = 30 * 60 * 1000, // 默认30分钟
|
||||
warningTime = 60 * 1000, // 默认提前1分钟警告
|
||||
onLogout,
|
||||
enabled = true,
|
||||
} = {}) => {
|
||||
const navigate = useNavigate();
|
||||
const timerRef = useRef(null);
|
||||
const warningTimerRef = useRef(null);
|
||||
const modalRef = useRef(null);
|
||||
const lastActivityRef = useRef(Date.now());
|
||||
|
||||
// 清除所有定时器
|
||||
const clearTimers = useCallback(() => {
|
||||
if (timerRef.current) {
|
||||
clearTimeout(timerRef.current);
|
||||
timerRef.current = null;
|
||||
}
|
||||
if (warningTimerRef.current) {
|
||||
clearTimeout(warningTimerRef.current);
|
||||
warningTimerRef.current = null;
|
||||
}
|
||||
if (modalRef.current) {
|
||||
modalRef.current.destroy();
|
||||
modalRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 执行登出
|
||||
const handleLogout = useCallback(() => {
|
||||
clearTimers();
|
||||
|
||||
// 清除登录状态
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('user');
|
||||
|
||||
// 执行自定义登出回调
|
||||
if (onLogout) {
|
||||
onLogout();
|
||||
}
|
||||
|
||||
message.warning('由于长时间未操作,您已自动退出登录', 3);
|
||||
navigate('/login');
|
||||
}, [clearTimers, navigate, onLogout]);
|
||||
|
||||
// 显示超时警告
|
||||
const showWarning = useCallback(() => {
|
||||
let countdown = Math.ceil(warningTime / 1000);
|
||||
|
||||
modalRef.current = Modal.warning({
|
||||
title: '即将超时退出',
|
||||
content: `由于长时间未操作,系统将在 ${countdown} 秒后自动退出登录,请重新登录。`,
|
||||
okText: '保持登录',
|
||||
onOk: () => {
|
||||
// 用户点击保持登录,重置计时器
|
||||
resetTimer();
|
||||
},
|
||||
maskClosable: false,
|
||||
keyboard: false,
|
||||
});
|
||||
|
||||
// 更新倒计时
|
||||
const countdownInterval = setInterval(() => {
|
||||
countdown -= 1;
|
||||
if (modalRef.current && countdown > 0) {
|
||||
modalRef.current.update({
|
||||
content: `由于长时间未操作,系统将在 ${countdown} 秒后自动退出登录,请重新登录。`,
|
||||
});
|
||||
}
|
||||
if (countdown <= 0) {
|
||||
clearInterval(countdownInterval);
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
// 警告时间结束后自动登出
|
||||
warningTimerRef.current = setTimeout(() => {
|
||||
clearInterval(countdownInterval);
|
||||
handleLogout();
|
||||
}, warningTime);
|
||||
}, [warningTime, handleLogout]);
|
||||
|
||||
// 重置计时器
|
||||
const resetTimer = useCallback(() => {
|
||||
if (!enabled) return;
|
||||
|
||||
clearTimers();
|
||||
lastActivityRef.current = Date.now();
|
||||
|
||||
// 设置警告定时器
|
||||
if (warningTime > 0 && timeout > warningTime) {
|
||||
warningTimerRef.current = setTimeout(() => {
|
||||
showWarning();
|
||||
}, timeout - warningTime);
|
||||
}
|
||||
|
||||
// 设置超时定时器
|
||||
timerRef.current = setTimeout(() => {
|
||||
handleLogout();
|
||||
}, timeout);
|
||||
}, [enabled, timeout, warningTime, clearTimers, showWarning, handleLogout]);
|
||||
|
||||
// 监听用户活动事件
|
||||
useEffect(() => {
|
||||
if (!enabled) {
|
||||
clearTimers();
|
||||
return;
|
||||
}
|
||||
|
||||
// 定义需要监听的事件
|
||||
const events = [
|
||||
'mousedown',
|
||||
'mousemove',
|
||||
'keydown',
|
||||
'scroll',
|
||||
'touchstart',
|
||||
'click',
|
||||
'wheel',
|
||||
];
|
||||
|
||||
// 事件处理函数
|
||||
const handleActivity = () => {
|
||||
// 防抖:避免频繁重置
|
||||
const now = Date.now();
|
||||
if (now - lastActivityRef.current < 1000) {
|
||||
return;
|
||||
}
|
||||
resetTimer();
|
||||
};
|
||||
|
||||
// 绑定事件监听
|
||||
events.forEach(event => {
|
||||
document.addEventListener(event, handleActivity, { passive: true });
|
||||
});
|
||||
|
||||
// 初始化计时器
|
||||
resetTimer();
|
||||
|
||||
// 清理函数
|
||||
return () => {
|
||||
events.forEach(event => {
|
||||
document.removeEventListener(event, handleActivity);
|
||||
});
|
||||
clearTimers();
|
||||
};
|
||||
}, [enabled, resetTimer, clearTimers]);
|
||||
|
||||
// 页面可见性变化处理
|
||||
useEffect(() => {
|
||||
if (!enabled) return;
|
||||
|
||||
const handleVisibilityChange = () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
// 页面重新可见时检查是否已超时
|
||||
const idleTime = Date.now() - lastActivityRef.current;
|
||||
if (idleTime >= timeout) {
|
||||
handleLogout();
|
||||
} else {
|
||||
resetTimer();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||
};
|
||||
}, [enabled, timeout, resetTimer, handleLogout]);
|
||||
|
||||
return {
|
||||
resetTimer,
|
||||
clearTimers,
|
||||
getIdleTime: () => Date.now() - lastActivityRef.current,
|
||||
};
|
||||
};
|
||||
|
||||
export default useIdleTimeout;
|
||||
@@ -2222,7 +2222,7 @@ function DeviceManagement() {
|
||||
{selectedDevice.type ? (
|
||||
<Space>
|
||||
{getDeviceTypeIcon(selectedDevice.type)}
|
||||
<span>{typeMap[selectedDevice.type] || selectedDevice.type}</span>
|
||||
<span>{getTypeLabel(selectedDevice.type)}</span>
|
||||
</Space>
|
||||
) : (
|
||||
'-'
|
||||
@@ -2283,13 +2283,13 @@ function DeviceManagement() {
|
||||
style={{
|
||||
marginLeft: 8,
|
||||
color: selectedDevice.status
|
||||
? statusMap[selectedDevice.status]?.color
|
||||
? getStatusConfig(selectedDevice.status).color
|
||||
: '#666',
|
||||
fontWeight: '600',
|
||||
}}
|
||||
>
|
||||
{selectedDevice.status
|
||||
? statusMap[selectedDevice.status]?.text || selectedDevice.status
|
||||
? getStatusConfig(selectedDevice.status).text || selectedDevice.status
|
||||
: '-'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -368,7 +368,7 @@ const SystemSettings = () => {
|
||||
'site_logo',
|
||||
'timezone',
|
||||
'date_format',
|
||||
'session_timeout',
|
||||
'idle_timeout',
|
||||
'max_login_attempts',
|
||||
'maintenance_mode',
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user