import { watch } from 'vue'
|
import { useIdle } from '@vueuse/core'
|
import { ElMessage } from 'element-plus'
|
|
/**
|
* 空闲超时自动退出(需求:30 分钟不操作才要求重新输入密码)。
|
*
|
* 背景
|
* ----
|
* 后端 token 是**固定有效期**(`system_oauth2_client.access_token_validity_seconds`,
|
* 当前 default 客户端 = 3600 秒),与"用户是否有操作"无关。
|
* 因此会出现两种都不理想的情况:
|
* · 页面挂着不动 → 到点被踢(用户觉得"我没停过怎么又要登录");
|
* · 一直操作 → token 到期也会被踢(期望是"持续操作就不该重登")。
|
*
|
* 本模块在前端补上**真正的空闲检测**:
|
* · 有任何鼠标/键盘/触摸/滚动操作 → 计时器重置;
|
* · 连续 30 分钟**无任何操作** → 主动清理登录态并跳转登录页(要求重新输密码);
|
* · 有操作时不主动登出(token 自身的续期由 axios 拦截器的 refresh-token 负责)。
|
* · 【本次】空闲登出**不再释放诊室工位**(工位保留,重新登录即可继续使用)。
|
*
|
* 用法(在 permission.ts 中调用一次即可,内部自带幂等保护):
|
* setupIdleLogout()
|
*/
|
|
/** 空闲阈值:30 分钟(需求指定) */
|
export const IDLE_TIMEOUT_MINUTES = 30
|
const IDLE_TIMEOUT_MS = IDLE_TIMEOUT_MINUTES * 60 * 1000
|
|
/**
|
* 视为"有操作"的事件。
|
* 刻意**不**包含 mousemove 的高频节流问题:useIdle 内部已做节流,
|
* 且 mousemove 是判断"人还在电脑前"的最可靠信号(例如只是阅读页面时会移动鼠标)。
|
*
|
* 注意:诊间屏/叫号屏是长时间无人操作的展示页面,但那些页面在路由白名单里
|
* (见 permission.ts 的 whiteList),不会进入本逻辑。
|
*/
|
const ACTIVITY_EVENTS = [
|
'mousemove',
|
'mousedown',
|
'mouseup',
|
'wheel',
|
'keydown',
|
'keyup',
|
'touchstart',
|
'touchend',
|
'click',
|
'scroll'
|
] as const
|
|
let started = false
|
let idleWatcher: (() => void) | null = null
|
|
/** 是否已启动(避免重复注册) */
|
export function isIdleLogoutStarted(): boolean {
|
return started
|
}
|
|
/** 清理(一般不需要调用;登出后若再登录,用 resetIdleLogout 重置) */
|
export function stopIdleLogout(): void {
|
if (idleWatcher) {
|
idleWatcher()
|
idleWatcher = null
|
}
|
started = false
|
}
|
|
/**
|
* 启动空闲登出检测。幂等:重复调用只生效一次。
|
*
|
* @param onTimeout 自定义超时动作(默认:清凭证 + 跳登录页)
|
*/
|
export function setupIdleLogout(onTimeout?: () => void): void {
|
if (started) return
|
started = true
|
|
const { idle, lastActive, stop } = useIdle(IDLE_TIMEOUT_MS, {
|
events: ACTIVITY_EVENTS as unknown as string[],
|
listen: true
|
})
|
|
// 暴露 stop 便于外部停止
|
idleWatcher = stop
|
|
// useIdle 的 idle 是基于"距上次活动是否超过阈值"的响应式布尔值。
|
// 这里用 watch 而不是 interval,避免轮询开销。
|
watch(idle, (isIdle: boolean) => {
|
if (!isIdle) return
|
// 双保险:再次核对距上次活动确实已超过阈值
|
const inactiveMs = Date.now() - (lastActive.value || 0)
|
if (inactiveMs < IDLE_TIMEOUT_MS) return
|
|
if (onTimeout) {
|
onTimeout()
|
return
|
}
|
void doIdleLogout()
|
})
|
}
|
|
/**
|
* 实际执行空闲登出:**只清本地凭证并跳转登录页,不释放工位**。
|
*
|
* 之所以不调用 userStore.loginOut()(那是"主动退出"):
|
* · 那个接口会请求后端注销 token — 对本场景没必要,且可能因网络问题卡住;
|
* · 空闲登出只需保证本地凭证清干净 + 回到登录页。
|
*
|
* 【重要变更(本次)】空闲超时**不再释放诊室工位**。
|
* ---------------------------------------------------------------
|
* 原先本函数会先调 `releaseBedOnExit('idle-timeout')`,把 `clinic_room.doc_id` 清空。
|
* 实测问题:医生离开电脑 30 分钟以上(例如去做检查、离开 1 小时回来),
|
* 回来时工位已经被释放 → 工作台只能只读、并提示"请重新就坐",
|
* 而医生本人并没有离座。这不符合实际使用场景。
|
*
|
* 现在的口径:
|
* · 空闲超时 → **只要求重新输入密码**(token 清掉、回登录页),**工位保留**;
|
* · 重新登录后 `roomStore.fetchMySeat()` 会自动恢复「我的工位」,可直接继续使用;
|
* · 工位的释放只发生在**用户主动操作**:手动「离座」、手动「退出登录」。
|
* · 医生真的下班忘离座的兜底:① 闭诊时系统统一清空当班工位;② 管理员可在
|
* 诊室管理页「清空工位」。
|
*/
|
async function doIdleLogout(): Promise<void> {
|
ElMessage.warning(`已超过 ${IDLE_TIMEOUT_MINUTES} 分钟未操作,请重新登录`)
|
|
cleanupCredentials()
|
|
// 用 replace 避免用户按"返回"回到已登出的页面
|
const redirect = encodeURIComponent(window.location.pathname + window.location.search)
|
setTimeout(() => {
|
window.location.replace(`/login?redirect=${redirect}`)
|
}, 800)
|
}
|
|
/**
|
* 清理登录凭证与用户缓存。
|
* 用动态 import 避免与 permission.ts 形成循环依赖。
|
*/
|
function cleanupCredentials(): void {
|
import('@/utils/auth')
|
.then(({ removeToken }) => {
|
removeToken()
|
return import('@/hooks/web/useCache')
|
})
|
.then(({ deleteUserCache }) => {
|
deleteUserCache()
|
})
|
.catch((e) => {
|
// 兜底:即使 import 失败也直接清 localStorage,保证不会带着旧 token 回到首页
|
try {
|
localStorage.removeItem('ACCESS_TOKEN')
|
localStorage.removeItem('REFRESH_TOKEN')
|
} catch {
|
/* ignore */
|
}
|
console.error('[idle-logout] 清理凭证失败', e)
|
})
|
}
|