declare global { interface Window { WxLogin?: (options: Record) => void } } const TEAM_API = (import.meta.env.VITE_TEAM_API_URL || 'http://localhost:8000').replace(/\/+$/, '') const WECHAT_APPID = import.meta.env.VITE_WECHAT_APPID || '' const WECHAT_ORIGIN = (import.meta.env.VITE_WECHAT_LOGIN_ORIGIN || window.location.origin).replace(/\/+$/, '') // 微信授权回调地址:默认落在当前域名的登录页;测试环境可用 VITE_WECHAT_CALLBACK_URL // 指向已登记授权回调域的域名(如 laimiaoai.com)再中转回测试域名。 const WECHAT_CALLBACK_URL = import.meta.env.VITE_WECHAT_CALLBACK_URL || `${WECHAT_ORIGIN}/login/` // 隐藏微信 iframe 自带的标题栏/状态栏,只保留二维码(与 tripcn.ai-web 登录页一致) const LOGIN_QR_CSS = 'data:text/css;base64,I3FyQ29kZUlmcmFtZSBpZnJhbWUgewogIHdpZHRoOiAxMDAlOwogIGhlaWdodDogMTAwJTsKICBvdmVyZmxvdzogaGlkZGVuOwp9Ci5pbXBvd2VyQm94IHsKICBkaXNwbGF5OiBmbGV4OwogIGp1c3RpZnktY29udGVudDogY2VudGVyOwogIGFsaWduLWl0ZW1zOiBjZW50ZXI7CiAgd2lkdGg6IDEwMCU7CiAgaGVpZ2h0OiAxMDAlOwp9Ci5pbXBvd2VyQm94IC5xcmNvZGUgewogIHdpZHRoOiAyMDBweDsKICBoZWlnaHQ6IDIwMHB4OwogIHBhZGRpbmc6IDBweDsKICBtYXJnaW46IGF1dG87CiAgYm9yZGVyOiBub25lOwp9Ci5pbXBvd2VyQm94IC50aXRsZSB7CiAgZGlzcGxheTogbm9uZTsKfQouaW1wb3dlckJveCAuaW5mbyB7CiAgZGlzcGxheTogbm9uZTsKfQouc3RhdHVzX2ljb24gewogIGRpc3BsYXk6IG5vbmU7Cn0KLmltcG93ZXJCb3ggLnN0YXR1cyB7CiAgZGlzcGxheTogbm9uZTsKfQ==' interface WeixinLoginResponse { uid: string nickname: string avatar: string mobile: string introduce: string access_token: string open_token: string ret?: number msg?: string } interface TeamBasicInfo { team_id: string | number team_name: string enable_invite: number role: string score: number } const TEAM_STORAGE_KEY = 'team_id' const TEAM_SCORE_KEY = 'team_score' const qrEl = document.getElementById('qr') as HTMLElement // 接口会返回未加引号的长整型 id,JSON.parse 会精度丢失甚至报错,先补引号 function parseApiJson(text: string): T { return JSON.parse(text.replace(/(\[[,:]\s*)(\d{16,})(\s*[\],}])/g, '$1"$2"$3')) as T } function showQrMessage(message: string): void { if (qrEl) qrEl.textContent = message } function renderQrCode(): void { if (!WECHAT_APPID) { showQrMessage('缺少微信 AppID 配置') return } if (typeof window.WxLogin !== 'function') { showQrMessage('二维码加载失败,请刷新重试') return } qrEl.textContent = '' window.WxLogin({ self_redirect: false, id: 'qr', appid: WECHAT_APPID, scope: 'snsapi_login', redirect_uri: encodeURIComponent(WECHAT_CALLBACK_URL), state: 'laimiao', href: LOGIN_QR_CSS, }) } // 只接受站内绝对路径,避免 ?redirect= 被当作开放重定向 function consumePostLoginRedirect(): string { try { const stored = sessionStorage.getItem('post_login_redirect') if (stored && stored.startsWith('/') && !stored.startsWith('//')) { sessionStorage.removeItem('post_login_redirect') return stored } } catch (error) { console.warn('读取登录后跳转地址失败', error) } return '' } function redirectTarget(): string { const redirect = new URLSearchParams(window.location.search).get('redirect') if (redirect && redirect.startsWith('/') && !redirect.startsWith('//')) { try { sessionStorage.removeItem('post_login_redirect') } catch (error) { console.warn('清理登录后跳转地址失败', error) } return redirect } return consumePostLoginRedirect() || '/generate' } async function cacheTeam(token: string): Promise { try { const res = await fetch(`${TEAM_API}/api/team/team_list`, { headers: { Authorization: token }, }) if (!res.ok) return const data = parseApiJson<{ ret?: number; msg?: string; data_list?: TeamBasicInfo[] }>( await res.text(), ) if (data.ret !== undefined && data.ret !== 0) return const teams = data.data_list || [] const personal = teams.find((team) => team.enable_invite === 0 && team.team_name === '我的') || teams.find((team) => team.enable_invite === 0) || teams[0] if (!personal) return localStorage.setItem(TEAM_STORAGE_KEY, String(personal.team_id)) localStorage.setItem(TEAM_SCORE_KEY, String(personal.score ?? 0)) } catch { // 团队信息预热失败不阻断登录 } } async function completeWeixinLogin(code: string): Promise { showQrMessage('正在登录…') try { const res = await fetch(`${TEAM_API}/api/user/weixin/login?code=${encodeURIComponent(code)}`, { headers: { 'Content-Type': 'application/json' }, }) if (!res.ok) throw new Error(`HTTP ${res.status}`) const data = parseApiJson(await res.text()) if (data.ret !== undefined && data.ret !== 0) throw new Error(data.msg || '登录失败') localStorage.setItem('access_token', data.access_token) if (data.open_token) localStorage.setItem('open_token', data.open_token) localStorage.setItem( 'user_info', JSON.stringify({ uid: data.uid, nickname: data.nickname || '未知用户', avatar: data.avatar || '', mobile: data.mobile || '', introduce: data.introduce || '', }), ) await cacheTeam(data.access_token) window.location.href = redirectTarget() } catch (error) { console.error('登录失败:', error) showQrMessage('登录失败,请刷新重试') } } const code = new URLSearchParams(window.location.search).get('code') if (code) { void completeWeixinLogin(code) } else { renderQrCode() }