在App开发中,为了保障用户数据安全或资源合理利用,常常需要实现 "无操作自动退出" 功能------当用户长时间未进行交互时,弹出倒计时提示,超时后自动登出并跳转到登录页。本文基于Uni-app框架,分享一套完整的全局无操作监听方案,包含核心原理、代码实现及工程化配置。
一、功能核心需求
- 全局监听:覆盖所有页面的用户交互(点击、滑动、输入等)
- 阈值配置:支持自定义"无操作阈值" 和 "弹窗倒计时"时长
- 白名单机制:指定页面 (如登录页)不启用无操作检测
- 状态联动:与登录状态绑定,未登录是不启动监听
- 友好提示:弹窗显示倒计时,支持点击取消退出
- 异常处理:应用前后台切换、页面切换时正常工作
二、技术方案设计
整体架构
采用「单例服务 + 全局组件 + 配置化」设计,核心分为 3 个模块:
- 监听服务(idleDetector.js):单例模式,负责监听用户操作、计时、状态管理
- 配置文件(idleConfig.js):集中管理阈值、白名单等配置
- 弹窗组件(AutoLogoutMask.vue):可视化提示,响应用户取消操作
- 全局挂载:在 App.vue 和 main.js 中集成,实现全量覆盖
核心原理
- 监听用户交互事件(click、touchstart、scroll 等),触发时重置空闲计时器
- 空闲时间达到阈值后,启动退出倒计时并显示弹窗
- 倒计时期间有用户操作则取消退出,超时则执行登出逻辑
- 页面切换、应用前后台切换时,根据当前页面是否在白名单动态启停监听
三、完整代码实现
1.配置文件:统一管理参数(/mixin/idleConfig.js)
html
export default {
// 无操作阈值(秒):用户多久不操作启动倒计时
inactivityThreshold: 300,
// 弹窗倒计时(秒):提示后多久自动退出
countdownSeconds: 30,
// 是否启用本地存储配置覆盖(可选)
enableStorageOverride: false,
// 白名单页面(路由路径):这些页面不启用无操作检测
whiteListPages: [
'/pages/index/index' // 示例:登录页不检测
]
};
2.核心服务:无操作监听与状态管理(/mixin/idleDetector.js)
html
import idleConfig from '@/mixin/idleConfig.js';
import { recordCabLog } from '../common/LogUtil.js';
class GlobalIdleService {
constructor() {
// 单例模式:防止重复创建实例
if (GlobalIdleService.instance) {
return GlobalIdleService.instance;
}
// 从配置读取参数
this.inactivityThreshold = idleConfig.inactivityThreshold;
this.countdownSeconds = idleConfig.countdownSeconds;
this.whiteListPages = idleConfig.whiteListPages || [];
this.enableStorageOverride = idleConfig.enableStorageOverride;
// 计时器管理
this.idleTime = 0; // 当前空闲时间(秒)
this.idleInterval = null; // 空闲检测计时器
this.countdownInterval = null; // 退出倒计时计时器
// 状态管理
this.isActive = false; // 服务是否激活
this.isCountingDown = false; // 是否正在倒计时退出
this.currentCountdown = 0; // 当前倒计时剩余时间
// 事件回调:与弹窗组件通信
this.onCountdownStart = null; // 倒计时开始回调
this.onCountdownUpdate = null; // 倒计时更新回调
this.onCountdownEnd = null; // 倒计时结束回调
this.onCountdownCancel = null; // 倒计时取消回调
// 事件监听缓存:用于销毁时移除监听
this.eventHandlers = new Map();
this.lifecycleHandlers = {};
GlobalIdleService.instance = this;
}
// 初始化服务
init() {
// 可选:从本地存储加载配置(如需动态修改阈值)
if (this.enableStorageOverride) {
this.loadConfigFromStorage();
}
// 设置全局事件监听(用户交互+应用生命周期)
this.setupGlobalListeners();
console.log('[GlobalIdleService] 初始化完成,无操作阈值:', this.inactivityThreshold, '秒');
}
// 从本地存储加载配置(可选功能)
loadConfigFromStorage() {
try {
const macAddressInfo = uni.getStorageSync('macAddressInfo') || {};
const autoExitValue = parseInt(macAddressInfo.autoexit, 10);
if (!isNaN(autoExitValue) && autoExitValue > 0) {
this.inactivityThreshold = autoExitValue;
}
} catch (error) {
console.error('[GlobalIdleService] 加载本地配置失败:', error);
}
}
// 判断当前页面是否在白名单
isCurrentPageInWhiteList() {
try {
const pages = getCurrentPages();
if (pages.length === 0) return false;
const currentRoute = `/${pages[pages.length - 1].route}`;
return this.whiteListPages.includes(currentRoute);
} catch (error) {
console.error('[GlobalIdleService] 获取当前页面路由失败:', error);
return false;
}
}
// 设置全局用户交互事件监听
setupGlobalListeners() {
// 监听的用户交互事件列表
const userEvents = ['click', 'touchstart', 'keydown', 'scroll', 'mousemove', 'tap'];
// 交互事件处理函数:重置空闲计时器
const handleUserAction = () => {
if (!this.isCurrentPageInWhiteList()) {
this.resetIdleTimer();
}
};
// 绑定所有交互事件(兼容Uni-app和H5)
userEvents.forEach(event => {
if (typeof uni !== 'undefined' && uni.on) {
uni.on(event, handleUserAction);
}
if (typeof document !== 'undefined') {
document.addEventListener(event, handleUserAction, { capture: true, passive: true });
}
this.eventHandlers.set(event, handleUserAction);
});
// 绑定应用/页面生命周期事件
this.setupPageLifecycleListeners();
}
// 设置应用/页面生命周期监听
setupPageLifecycleListeners() {
// 应用显示:恢复检测
this.lifecycleHandlers.appShow = () => {
if (!this.isCurrentPageInWhiteList() && this.$store?.state?.isLoggedIn) {
this.resume();
}
};
uni.onAppShow(this.lifecycleHandlers.appShow);
// 应用隐藏:暂停检测
this.lifecycleHandlers.appHide = () => {
this.pause();
};
uni.onAppHide(this.lifecycleHandlers.appHide);
// 页面显示:根据白名单动态启停
this.lifecycleHandlers.pageShow = () => {
if (this.isCurrentPageInWhiteList()) {
this.pause();
} else if (this.$store?.state?.isLoggedIn) {
this.resetIdleTimer();
}
};
uni.onPageShow(this.lifecycleHandlers.pageShow);
}
// 启动空闲检测
startIdleDetection() {
if (this.isCurrentPageInWhiteList() || !this.isActive || this.inactivityThreshold <= 0) return;
// 清除已有计时器
if (this.idleInterval) {
clearInterval(this.idleInterval);
}
// 每秒检查一次空闲时间
this.idleInterval = setInterval(() => {
if (this.isCurrentPageInWhiteList()) {
this.idleTime = 0;
return;
}
this.idleTime++;
// 达到无操作阈值,启动退出倒计时
if (this.idleTime >= this.inactivityThreshold && !this.isCountingDown) {
this.startCountdown();
}
}, 1000);
}
// 启动退出倒计时
startCountdown() {
if (this.isCurrentPageInWhiteList()) {
this.clearCountdown();
return;
}
this.isCountingDown = true;
this.currentCountdown = this.countdownSeconds;
// 通知弹窗组件:开始倒计时
if (this.onCountdownStart) {
this.onCountdownStart(this.currentCountdown);
}
// 倒计时更新(每秒一次)
this.countdownInterval = setInterval(() => {
if (this.isCurrentPageInWhiteList()) {
this.clearCountdown();
return;
}
this.currentCountdown--;
// 通知弹窗组件:更新倒计时
if (this.onCountdownUpdate) {
this.onCountdownUpdate(this.currentCountdown);
}
// 倒计时结束,执行退出
if (this.currentCountdown <= 0) {
this.finishCountdown();
}
}, 1000);
}
// 完成倒计时:执行自动登出
finishCountdown() {
this.clearCountdown();
// 通知弹窗组件:倒计时结束
if (this.onCountdownEnd) {
this.onCountdownEnd();
}
// 执行登出逻辑
this.handleAutoLogout();
}
// 清除倒计时(取消退出)
clearCountdown() {
if (this.countdownInterval) {
clearInterval(this.countdownInterval);
this.countdownInterval = null;
}
this.isCountingDown = false;
this.currentCountdown = 0;
// 通知弹窗组件:取消倒计时
if (this.onCountdownCancel) {
this.onCountdownCancel();
}
}
// 重置空闲计时器(用户有操作时调用)
resetIdleTimer() {
if (this.isCurrentPageInWhiteList() || !this.isActive) return;
this.idleTime = 0;
// 若正在倒计时,取消倒计时
if (this.isCountingDown) {
this.clearCountdown();
}
// 重启空闲检测
this.restartIdleDetection();
}
// 重启空闲检测
restartIdleDetection() {
if (this.isCurrentPageInWhiteList()) return;
if (this.idleInterval) {
clearInterval(this.idleInterval);
}
if (this.isActive && this.inactivityThreshold > 0) {
this.startIdleDetection();
}
}
// 启动服务(登录后调用)
start() {
this.isActive = true;
this.idleTime = 0;
if (!this.isCurrentPageInWhiteList()) {
this.startIdleDetection();
}
console.log('[GlobalIdleService] 服务已启动');
}
// 暂停服务(未登录/应用隐藏/白名单页面时调用)
pause() {
this.isActive = false;
if (this.idleInterval) {
clearInterval(this.idleInterval);
this.idleInterval = null;
}
this.clearCountdown();
console.log('[GlobalIdleService] 服务已暂停');
}
// 恢复服务(应用显示/离开白名单页面时调用)
resume() {
this.isActive = true;
this.idleTime = 0;
if (!this.isCurrentPageInWhiteList()) {
this.startIdleDetection();
}
console.log('[GlobalIdleService] 服务已恢复');
}
// 处理自动登出逻辑
handleAutoLogout() {
console.log('[GlobalIdleService] 执行自动登出');
// 记录退出日志(可选)
recordCabLog({
logType: 'YCDL',
logData: '无操作自动退出',
}).catch(error => {
console.error('自动退出日志记录失败', error);
});
// 销毁服务
this.destroy();
// 清除存储的用户信息
uni.clearStorageSync();
// 跳转到登录页
uni.reLaunch({
url: '/pages/index/index'
});
}
// 销毁服务(应用退出/登出时调用)
destroy() {
this.pause();
// 移除所有事件监听
this.eventHandlers.forEach((handler, event) => {
try {
if (typeof uni !== 'undefined' && uni.off) {
uni.off(event, handler);
}
if (typeof document !== 'undefined') {
document.removeEventListener(event, handler, true);
}
} catch (error) {
console.warn(`移除${event}事件监听失败:`, error);
}
});
this.eventHandlers.clear();
// 移除生命周期监听
try {
if (uni.offAppShow && this.lifecycleHandlers.appShow) {
uni.offAppShow(this.lifecycleHandlers.appShow);
}
if (uni.offAppHide && this.lifecycleHandlers.appHide) {
uni.offAppHide(this.lifecycleHandlers.appHide);
}
if (uni.offPageShow && this.lifecycleHandlers.pageShow) {
uni.offPageShow(this.lifecycleHandlers.pageShow);
}
} catch (error) {
console.warn('移除生命周期监听失败:', error);
}
this.lifecycleHandlers = {};
console.log('[GlobalIdleService] 服务已销毁');
}
}
// 创建单例实例并导出
const globalIdleService = new GlobalIdleService();
export default globalIdleService;
3.弹窗组件:可视化提示与用户监交互(/components/AutoLogoutMask.vue)
html
<template>
<!-- 无操作提示弹窗:v-if控制显示隐藏 -->
<view class="auto-logout-mask" v-if="showMask" @click="handleMaskClick">
<!-- 半透明背景遮罩 -->
<view class="mask-bg"></view>
<!-- 倒计时弹窗容器 -->
<view class="countdown-container">
<view class="countdown-box">
<text class="title">长时间未操作</text>
<view class="countdown-info">
<text>系统将在 </text>
<text class="countdown-number">{{ countdown }}</text>
<text> 秒后自动退出</text>
</view>
<text class="tip">点击任意位置取消</text>
</view>
</view>
</view>
</template>
<script>
import globalIdleService from '@/mixin/idleDetector.js';
import idleConfig from '@/mixin/idleConfig.js';
export default {
name: 'AutoLogoutMask',
data() {
return {
showMask: false, // 弹窗显示状态
countdown: idleConfig.countdownSeconds // 倒计时初始值
};
},
created() {
// 注册回调函数:与监听服务通信
this.registerGlobalCallbacks();
},
methods: {
// 注册服务回调:响应倒计时状态变化
registerGlobalCallbacks() {
// 倒计时开始:显示弹窗
globalIdleService.onCountdownStart = (seconds) => {
this.countdown = seconds;
this.showMask = true;
console.log('[AutoLogoutMask] 开始倒计时:', seconds);
};
// 倒计时更新:同步显示剩余时间
globalIdleService.onCountdownUpdate = (seconds) => {
this.countdown = seconds;
};
// 倒计时取消:隐藏弹窗
globalIdleService.onCountdownCancel = () => {
this.showMask = false;
console.log('[AutoLogoutMask] 倒计时已取消');
};
// 倒计时结束:隐藏弹窗
globalIdleService.onCountdownEnd = () => {
this.showMask = false;
console.log('[AutoLogoutMask] 倒计时结束');
};
},
// 点击遮罩:取消退出(重置空闲计时器)
handleMaskClick() {
globalIdleService.resetIdleTimer();
}
},
beforeDestroy() {
// 组件销毁:清除回调引用,避免内存泄漏
console.log('[AutoLogoutMask] 组件销毁,清除回调');
globalIdleService.onCountdownStart = null;
globalIdleService.onCountdownUpdate = null;
globalIdleService.onCountdownCancel = null;
globalIdleService.onCountdownEnd = null;
}
};
</script>
<style scoped>
/* 遮罩样式:全屏半透明背景 */
.mask-bg {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.7);
z-index: 9998;
}
/* 弹窗容器:居中显示 */
.countdown-container {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 9999;
display: flex;
justify-content: center;
align-items: center;
}
/* 弹窗盒子样式 */
.countdown-box {
width: 70%;
background-color: #fff;
border-radius: 16px;
padding: 30rpx;
text-align: center;
box-shadow: 0 10rpx 30rpx rgba(0, 0, 0, 0.3);
}
/* 标题样式 */
.title {
font-size: 36rpx;
font-weight: bold;
color: #333;
display: block;
margin-bottom: 20rpx;
}
/* 倒计时信息样式 */
.countdown-info {
font-size: 32rpx;
color: #666;
margin: 30rpx 0;
}
/* 倒计时数字样式:红色+脉冲动画 */
.countdown-number {
color: #ff4d4f;
font-size: 40rpx;
font-weight: bold;
margin: 0 10rpx;
display: inline-block;
animation: pulse 1s infinite;
}
/* 提示文本样式 */
.tip {
font-size: 28rpx;
color: #999;
display: block;
margin-top: 20rpx;
}
/* 数字脉冲动画:增强视觉提醒 */
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.1); }
100% { transform: scale(1); }
}
</style>
4.全局集成:挂载组件与启动服务
(1)main.js:全局注册组件
html
// main.js
import App from "./App";
// 导入无操作弹窗组件
import AutoLogoutMask from '@/components/AutoLogoutMask.vue';
// #ifndef VUE3
import Vue from "vue";
// 全局注册组件(Vue2)
Vue.component("AutoLogoutMask", AutoLogoutMask);
// #endif
// #ifdef VUE3
import { createSSRApp } from "vue";
function createApp() {
const app = createSSRApp(App);
// 全局注册组件(Vue3)
app.component("AutoLogoutMask", AutoLogoutMask);
return { app };
}
// #endif
(2)App.vue:集成服务与状态联动
html
<template>
<div id="app">
<router-view />
<!-- 全局挂载无操作弹窗组件 -->
<AutoLogoutMask />
</div>
</template>
<script>
import globalIdleService from '@/mixin/idleDetector.js';
export default {
onLaunch: function() {
console.log('App Launch');
// 初始化登录状态(从缓存读取)
const loginSession = uni.getStorageSync('LoginSession');
this.$store.dispatch('updateLoginStatus', !!loginSession);
// 初始化无操作监听服务
globalIdleService.init();
// 已登录则启动服务,未登录则不启动
if (loginSession) {
globalIdleService.start();
}
// 监听登录状态变化:动态启停服务
this.$store.watch(
state => state.isLoggedIn,
(isLoggedIn) => {
if (isLoggedIn) {
globalIdleService.start();
} else {
globalIdleService.pause();
}
}
);
},
onShow: function() {
console.log('App Show');
// 应用显示时,若已登录则恢复服务
if (this.$store.state.isLoggedIn) {
globalIdleService.resume();
}
},
onHide: function() {
console.log('App Hide');
// 应用隐藏时,暂停服务
globalIdleService.pause();
},
onExit: function() {
console.log('App Exit');
// 应用退出时,销毁服务
try {
globalIdleService.destroy();
} catch (error) {
console.warn('销毁空闲检测服务失败', error);
}
}
};
</script>
四、关键功能说明
1.白名单机制
在dileConfig.js中配置了whiteListPages,支持指定页面跳过无操作检测(如登录页、支付页等不需要自动退出的场景)。isCurrentPageInWhiteList方法中,通过getCurrentPages()获取当前页面路由并匹配白名单。
2.状态联动
- 登录状态:未登录时不启动监听,登录后自动启动
- 应用生命周期:应用隐藏时暂停监听,显示时恢复
- 页面切换:切换到白名单页面时暂停,离开时恢复
3.用户体验优化
- 弹窗居中显示,半透明遮罩层突出提示
- 倒计时数字添加脉冲动画,增强视觉提醒
- 支持点击任意位置取消退出,操作便捷
- 所有计时器在组件销毁 / 服务暂停时清理,避免内存泄漏
4.异常处理
- 兼容 Uni-app 多端(App、H5)的事件监听方式
- 销毁服务时移除所有事件监听,避免残留
- 本地存储读取失败时降级使用默认配置
- 页面路由获取失败时默认不启用白名单
五、使用与扩展
1.基础使用
- 修改
idleConfig.js中的inactivityThreshold(无操作阈值)和countdownSeconds(倒计时时长)可自定义时效 - 在whiteListPages中添加需要跳过检测的页面路由(格式为
/pages/xxx/xxx) - 登录时存储LoginSession到缓存,登出时清除,服务会自动联动状态
2.功能扩展
- 动态修改阈值:通过
globalIdleService.inactivityThreshold = 60动态修改无操作阈值 - 自定义退出逻辑:修改
handleAutoLogout方法,可添加接口请求、数据备份等操作 - 多语言支持:将弹窗文本提取到语言包,根据当前语言动态显示
- 自定义弹窗样式:修改
AutoLogoutMask.vue的样式,适配项目设计风格
六、注意事项
- 事件监听冲突:若项目中已有全局事件监听,需避免重复绑定同一事件
- 性能优化:监听的事件列表不宜过多,当前已覆盖核心交互事件,无需额外添加
- 白名单路由格式:必须与
getCurrentPages()返回的route字段一致(不带参数) - 缓存清理:登出时需确保
uni.clearStorageSync()正确执行,避免残留登录状态 - 单例模式:
GlobalIdleService采用单例设计,不可重复创建实例
总结
本文实现的全局无操作监听方案,基于Uni-app框架特性,通过单例服务 + 全局组件 + 配置化设计,实现了灵活、可开的自动退出功能。核心优势在于:
- 全局覆盖:监听所有页面的用户交互,无遗漏
- 配置灵活:支持阈值、白名单等参数自定义
- 状态联动:与登录状态、应用声明周期深度集成
- 体验友好:可视化倒计时提示,支持用户取消操作
- 稳定可靠:完善的异常处理和内存泄露防护
该方案可直接应用于需要安全防护的App项目(如金融、医疗、企业办公等),也可根据实际需求扩展更多功能。