import { useState, useEffect, useRef } from 'react' import { Dialog, Toast } from 'antd-mobile' import { QRCodeSVG } from 'qrcode.react' import { useNavigate } from 'react-router-dom' import styles from './QRCodeLogin.module.css' // 声明飞书 QR SDK 类型 declare global { interface Window { QRLogin?: (config: { id: string goto: string width?: string height?: string style?: string }) => { matchOrigin: (origin: string) => boolean matchData: (data: any) => boolean } LarkSSOSDKWebQRCode?: any } } interface QRCodeLoginProps { visible: boolean onClose: () => void } export default function QRCodeLogin({ visible, onClose }: QRCodeLoginProps) { const navigate = useNavigate() const [status, setStatus] = useState<'loading' | 'pending' | 'scanning' | 'success'>('loading') const [useFallback, setUseFallback] = useState(false) const [qrcodeUrl, setQrcodeUrl] = useState('') const qrContainerRef = useRef(null) const qrObjRef = useRef(null) const pollTimerRef = useRef(null) const sessionIdRef = useRef('') useEffect(() => { if (visible) { initQRCode() } // 监听来自回调页面的成功消息 const handleCallbackMessage = (event: MessageEvent) => { if (event.data?.type === 'feishu-login-success') { console.log('✅ Received login success from callback popup') // 轮询应该会检测到成功状态,但这里作为额外的触发器 // 如果轮询已经检测到并处理了,这个消息就忽略 } } window.addEventListener('message', handleCallbackMessage) return () => { // 清理 message 事件监听器 if (qrObjRef.current?._cleanup) { qrObjRef.current._cleanup() } // 清理回调消息监听器 window.removeEventListener('message', handleCallbackMessage) // 清理容器 if (qrContainerRef.current) { qrContainerRef.current.innerHTML = '' } // 清理轮询 if (pollTimerRef.current) { clearTimeout(pollTimerRef.current) } // 重置状态 setUseFallback(false) setQrcodeUrl('') qrObjRef.current = null } }, [visible]) const initQRCode = async () => { setStatus('loading') setUseFallback(false) setQrcodeUrl('') try { // 等待一小段时间确保 SDK 已加载 await new Promise(resolve => setTimeout(resolve, 100)) // 检查飞书 QR SDK 是否已加载 if (!window.QRLogin) { console.warn('⚠️ Feishu QR SDK not loaded, trying to wait...') // 再等待一段时间 await new Promise(resolve => setTimeout(resolve, 500)) } console.log('🔍 window.QRLogin:', !!window.QRLogin) // 从 URL 获取 company 参数(支持两种方式:直接参数或从 redirect 中提取) const urlParams = new URLSearchParams(window.location.search) let companyParam = urlParams.get('company') // 如果当前 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('🔍 最终使用的 company 参数:', companyParam) // 生成登录会话 const requestBody: any = {} if (companyParam) { // 如果有 company 参数,传递给后端 requestBody.company = companyParam } const response = await fetch('/api/v1/auth/qrcode-login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(requestBody), }) const result = await response.json() if (result.code === 0) { const authUrl = result.data.auth_url sessionIdRef.current = result.data.session_id // 检查飞书 QR SDK 是否已加载(在 index.html 中引入) if (window.QRLogin) { console.log('✅ Using Feishu QR SDK') setStatus('pending') // 确保 DOM 已渲染后再调用 SDK // Dialog 内容可能需要更长时间渲染,使用多次尝试 const initQRSDK = (retryCount = 0) => { const container = document.getElementById('feishu-qrcode-container') console.log('🔍 Looking for container, attempt', retryCount + 1, 'found:', !!container) if (container) { // 清空容器 container.innerHTML = '' try { console.log('🔧 Initializing QR SDK with goto:', authUrl.substring(0, 50) + '...') qrObjRef.current = window.QRLogin({ id: 'feishu-qrcode-container', goto: authUrl, width: '250', height: '250', style: 'width:300px;height:270px;border:none' }) console.log('✅ Feishu QR SDK initialized successfully') console.log('📦 Container children count:', container.children.length) setStatus('pending') startPolling(result.data.session_id) // 监听扫码后的 message 事件(关键步骤!) const handleMessage = (event: MessageEvent) => { console.log('📨 Received message event:', event.origin, event.data) // 安全校验 if (!qrObjRef.current?.matchOrigin(event.origin)) { console.warn('⚠️ Origin mismatch:', event.origin) return } if (!qrObjRef.current?.matchData(event.data)) { console.warn('⚠️ Data mismatch:', event.data) return } // 获取临时授权码 const loginTmpCode = event.data?.tmp_code if (loginTmpCode) { console.log('✅ Got tmp_code:', loginTmpCode) console.log('🔀 Redirecting to Feishu auth page...') // 保存当前 URL,登录成功后返回 const currentUrl = window.location.href sessionStorage.setItem('feishu_login_return_url', currentUrl) // 重定向到飞书授权页面(官方文档推荐方式) const authUrlWithCode = `${authUrl}&tmp_code=${loginTmpCode}` window.location.href = authUrlWithCode } } window.addEventListener('message', handleMessage) // 保存清理函数 qrObjRef.current._cleanup = () => { window.removeEventListener('message', handleMessage) } } catch (e) { console.error('❌ QR SDK init error:', e) // Fallback to normal QR code setQrcodeUrl(authUrl) setUseFallback(true) startPolling(result.data.session_id) } } else if (retryCount < 10) { // 容器还未渲染,继续等待 setTimeout(() => initQRSDK(retryCount + 1), 100) } else { console.error('❌ Container not found after 10 attempts, using fallback') setQrcodeUrl(authUrl) setUseFallback(true) startPolling(result.data.session_id) } } // 开始初始化 setTimeout(() => initQRSDK(), 100) } else { // SDK 未加载,使用普通二维码 console.warn('⚠️ Feishu QR SDK not loaded, using fallback') setQrcodeUrl(authUrl) setUseFallback(true) setStatus('pending') startPolling(result.data.session_id) } } else { Toast.show(result.message || '生成二维码失败') } } catch (error) { console.error('生成二维码失败:', error) Toast.show('生成二维码失败') } } const startPolling = (id: string) => { console.log('🔄 Starting polling for session:', id) const poll = async () => { try { const response = await fetch(`/api/v1/auth/qrcode-login/status?session_id=${id}`) const result = await response.json() console.log('🔄 Polling session', id, 'result:', result) if (result.code === 0) { if (result.data.status === 'success') { console.log('✅ Login success detected, navigating to dashboard...') setStatus('success') // 登录成功,保存 token const { token, user } = result.data localStorage.setItem('token', token) localStorage.setItem('i18nextLng', 'zh') // 触发登录成功事件,让 store 更新 window.dispatchEvent(new CustomEvent('feishu-login-success', { detail: { user, token } })) Toast.show('登录成功') setTimeout(() => { console.log('🚀 Navigating...') onClose() // 检查是否有 redirect 参数 const urlParams = new URLSearchParams(window.location.search) const redirect = urlParams.get('redirect') if (redirect) { console.log('🔗 登录成功,跳转到:', redirect) window.location.href = redirect } else { navigate('/dashboard') } }, 500) } else if (result.data.status === 'scanning') { console.log('📱 Status: scanning') setStatus('scanning') // 继续轮询 pollTimerRef.current = setTimeout(poll, 2000) } else { console.log('⏳ Status:', result.data.status) // pending,继续轮询 pollTimerRef.current = setTimeout(poll, 2000) } } else { console.log('❌ Poll error:', result.message) // 继续轮询 pollTimerRef.current = setTimeout(poll, 2000) } } catch (error) { console.error('❌ 轮询登录状态失败:', error) // 继续轮询 pollTimerRef.current = setTimeout(poll, 2000) } } pollTimerRef.current = setTimeout(poll, 2000) } const getStatusText = () => { switch (status) { case 'loading': return '正在生成二维码...' case 'pending': return '请使用飞书扫码登录' case 'scanning': return '已扫码,请在手机上确认' case 'success': return '登录成功!' default: return '' } } const getStatusColor = () => { switch (status) { case 'pending': return '#1677ff' case 'scanning': return '#faad14' case 'success': return '#52c41a' default: return '#999' } } const handleRefresh = () => { if (pollTimerRef.current) { clearTimeout(pollTimerRef.current) } initQRCode() } return ( {status === 'loading' &&
正在生成二维码...
} {useFallback && qrcodeUrl && ( <>
{getStatusText()}

💡 使用飞书扫码登录

)} {!useFallback && status !== 'loading' && ( <>

💡 打开飞书 APP,扫一扫登录

)} } onClose={onClose} closeOnMaskClick={status !== 'scanning' && status !== 'success'} /> ) }