页面长时间未操作,设置自动退出或者保持登录提示
import { useCallback, useEffect, useRef, useState } from 'react';
export interface AutoLogoutParams {
isAutoLogoutStart: boolean;
logoutCallback: () => void;
timeoutMinutes: number; // 默认5分钟超时
warningMinutes: number; // 默认提前1分钟提醒
inactivityEvents?: string[]; // 监听的事件
}
/**
* @description: 自动登出hooks
* @param {AutoLogoutParams} options
* @return
* showWarning: 是否显示提醒,外部实现
resetTimeout: 外部提醒的是否继续保持登录,保持登录则重置计时器
remainingTime: 倒计时
remainingFormatTime: 格式化倒计时
*/
export const useAutoLogout = (options: AutoLogoutParams) => {
const {
isAutoLogoutStart,
logoutCallback,
timeoutMinutes = 30, // 默认5分钟超时
warningMinutes = 5, // 默认提前1分钟提醒
inactivityEvents = ['mousemove', 'keydown', 'click', 'scroll'], // 监听的事件
} = options;
// 时间转换
const timeoutMs = timeoutMinutes * 60 * 1000;
const warningMs = warningMinutes * 60 * 1000;
// Refs 存储定时器和时间
const timeoutId = useRef<NodeJS.Timeout | null>(null);
const warningId = useRef<NodeJS.Timeout | null>(null);
const lastActiveTime = useRef(Date.now());
const hiddenTime = useRef(0);
// State
const [remainingTime, setRemainingTime] = useState(timeoutMs);
const [showWarning, setShowWarning] = useState(false);
const [isTabActive, setIsTabActive] = useState(true);
// 格式化时间显示
const formatTime = (ms: number) => {
const totalSeconds = Math.ceil(ms / 1000);
return `${Math.floor(totalSeconds / 60)}:${String(totalSeconds % 60).padStart(2, '0')}`;
};
// 清理所有定时器
const clearTimers = () => {
timeoutId.current && clearTimeout(timeoutId.current);
warningId.current && clearTimeout(warningId.current);
timeoutId.current = null;
warningId.current = null;
};
// 启动/重置定时器
const startTimers = useCallback(() => {
clearTimers();
// 主超时定时器
timeoutId.current = setTimeout(() => {
logoutCallback?.();
clearTimers();
}, remainingTime);
// 提前警告定时器
if (remainingTime > warningMs) {
warningId.current = setTimeout(() => {
setShowWarning(true);
}, remainingTime - warningMs);
} else {
setShowWarning(true);
}
}, [remainingTime, logoutCallback, warningMs]);
// 处理用户活动
const handleActivity = useCallback(() => {
if (!isTabActive) return;
setShowWarning(false);
setRemainingTime(timeoutMs);
lastActiveTime.current = Date.now();
startTimers();
}, [isTabActive, startTimers, timeoutMs]);
// 处理标签页可见性变化
useEffect(() => {
const handleVisibilityChange = () => {
const isVisible = document.visibilityState === 'visible';
setIsTabActive(isVisible);
if (isVisible) {
// 计算隐藏期间的离线时间
const hiddenDuration = Date.now() - hiddenTime.current;
setRemainingTime((prev) => Math.max(prev - hiddenDuration, 0));
startTimers();
} else {
hiddenTime.current = Date.now();
clearTimers();
}
};
document.addEventListener('visibilitychange', handleVisibilityChange);
return () => document.removeEventListener('visibilitychange', handleVisibilityChange);
}, [startTimers]);
// 实时更新时间显示
useEffect(() => {
const interval = setInterval(() => {
if (isTabActive && isAutoLogoutStart) {
setRemainingTime((prev) => {
const newTime = Math.max(prev - 1000, 0);
if (newTime <= warningMs && !showWarning) setShowWarning(true);
return newTime;
});
}
}, 1000);
return () => clearInterval(interval);
}, [isTabActive, warningMs, showWarning, isAutoLogoutStart]);
// 初始化事件监听
useEffect(() => {
const handleEvent = () => handleActivity();
inactivityEvents.forEach((event: any) => {
window.addEventListener(event, handleEvent);
});
startTimers();
return () => {
inactivityEvents.forEach((event: any) => {
window.removeEventListener(event, handleEvent);
});
clearTimers();
};
}, [handleActivity, inactivityEvents, startTimers]);
return {
remainingTime: remainingTime,
remainingFormatTime: formatTime(remainingTime),
showWarning,
resetTimeout: handleActivity,
};
};
import { useInterval } from "ahooks";
import { Modal } from "antd";
import React, { useCallback, useEffect, useState } from "react";
import { AutoLogoutParams, useAutoLogout } from "./useAutoLogout.tsx";
export const AutoLogoutRemind: React.FC<AutoLogoutParams> = (props) => {
const { remainingTime, showWarning, resetTimeout } = useAutoLogout(props);
const [isWarningModalShow, setIsWarningModalShow] = useState(false);
const [modalRemindTime, setModalRemindTime] = useState<number>(props.timeoutMinutes * 60 * 1000);
// 格式化时间显示
const getFormatTime = useCallback((ms: number) => {
const totalSeconds = Math.ceil(ms / 1000);
return `${Math.floor(totalSeconds / 60)}:${String(totalSeconds % 60).padStart(2, '0')}`;
}, []);
useEffect(() => {
if (showWarning) {
setIsWarningModalShow(true);
setModalRemindTime(remainingTime);
}
}, [showWarning]);
useInterval(() => {
setModalRemindTime(modalRemindTime - 1000);
}, 1000);
useEffect(() => {
if (modalRemindTime < 0 && isWarningModalShow) {
props.logoutCallback();
}
}, [modalRemindTime])
return <Modal
title="提示"
open={isWarningModalShow}
okText="保持登录"
cancelText="退出登录"
closable={false}
maskClosable={false}
onOk={() => {
setIsWarningModalShow(false);
resetTimeout();
}}
onCancel={() => {
setIsWarningModalShow(false);
props.logoutCallback();
}}>
<p>因页面长时间未操作,将于{getFormatTime(modalRemindTime)}后自动登出,是否保持登录?</p>
</Modal>
}
使用
const AutoLogoutParams = {
isAutoLogoutStart: location.pathname !== Routes.LOGIN.path,
logoutCallback: () => {
},
timeoutMinutes: +(localStorage.getItem('UAI_AUTO_LOGOUT_TIME') || '2'),
warningMinutes: +(localStorage.getItem('UAI_AUTO_LOGOUT_REMIND_TIME') || '5'),
}
{AutoLogoutParams.isAutoLogoutStart && <AutoLogoutRemind {...AutoLogoutParams} />}