143 lines
5.0 KiB
TypeScript
143 lines
5.0 KiB
TypeScript
declare global {
|
|
interface Window {
|
|
WxLogin?: (options: Record<string, unknown>) => 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<T>(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 redirectTarget(): string {
|
|
const redirect = new URLSearchParams(window.location.search).get('redirect')
|
|
if (redirect && redirect.startsWith('/') && !redirect.startsWith('//')) return redirect
|
|
return '/generate'
|
|
}
|
|
|
|
async function cacheTeam(token: string): Promise<void> {
|
|
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<void> {
|
|
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<WeixinLoginResponse>(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()
|
|
}
|