/** * 企微 WwLogin:iframe 内授权完成后通过 postMessage 把「完整回调 URL」交给父窗口, * 随后官方脚本会执行 window.location.href = url 触发整页刷新。 * 部分环境下刷新后地址栏没有 code,Vue 不会走 URL 分支 → bindWorkWechat 从不发起。 * 在 window 上以 capture 监听 message,抢先解析 code 并 stopImmediatePropagation,避免整页跳转。 */ const DEFAULT_STATE = 'bind_wxwork' function isWecomPostMessageOrigin(origin: string): boolean { return origin.includes('work.weixin.qq.com') || origin.includes('tencent.com') } function parseOAuthAccept( params: URLSearchParams, wxBindState: string ): { code: string } | null { const wxCode = (params.get('code') || '').trim() if (!wxCode) return null const wxState = (params.get('state') ?? '').trim() const stateOk = wxState === wxBindState || wxState === 'admin_bind_wx' || wxState.toLowerCase() === wxBindState.toLowerCase() const bindFlagOk = params.get('bind_wxwork') === '1' const implicitOk = wxState === '' if (!(stateOk || bindFlagOk || implicitOk)) return null return { code: wxCode } } export type WecomOauthCaptureOptions = { /** 回调 URL 的 pathname 须包含此片段,如 bind-work-wechat、user/setting */ pathIncludes: string lockKey: string wxBindState?: string onCode: (code: string) => void | Promise } /** * @returns 卸载时调用以 removeEventListener */ export function attachWecomOAuthMessageCapture(options: WecomOauthCaptureOptions): () => void { const wxBindState = options.wxBindState ?? DEFAULT_STATE const handler = (event: MessageEvent) => { if (!isWecomPostMessageOrigin(event.origin)) return const data = event.data if (typeof data !== 'string' || !/^https?:\/\//i.test(data)) return let u: URL try { u = new URL(data) } catch { return } if (u.host !== window.location.host) return if (!u.pathname.includes(options.pathIncludes)) return const parsed = parseOAuthAccept(new URLSearchParams(u.search), wxBindState) if (!parsed) return if (sessionStorage.getItem(options.lockKey) === parsed.code) return event.stopImmediatePropagation() sessionStorage.setItem(options.lockKey, parsed.code) void Promise.resolve(options.onCode(parsed.code)).finally(() => { sessionStorage.removeItem(options.lockKey) }) } window.addEventListener('message', handler, true) return () => window.removeEventListener('message', handler, true) }