- 新增登录设备列表与会话下线(ListLoginDevices/RevokeLoginDevice),按设备指纹识别当前会话 - Markdown 编辑器新增图片插入弹窗与图片上传(ImageInsertModal) - 实时总线、账号守卫与用户事件适配
60 lines
2.6 KiB
TypeScript
60 lines
2.6 KiB
TypeScript
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<User>): void {
|
||
if (typeof window === "undefined") return;
|
||
window.dispatchEvent(new CustomEvent(USER_UPDATED_EVENT, { detail: patch }));
|
||
}
|
||
|
||
export function onUserUpdate(handler: (patch: Partial<User>) => void): () => void {
|
||
if (typeof window === "undefined") return () => {};
|
||
const listener = (e: Event) => {
|
||
const detail = (e as CustomEvent<Partial<User>>).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);
|
||
}
|