feat: 添加飞书免登自动登录功能
修改内容: 1. approval/detail.tsx - 在加载详情前检查登录状态,如果在飞书内则自动免登 2. login/index.tsx - 改进飞书环境检测和tt对象注入逻辑 业务流程: 1. 用户在飞书中点击消息按钮 2. 打开审批详情页 → 检测未登录 3. 如果在飞书客户端内 → 自动调用 tt.requestAuthCode 4. 免登成功 → 自动加载详情 5. 免登失败 → 跳转到登录页面 相关文件: - frontend/src/pages/approval/detail.tsx - frontend/src/pages/login/index.tsx Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -11,9 +11,18 @@ import {
|
|||||||
} from 'antd-mobile-icons'
|
} from 'antd-mobile-icons'
|
||||||
import { approvalApi } from '../../services/api'
|
import { approvalApi } from '../../services/api'
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
|
import { authApi } from '../../services/api'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import styles from './detail.module.css'
|
import styles from './detail.module.css'
|
||||||
|
|
||||||
|
// 声明飞书 JSAPI 类型
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
tt?: any
|
||||||
|
ttjsBridge?: any
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 状态配置
|
// 状态配置
|
||||||
const statusConfig: Record<string, { color: string; icon: React.ReactNode; text: string }> = {
|
const statusConfig: Record<string, { color: string; icon: React.ReactNode; text: string }> = {
|
||||||
draft: { color: 'default', icon: <ClockCircleOutline />, text: '草稿' },
|
draft: { color: 'default', icon: <ClockCircleOutline />, text: '草稿' },
|
||||||
@@ -99,18 +108,80 @@ export default function ApprovalDetail() {
|
|||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { id } = useParams()
|
const { id } = useParams()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const { user } = useUserStore()
|
const { user, login } = useUserStore()
|
||||||
|
|
||||||
const [detail, setDetail] = useState<ApprovalDetail | null>(null)
|
const [detail, setDetail] = useState<ApprovalDetail | null>(null)
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [commentVisible, setCommentVisible] = useState(false)
|
const [commentVisible, setCommentVisible] = useState(false)
|
||||||
const [comment, setComment] = useState('')
|
const [comment, setComment] = useState('')
|
||||||
const [actionType, setActionType] = useState<'approve' | 'reject' | 'cancel'>('approve')
|
const [actionType, setActionType] = useState<'approve' | 'reject' | 'cancel'>('approve')
|
||||||
|
const [tryingFeishuLogin, setTryingFeishuLogin] = useState(false)
|
||||||
|
|
||||||
|
// 飞书免登
|
||||||
|
const tryFeishuLogin = (): Promise<boolean> => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const ua = navigator.userAgent
|
||||||
|
const isInFeishu = ua.includes('Lark') || ua.includes('feishu') || ua.includes('lark')
|
||||||
|
|
||||||
|
if (!isInFeishu || !window.tt) {
|
||||||
|
console.log('❌ 不在飞书内或 tt 对象不可用')
|
||||||
|
resolve(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🚀 尝试飞书免登')
|
||||||
|
const appId = 'cli_a8033d607cba500d'
|
||||||
|
|
||||||
|
window.tt.requestAuthCode({
|
||||||
|
appId: appId,
|
||||||
|
success: async (res: any) => {
|
||||||
|
console.log('✅ 飞书免登成功:', res.code?.substring(0, 20))
|
||||||
|
try {
|
||||||
|
const result = await authApi.feishuLogin(res.code)
|
||||||
|
login(result.user, result.token)
|
||||||
|
Toast.show('登录成功')
|
||||||
|
resolve(true)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 飞书登录API失败:', error)
|
||||||
|
resolve(false)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fail: (error: any) => {
|
||||||
|
console.error('❌ 飞书免登失败:', error)
|
||||||
|
resolve(false)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// 加载详情
|
// 加载详情
|
||||||
const loadDetail = async () => {
|
const loadDetail = async () => {
|
||||||
if (!id) return
|
if (!id) return
|
||||||
|
|
||||||
|
// 检查登录状态,如果未登录且在飞书内,先尝试飞书免登
|
||||||
|
const token = localStorage.getItem('token')
|
||||||
|
const isLoggedIn = !!user
|
||||||
|
|
||||||
|
if (!isLoggedIn && !token) {
|
||||||
|
const ua = navigator.userAgent
|
||||||
|
const isInFeishu = ua.includes('Lark') || ua.includes('feishu') || ua.includes('lark')
|
||||||
|
|
||||||
|
if (isInFeishu && window.tt && !tryingFeishuLogin) {
|
||||||
|
setTryingFeishuLogin(true)
|
||||||
|
const success = await tryFeishuLogin()
|
||||||
|
setTryingFeishuLogin(false)
|
||||||
|
if (!success) {
|
||||||
|
// 免登失败,跳转到登录页面
|
||||||
|
window.location.href = '/login?redirect=' + encodeURIComponent(window.location.pathname + window.location.search)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 不在飞书内或免登不可用,跳转到登录页面
|
||||||
|
window.location.href = '/login?redirect=' + encodeURIComponent(window.location.pathname + window.location.search)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
const data = await approvalApi.getDetail(Number(id))
|
const data = await approvalApi.getDetail(Number(id))
|
||||||
|
|||||||
@@ -49,19 +49,30 @@ export default function Login() {
|
|||||||
|
|
||||||
console.log('=== 环境检测 ===')
|
console.log('=== 环境检测 ===')
|
||||||
console.log('当前 URL:', currentUrl)
|
console.log('当前 URL:', currentUrl)
|
||||||
|
console.log('User Agent:', ua)
|
||||||
console.log('在飞书中:', isInFeishu)
|
console.log('在飞书中:', isInFeishu)
|
||||||
console.log('window.tt:', window.tt)
|
console.log('window.tt:', window.tt)
|
||||||
|
|
||||||
// 关键修复:在飞书中但 tt 对象不存在时,刷新页面让飞书注入 tt 对象
|
// 检查 URL 中是否来自飞书消息通知
|
||||||
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 urlParams = new URLSearchParams(window.location.search)
|
||||||
const authCode = urlParams.get('auth_code')
|
const authCode = urlParams.get('auth_code')
|
||||||
|
const fromFeishu = urlParams.get('from') === 'feishu'
|
||||||
|
|
||||||
|
// 关键修复:在飞书中但 tt 对象不存在时,刷新页面让飞书注入 tt 对象
|
||||||
|
// 增加多次重试机制
|
||||||
|
if (isInFeishu && !window.tt) {
|
||||||
|
const reloadCount = parseInt(urlParams.get('reload') || '0')
|
||||||
|
if (reloadCount < 3) {
|
||||||
|
console.log(`⚠️ 在飞书中但 tt 对象不存在,第 ${reloadCount + 1} 次刷新页面`)
|
||||||
|
const separator = currentUrl.indexOf('?') > 0 ? '&' : '?'
|
||||||
|
// 清除旧的 reload 参数并添加新的
|
||||||
|
const newUrl = currentUrl.split('?')[0] + '?' + urlParams.toString().replace(/reload=\d+/g, '').replace(/^&+/, '')
|
||||||
|
window.location.href = newUrl + (newUrl.includes('?') ? '&' : '?') + `reload=${reloadCount + 1}`
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
console.log('❌ 多次刷新后 tt 对象仍不存在,放弃自动登录')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (authCode) {
|
if (authCode) {
|
||||||
// 有授权码,直接处理登录
|
// 有授权码,直接处理登录
|
||||||
@@ -69,10 +80,18 @@ export default function Login() {
|
|||||||
handleFeishuLogin(authCode)
|
handleFeishuLogin(authCode)
|
||||||
} else if (isInFeishu && window.tt) {
|
} else if (isInFeishu && window.tt) {
|
||||||
// 飞书内且有 tt 对象:静默授权
|
// 飞书内且有 tt 对象:静默授权
|
||||||
|
console.log('✅ 飞书内环境,tt 对象可用,开始免登')
|
||||||
setEnvironment('feishu')
|
setEnvironment('feishu')
|
||||||
handleFeishuAutoLogin()
|
handleFeishuAutoLogin()
|
||||||
|
} else if (isInFeishu) {
|
||||||
|
// 飞书内但 tt 对象不可用:提示用户
|
||||||
|
console.log('⚠️ 在飞书内但 tt 对象不可用')
|
||||||
|
setEnvironment('feishu')
|
||||||
|
setLoading(false)
|
||||||
|
Toast.show('请在飞书客户端中打开,或使用扫码登录')
|
||||||
} else {
|
} else {
|
||||||
// 飞书外或 tt 对象不可用:显示登录选项
|
// 飞书外:显示登录选项
|
||||||
|
console.log('📱 飞书外环境,显示登录选项')
|
||||||
setEnvironment('external')
|
setEnvironment('external')
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user