feat: 增加闲置超时退出控制

This commit is contained in:
写意 2025-04-11 10:55:36 +08:00
parent 7cc86ce424
commit 9e879f5c2f
9 changed files with 190 additions and 0 deletions

View File

@ -131,6 +131,11 @@ public class ConfigConst
/// </summary>
public const string SysI18NSwitch = "sys_i18n_switch";
/// <summary>
/// 闲置超时时间
/// </summary>
public const string SysIdleTimeout = "sys_idle_timeout";
/// <summary>
/// 支付宝授权页面地址
/// </summary>

View File

@ -39,6 +39,7 @@ public class SysConfigSeedData : ISqlSugarEntitySeedData<SysConfig>
new SysConfig{ Id=1300000000261, Name="密码历史记录验证", Code=ConfigConst.SysPasswordRecord, Value="False", SysFlag=YesNoEnum.Y, Remark="是否验证历史密码禁止再次使用", OrderNo=210, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2024-12-17 00:00:00") },
new SysConfig{ Id=1300000000271, Name="显示系统更新日志", Code=ConfigConst.SysUpgrade, Value="True", SysFlag=YesNoEnum.Y, Remark="是否显示系统更新日志", OrderNo=220, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2024-12-20 00:00:00") },
new SysConfig{ Id=1300000000281, Name="开启多语言切换", Code=ConfigConst.SysI18NSwitch, Value="True", SysFlag=YesNoEnum.Y, Remark="是否显示多语言切换按钮", OrderNo=230, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2024-12-20 00:00:00") },
new SysConfig{ Id=1300000000282, Name="闲置超时时间", Code=ConfigConst.SysIdleTimeout, Value="0", SysFlag=YesNoEnum.Y, Remark="闲置超时时间分钟超时强制退出0 表示不限制", OrderNo=240, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2024-12-20 00:00:00") },
new SysConfig{ Id=1300000000999, Name="系统版本号", Code=ConfigConst.SysVersion, Value="0", SysFlag=YesNoEnum.Y, Remark= "系统版本号,用于自动升级,请勿手动填写", OrderNo=1000, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2025-04-10 00:00:00") },
];

View File

@ -588,6 +588,7 @@ public class SysTenantService : IDynamicApiController, ITransient
var forceChangePassword = await _sysConfigService.GetConfigValueByCode<bool>(ConfigConst.SysForceChangePassword); // 强制修改密码
var passwordExpirationTime = await _sysConfigService.GetConfigValueByCode<int>(ConfigConst.SysPasswordExpirationTime); // 密码有效期
var i18NSwitch = await _sysConfigService.GetConfigValueByCode<bool>(ConfigConst.SysI18NSwitch); // 开启多语言切换
var idleTimeout = await _sysConfigService.GetConfigValueByCode<int>(ConfigConst.SysIdleTimeout); // 闲置超时时间
var publicKey = App.GetConfig<string>("Cryptogram:PublicKey", true); // 获取密码加解密公钥配置
return new
@ -612,6 +613,7 @@ public class SysTenantService : IDynamicApiController, ITransient
PublicKey = publicKey,
CarouselFiles = carouselFiles,
I18NSwitch = i18NSwitch,
IdleTimeout = idleTimeout,
};
}

View File

@ -21,6 +21,7 @@ import { Local, Session } from '/@/utils/storage';
import mittBus from '/@/utils/mitt';
import setIntroduction from '/@/utils/setIconfont';
// import Watermark from '/@/utils/watermark';
import { initIdleTimeout } from './utils/idleTimeout';
//
const LockScreen = defineAsyncComponent(() => import('/@/layout/lockScreen/index.vue'));
@ -102,6 +103,11 @@ document.body.ondrop = function (event) {
event.preventDefault();
event.stopPropagation();
};
//
initIdleTimeout({
timeout: themeConfig.value.idleTimeout,
});
</script>
<style lang="scss">

View File

@ -353,5 +353,7 @@ export default {
confirmCopyRole: '确定复制角色:【{roleName}】?',
authAccount: '授权账号',
successCopy: '复制成功',
sysMessage: '系统消息',
idleTimeoutMessage: '长时间未操作,已退出系统',
}
};

View File

@ -154,6 +154,8 @@ export const useThemeConfig = defineStore('themeConfig', {
icpUrl: 'https://beian.miit.gov.cn',
// 是否开启多语言切换
i18NSwitch: true,
// 闲置超时时间
idleTimeout: 0,
},
}),
actions: {

View File

@ -105,5 +105,6 @@ declare interface ThemeConfigState {
forceChangePassword?: boolean; // 是否开启强制修改密码
passwordExpirationTime?: number; // 是否验证密码有效期
i18NSwitch: boolean; // 是否开启多语言切换
idleTimeout: number; // 闲置超时时间
};
}

View File

@ -0,0 +1,165 @@
import { debounce } from 'lodash-es';
import { Local, Session } from '/@/utils/storage';
import { signalR } from '/@/views/system/onlineUser/signalR';
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;
};
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();
// 关闭连接
signalR.stop();
// TODO: 如果要改成调用 logout 登出接口,需要调整 clearAccessTokens 会 reload 页面的问题
// 清除 token
Local.remove(accessTokenKey);
Local.remove(refreshAccessTokenKey);
// 清除其他
Session.clear();
ElMessageBox.alert(i18n.global.t('message.list.idleTimeoutMessage'), i18n.global.t('message.list.sysMessage'), {
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;
}
}

View File

@ -5,6 +5,7 @@ import logoImg from '/@/assets/logo.png';
import { SysTenantApi } from '/@/api-services';
import { feature, getAPI } from '/@/utils/axios-utils';
import { updateIdleTimeout } from './idleTimeout';
const storesThemeConfig = useThemeConfig();
const { themeConfig } = storeToRefs(storesThemeConfig);
@ -56,12 +57,17 @@ export async function loadSysInfo(tenantid: number) {
themeConfig.value.passwordExpirationTime = data.passwordExpirationTime;
// 开启多语言切换
themeConfig.value.i18NSwitch = data.i18NSwitch;
// 闲置超时时间
themeConfig.value.idleTimeout = data.idleTimeout;
// 密码加解密公匙
window.__env__.VITE_SM_PUBLIC_KEY = data.publicKey;
// 更新 favicon
updateFavicon(data.logo);
// 更新空闲超时时间
updateIdleTimeout(themeConfig.value.idleTimeout ?? 0);
// 保存配置
Local.remove('themeConfig');
Local.set('themeConfig', storesThemeConfig.themeConfig);