diff --git a/backend/app/service/ApprovalService.php b/backend/app/service/ApprovalService.php index 40a2a85..3c7149e 100644 --- a/backend/app/service/ApprovalService.php +++ b/backend/app/service/ApprovalService.php @@ -72,9 +72,12 @@ class ApprovalService } $currentNodeIds = $order->current_nodes ?? []; + error_log("🔍 当前节点 IDs: " . json_encode($currentNodeIds)); + $currentNode = ApprovalNode::whereIn('id', $currentNodeIds)->first(); if (!$currentNode) { + error_log("❌ 未找到节点,IDs: " . json_encode($currentNodeIds)); throw new \Exception('当前节点不存在'); } @@ -501,18 +504,8 @@ class ApprovalService { $baseUrl = 'https://xsm.dev.hnxinyouhe.com'; - switch ($order->business_type) { - case 'business_trip': - return "{$baseUrl}/business-trip/detail/{$order->business_id}"; - case 'expense': - return "{$baseUrl}/expense/detail/{$order->business_id}"; - case 'leave': - return "{$baseUrl}/leave/detail/{$order->business_id}"; - case 'purchase': - return "{$baseUrl}/purchase/detail/{$order->business_id}"; - default: - return "{$baseUrl}/approval/detail/{$order->id}"; - } + // 统一使用审批详情页,不再跳转到具体业务模块的详情页 + return "{$baseUrl}/approval/detail/{$order->id}"; } /** diff --git a/backend/app/service/BusinessTripService.php b/backend/app/service/BusinessTripService.php index e90817d..47a2bdf 100644 --- a/backend/app/service/BusinessTripService.php +++ b/backend/app/service/BusinessTripService.php @@ -295,12 +295,9 @@ class BusinessTripService return; } - // 构建审批链接(使用 approval_order 的 ID) + // 构建审批链接(统一使用审批详情页) $approvalUrl = "https://xsm.dev.hnxinyouhe.com/approval/detail/{$approvalOrder->id}"; - // 构建业务详情链接(用于查看详情按钮) - $detailUrl = "https://xsm.dev.hnxinyouhe.com/business-trip/detail/{$businessTrip->id}"; - // 构建内容 $content = "**出差原因**: {$businessTrip->reason}\n"; $content .= "**目的地**: {$businessTrip->destination}\n"; @@ -317,7 +314,7 @@ class BusinessTripService 'applicant' => $businessTrip->user->name, 'type' => '出差申请', 'content' => $content, - 'url' => $detailUrl, + 'url' => $approvalUrl, 'order_id' => $approvalOrder->id, // 添加 order_id 以显示审批按钮 ]); error_log("✅ 审批人通知成功: approver_id={$approverId}"); @@ -356,7 +353,16 @@ class BusinessTripService { try { $notificationService = new \app\service\FeishuNotificationService(); - $detailUrl = "https://xsm.dev.hnxinyouhe.com/business-trip/detail/{$businessTrip->id}"; + + // 获取关联的审批订单 + $approvalOrder = \app\model\ApprovalOrder::where('business_id', $businessTrip->id) + ->where('business_type', 'business_trip') + ->first(); + + // 统一使用审批详情页 + $detailUrl = $approvalOrder + ? "https://xsm.dev.hnxinyouhe.com/approval/detail/{$approvalOrder->id}" + : "https://xsm.dev.hnxinyouhe.com/approval/list"; if ($action === 'approve') { $title = '出差申请已通过'; @@ -642,7 +648,9 @@ class BusinessTripService { try { $notificationService = new \app\service\FeishuNotificationService(); - $detailUrl = "https://xsm.dev.hnxinyouhe.com/business-trip/detail/{$businessTrip->id}"; + + // 统一使用审批详情页 + $detailUrl = "https://xsm.dev.hnxinyouhe.com/approval/detail/{$approvalOrder->id}"; // 获取审批人和抄送人 $formData = $approvalOrder->form_data ?? []; diff --git a/frontend/src/pages/approval/detail.tsx b/frontend/src/pages/approval/detail.tsx index d980342..1819894 100644 --- a/frontend/src/pages/approval/detail.tsx +++ b/frontend/src/pages/approval/detail.tsx @@ -135,17 +135,22 @@ export default function ApprovalDetail() { return } - const appId = 'cli_a8033d607cba500d' + // 从 URL 获取 company 参数,不需要 appId(后端会自动识别) + const urlParams = new URLSearchParams(window.location.search) + const companyParam = urlParams.get('company') + console.log('🔍 URL 中的 company 参数:', companyParam) + const requestAuthCode = (apiObj: any, source: string) => { console.log(`🚀 使用 ${source} 调用 requestAuthCode`) + // 不传 appId,让飞书自动选择(后端会根据 company 参数处理) apiObj.requestAuthCode({ - appId: appId, success: async (res: any) => { console.log(`✅ ${source}.requestAuthCode 成功:`, res) console.log('🔑 authCode:', res.code?.substring(0, 30) + '...') try { console.log('📡 调用后端登录API...') - const result = await authApi.feishuLogin(res.code) + // 传递 company 参数 + const result = await authApi.feishuLogin(res.code, companyParam || undefined) console.log('✅ 后端登录成功:', result.user?.name) login(result.user, result.token) Toast.show('登录成功') @@ -243,14 +248,18 @@ export default function ApprovalDetail() { if (!success) { console.log('❌ 免登失败,跳转登录页') Toast.show('免登失败,请使用其他方式登录') - window.location.href = '/login?redirect=' + encodeURIComponent(window.location.pathname + window.location.search) + // 保留 company 参数 + const currentUrl = window.location.pathname + window.location.search + window.location.href = '/login?redirect=' + encodeURIComponent(currentUrl) return } console.log('✅ 免登成功,继续加载详情') } else { console.log('⚠️ 不在飞书内或 tt 对象不可用') Toast.show('请先登录') - window.location.href = '/login?redirect=' + encodeURIComponent(window.location.pathname + window.location.search) + // 保留 company 参数 + const currentUrl = window.location.pathname + window.location.search + window.location.href = '/login?redirect=' + encodeURIComponent(currentUrl) return } } else { @@ -676,13 +685,19 @@ export default function ApprovalDetail() { {(canCancel || canApprove) && (
{canCancel && ( - )} {canApprove && ( <> + +
+ + + + ) +} diff --git a/frontend/src/pages/business-trip/detail.tsx b/frontend/src/pages/business-trip/detail.tsx index 4c0750a..7dd4fb9 100644 --- a/frontend/src/pages/business-trip/detail.tsx +++ b/frontend/src/pages/business-trip/detail.tsx @@ -10,11 +10,19 @@ import { ExclamationCircleOutline, DeleteOutline } from 'antd-mobile-icons' -import { businessTripApi, userApi } from '../../services/api' +import { businessTripApi, userApi, authApi } from '../../services/api' import { useUserStore } from '@/stores/user' 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: '草稿' }, @@ -78,31 +86,171 @@ export default function BusinessTripDetail() { const { t } = useTranslation() const navigate = useNavigate() const { id } = useParams() - 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'>('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') + + console.log('🔍 tryFeishuLogin 开始') + console.log('📱 isInFeishu:', isInFeishu) + + if (!isInFeishu) { + console.log('❌ 不在飞书内,跳过免登') + resolve(false) + return + } + + // 从 URL 获取 company 参数 + const urlParams = new URLSearchParams(window.location.search) + const companyParam = urlParams.get('company') + console.log('🔍 URL 中的 company 参数:', companyParam) + + const requestAuthCode = (apiObj: any, source: string) => { + console.log(`🚀 使用 ${source} 调用 requestAuthCode`) + // 不传 appId,让飞书自动选择 + apiObj.requestAuthCode({ + success: async (res: any) => { + console.log(`✅ ${source}.requestAuthCode 成功:`, res) + console.log('🔑 authCode:', res.code?.substring(0, 30) + '...') + try { + console.log('📡 调用后端登录API...') + const result = await authApi.feishuLogin(res.code, companyParam || undefined) + console.log('✅ 后端登录成功:', result.user?.name) + login(result.user, result.token) + Toast.show('登录成功') + resolve(true) + } catch (error) { + console.error('❌ 飞书登录API失败:', error) + Toast.show('登录失败: ' + (error.message || '未知错误')) + resolve(false) + } + }, + fail: (error: any) => { + console.error(`❌ ${source}.requestAuthCode 失败:`, error) + Toast.show('免登失败: ' + (error.errorMessage || error.errMsg || '未知错误')) + resolve(false) + }, + }) + } + + // 优先使用 tt 对象 + if (window.tt && typeof window.tt.requestAuthCode === 'function') { + requestAuthCode(window.tt, 'tt') + return + } + + // 尝试使用其他方式 + if (window.ttjsBridge && typeof window.ttjsBridge.requestAuthCode === 'function') { + requestAuthCode(window.ttjsBridge, 'ttjsBridge') + return + } + + // 所有方法都不可用 + console.log('❌ 所有飞书 API 都不可用') + Toast.show('飞书API不可用,请使用其他方式登录') + resolve(false) + }) + } // 加载详情 const loadDetail = async () => { if (!id) return + console.log('📋 loadDetail 开始, id:', id) + console.log('📋 当前 URL:', window.location.href) + + // 检查登录状态 + const token = localStorage.getItem('token') + const isLoggedIn = !!user + console.log('🔐 登录状态检查:', { isLoggedIn: !!isLoggedIn, hasToken: !!token, userId: user?.id }) + + if (!isLoggedIn && !token) { + console.log('⚠️ 用户未登录,检查飞书环境') + + const ua = navigator.userAgent + const isInFeishu = ua.includes('Lark') || ua.includes('feishu') || ua.includes('lark') + const currentUrl = window.location.href + const urlParams = new URLSearchParams(window.location.search) + + console.log('📱 飞书环境检测:', { isInFeishu, ua: ua.substring(0, 100) }) + + // 在飞书中但 tt 对象不存在时,刷新页面 + if (isInFeishu && !window.tt) { + const reloadCount = parseInt(urlParams.get('reload') || '0') + if (reloadCount < 3) { + console.log(`⚠️ 在飞书中但 tt 对象不存在,第 ${reloadCount + 1} 次刷新页面`) + const newUrl = currentUrl.split('?')[0] + '?' + urlParams.toString().replace(/reload=\d+/g, '').replace(/^&+/, '') + window.location.href = newUrl + (newUrl.includes('?') ? '&' : '?') + `reload=${reloadCount + 1}` + return + } + } + + if (isInFeishu && window.tt && !tryingFeishuLogin) { + console.log('🚀 开始飞书免登...') + setTryingFeishuLogin(true) + const success = await tryFeishuLogin() + setTryingFeishuLogin(false) + console.log('✅ 免登结果:', success) + + if (!success) { + console.log('❌ 免登失败,跳转登录页') + // 保留完整 URL 参数 + window.location.href = '/login?redirect=' + encodeURIComponent(currentUrl) + return + } + console.log('✅ 免登成功,继续加载详情') + } else { + console.log('⚠️ 不在飞书内或 tt 对象不可用') + Toast.show('请先登录') + const redirectUrl = '/login?redirect=' + encodeURIComponent(currentUrl) + console.log('🚨 准备跳转到登录页 (非飞书环境), redirectUrl:', redirectUrl) + window.location.href = redirectUrl + return + } + } else { + console.log('✅ 用户已登录,直接加载详情') + } + + console.log('🔄 开始加载详情...') setLoading(true) try { const data = await businessTripApi.getDetail(Number(id)) + console.log('✅ 详情加载成功:', data) setDetail(data) } catch (error: any) { - Toast.show(error.message || '加载失败') - navigate(-1) + console.error('❌ 详情加载失败:', error) + // 如果是 401 错误,handleUnauthorized 会处理跳转 + if (error.message !== '登录已过期') { + Toast.show(error.message || '加载失败') + navigate(-1) + } } finally { setLoading(false) } } useEffect(() => { + console.log('🔍 BusinessTripDetail mount, id:', id) + console.log('🔍 User:', user) + console.log('🔍 Token:', localStorage.getItem('token')?.substring(0, 20) + '...') + + // 检查飞书环境 + const ua = navigator.userAgent + const isInFeishu = ua.includes('Lark') || ua.includes('feishu') || ua.includes('lark') + console.log('🔍 User Agent:', ua) + console.log('🔍 在飞书中:', isInFeishu) + console.log('🔍 window.tt:', window.tt) + loadDetail() }, [id]) @@ -217,6 +365,22 @@ export default function BusinessTripDetail() {
navigate(-1)}>出差申请详情 + {/* 调试信息 */} + {process.env.NODE_ENV === 'development' && ( +
+
🔍 登录状态: {user ? `${user.name} (id:${user.id}, company:${user.company_id})` : '未登录'}
+
📱 在飞书中: {(navigator.userAgent.includes('Lark') || navigator.userAgent.includes('feishu') || navigator.userAgent.includes('lark')) ? '是' : '否'}
+
🔧 tt对象: {window.tt ? '存在' : '不存在'}
+
📍 URL参数: {new URLSearchParams(window.location.search).get('company') || '无'}
+
+ )} +
{/* 状态卡片 */} @@ -370,6 +534,7 @@ export default function BusinessTripDetail() {
{canDelete && ( )} {canCancel && ( - )} {canApprove && ( <>