/** * 微信扫码登录 WxLogin JS-SDK 动态加载与授权参数解析 * * 微信官网嵌入式二维码方案:页面引入 https://res.wx.qq.com/connect/zh_CN/htmledition/js/wxLogin.js * 后挂载全局 window.WxLogin,new WxLogin({...}) 会在指定容器内渲染二维码 iframe。 * 本模块负责:动态加载该脚本(带超时/失败检测)、从后端返回的 auth_url 中解析 * WxLogin 所需的 appid / redirect_uri / state。 */ const WX_LOGIN_SRC = "https://res.wx.qq.com/connect/zh_CN/htmledition/js/wxLogin.js" /** 脚本加载超时(毫秒):超时视为加载失败,调用方回退整页跳转 */ const WX_LOGIN_LOAD_TIMEOUT = 8000 /** WxLogin 构造参数(微信官方字段,保持原名) */ export interface WxLoginOptions { /** 是否内嵌二维码(回调在 iframe 内完成) */ self_redirect: boolean /** 二维码容器元素 id */ id: string /** 微信开放平台 AppID */ appid: string /** 应用授权作用域,网站应用固定 snsapi_login */ scope: "snsapi_login" /** 回调地址(需与微信开放平台配置一致,WxLogin 内部会 encodeURIComponent) */ redirect_uri: string /** 防 CSRF 随机串,由后端 state store 生成并在回调时一次性消费 */ state: string /** 二维码样式:black / white */ style?: "black" | "white" /** 自定义样式链接(可选) */ href?: string } /** 微信脚本挂载到 window 上的全局构造函数类型 */ export interface WxLoginConstructor { new (options: WxLoginOptions): unknown } declare global { interface Window { WxLogin?: WxLoginConstructor } } let loadPromise: Promise | null = null /** * 动态加载微信 WxLogin JS(单例:并发调用复用同一个 promise)。 * 加载失败或超时会 reject,调用方应回退到整页跳转授权方式。 */ export function loadWxLoginScript(): Promise { if (window.WxLogin) return Promise.resolve(window.WxLogin) if (loadPromise) return loadPromise loadPromise = new Promise((resolve, reject) => { const script = document.createElement("script") script.src = WX_LOGIN_SRC script.async = true script.onload = () => { if (window.WxLogin) { resolve(window.WxLogin) } else { loadPromise = null reject(new Error("微信登录脚本加载完成但 WxLogin 未挂载")) } } script.onerror = () => { loadPromise = null script.remove() reject(new Error("微信登录脚本加载失败")) } document.head.appendChild(script) // 超时兜底:部分网络环境下脚本既不 onload 也不 onerror window.setTimeout(() => { if (window.WxLogin) { resolve(window.WxLogin) return } loadPromise = null script.remove() reject(new Error("微信登录脚本加载超时")) }, WX_LOGIN_LOAD_TIMEOUT) }) return loadPromise } /** 从微信授权链接 query 中解析出的 WxLogin 所需参数 */ export interface ParsedWxAuthParams { appid: string /** 已 URL 解码的回调地址(传给 WxLogin 时由其内部再次编码) */ redirect_uri: string state: string } /** * 从后端返回的微信授权链接(https://open.weixin.qq.com/connect/qrconnect?appid=...&redirect_uri=...&state=...) * 中解析 appid / redirect_uri / state。解析失败时返回 null,由调用方回退整页跳转。 */ export function parseWxAuthUrl(authUrl: string, stateFallback?: string): ParsedWxAuthParams | null { try { const url = new URL(authUrl) const appid = url.searchParams.get("appid") const redirectUri = url.searchParams.get("redirect_uri") const state = url.searchParams.get("state") || stateFallback || "" if (!appid || !redirectUri || !state) return null return { appid, redirect_uri: redirectUri, state } } catch { return null } }