diff --git a/frontend/src/pages/login/index.tsx b/frontend/src/pages/login/index.tsx index b385000..015e006 100644 --- a/frontend/src/pages/login/index.tsx +++ b/frontend/src/pages/login/index.tsx @@ -8,464 +8,178 @@ 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 { 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') - 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 + } else { + navigate('/dashboard') } - // 否则跳转到 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]) + }, [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') + const companyParam = urlParams.get('company') - // 关键修复:在飞书中但 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) + // 如果 URL 中有授权码,直接处理登录 + if (authCode) { + handleFeishuLogin(authCode, companyParam || undefined) 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 对象可用,开始免登') + if (isInFeishu) { + // 飞书内环境,尝试免登 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) + attemptFeishuAutoLogin(companyParam || undefined) } 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) + // 飞书内免登 + const attemptFeishuAutoLogin = async (companyParam?: string) => { + // 如果 tt 对象已存在,直接使用 if (window.tt) { - console.log('✅ tt 对象已存在,直接调用免登') - performAuthCodeRequest() + performFeishuLogin(companyParam) return } - // 如果 tt 不存在但 h5sdk 存在,使用 h5sdk.ready() 等待 - if (window.h5sdk && typeof window.h5sdk.ready === 'function') { - console.log('✅ 检测到 h5sdk,使用 h5sdk.ready() 等待 tt 对象') - setEnvironment('feishu') - + // 如果 h5sdk 存在,使用 h5sdk.ready() 等待 + if (window.h5sdk?.ready) { let readyCalled = false const timeoutId = setTimeout(() => { if (!readyCalled) { - console.error('❌ h5sdk.ready() 超时(5秒)') + console.error('h5sdk.ready() 超时') 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('飞书环境异常,请刷新重试') - } + performFeishuLogin(companyParam) }) - - // 设置错误处理 - 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('飞书环境异常,请刷新重试') + // 轮询等待 tt 对象(最多 5 秒) + let attempts = 0 + const maxAttempts = 50 + + const checkInterval = setInterval(() => { + attempts++ + if (window.tt) { + clearInterval(checkInterval) + performFeishuLogin(companyParam) + } else if (attempts >= maxAttempts) { + clearInterval(checkInterval) + console.error('等待 tt 对象超时') + setLoading(false) + } + }, 100) } - // 执行实际的 requestAuthCode 调用 - const performAuthCodeRequest = async () => { - console.log('🔧 准备调用 tt.requestAuthCode') - console.log('🔍 当前 window.tt:', window.tt) - console.log('🔍 tt 的属性:', Object.keys(window.tt || {})) - + // 执行飞书免登 + const performFeishuLogin = async (companyParam?: string) => { if (!window.tt) { - console.error('❌ tt 对象不存在') setLoading(false) - Toast.show('飞书环境异常(tt 对象不存在),请刷新重试') + Toast.show('飞书环境异常,请刷新重试') return } - // 获取 appId:优先从 URL 参数,否则从后端获取 - const urlParams = new URLSearchParams(window.location.search) - let appId = urlParams.get('app_id') - let companyParam = urlParams.get('company') + try { + // 获取 appId + let appId: string | undefined | null - console.log('🔍 URL 参数 - app_id:', appId, 'company:', companyParam) + // 优先从 URL 参数获取 + const urlParams = new URLSearchParams(window.location.search) + appId = urlParams.get('app_id') - // 如果没有 appId 但有 company,从后端获取 - if (!appId && companyParam) { - try { - console.log('📡 从后端获取 appId...') + // 如果没有,从后端获取 + if (!appId && companyParam) { 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') - console.log(' appId:', appId) - console.log(' appId 长度:', appId?.length) - console.log(' appId 前缀:', appId?.substring(0, 4)) - - // 验证 appId 格式 - if (!appId || !appId.startsWith('cli_')) { - console.error('❌ appId 格式错误,应以 cli_ 开头') + if (!appId) { setLoading(false) - Toast.show('飞书应用配置错误:appId 格式无效') + Toast.show('飞书应用配置错误') return } - // 构建 redirect_uri - const redirectUri = `${window.location.origin}/login/callback` - console.log(' redirect_uri:', redirectUri) - + // 调用飞书免登 window.tt.requestAuthCode({ appId: appId, - redirectUri: redirectUri, success: (res: any) => { - console.log('✅ 成功获取授权码:', res.code?.substring(0, 30)) - handleFeishuLogin(res.code) + handleFeishuLogin(res.code, companyParam) }, fail: (error: any) => { - console.error('❌ requestAuthCode 失败:', error) - console.error(' 错误详情:', JSON.stringify(error)) + console.error('requestAuthCode 失败:', error) setLoading(false) - Toast.show('免登失败:' + (error.errorMessage || error.errMsg || JSON.stringify(error))) + Toast.show('免登失败:' + (error.errMsg || JSON.stringify(error))) }, }) } catch (e) { - console.error('❌ 调用异常:', 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) => { + const handleFeishuLogin = async (authCode: string, companyParam?: 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 参数 + app_id: urlAppId || undefined, + company: companyParam || undefined, }), }) @@ -475,12 +189,9 @@ export default function Login() { 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') @@ -489,53 +200,22 @@ export default function Login() { Toast.show(result.message || '登录失败') setLoading(false) } - } catch (error: any) { + } catch (error) { 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') } + // 飞书扫码登录 + const handleQRCodeLogin = () => { + setShowQRCode(true) + } if (loading) { return ( @@ -544,28 +224,6 @@ export default function Login() {

企业工具台

正在登录...

- {/* 调试信息 */} -
-
调试信息:
-
URL: {window.location.href.substring(0, 60)}...
-
UA: {navigator.userAgent.includes('Lark') ? '✅ 飞书' : '❌ 非飞书'}
-
tt: {window.tt ? '✅ 存在' : '❌ 不存在'}
- {window.tt && ( -
- tt属性: {Object.keys(window.tt).join(', ')} -
- )} -
) @@ -576,7 +234,6 @@ export default function Login() {

企业工具台

- {/* 飞书内免登失败的提示 */} {environment === 'feishu' && (
⚠️ 飞书免登失败
- 可能原因:1) 应用配置未生效 2) 需要重新从工作台进入 + 请尝试其他登录方式或联系管理员
)} @@ -607,7 +264,7 @@ export default function Login() { {environment !== 'feishu' && ( -