| | |
| | | import { getAccessToken } from '@/utils/auth' |
| | | |
| | | /** |
| | | * 退出系统时自动释放诊室工位(需求:医生常忘记「离座」就退出系统)。 |
| | | * 手动「退出登录」时释放诊室工位。 |
| | | * |
| | | * 背景 |
| | | * ---- |
| | |
| | | * (后端写在 `clinic_room.doc_id` / `status`)。 |
| | | * 若医生不点「离座」而直接退出系统,床位会一直显示被占用,其他医生无法使用。 |
| | | * |
| | | * 覆盖的退出路径 |
| | | * -------------- |
| | | * 1. 手动点「退出登录」 → 后端 `/system/auth/logout` 已会离座(AuthController), |
| | | * 本函数在其之前再兜一次,保证即使 logout 请求失败也已释放; |
| | | * 2. **空闲 30 分钟自动登出**(`useIdleLogout`)→ 原先只清本地凭证、不调后端 ⇒ 床位不释放, |
| | | * 这是本次修复的核心场景; |
| | | * 3. **令牌失效/刷新失败被强制登出**(`config/axios/service.ts` 的 `handleAuthorized`); |
| | | * 4. 关闭/刷新页面前的 `pagehide` 兜底。 |
| | | * 【重要变更(本次)】只保留「手动退出登录」这一条路径 |
| | | * --------------------------------------------------------------- |
| | | * 原先覆盖 4 条路径:手动退出、空闲 30 分钟自动登出、令牌失效(401)、`pagehide`(刷新/关页)。 |
| | | * 实测问题:后三条会把「令牌到期 / 刷新页面 / 离开一会儿」误当成「医生离座」, |
| | | * 导致工位被清空 —— 医生回来看到工作台只剩只读、并提示"请重新就坐",而工位已变空闲。 |
| | | * |
| | | * 现在的口径: |
| | | * · **只有手动「退出登录」**(以及诊室选择页的手动「离座」)才释放工位; |
| | | * · 空闲超时(`useIdleLogout`)只要求重新输密码,工位保留; |
| | | * · 401 / 刷新令牌失败只清本地凭证,工位保留; |
| | | * · 不再注册 `pagehide` 兜底(刷新页面不应该丢工位); |
| | | * · 医生真的下班忘离座的兜底:① 闭诊时系统统一清空当班工位;② 管理员「清空工位」。 |
| | | * |
| | | * 为什么用**裸 axios** 而不是项目的 request |
| | | * ---------------------------------------- |
| | | * 项目的 `request` 挂了 401 拦截器,而本函数**恰恰是在 401 之后**被调用的。 |
| | | * 若走拦截器,会再次触发"刷新令牌失败 → 强制登出 → 再调本函数"的递归。 |
| | | * 裸 axios 不挂任何拦截器,天然断开这条链。 |
| | | * 本函数可能在鉴权状态不稳时被调用,若走项目 request 的 401 拦截器, |
| | | * 会再次触发"刷新令牌失败 → 强制登出"的递归。裸 axios 不挂任何拦截器,天然断开这条链。 |
| | | * |
| | | * 为什么用**服务端按 userId 反查**而不是前端传床位号 |
| | | * ------------------------------------------------ |
| | |
| | | * 就会"释放不掉"。故后端提供无参接口 `/ecg/doctor/release-my-beds`,按当前登录用户 |
| | | * 反查其占用的全部床位。前端只需带上令牌。 |
| | | * |
| | | * ⚠️ 本文件**不要 import `@/config/axios/config`**: |
| | | * `config/axios/service.ts` 需要 import 本文件(在 401 处理里释放床位), |
| | | * 若本文件反向 import 它的 config,会形成循环依赖。 |
| | | * ⚠️ 本文件**不要 import `@/config/axios/config`**:避免与 service.ts 形成循环依赖。 |
| | | * 这里直接用同一个环境变量拼接,效果等价且无环。 |
| | | * |
| | | * 失败处理:本函数**永不抛异常**(退出流程不应因它而中断)。 |
| | |
| | | } finally { |
| | | releasing = false |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 注册 `pagehide` 兜底:关闭标签页/刷新/跳走时尝试释放床位。 |
| | | * |
| | | * 用 `sendBeacon` 而非 ajax —— 页面卸载时普通请求会被浏览器取消,beacon 不会。 |
| | | * 但 beacon **无法自定义请求头**(不能带 Authorization),故改用 URL 传令牌: |
| | | * 后端 `SecurityProperties.tokenParameter` 默认就是 `"token"`, |
| | | * 且 `TokenAuthenticationFilter` 会读取它(见 `obtainAuthorization`)。 |
| | | * |
| | | * 注意:本项是**尽力而为**的兜底,不作为主要手段 —— |
| | | * 主要手段是上面 1/2/3 三条明确路径。 |
| | | */ |
| | | export function setupBedReleaseOnUnload(): void { |
| | | if (typeof window === 'undefined') return |
| | | // 幂等:避免 HMR / 重复调用导致重复注册 |
| | | if ((window as any).__bedReleaseUnloadBound) return |
| | | ;(window as any).__bedReleaseUnloadBound = true |
| | | |
| | | window.addEventListener('pagehide', () => { |
| | | const token = getAccessToken() |
| | | if (!token) return |
| | | try { |
| | | const url = `${BASE_URL}/ecg/doctor/release-my-beds?token=${encodeURIComponent(token)}` |
| | | const ok = navigator.sendBeacon?.(url) |
| | | if (!ok) { |
| | | // sendBeacon 不可用/被拒绝时,退回同步 XHR 最后一搏 |
| | | const xhr = new XMLHttpRequest() |
| | | xhr.open('POST', url, false) // 同步,确保卸载前发出 |
| | | xhr.send() |
| | | } |
| | | } catch { |
| | | /* 忽略:卸载阶段不应报错 */ |
| | | } |
| | | }) |
| | | } |