Files
x-server-mobile/frontend/src/pages/login/index.tsx
T
2026-02-28 10:38:27 +08:00

305 lines
8.8 KiB
TypeScript

import { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { Toast } from 'antd-mobile'
import { useUserStore } from '@/stores/user'
import QRCodeLogin from './QRCodeLogin'
import styles from './login.module.css'
// 声明飞书 JSAPI 类型
declare global {
interface Window {
tt?: any
h5sdk?: {
ready: (callback: () => void) => void
}
}
}
export default function Login() {
const navigate = useNavigate()
const { login } = useUserStore()
const [loading, setLoading] = useState(true)
const [environment, setEnvironment] = useState<'feishu' | 'external' | null>(null)
const [showQRCode, setShowQRCode] = useState(false)
// 检查登录状态
useEffect(() => {
const token = localStorage.getItem('token')
const urlParams = new URLSearchParams(window.location.search)
const redirect = urlParams.get('redirect')
if (token) {
if (redirect) {
window.location.href = redirect
} else {
navigate('/dashboard')
}
return
}
// 监听飞书登录成功事件
const handleFeishuLoginSuccess = ((e: any) => {
const { user, token } = e.detail
login(user, token)
}) as EventListener
window.addEventListener('feishu-login-success', handleFeishuLoginSuccess)
return () => {
window.removeEventListener('feishu-login-success', handleFeishuLoginSuccess)
}
}, [login, navigate])
// 环境检测和免登
useEffect(() => {
const ua = navigator.userAgent
const isInFeishu = ua.includes('Lark') || ua.includes('feishu') || ua.includes('lark')
const urlParams = new URLSearchParams(window.location.search)
const authCode = urlParams.get('auth_code')
const companyParam = urlParams.get('company')
// 如果 URL 中有授权码,直接处理登录
if (authCode) {
handleFeishuLogin(authCode, companyParam || undefined)
return
}
if (isInFeishu) {
// 飞书内环境,尝试免登
setEnvironment('feishu')
attemptFeishuAutoLogin(companyParam || undefined)
} else {
// 飞书外环境,显示登录选项
setEnvironment('external')
setLoading(false)
}
}, [])
// 飞书内免登
const attemptFeishuAutoLogin = async (companyParam?: string) => {
// 如果 h5sdk 存在,等待 ready 后再调用
if (window.h5sdk?.ready) {
window.h5sdk.ready(() => {
console.log('h5sdk ready, 调用免登')
performFeishuLoginWithAppId(companyParam)
})
return
}
// 如果 tt 对象已存在,直接使用
if (window.tt) {
performFeishuLoginWithAppId(companyParam)
return
}
// 轮询等待 tt 对象(最多 5 秒)
let attempts = 0
const maxAttempts = 50
const checkInterval = setInterval(() => {
attempts++
if (window.tt) {
clearInterval(checkInterval)
performFeishuLoginWithAppId(companyParam)
} else if (attempts >= maxAttempts) {
clearInterval(checkInterval)
console.error('等待 tt 对象超时')
setLoading(false)
}
}, 100)
}
// 获取 appId 并执行免登
const performFeishuLoginWithAppId = async (companyParam?: string) => {
let appId: string | undefined | null
const urlParams = new URLSearchParams(window.location.search)
appId = urlParams.get('app_id')
if (!appId && companyParam) {
try {
const response = await fetch(`/api/v1/auth/feishu-config?company=${companyParam}`)
const result = await response.json()
if (result.code === 0 && result.data.app_id) {
appId = result.data.app_id
}
} catch (e) {
console.error('获取 appId 失败:', e)
}
}
if (!appId) {
setLoading(false)
Toast.show('飞书应用配置错误')
return
}
performFeishuLogin(companyParam, appId)
}
// 执行飞书免登
const performFeishuLogin = async (companyParam?: string, appId?: string | null) => {
if (!window.tt) {
setLoading(false)
Toast.show('飞书环境异常,请刷新重试')
return
}
if (!appId) {
setLoading(false)
Toast.show('飞书应用配置错误')
return
}
try {
console.log('=== 开始免登 ===')
console.log('appId:', appId)
console.log('appId长度:', appId.length)
console.log('appId前缀:', appId.substring(0, 4))
console.log('当前URL:', window.location.href)
console.log('调用 tt.requestAuthCode')
// 调用飞书免登
window.tt.requestAuthCode({
appId: appId,
success: (res: any) => {
console.log('✅ 免登成功')
console.log('code:', res.code)
handleFeishuLogin(res.code, companyParam)
},
fail: (error: any) => {
console.error('❌ 免登失败')
console.error('完整错误对象:', JSON.stringify(error))
console.error('errMsg:', error.errMsg)
console.error('errorCode:', error.errorCode)
setLoading(false)
Toast.show('免登失败:' + (error.errMsg || JSON.stringify(error)))
},
})
} catch (e) {
console.error('❌ 免登异常:', e)
setLoading(false)
Toast.show('免登失败,请重试')
}
}
// 处理飞书登录
const handleFeishuLogin = async (authCode: string, companyParam?: string) => {
try {
const urlParams = new URLSearchParams(window.location.search)
const urlAppId = urlParams.get('app_id')
const response = await fetch('/api/v1/auth/feishu-login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
auth_code: authCode,
app_id: urlAppId || undefined,
company: companyParam || undefined,
}),
})
const result = await response.json()
if (result.code === 0) {
login(result.data.user, result.data.token)
Toast.show('登录成功')
const urlParams = new URLSearchParams(window.location.search)
const redirect = urlParams.get('redirect')
if (redirect) {
window.location.href = redirect
} else {
navigate('/dashboard')
}
} else {
Toast.show(result.message || '登录失败')
setLoading(false)
}
} catch (error) {
console.error('登录失败:', error)
Toast.show('登录失败,请重试')
setLoading(false)
}
}
// 邮箱密码登录
const handlePasswordLogin = () => {
navigate('/login-simple')
}
// 飞书扫码登录
const handleQRCodeLogin = () => {
setShowQRCode(true)
}
if (loading) {
return (
<div className={styles.login}>
<div className={styles.container}>
<h1>企业工具台</h1>
<p>正在登录...</p>
<div className={styles.spinner}></div>
</div>
</div>
)
}
return (
<div className={styles.login}>
<div className={styles.container}>
<h1>企业工具台</h1>
{environment === 'feishu' && (
<div style={{
background: '#fff3cd',
border: '1px solid #ffc107',
borderRadius: '8px',
padding: '12px',
marginBottom: '20px',
fontSize: '14px',
color: '#856404',
textAlign: 'center'
}}>
<div style={{ marginBottom: '5px', fontWeight: 'bold' }}>⚠️ 飞书免登失败</div>
<div style={{ fontSize: '12px' }}>
请尝试其他登录方式或联系管理员
</div>
</div>
)}
<p className={styles.subtitle}>请选择登录方式</p>
<div className={styles.loginMethods}>
<button className={styles.methodBtn} onClick={handlePasswordLogin}>
<div className={styles.methodIcon}>📧</div>
<div className={styles.methodText}>
<div className={styles.methodTitle}>邮箱密码登录</div>
<div className={styles.methodDesc}>使用企业邮箱登录</div>
</div>
</button>
{environment !== 'feishu' && (
<button className={styles.methodBtn} onClick={handleQRCodeLogin}>
<div className={styles.methodIcon}>📱</div>
<div className={styles.methodText}>
<div className={styles.methodTitle}>飞书扫码登录</div>
<div className={styles.methodDesc}>使用飞书 APP 扫码登录</div>
</div>
</button>
)}
</div>
{environment === 'external' && (
<p className={styles.tip}>
💡 提示:在飞书客户端内打开可自动登录
</p>
)}
<QRCodeLogin
visible={showQRCode}
onClose={() => setShowQRCode(false)}
/>
</div>
</div>
)
}