From 3ad8f34914d05c5ea0a6d1017c8932ab62990818 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 26 Feb 2026 19:14:19 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E9=A3=9E=E4=B9=A6?= =?UTF-8?q?=E5=85=8D=E7=99=BB=E8=87=AA=E5=8A=A8=E7=99=BB=E5=BD=95=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修改内容: 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 --- frontend/src/pages/approval/detail.tsx | 73 +++++++++++++++++++++++++- frontend/src/pages/login/index.tsx | 37 +++++++++---- 2 files changed, 100 insertions(+), 10 deletions(-) diff --git a/frontend/src/pages/approval/detail.tsx b/frontend/src/pages/approval/detail.tsx index d8555c7..d593a19 100644 --- a/frontend/src/pages/approval/detail.tsx +++ b/frontend/src/pages/approval/detail.tsx @@ -11,9 +11,18 @@ import { } from 'antd-mobile-icons' import { approvalApi } from '../../services/api' import { useUserStore } from '@/stores/user' +import { authApi } from '../../services/api' import dayjs from 'dayjs' import styles from './detail.module.css' +// 声明飞书 JSAPI 类型 +declare global { + interface Window { + tt?: any + ttjsBridge?: any + } +} + // 状态配置 const statusConfig: Record = { draft: { color: 'default', icon: , text: '草稿' }, @@ -99,18 +108,80 @@ export default function ApprovalDetail() { const navigate = useNavigate() const { id } = useParams() const location = useLocation() - const { user } = useUserStore() + const { user, login } = useUserStore() const [detail, setDetail] = useState(null) const [loading, setLoading] = useState(true) const [commentVisible, setCommentVisible] = useState(false) const [comment, setComment] = useState('') const [actionType, setActionType] = useState<'approve' | 'reject' | 'cancel'>('approve') + const [tryingFeishuLogin, setTryingFeishuLogin] = useState(false) + + // 飞书免登 + const tryFeishuLogin = (): Promise => { + 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 () => { 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) try { const data = await approvalApi.getDetail(Number(id)) diff --git a/frontend/src/pages/login/index.tsx b/frontend/src/pages/login/index.tsx index ed5f988..333bfee 100644 --- a/frontend/src/pages/login/index.tsx +++ b/frontend/src/pages/login/index.tsx @@ -49,19 +49,30 @@ export default function Login() { console.log('=== 环境检测 ===') console.log('当前 URL:', currentUrl) + console.log('User Agent:', ua) console.log('在飞书中:', isInFeishu) console.log('window.tt:', window.tt) - // 关键修复:在飞书中但 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 参数(飞书授权回调) + // 检查 URL 中是否来自飞书消息通知 const urlParams = new URLSearchParams(window.location.search) 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) { // 有授权码,直接处理登录 @@ -69,10 +80,18 @@ export default function Login() { handleFeishuLogin(authCode) } else if (isInFeishu && window.tt) { // 飞书内且有 tt 对象:静默授权 + console.log('✅ 飞书内环境,tt 对象可用,开始免登') setEnvironment('feishu') handleFeishuAutoLogin() + } else if (isInFeishu) { + // 飞书内但 tt 对象不可用:提示用户 + console.log('⚠️ 在飞书内但 tt 对象不可用') + setEnvironment('feishu') + setLoading(false) + Toast.show('请在飞书客户端中打开,或使用扫码登录') } else { - // 飞书外或 tt 对象不可用:显示登录选项 + // 飞书外:显示登录选项 + console.log('📱 飞书外环境,显示登录选项') setEnvironment('external') setLoading(false) }