feat: 完善鉴权和密码功能,添加飞书通知模块

- 完善Token过期/不存在时跳转登录重新认证
- 实现正常的账号密码登录验证(邮箱/手机号+密码)
- 用户初始化密码设为工号(sn)
- 添加修改密码功能(API+前端页面)
- 修复扫码登录后跳转问题(zustand persist格式)
- 创建飞书通知服务 FeishuNotificationService
- 设置页面添加修改密码入口

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
root
2026-02-26 15:19:37 +08:00
parent ba555a59b7
commit 2e5f570b40
26 changed files with 2925 additions and 158 deletions
+158 -71
View File
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react'
import { useEffect, useState, useRef } from 'react'
import { useNavigate } from 'react-router-dom'
import { Toast } from 'antd-mobile'
import { useUserStore } from '@/stores/user'
@@ -9,17 +9,13 @@ import styles from './login.module.css'
declare global {
interface Window {
// 官方 tt API(H5 应用免登)
tt?: {
requestAuthCode: (params: {
appId: string
success?: (res: { code: string }) => void
fail?: (err: any) => void
}) => void
}
tt?: any
// 旧版 ttjsBridge(兼容性)
ttjsBridge?: {
getAuthCode: (params: any) => void
}
ttjsBridge?: any
// 其他可能的飞书 API
h5sdk?: any
bridge?: any
_tt?: any
}
}
@@ -46,76 +42,104 @@ export default function Login() {
}, [])
useEffect(() => {
// 1. 检测是否在飞书客户端内
const ua = navigator.userAgent.toLowerCase()
const isInFeishu = ua.includes('feishu') || ua.includes('lark') || window.tt !== undefined
// 打印详细的环境信息
const ua = navigator.userAgent
const isInFeishu = ua.includes('Lark') || ua.includes('feishu') || ua.includes('lark')
const currentUrl = window.location.href
console.log('🔍 Environment check:', { ua, isInFeishu, hasTt: window.tt !== undefined })
console.log('=== 环境检测 ===')
console.log('当前 URL:', currentUrl)
console.log('在飞书中:', isInFeishu)
console.log('window.tt:', window.tt)
if (isInFeishu) {
// 飞书内:静默授权
// 关键修复:在飞书中但 tt 对象不存在时,刷新页面让飞书注入 tt 对象
if (isInFeishu && !window.tt && currentUrl.indexOf('reloaded=1') === -1) {
console.log('⚠️ 在飞书中但 tt 对象不存在,刷新页面')
window.location.href = currentUrl + (currentUrl.indexOf('?') > 0 ? '&' : '?') + 'reloaded=1'
return
}
// 检查 URL 中是否有 auth_code 参数(飞书授权回调)
const urlParams = new URLSearchParams(window.location.search)
const authCode = urlParams.get('auth_code')
if (authCode) {
// 有授权码,直接处理登录
console.log('✅ Found auth_code in URL:', authCode.substring(0, 20) + '...')
handleFeishuLogin(authCode)
} else if (isInFeishu && window.tt) {
// 飞书内且有 tt 对象:静默授权
setEnvironment('feishu')
handleFeishuAutoLogin()
} else {
// 飞书外:检查 URL 中是否有 auth_code 参数(飞书授权回调)
const urlParams = new URLSearchParams(window.location.search)
const authCode = urlParams.get('auth_code')
const qrcodeId = urlParams.get('qrcode_id')
if (qrcodeId) {
// 扫码登录回调
setEnvironment('external')
handleQrcodeCallback(qrcodeId)
} else if (authCode) {
// 直接访问授权页面(不太可能)
setEnvironment('external')
handleFeishuCallback(authCode)
} else {
setEnvironment('external')
setLoading(false)
}
// 飞书外或 tt 对象不可用:显示登录选项
setEnvironment('external')
setLoading(false)
}
}, [])
// 飞书内静默授权(使用官方 tt.requestAuthCode API)
// 飞书内静默授权
const handleFeishuAutoLogin = () => {
console.log('🚀 Feishu auto login started')
console.log('🚀 开始飞书免登')
console.log('🔍 window.tt:', window.tt)
console.log('🔍 window.tt 类型:', typeof window.tt)
const appId = 'cli_a8033d607cba500d'
// 详细检查 tt 对象
if (window.tt) {
// 使用官方 tt.requestAuthCode API
window.tt.requestAuthCode({
appId: appId,
success: (res) => {
console.log('✅ Got auth code from tt.requestAuthCode:', res.code)
handleFeishuLogin(res.code)
},
fail: (error: any) => {
console.error('❌ requestAuthCode failed:', error)
setLoading(false)
Toast.show('获取授权失败,请重试')
},
})
} else if ((window as any).ttjsBridge) {
// 兼容旧版 ttjsBridge
console.warn('⚠️ Using legacy ttjsBridge API')
;(window as any).ttjsBridge.getAuthCode({
success: (authCode: string) => {
console.log('✅ Got auth code from ttjsBridge:', authCode)
handleFeishuLogin(authCode)
},
fail: (error: any) => {
console.error('❌ Get auth code failed:', error)
setLoading(false)
Toast.show('获取授权失败,请重试')
},
})
console.log('✅ tt 对象存在')
console.log('🔍 tt 的所有属性:', Object.keys(window.tt))
console.log('🔍 tt.requestAuthCode 类型:', typeof window.tt.requestAuthCode)
// 检查 requestAuthCode 是否存在
if ('requestAuthCode' in window.tt) {
console.log('✅ requestAuthCode 属性存在')
console.log('🔍 类型:', typeof window.tt.requestAuthCode)
} else {
console.log('❌ requestAuthCode 属性不存在')
}
} else {
console.warn('⚠️ No Feishu bridge available')
console.log('❌ tt 对象不存在')
setLoading(false)
Toast.show('请在飞书中打开')
Toast.show('飞书 API 未加载,请刷新重试')
return
}
// 等待一小段时间,确保 tt 完全初始化
setTimeout(() => {
console.log('⏰ 延迟后再次检查 tt:', !!window.tt)
if (!window.tt) {
setLoading(false)
Toast.show('飞书 API 加载失败')
return
}
// 尝试调用 requestAuthCode
try {
console.log('🔧 调用 tt.requestAuthCode,appId:', appId)
window.tt.requestAuthCode({
appId: appId,
success: (res: any) => {
console.log('✅ 成功获取授权码:', res)
console.log('✅ code:', res.code?.substring(0, 30))
handleFeishuLogin(res.code)
},
fail: (error: any) => {
console.error('❌ requestAuthCode 失败:', error)
console.error('❌ 错误详情:', JSON.stringify(error))
setLoading(false)
Toast.show('免登失败:' + (error.errorMessage || error.errMsg || JSON.stringify(error)))
},
})
} catch (e) {
console.error('❌ 调用异常:', e)
setLoading(false)
Toast.show('免登失败,请使用其他方式登录')
}
}, 500)
}
// 跳转到飞书授权页面(飞书外扫码登录)
@@ -155,14 +179,18 @@ export default function Login() {
// 处理飞书登录
const handleFeishuLogin = async (authCode: string) => {
try {
const tenantKey = '14e7bc0fe5985740'
// 从 URL 获取 app_id 参数
const urlParams = new URLSearchParams(window.location.search)
const urlAppId = urlParams.get('app_id')
console.log('🔍 URL 中的 app_id:', urlAppId)
const response = await fetch('/api/v1/auth/feishu-login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
auth_code: authCode,
tenant_key: tenantKey,
app_id: urlAppId || undefined, // 如果 URL 有 app_id 就传递
}),
})
@@ -183,10 +211,6 @@ export default function Login() {
}
}
// 邮箱密码登录
const handlePasswordLogin = () => {
navigate('/login-simple')
}
if (loading) {
return (
@@ -194,6 +218,69 @@ export default function Login() {
<div className={styles.container}>
<h1>企业工具台</h1>
<p>正在登录...</p>
<div className={styles.spinner}></div>
{/* 调试信息 */}
<div style={{
marginTop: '20px',
padding: '10px',
background: 'rgba(0,0,0,0.2)',
borderRadius: '8px',
fontSize: '12px',
textAlign: 'left',
fontFamily: 'monospace',
maxWidth: '100%',
overflow: 'auto'
}}>
<div style={{ marginBottom: '5px', color: '#aaa' }}>调试信息:</div>
<div style={{ marginBottom: '3px' }}>URL: {window.location.href.substring(0, 60)}...</div>
<div style={{ marginBottom: '3px' }}>UA: {navigator.userAgent.includes('Lark') ? '✅ 飞书' : '❌ 非飞书'}</div>
<div style={{ marginBottom: '3px' }}>tt: {window.tt ? '✅ 存在' : '❌ 不存在'}</div>
{window.tt && (
<div style={{ marginBottom: '3px', wordBreak: 'break-all' }}>
tt属性: {Object.keys(window.tt).join(', ')}
</div>
)}
</div>
</div>
</div>
)
}
// 邮箱密码登录
const handlePasswordLogin = () => {
navigate('/login-simple')
}
if (loading) {
return (
<div className={styles.login}>
<div className={styles.container}>
<h1>企业工具台</h1>
<p>正在登录...</p>
<div className={styles.spinner}></div>
{/* 调试信息 */}
<div style={{
marginTop: '20px',
padding: '10px',
background: 'rgba(0,0,0,0.2)',
borderRadius: '8px',
fontSize: '12px',
textAlign: 'left',
fontFamily: 'monospace',
maxWidth: '100%',
overflow: 'auto'
}}>
<div style={{ marginBottom: '5px', color: '#aaa' }}>调试信息:</div>
<div style={{ marginBottom: '3px' }}>URL: {window.location.href.substring(0, 60)}...</div>
<div style={{ marginBottom: '3px' }}>UA: {navigator.userAgent.includes('Lark') ? '✅ 飞书' : '❌ 非飞书'}</div>
<div style={{ marginBottom: '3px' }}>tt: {window.tt ? '✅ 存在' : '❌ 不存在'}</div>
{window.tt && (
<div style={{ marginBottom: '3px', wordBreak: 'break-all' }}>
tt属性: {Object.keys(window.tt).join(', ')}
</div>
)}
</div>
</div>
</div>
)