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 API(H5 应用免登) tt?: any // 旧版 ttjsBridge(兼容性) ttjsBridge?: any // H5 JSSDK(网页应用免登) h5sdk?: { ready: (callback: () => void) => void config: (config: any) => void error?: (handler: (error: any) => void) => void } // 其他可能的飞书 API bridge?: any _tt?: any Lark?: any Feishu?: any } } export default function Login() { const navigate = useNavigate() const { login, user } = 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') console.log('🔐 登录页检查:', { hasToken: !!token, hasUser: !!user, redirect }) // 如果有 token,说明已登录 if (token) { console.log('✅ 检测到 token,用户已登录') // 如果有 redirect 参数,跳转到目标页面 if (redirect) { console.log('✅ 跳转到 redirect 页面:', redirect) window.location.href = redirect return } // 否则跳转到 dashboard console.log('✅ 跳转到 dashboard') navigate('/dashboard') return } // 没有 token,但不立即显示登录界面 // 等待第二个 useEffect 检测飞书环境并尝试免登 // 只有在非飞书环境或免登失败时才显示登录界面 // 监听飞书登录成功事件 const handleFeishuLoginSuccess = ((e: any) => { console.log('🎉 收到飞书登录成功事件:', e.detail) const { user, token } = e.detail login(user, token) }) as EventListener window.addEventListener('feishu-login-success', handleFeishuLoginSuccess) return () => { window.removeEventListener('feishu-login-success', handleFeishuLoginSuccess) } }, [user, login, navigate]) useEffect(() => { // 打印详细的环境信息 const ua = navigator.userAgent const isInFeishu = ua.includes('Lark') || ua.includes('feishu') || ua.includes('lark') const currentUrl = window.location.href const currentOrigin = window.location.origin console.log('=== 环境检测 ===') console.log('当前 URL:', currentUrl) console.log('当前域名:', currentOrigin) console.log('User Agent:', ua) console.log('在飞书中:', isInFeishu) console.log('window.tt:', window.tt) // 检查域名是否匹配线上环境 const expectedDomain = 'uniservice-mobile.hnxinyouhe.com' const isExpectedDomain = currentOrigin.includes(expectedDomain) || currentOrigin.includes('xsm.dev') console.log('域名检查:', { expected: expectedDomain, current: currentOrigin, match: isExpectedDomain }) // 检查所有可能的飞书相关全局对象 const feishuObjects: Record = {} for (const key in window) { if (key.toLowerCase().includes('tt') || key.toLowerCase().includes('lark') || key.toLowerCase().includes('feishu') || key.toLowerCase().includes('bridge') || key.toLowerCase().includes('h5sdk')) { feishuObjects[key] = typeof (window as any)[key] } } console.log('🔍 飞书相关全局对象:', feishuObjects) console.log('🔍 h5sdk 对象:', window.h5sdk) if (window.h5sdk) { console.log(' h5sdk 属性:', Object.keys(window.h5sdk)) } // 检查 URL 中是否来自飞书消息通知 const urlParams = new URLSearchParams(window.location.search) const authCode = urlParams.get('auth_code') // 关键修复:在飞书中但 tt 对象不存在时,检查 h5sdk 并等待环境初始化 // 网页应用会先注入 h5sdk,然后通过 h5sdk.ready() 等待 tt 对象 if (isInFeishu && !window.tt) { console.log('⚠️ 在飞书中但 tt 对象不存在,检查 h5sdk...') // 如果有 h5sdk,使用 h5sdk.ready() 等待 if (window.h5sdk && typeof window.h5sdk.ready === 'function') { console.log('✅ 检测到 h5sdk,使用 h5sdk.ready() 等待 tt 对象注入') setEnvironment('feishu') let readyCalled = false const timeoutId = setTimeout(() => { if (!readyCalled) { console.error('❌ h5sdk.ready() 超时(5秒)') setLoading(false) } }, 5000) window.h5sdk.ready(() => { readyCalled = true clearTimeout(timeoutId) console.log('✅ h5sdk.ready() 回调触发,检查 tt 对象:', !!window.tt) if (window.tt) { console.log('✅ tt 对象已注入,开始免登') handleFeishuAutoLogin() } else { console.error('❌ h5sdk.ready() 后 tt 对象仍不存在') setLoading(false) } }) // 设置错误处理 if (window.h5sdk.error) { window.h5sdk.error((error: any) => { console.error('❌ h5sdk error:', error) setLoading(false) }) } return } // 如果没有 h5sdk,轮询等待 tt 对象 console.log('⚠️ 未检测到 h5sdk,开始轮询等待 tt 对象...') setEnvironment('feishu') let attempts = 0 const maxAttempts = 100 // 100 * 100ms = 10秒 const checkInterval = setInterval(() => { attempts++ console.log(`🔍 检查 tt 对象 (${attempts}/${maxAttempts}):`, !!window.tt, typeof window.tt) if (window.tt && typeof window.tt === 'object') { clearInterval(checkInterval) console.log('✅ tt 对象已注入,开始免登') handleFeishuAutoLogin() } else if (attempts >= maxAttempts) { clearInterval(checkInterval) console.error('❌ 等待 tt 对象超时') console.error('❌ 请检查飞书开放平台配置:') console.error(' 1. H5 可信域名配置是否正确') console.error(' 2. 确认应用已发布') console.error(' 3. 尝试从飞书工作台重新进入应用') console.error(' 4. 检查是否正确加载了 H5 JSSDK') setLoading(false) } }, 100) return } if (authCode) { // 有授权码,直接处理登录 console.log('✅ Found auth_code in URL:', authCode.substring(0, 20) + '...') handleFeishuLogin(authCode) } else if (isInFeishu && window.tt) { // 飞书内且有 tt 对象:静默授权 console.log('✅ 飞书内环境,tt 对象可用,开始免登') setEnvironment('feishu') handleFeishuAutoLogin() } else if (isInFeishu) { // 飞书内环境,tt 对象由飞书客户端自动注入 // 使用轮询等待 tt 对象可用(最多 10 秒) console.log('⏳ 在飞书内环境,等待飞书客户端注入 tt 对象...') setEnvironment('feishu') let attempts = 0 const maxAttempts = 100 // 100 * 100ms = 10秒 const checkInterval = setInterval(() => { attempts++ console.log(`🔍 检查 tt 对象 (${attempts}/${maxAttempts}):`, !!window.tt, typeof window.tt) if (window.tt && typeof window.tt === 'object') { clearInterval(checkInterval) console.log('✅ tt 对象已由飞书客户端注入,开始免登') handleFeishuAutoLogin() } else if (attempts >= maxAttempts) { clearInterval(checkInterval) console.error('❌ 等待 tt 对象超时,最后状态 window.tt:', window.tt, typeof window.tt) console.error('❌ 请检查:1) 飞书开放平台是否正确配置 H5 应用地址 2) 页面是否从飞书应用入口进入 3) 应用是否已发布') // 免登失败,显示登录界面(飞书内显示提示) setLoading(false) setEnvironment('feishu') // 不显示二维码,而是显示手动登录选项 } }, 100) } else { // 飞书外:显示登录选项 console.log('📱 飞书外环境,显示登录选项') setEnvironment('external') setLoading(false) } }, []) // 飞书内静默授权 const handleFeishuAutoLogin = () => { console.log('🚀 开始飞书免登') // 从 URL 获取 company 参数(可能来自当前 URL 或 redirect URL) const urlParams = new URLSearchParams(window.location.search) let companyParam = urlParams.get('company') console.log('🔍 URL 中的 company 参数:', companyParam) // 如果当前 URL 没有 company,尝试从 redirect 参数中获取 if (!companyParam) { const redirect = urlParams.get('redirect') if (redirect) { const redirectUrl = new URL(redirect, window.location.origin) companyParam = redirectUrl.searchParams.get('company') console.log('🔍 从 redirect URL 中提取的 company 参数:', companyParam) } } console.log('🔍 window.tt:', window.tt) console.log('🔍 window.h5sdk:', window.h5sdk) console.log('🔍 tt 的属性:', Object.keys(window.tt || {})) // 如果 tt 对象已存在,直接使用(无需等待 h5sdk.ready) if (window.tt) { console.log('✅ tt 对象已存在,直接调用免登') performAuthCodeRequest() return } // 如果 tt 不存在但 h5sdk 存在,使用 h5sdk.ready() 等待 if (window.h5sdk && typeof window.h5sdk.ready === 'function') { console.log('✅ 检测到 h5sdk,使用 h5sdk.ready() 等待 tt 对象') setEnvironment('feishu') let readyCalled = false const timeoutId = setTimeout(() => { if (!readyCalled) { console.error('❌ h5sdk.ready() 超时(5秒)') setLoading(false) Toast.show('飞书环境初始化超时,请刷新重试') } }, 5000) window.h5sdk.ready(() => { readyCalled = true clearTimeout(timeoutId) console.log('✅ h5sdk.ready() 回调触发,检查 tt 对象:', !!window.tt) if (window.tt) { console.log('✅ tt 对象已注入,开始免登') performAuthCodeRequest() } else { console.error('❌ h5sdk.ready() 后 tt 对象仍不存在') setLoading(false) Toast.show('飞书环境异常,请刷新重试') } }) // 设置错误处理 if (window.h5sdk.error) { window.h5sdk.error((error: any) => { console.error('❌ h5sdk error:', error) setLoading(false) Toast.show('飞书环境异常:' + JSON.stringify(error)) }) } return } // 都不存在,报错 console.error('❌ tt 对象和 h5sdk 都不存在') setLoading(false) Toast.show('飞书环境异常,请刷新重试') } // 执行实际的 requestAuthCode 调用 const performAuthCodeRequest = async () => { console.log('🔧 准备调用 tt.requestAuthCode') console.log('🔍 当前 window.tt:', window.tt) console.log('🔍 tt 的属性:', Object.keys(window.tt || {})) if (!window.tt) { console.error('❌ tt 对象不存在') setLoading(false) Toast.show('飞书环境异常(tt 对象不存在),请刷新重试') return } // 获取 appId:优先从 URL 参数,否则从后端获取 const urlParams = new URLSearchParams(window.location.search) let appId = urlParams.get('app_id') let companyParam = urlParams.get('company') console.log('🔍 URL 参数 - app_id:', appId, 'company:', companyParam) // 如果没有 appId 但有 company,从后端获取 if (!appId && companyParam) { try { console.log('📡 从后端获取 appId...') 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 console.log('✅ 从后端获取到 appId:', appId?.substring(0, 15) + '...') } else { console.error('❌ 获取 appId 失败:', result.message) setLoading(false) Toast.show('获取飞书配置失败:' + (result.message || '未知错误')) return } } catch (e) { console.error('❌ 获取 appId 异常:', e) setLoading(false) Toast.show('获取飞书配置失败,请重试') return } } if (!appId) { console.error('❌ 无法获取 appId') setLoading(false) Toast.show('缺少飞书应用配置') return } // 调用 tt.requestAuthCode 获取授权码 try { console.log('🔧 调用 tt.requestAuthCode (appId:', appId?.substring(0, 15) + '...)') window.tt.requestAuthCode({ appId: appId, success: (res: any) => { console.log('✅ 成功获取授权码:', res.code?.substring(0, 30)) handleFeishuLogin(res.code) }, fail: (error: any) => { console.error('❌ requestAuthCode 失败:', error) setLoading(false) Toast.show('免登失败:' + (error.errorMessage || error.errMsg || JSON.stringify(error))) }, }) } catch (e) { console.error('❌ 调用异常:', e) setLoading(false) Toast.show('免登失败,请重试') } } // 跳转到飞书授权页面(飞书外扫码登录) const redirectToFeishu = () => { // 直接打开扫码登录弹窗 setShowQRCode(true) } // 处理飞书回调(飞书外扫码后回调) const handleFeishuCallback = async (authCode: string) => { try { await handleFeishuLogin(authCode) } catch (error) { console.error('登录失败:', error) setLoading(false) } } // 处理扫码登录回调 const handleQrcodeCallback = async (qrcodeId: string) => { try { // 飞书扫码后会带着 qrcode_id 参数跳转回来 // 这个时候前端需要显示 loading 状态 // 后端会处理 qrcodeLoginCallback,更新 session 状态 // 前端只需要轮询状态即可 setShowQRCode(true) setLoading(false) // 设置轮询 session_id // 实际上这个逻辑在 QRCodeLogin 组件中处理 } catch (error) { console.error('处理扫码回调失败:', error) setLoading(false) } } // 处理飞书登录 const handleFeishuLogin = async (authCode: string) => { try { // 从 URL 获取参数 const urlParams = new URLSearchParams(window.location.search) let companyParam = urlParams.get('company') const urlAppId = urlParams.get('app_id') // 如果当前 URL 没有 company,尝试从 redirect 参数中获取 if (!companyParam) { const redirect = urlParams.get('redirect') if (redirect) { try { const redirectUrl = new URL(redirect, window.location.origin) companyParam = redirectUrl.searchParams.get('company') console.log('🔍 从 redirect URL 中提取的 company 参数:', companyParam) } catch (e) { console.log('⚠️ 无法解析 redirect URL:', redirect) } } } console.log('🔍 URL 中的参数:') console.log(' - app_id:', urlAppId) console.log(' - company:', companyParam) 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, // 如果 URL 有 app_id 就传递 company: companyParam || undefined, // 传递 company 参数 }), }) const result = await response.json() if (result.code === 0) { login(result.data.user, result.data.token) Toast.show('登录成功') // 检查是否有 redirect 参数 const urlParams = new URLSearchParams(window.location.search) const redirect = urlParams.get('redirect') if (redirect) { console.log('🔗 登录成功,跳转到:', redirect) // redirect 参数可能包含 ?company=xxx,需要直接跳转 window.location.href = redirect } else { navigate('/dashboard') } } else { Toast.show(result.message || '登录失败') setLoading(false) } } catch (error: any) { console.error('登录失败:', error) Toast.show('登录失败,请重试') setLoading(false) } } if (loading) { return (

