import type { User } from "./api"; /** * 全局用户状态事件总线。 * * 背景:Header 等客户端组件挂载在持久化的 root layout 上,仅靠 * router.refresh() 刷新 props 在上传/删除头像等场景下存在时序与 * Router Cache 的不确定性。同页内的数据更新必须走显式的状态链路 * (React 状态/事件),storage 事件、轮询等方案对本标签页无效。 * * 任意位置修改了当前用户资料后 dispatch 一个补丁,Header 等订阅方 * 立即合并更新;router.refresh() 仍照常调用,负责校正服务端渲染区域。 */ export const USER_UPDATED_EVENT = "j13:user-updated"; export function emitUserUpdate(patch: Partial): void { if (typeof window === "undefined") return; window.dispatchEvent(new CustomEvent(USER_UPDATED_EVENT, { detail: patch })); } export function onUserUpdate(handler: (patch: Partial) => void): () => void { if (typeof window === "undefined") return () => {}; const listener = (e: Event) => { const detail = (e as CustomEvent>).detail; if (detail) handler(detail); }; window.addEventListener(USER_UPDATED_EVENT, listener); return () => window.removeEventListener(USER_UPDATED_EVENT, listener); } /** * 强制下线事件。 * * 触发源统一收敛在 lib/api.ts:任何客户端请求收到后端的 * code=account_banned(中间件/refresh/登录)或 /me 的 banned 标记时派发; * 登录态被顶/被剔除(其他浏览器登录、安全设置剔除设备)经探测确认后 * 以 reason=session_replaced 派发。Header 负责复用登出清理链路 * (清 cookie、游客化、router.refresh),AccountGuard 负责弹出对应告知—— * 状态清理与告知 UI 解耦,避免各请求调用方各自处理导致残留 * (参考强制下线单一入口原则)。 */ export type ForceLogoutReason = "banned" | "session_replaced"; export const FORCE_LOGOUT_EVENT = "j13:force-logout"; export function emitForceLogout(reason: ForceLogoutReason = "banned"): void { if (typeof window === "undefined") return; window.dispatchEvent(new CustomEvent(FORCE_LOGOUT_EVENT, { detail: { reason } })); } export function onForceLogout(handler: (reason: ForceLogoutReason) => void): () => void { if (typeof window === "undefined") return () => {}; const listener = (e: Event) => { const detail = (e as CustomEvent<{ reason: ForceLogoutReason }>).detail; handler(detail?.reason ?? "banned"); }; window.addEventListener(FORCE_LOGOUT_EVENT, listener); return () => window.removeEventListener(FORCE_LOGOUT_EVENT, listener); }