feat: 完善鉴权和密码功能,添加飞书通知模块
- 完善Token过期/不存在时跳转登录重新认证 - 实现正常的账号密码登录验证(邮箱/手机号+密码) - 用户初始化密码设为工号(sn) - 添加修改密码功能(API+前端页面) - 修复扫码登录后跳转问题(zustand persist格式) - 创建飞书通知服务 FeishuNotificationService - 设置页面添加修改密码入口 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,266 @@
|
||||
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() {
|
||||
const appId = 'cli_a8033d607cba500d'
|
||||
|
||||
// 打印 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')
|
||||
window.tt.requestAuthCode({
|
||||
appId,
|
||||
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,
|
||||
tenant_key: '14e7bc0fe5985740',
|
||||
}),
|
||||
})
|
||||
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user