b1ca2c58b7
后端改动: - FeishuService: 从飞书用户信息获取 tenant_key 和手机号 - FeishuService: 新增 user_sn 表工号分配机制 - FeishuService: 优先使用 enterprise_email,支持手机号 - AuthController: 飞书免登支持 company 参数 前端改动: - FeishuAutoLogin: 支持多租户 company 参数 - login/index: 支持 redirect 参数和已登录检测 - QRCodeLogin: 支持多租户参数传递 - router: 新增路由守卫和参数处理 - api.ts: 优化飞书登录 API 调用 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
270 lines
8.1 KiB
TypeScript
270 lines
8.1 KiB
TypeScript
import { useEffect, useState } from 'react'
|
|
import { useNavigate } from 'react-router-dom'
|
|
import { useUserStore } from '@/stores/user'
|
|
import QRCodeLogin from '@/pages/login/QRCodeLogin'
|
|
|
|
// 声明飞书 JSAPI 类型
|
|
declare global {
|
|
interface Window {
|
|
tt?: any
|
|
ttjsBridge?: any
|
|
}
|
|
}
|
|
|
|
export default function FeishuAutoLogin() {
|
|
const navigate = useNavigate()
|
|
const { login, isLoggedIn } = useUserStore()
|
|
const [status, setStatus] = useState<'checking' | 'success' | 'failed' | 'show_options'>('checking')
|
|
const [showQRCode, setShowQRCode] = useState(false)
|
|
const [debugInfo, setDebugInfo] = useState<any>({})
|
|
|
|
useEffect(() => {
|
|
// 如果已登录,直接跳转
|
|
if (isLoggedIn) {
|
|
navigate('/dashboard')
|
|
return
|
|
}
|
|
|
|
const ua = navigator.userAgent
|
|
const currentUrl = window.location.href
|
|
const isInFeishu = ua.includes('Lark') || ua.includes('feishu') || ua.includes('lark')
|
|
|
|
console.log('=== 首页免登检测 ===')
|
|
console.log('URL:', currentUrl)
|
|
console.log('在飞书中:', isInFeishu)
|
|
console.log('window.tt:', !!window.tt)
|
|
|
|
setDebugInfo({
|
|
url: currentUrl,
|
|
isInFeishu,
|
|
hasTT: !!window.tt,
|
|
})
|
|
|
|
// 检查 URL 中是否有 auth_code
|
|
const urlParams = new URLSearchParams(window.location.search)
|
|
const authCode = urlParams.get('auth_code')
|
|
|
|
if (authCode) {
|
|
console.log('✅ 发现 auth_code,直接登录')
|
|
handleLogin(authCode)
|
|
return
|
|
}
|
|
|
|
if (!isInFeishu) {
|
|
console.log('不在飞书中,显示登录选项')
|
|
setStatus('show_options')
|
|
return
|
|
}
|
|
|
|
// 在飞书中,尝试免登
|
|
tryFeishuAutoLogin()
|
|
|
|
function tryFeishuAutoLogin() {
|
|
// 从 URL 获取 company 参数
|
|
const urlParams = new URLSearchParams(window.location.search)
|
|
const companyParam = urlParams.get('company')
|
|
console.log('🔍 URL 中的 company 参数:', companyParam)
|
|
|
|
// 打印 tt 对象的所有方法
|
|
if (window.tt) {
|
|
console.log('tt 对象的方法:')
|
|
for (let key in window.tt) {
|
|
if (typeof window.tt[key] === 'function') {
|
|
console.log(' -', key)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 方式1: tt.requestAuthCode
|
|
if (window.tt && typeof window.tt.requestAuthCode === 'function') {
|
|
console.log('使用 tt.requestAuthCode')
|
|
// 不传 appId,让飞书自动选择(后端会根据 company 参数处理)
|
|
window.tt.requestAuthCode({
|
|
success: (res: any) => {
|
|
console.log('✅ 获取到授权码:', res.code?.substring(0, 20))
|
|
handleLogin(res.code)
|
|
},
|
|
fail: (err: any) => {
|
|
console.error('❌ 免登失败:', err)
|
|
setStatus('show_options')
|
|
},
|
|
})
|
|
return
|
|
}
|
|
|
|
// 方式2: ttjsBridge
|
|
if (window.ttjsBridge && typeof window.ttjsBridge.getAuthCode === 'function') {
|
|
console.log('使用 ttjsBridge.getAuthCode')
|
|
window.ttjsBridge.getAuthCode({
|
|
success: (code: string) => {
|
|
console.log('✅ 获取到授权码:', code?.substring(0, 20))
|
|
handleLogin(code)
|
|
},
|
|
fail: (err: any) => {
|
|
console.error('❌ 免登失败:', err)
|
|
setStatus('show_options')
|
|
},
|
|
})
|
|
return
|
|
}
|
|
|
|
// tt API 不可用,显示登录选项
|
|
console.log('⚠️ tt API 不可用')
|
|
setStatus('show_options')
|
|
}
|
|
|
|
async function handleLogin(authCode: string) {
|
|
try {
|
|
const response = await fetch('/api/v1/auth/feishu-login', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
auth_code: authCode,
|
|
company: urlParams.get('company') || undefined,
|
|
}),
|
|
})
|
|
|
|
const result = await response.json()
|
|
|
|
if (result.code === 0) {
|
|
console.log('✅ 登录成功')
|
|
login(result.data.user, result.data.token)
|
|
setStatus('success')
|
|
setTimeout(() => navigate('/dashboard'), 500)
|
|
} else {
|
|
console.error('❌ 登录失败:', result.message)
|
|
setStatus('show_options')
|
|
}
|
|
} catch (error) {
|
|
console.error('❌ 登录异常:', error)
|
|
setStatus('show_options')
|
|
}
|
|
}
|
|
}, [isLoggedIn, login, navigate])
|
|
|
|
// 检测中的状态
|
|
if (status === 'checking') {
|
|
return (
|
|
<div style={{
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
height: '100vh',
|
|
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
|
color: 'white'
|
|
}}>
|
|
<h1>企业工具台</h1>
|
|
<p style={{ marginTop: 20 }}>正在登录...</p>
|
|
<div style={{
|
|
width: 40,
|
|
height: 40,
|
|
border: '4px solid rgba(255,255,255,0.3)',
|
|
borderTopColor: 'white',
|
|
borderRadius: '50%',
|
|
animation: 'spin 1s linear infinite',
|
|
marginTop: 20
|
|
}}></div>
|
|
{/* 调试信息 */}
|
|
<div style={{
|
|
marginTop: 30,
|
|
padding: 10,
|
|
background: 'rgba(0,0,0,0.2)',
|
|
borderRadius: 8,
|
|
fontSize: 12,
|
|
textAlign: 'left'
|
|
}}>
|
|
<div>URL: {debugInfo.url?.substring(0, 50)}...</div>
|
|
<div>在飞书中: {debugInfo.isInFeishu ? '✅' : '❌'}</div>
|
|
<div>tt 对象: {debugInfo.hasTT ? '✅' : '❌'}</div>
|
|
</div>
|
|
<style>{`@keyframes spin { to { transform: rotate(360deg); } }`}</style>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// 成功状态
|
|
if (status === 'success') {
|
|
return (
|
|
<div style={{
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
height: '100vh',
|
|
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
|
color: 'white'
|
|
}}>
|
|
<h1>✅ 登录成功</h1>
|
|
<p style={{ marginTop: 20 }}>正在进入应用...</p>
|
|
<div style={{
|
|
width: 40,
|
|
height: 40,
|
|
border: '4px solid rgba(255,255,255,0.3)',
|
|
borderTopColor: 'white',
|
|
borderRadius: '50%',
|
|
animation: 'spin 1s linear infinite',
|
|
marginTop: 20
|
|
}}></div>
|
|
<style>{`@keyframes spin { to { transform: rotate(360deg); } }`}</style>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// 显示登录选项
|
|
if (status === 'show_options') {
|
|
return (
|
|
<div style={{
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
minHeight: '100vh',
|
|
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
|
color: 'white',
|
|
padding: 20
|
|
}}>
|
|
<h1 style={{ marginBottom: 10 }}>企业工具台</h1>
|
|
<p style={{ marginBottom: 40, opacity: 0.9 }}>请选择登录方式</p>
|
|
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 16, width: '100%', maxWidth: 320 }}>
|
|
{/* 飞书扫码登录 */}
|
|
<button
|
|
onClick={() => setShowQRCode(true)}
|
|
style={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: 16,
|
|
width: '100%',
|
|
padding: 16,
|
|
background: '#fff',
|
|
border: 'none',
|
|
borderRadius: 12,
|
|
cursor: 'pointer'
|
|
}}
|
|
>
|
|
<span style={{ fontSize: 28 }}>📱</span>
|
|
<div style={{ flex: 1, textAlign: 'left' }}>
|
|
<div style={{ fontSize: 16, fontWeight: 500, color: '#333', marginBottom: 2 }}>飞书扫码登录</div>
|
|
<div style={{ fontSize: 12, color: '#999' }}>使用飞书 APP 扫码登录</div>
|
|
</div>
|
|
</button>
|
|
</div>
|
|
|
|
{debugInfo.isInFeishu && (
|
|
<p style={{ marginTop: 30, fontSize: 12, opacity: 0.7, textAlign: 'center' }}>
|
|
💡 检测到在飞书客户端中,但免登功能不可用
|
|
</p>
|
|
)}
|
|
|
|
<QRCodeLogin
|
|
visible={showQRCode}
|
|
onClose={() => setShowQRCode(false)}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return null
|
|
}
|