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 { QR?: (config: { id: string goto: string width?: string height?: string }) => { matchOrigin: (origin: string) => boolean matchData: (data: any) => boolean } } } interface QRCodeLoginProps { visible: boolean onClose: () => void } // 动态加载飞书 QR SDK const loadFeishuQRSDK = (): Promise => { return new Promise((resolve) => { // 检查是否已经加载 if (window.QR) { console.log('✅ Feishu QR SDK already loaded') resolve(true) return } console.log('📦 Loading Feishu QR SDK...') const script = document.createElement('script') script.id = 'feishu-qr-sdk' script.src = 'https://lf-package-cn.feishucdn.com/obj/feishu-static/lark/passport/qrcode/LarkSSOSDKWebQRCode-1.0.3.js' script.async = true script.timeout = 5000 // 5秒超时 const timeoutId = setTimeout(() => { console.warn('⚠️ Feishu QR SDK load timeout, using fallback') resolve(false) }, 5000) script.onload = () => { clearTimeout(timeoutId) setTimeout(() => { if (window.QR) { console.log('✅ Feishu QR SDK loaded successfully') resolve(true) } else { console.warn('⚠️ QR SDK loaded but window.QR not found, using fallback') resolve(false) } }, 100) } script.onerror = () => { clearTimeout(timeoutId) console.warn('⚠️ Failed to load Feishu QR SDK, using fallback') resolve(false) // 不 reject,而是返回 false 使用降级方案 } document.head.appendChild(script) }) } 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 { // 尝试加载飞书 QR SDK const sdkLoaded = await loadFeishuQRSDK() // 生成登录会话 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 if (sdkLoaded && window.QR) { // 使用飞书官方 QR SDK if (qrContainerRef.current) { qrContainerRef.current.innerHTML = '' } qrObjRef.current = window.QR({ id: 'feishu-qrcode-container', goto: authUrl, width: '200', height: '200', }) setStatus('pending') startPolling(result.data.session_id) } else { // SDK 加载失败,使用普通二维码 console.log('📱 Using fallback QR code') 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'} /> ) }