2025-04-11 10:55:36 +08:00
|
|
|
|
import { debounce } from 'lodash-es';
|
|
|
|
|
|
import { Local, Session } from '/@/utils/storage';
|
|
|
|
|
|
import { ElMessageBox } from 'element-plus';
|
|
|
|
|
|
import { accessTokenKey, refreshAccessTokenKey } from '/@/utils/axios-utils';
|
|
|
|
|
|
import { i18n } from '/@/i18n';
|
|
|
|
|
|
|
|
|
|
|
|
type IdleTimeoutConfig = {
|
|
|
|
|
|
/** 空闲超时时间(秒),默认30分钟 */
|
|
|
|
|
|
timeout?: number;
|
|
|
|
|
|
/** 用于设置最后活动时间的监听事件列表 */
|
|
|
|
|
|
events?: string[];
|
|
|
|
|
|
/** 登出回调函数,在超时发生时执行 */
|
|
|
|
|
|
onTimeout?: () => void;
|
|
|
|
|
|
/** 监听事件防抖间隔(毫秒),默认 200,设为0表示不启用防抖 */
|
|
|
|
|
|
debounceInterval?: number;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-04-18 14:28:03 +08:00
|
|
|
|
// 动态导入 signalR
|
|
|
|
|
|
let signalR: any;
|
|
|
|
|
|
|
|
|
|
|
|
/** 动态导入 signalR */
|
|
|
|
|
|
async function loadSignalR() {
|
|
|
|
|
|
if (!signalR) {
|
|
|
|
|
|
const module = await import('/@/views/system/onlineUser/signalR');
|
|
|
|
|
|
signalR = module.signalR;
|
|
|
|
|
|
}
|
|
|
|
|
|
return signalR;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-11 10:55:36 +08:00
|
|
|
|
class IdleTimeoutManager {
|
|
|
|
|
|
private timerId: number | null = null;
|
|
|
|
|
|
private readonly config: Required<IdleTimeoutConfig>;
|
|
|
|
|
|
private readonly debouncedReset: () => void;
|
|
|
|
|
|
/** 检查闲置超时时间间隔 */
|
|
|
|
|
|
private readonly checkTimeoutInterval = 2 * 1000;
|
|
|
|
|
|
|
|
|
|
|
|
constructor(config: IdleTimeoutConfig = {}) {
|
|
|
|
|
|
this.config = {
|
|
|
|
|
|
timeout: 30 * 60,
|
|
|
|
|
|
events: ['mousewheel', 'keydown', 'click'],
|
|
|
|
|
|
onTimeout: this.timeOutExec.bind(this),
|
|
|
|
|
|
debounceInterval: 200,
|
|
|
|
|
|
...config,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 防抖处理
|
|
|
|
|
|
this.debouncedReset = this.config.debounceInterval > 0 ? debounce(this.setLastActivityTime.bind(this), this.config.debounceInterval) : this.setLastActivityTime.bind(this);
|
|
|
|
|
|
|
|
|
|
|
|
this.init();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private init() {
|
|
|
|
|
|
// 初始化事件监听
|
|
|
|
|
|
this.config.events.forEach((event) => {
|
|
|
|
|
|
window.addEventListener(event, this.debouncedReset);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 页面可见性监听
|
|
|
|
|
|
document.addEventListener('visibilitychange', this.handleVisibilityChange);
|
|
|
|
|
|
|
|
|
|
|
|
// 设置最后活动时间
|
|
|
|
|
|
this.setLastActivityTime();
|
|
|
|
|
|
|
|
|
|
|
|
// 更新空闲超时时间(会按需启动检查定时器)
|
|
|
|
|
|
this.updateIdleTimeout(this.config.timeout);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private handleVisibilityChange = () => {
|
|
|
|
|
|
if (document.visibilityState === 'visible') {
|
|
|
|
|
|
this.setLastActivityTime();
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/** 设置最后活动时间 */
|
|
|
|
|
|
public setLastActivityTime() {
|
|
|
|
|
|
Local.set('lastActivityTime', new Date().getTime());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 更新空闲超时时间
|
|
|
|
|
|
* @param timeout - 新的超时时间(毫秒)
|
|
|
|
|
|
*/
|
|
|
|
|
|
public updateIdleTimeout(timeout: number) {
|
|
|
|
|
|
this.config.timeout = timeout;
|
|
|
|
|
|
// 如果闲置超时时间大于0且检测定时器没启动
|
|
|
|
|
|
if (this.config.timeout > 0 && this.timerId == null) {
|
|
|
|
|
|
this.timerId = window.setInterval(this.checkTimeout.bind(this), this.checkTimeoutInterval);
|
|
|
|
|
|
} else if (this.config.timeout == 0 && this.timerId != null) {
|
|
|
|
|
|
window.clearInterval(this.timerId);
|
|
|
|
|
|
this.timerId = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 检查是否超时 */
|
|
|
|
|
|
public checkTimeout() {
|
|
|
|
|
|
const currentTime = new Date().getTime(); // 当前时间
|
|
|
|
|
|
const lastActivityTime = Number(Local.get('lastActivityTime'));
|
|
|
|
|
|
if (lastActivityTime == 0) return;
|
|
|
|
|
|
const accessToken = Local.get(accessTokenKey);
|
|
|
|
|
|
if (!accessToken || accessToken == 'invalid_token') return;
|
|
|
|
|
|
const timeout = this.config.timeout * 1000;
|
|
|
|
|
|
if (currentTime - lastActivityTime > timeout) {
|
|
|
|
|
|
this.destroy();
|
|
|
|
|
|
this.config.onTimeout();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 销毁实例 */
|
|
|
|
|
|
public destroy() {
|
|
|
|
|
|
this.config.events.forEach((event) => {
|
|
|
|
|
|
window.removeEventListener(event, this.debouncedReset);
|
|
|
|
|
|
});
|
|
|
|
|
|
document.removeEventListener('visibilitychange', this.handleVisibilityChange);
|
|
|
|
|
|
|
|
|
|
|
|
if (this.timerId !== null) {
|
|
|
|
|
|
window.clearInterval(this.timerId);
|
|
|
|
|
|
this.timerId = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 超时时执行 */
|
|
|
|
|
|
private timeOutExec() {
|
|
|
|
|
|
// 移除 app 元素,即清空主界面
|
|
|
|
|
|
const appEl = document.getElementById('app')!;
|
|
|
|
|
|
appEl?.remove();
|
|
|
|
|
|
|
2025-04-18 14:28:03 +08:00
|
|
|
|
// 动态加载 signalR 并调用 stop 方法
|
|
|
|
|
|
loadSignalR().then((signalR) => {
|
|
|
|
|
|
signalR.stop();
|
|
|
|
|
|
});
|
2025-04-11 10:55:36 +08:00
|
|
|
|
|
|
|
|
|
|
// 清除 token
|
|
|
|
|
|
Local.remove(accessTokenKey);
|
|
|
|
|
|
Local.remove(refreshAccessTokenKey);
|
|
|
|
|
|
|
|
|
|
|
|
// 清除其他
|
|
|
|
|
|
Session.clear();
|
|
|
|
|
|
|
|
|
|
|
|
ElMessageBox.alert(i18n.global.t('message.list.idleTimeoutMessage'), i18n.global.t('message.list.sysMessage'), {
|
2025-04-14 11:46:31 +08:00
|
|
|
|
type: 'warning',
|
2025-04-11 10:55:36 +08:00
|
|
|
|
draggable: true,
|
|
|
|
|
|
callback: () => {
|
|
|
|
|
|
window.location.reload();
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 初始化函数(在应用启动时调用) */
|
|
|
|
|
|
export function initIdleTimeout(config?: IdleTimeoutConfig) {
|
|
|
|
|
|
// 确保单例模式
|
|
|
|
|
|
if (!window.__IDLE_TIMEOUT__) {
|
|
|
|
|
|
window.__IDLE_TIMEOUT__ = new IdleTimeoutManager(config);
|
|
|
|
|
|
}
|
|
|
|
|
|
return window.__IDLE_TIMEOUT__;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 销毁函数(在需要时调用) */
|
|
|
|
|
|
export function destroyIdleTimeout() {
|
|
|
|
|
|
if (window.__IDLE_TIMEOUT__) {
|
|
|
|
|
|
window.__IDLE_TIMEOUT__.destroy();
|
|
|
|
|
|
window.__IDLE_TIMEOUT__ = undefined;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 更新空闲超时时间(毫秒) */
|
|
|
|
|
|
export function updateIdleTimeout(timeout: number) {
|
|
|
|
|
|
if (window.__IDLE_TIMEOUT__) {
|
|
|
|
|
|
window.__IDLE_TIMEOUT__.updateIdleTimeout(timeout);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 类型扩展
|
|
|
|
|
|
declare global {
|
|
|
|
|
|
interface Window {
|
|
|
|
|
|
__IDLE_TIMEOUT__?: IdleTimeoutManager;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|