init: laimiao-web-new
This commit is contained in:
commit
c226cf7054
122 files changed
+18458
No files matched your search
+142
@@ -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()
|
||||
}
|
||||
Reference in new issue
Block a user