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 }) => { matchOrigin: (origin: string) => boolean matchData: (data: any) => boolean } } } 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() } return () => { // 清理 if (qrContainerRef.current) { qrContainerRef.current.innerHTML = '' } if (pollTimerRef.current) { clearTimeout(pollTimerRef.current) } // 重置状态 setUseFallback(false) setQrcodeUrl('') } }, [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) // 生成登录会话 const response = await fetch('/api/v1/auth/qrcode-login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ tenant_key: '14e7bc0fe5985740', }), }) 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') // 使用飞书官方 QR SDK // 先清空容器内容(保留容器元素本身) const container = document.getElementById('feishu-qrcode-container') if (container) { container.innerHTML = '' } // 确保 DOM 已渲染后再调用 SDK setTimeout(() => { qrObjRef.current = window.QRLogin({ id: 'feishu-qrcode-container', goto: authUrl, width: '200', height: '200', }) }, 50) setStatus('pending') startPolling(result.data.session_id) } 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) => { const poll = async () => { try { const response = await fetch(`/api/v1/auth/qrcode-login/status?session_id=${id}`) const result = await response.json() if (result.code === 0) { if (result.data.status === 'success') { 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(() => { onClose() navigate('/dashboard') }, 500) } else if (result.data.status === 'scanning') { setStatus('scanning') // 继续轮询 pollTimerRef.current = setTimeout(poll, 2000) } else { // pending,继续轮询 pollTimerRef.current = setTimeout(poll, 2000) } } else { // 继续轮询 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' && ( <>
{getStatusText()}

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

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