自动退出

46 阅读2分钟

页面长时间未操作,设置自动退出或者保持登录提示

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} />}