305 lines
8.8 KiB
TypeScript
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>
|
|
)
|
|
}
|