init: laimiao-web-new

This commit is contained in:
light committed 2026-11-07 10:43:06 +08:00
commit c226cf7054
122 files changed
+18458

No files matched your search

+142
View File
@@ -0,0 +1,142 @@
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()
}