190 lines
5.0 KiB
JavaScript
190 lines
5.0 KiB
JavaScript
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;
|