yxh
2026-10-18 01ee694a3bb8048372fc793a8de314d70b688b35
src/utils/releaseBed.ts
@@ -2,7 +2,7 @@
import { getAccessToken } from '@/utils/auth'
/**
 * 退出系统时自动释放诊室工位(需求:医生常忘记「离座」就退出系统)。
 * 手动「退出登录」时释放诊室工位。
 *
 * 背景
 * ----
@@ -10,20 +10,23 @@
 * (后端写在 `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 反查**而不是前端传床位号
 * ------------------------------------------------
@@ -31,9 +34,7 @@
 * 就会"释放不掉"。故后端提供无参接口 `/ecg/doctor/release-my-beds`,按当前登录用户
 * 反查其占用的全部床位。前端只需带上令牌。
 *
 * ⚠️ 本文件**不要 import `@/config/axios/config`**:
 *   `config/axios/service.ts` 需要 import 本文件(在 401 处理里释放床位),
 *   若本文件反向 import 它的 config,会形成循环依赖。
 * ⚠️ 本文件**不要 import `@/config/axios/config`**:避免与 service.ts 形成循环依赖。
 *   这里直接用同一个环境变量拼接,效果等价且无环。
 *
 * 失败处理:本函数**永不抛异常**(退出流程不应因它而中断)。
@@ -68,39 +69,4 @@
  } 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 {
      /* 忽略:卸载阶段不应报错 */
    }
  })
}