企业工具台

正在登录...

{/* 调试信息 */}
调试信息:
URL: {window.location.href.substring(0, 60)}...
UA: {navigator.userAgent.includes('Lark') ? '✅ 飞书' : '❌ 非飞书'}
tt: {window.tt ? '✅ 存在' : '❌ 不存在'}
{window.tt && (
tt属性: {Object.keys(window.tt).join(', ')}
)}
) } // 邮箱密码登录 const handlePasswordLogin = () => { navigate('/login-simple') } if (loading) { return (

企业工具台

正在登录...

{/* 调试信息 */}
调试信息:
URL: {window.location.href.substring(0, 60)}...
UA: {navigator.userAgent.includes('Lark') ? '✅ 飞书' : '❌ 非飞书'}
tt: {window.tt ? '✅ 存在' : '❌ 不存在'}
{window.tt && (
tt属性: {Object.keys(window.tt).join(', ')}
)}
) } return (

企业工具台

{/* 飞书内免登失败的提示 */} {environment === 'feishu' && (
⚠️ 飞书免登失败
可能原因:1) 应用配置未生效 2) 需要重新从工作台进入
)}

请选择登录方式

{environment !== 'feishu' && ( )}
{environment === 'external' && (

💡 提示:在飞书客户端内打开可自动登录

)} setShowQRCode(false)} />
) }