feat: 增加闲置超时退出控制
This commit is contained in:
parent
7cc86ce424
commit
9e879f5c2f
@ -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>
|
||||
|
||||
@ -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") },
|
||||
];
|
||||
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -353,5 +353,7 @@ export default {
|
||||
confirmCopyRole: '确定复制角色:【{roleName}】?',
|
||||
authAccount: '授权账号',
|
||||
successCopy: '复制成功',
|
||||
sysMessage: '系统消息',
|
||||
idleTimeoutMessage: '长时间未操作,已退出系统',
|
||||
}
|
||||
};
|
||||
|
||||
@ -154,6 +154,8 @@ export const useThemeConfig = defineStore('themeConfig', {
|
||||
icpUrl: 'https://beian.miit.gov.cn',
|
||||
// 是否开启多语言切换
|
||||
i18NSwitch: true,
|
||||
// 闲置超时时间
|
||||
idleTimeout: 0,
|
||||
},
|
||||
}),
|
||||
actions: {
|
||||
|
||||
1
Web/src/types/pinia.d.ts
vendored
1
Web/src/types/pinia.d.ts
vendored
@ -105,5 +105,6 @@ declare interface ThemeConfigState {
|
||||
forceChangePassword?: boolean; // 是否开启强制修改密码
|
||||
passwordExpirationTime?: number; // 是否验证密码有效期
|
||||
i18NSwitch: boolean; // 是否开启多语言切换
|
||||
idleTimeout: number; // 闲置超时时间
|
||||
};
|
||||
}
|
||||
|
||||
165
Web/src/utils/idleTimeout.ts
Normal file
165
Web/src/utils/idleTimeout.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user