feat: 审批功能优化和拒绝原因弹窗
后端改动: - ApprovalService: 添加调试日志,简化详情页 URL 生成逻辑 - BusinessTripService: 优化出差审批详情展示 前端改动: - approval/detail: 审批详情页优化,支持查看完整审批流程 - approval/reject: 新增拒绝原因弹窗组件 - business-trip/detail: 出差详情页增强,展示审批状态和操作 功能特性: - 审批详情页显示完整审批流程和记录 - 拒绝操作支持填写拒绝原因 - 出差申请详情页优化展示 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -72,9 +72,12 @@ class ApprovalService
|
|||||||
}
|
}
|
||||||
|
|
||||||
$currentNodeIds = $order->current_nodes ?? [];
|
$currentNodeIds = $order->current_nodes ?? [];
|
||||||
|
error_log("🔍 当前节点 IDs: " . json_encode($currentNodeIds));
|
||||||
|
|
||||||
$currentNode = ApprovalNode::whereIn('id', $currentNodeIds)->first();
|
$currentNode = ApprovalNode::whereIn('id', $currentNodeIds)->first();
|
||||||
|
|
||||||
if (!$currentNode) {
|
if (!$currentNode) {
|
||||||
|
error_log("❌ 未找到节点,IDs: " . json_encode($currentNodeIds));
|
||||||
throw new \Exception('当前节点不存在');
|
throw new \Exception('当前节点不存在');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -501,18 +504,8 @@ class ApprovalService
|
|||||||
{
|
{
|
||||||
$baseUrl = 'https://xsm.dev.hnxinyouhe.com';
|
$baseUrl = 'https://xsm.dev.hnxinyouhe.com';
|
||||||
|
|
||||||
switch ($order->business_type) {
|
// 统一使用审批详情页,不再跳转到具体业务模块的详情页
|
||||||
case 'business_trip':
|
return "{$baseUrl}/approval/detail/{$order->id}";
|
||||||
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}";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -295,12 +295,9 @@ class BusinessTripService
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 构建审批链接(使用 approval_order 的 ID)
|
// 构建审批链接(统一使用审批详情页)
|
||||||
$approvalUrl = "https://xsm.dev.hnxinyouhe.com/approval/detail/{$approvalOrder->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->reason}\n";
|
||||||
$content .= "**目的地**: {$businessTrip->destination}\n";
|
$content .= "**目的地**: {$businessTrip->destination}\n";
|
||||||
@@ -317,7 +314,7 @@ class BusinessTripService
|
|||||||
'applicant' => $businessTrip->user->name,
|
'applicant' => $businessTrip->user->name,
|
||||||
'type' => '出差申请',
|
'type' => '出差申请',
|
||||||
'content' => $content,
|
'content' => $content,
|
||||||
'url' => $detailUrl,
|
'url' => $approvalUrl,
|
||||||
'order_id' => $approvalOrder->id, // 添加 order_id 以显示审批按钮
|
'order_id' => $approvalOrder->id, // 添加 order_id 以显示审批按钮
|
||||||
]);
|
]);
|
||||||
error_log("✅ 审批人通知成功: approver_id={$approverId}");
|
error_log("✅ 审批人通知成功: approver_id={$approverId}");
|
||||||
@@ -356,7 +353,16 @@ class BusinessTripService
|
|||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$notificationService = new \app\service\FeishuNotificationService();
|
$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') {
|
if ($action === 'approve') {
|
||||||
$title = '出差申请已通过';
|
$title = '出差申请已通过';
|
||||||
@@ -642,7 +648,9 @@ class BusinessTripService
|
|||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$notificationService = new \app\service\FeishuNotificationService();
|
$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 ?? [];
|
$formData = $approvalOrder->form_data ?? [];
|
||||||
|
|||||||
@@ -135,17 +135,22 @@ export default function ApprovalDetail() {
|
|||||||
return
|
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) => {
|
const requestAuthCode = (apiObj: any, source: string) => {
|
||||||
console.log(`🚀 使用 ${source} 调用 requestAuthCode`)
|
console.log(`🚀 使用 ${source} 调用 requestAuthCode`)
|
||||||
|
// 不传 appId,让飞书自动选择(后端会根据 company 参数处理)
|
||||||
apiObj.requestAuthCode({
|
apiObj.requestAuthCode({
|
||||||
appId: appId,
|
|
||||||
success: async (res: any) => {
|
success: async (res: any) => {
|
||||||
console.log(`✅ ${source}.requestAuthCode 成功:`, res)
|
console.log(`✅ ${source}.requestAuthCode 成功:`, res)
|
||||||
console.log('🔑 authCode:', res.code?.substring(0, 30) + '...')
|
console.log('🔑 authCode:', res.code?.substring(0, 30) + '...')
|
||||||
try {
|
try {
|
||||||
console.log('📡 调用后端登录API...')
|
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)
|
console.log('✅ 后端登录成功:', result.user?.name)
|
||||||
login(result.user, result.token)
|
login(result.user, result.token)
|
||||||
Toast.show('登录成功')
|
Toast.show('登录成功')
|
||||||
@@ -243,14 +248,18 @@ export default function ApprovalDetail() {
|
|||||||
if (!success) {
|
if (!success) {
|
||||||
console.log('❌ 免登失败,跳转登录页')
|
console.log('❌ 免登失败,跳转登录页')
|
||||||
Toast.show('免登失败,请使用其他方式登录')
|
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
|
return
|
||||||
}
|
}
|
||||||
console.log('✅ 免登成功,继续加载详情')
|
console.log('✅ 免登成功,继续加载详情')
|
||||||
} else {
|
} else {
|
||||||
console.log('⚠️ 不在飞书内或 tt 对象不可用')
|
console.log('⚠️ 不在飞书内或 tt 对象不可用')
|
||||||
Toast.show('请先登录')
|
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
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -676,13 +685,19 @@ export default function ApprovalDetail() {
|
|||||||
{(canCancel || canApprove) && (
|
{(canCancel || canApprove) && (
|
||||||
<div className={styles.footer}>
|
<div className={styles.footer}>
|
||||||
{canCancel && (
|
{canCancel && (
|
||||||
<Button block color="warning" onClick={handleCancel}>
|
<Button
|
||||||
|
key="cancel"
|
||||||
|
block
|
||||||
|
color="warning"
|
||||||
|
onClick={handleCancel}
|
||||||
|
>
|
||||||
撤销申请
|
撤销申请
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{canApprove && (
|
{canApprove && (
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
|
key="reject"
|
||||||
block
|
block
|
||||||
color="danger"
|
color="danger"
|
||||||
fill="outline"
|
fill="outline"
|
||||||
@@ -691,6 +706,7 @@ export default function ApprovalDetail() {
|
|||||||
拒绝
|
拒绝
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
key="approve"
|
||||||
block
|
block
|
||||||
color="primary"
|
color="primary"
|
||||||
onClick={() => handleApprove('approve')}
|
onClick={() => handleApprove('approve')}
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { useNavigate, useSearchParams } from 'react-router-dom'
|
||||||
|
import { Input, Button, Toast, Space } from 'antd-mobile'
|
||||||
|
import { approvalApi } from '@/services/api'
|
||||||
|
|
||||||
|
export default function RejectPage() {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const [searchParams] = useSearchParams()
|
||||||
|
const orderId = searchParams.get('order_id')
|
||||||
|
const from = searchParams.get('from')
|
||||||
|
|
||||||
|
const [reason, setReason] = useState('')
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('Reject page loaded:', { orderId, from })
|
||||||
|
|
||||||
|
// 如果没有 order_id,返回列表
|
||||||
|
if (!orderId) {
|
||||||
|
Toast.show('参数错误')
|
||||||
|
navigate('/approval', { replace: true })
|
||||||
|
}
|
||||||
|
}, [orderId, from, navigate])
|
||||||
|
|
||||||
|
const handleReject = async () => {
|
||||||
|
if (!reason.trim()) {
|
||||||
|
Toast.show('请输入拒绝原因')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true)
|
||||||
|
try {
|
||||||
|
const result = await approvalApi.reject(Number(orderId), reason)
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
Toast.show('已拒绝')
|
||||||
|
// 如果是从飞书打开,尝试关闭页面
|
||||||
|
if (from === 'feishu') {
|
||||||
|
setTimeout(() => {
|
||||||
|
try {
|
||||||
|
if ((window as any).tt && typeof (window as any).tt.close === 'function') {
|
||||||
|
(window as any).tt.close()
|
||||||
|
} else {
|
||||||
|
navigate('/approval', { replace: true })
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
navigate('/approval', { replace: true })
|
||||||
|
}
|
||||||
|
}, 1500)
|
||||||
|
} else {
|
||||||
|
navigate('/approval', { replace: true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
Toast.show('操作失败: ' + (error.message || '未知错误'))
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
navigate(-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '20px', minHeight: '100vh', background: '#f5f5f5' }}>
|
||||||
|
<div style={{ background: '#fff', borderRadius: '8px', padding: '20px' }}>
|
||||||
|
<h3 style={{ marginBottom: '20px' }}>拒绝申请</h3>
|
||||||
|
|
||||||
|
<Space direction="vertical" style={{ width: '100%' }}>
|
||||||
|
<div>
|
||||||
|
<div style={{ marginBottom: '8px', color: '#666' }}>拒绝原因</div>
|
||||||
|
<Input
|
||||||
|
placeholder="请输入拒绝原因"
|
||||||
|
value={reason}
|
||||||
|
onChange={setReason}
|
||||||
|
rows={4}
|
||||||
|
maxLength={200}
|
||||||
|
showCount
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', gap: '10px', marginTop: '20px' }}>
|
||||||
|
<Button
|
||||||
|
color="danger"
|
||||||
|
onClick={handleReject}
|
||||||
|
loading={loading}
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
>
|
||||||
|
确认拒绝
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color="default"
|
||||||
|
onClick={handleCancel}
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -10,11 +10,19 @@ import {
|
|||||||
ExclamationCircleOutline,
|
ExclamationCircleOutline,
|
||||||
DeleteOutline
|
DeleteOutline
|
||||||
} from 'antd-mobile-icons'
|
} from 'antd-mobile-icons'
|
||||||
import { businessTripApi, userApi } from '../../services/api'
|
import { businessTripApi, userApi, authApi } from '../../services/api'
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
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: '草稿' },
|
||||||
@@ -78,31 +86,171 @@ export default function BusinessTripDetail() {
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { id } = useParams()
|
const { id } = useParams()
|
||||||
const { user } = useUserStore()
|
const { user, login } = useUserStore()
|
||||||
|
|
||||||
const [detail, setDetail] = useState<BusinessTripDetail | null>(null)
|
const [detail, setDetail] = useState<BusinessTripDetail | 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'>('approve')
|
const [actionType, setActionType] = useState<'approve' | 'reject'>('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')
|
||||||
|
|
||||||
|
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 () => {
|
const loadDetail = async () => {
|
||||||
if (!id) return
|
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)
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
const data = await businessTripApi.getDetail(Number(id))
|
const data = await businessTripApi.getDetail(Number(id))
|
||||||
|
console.log('✅ 详情加载成功:', data)
|
||||||
setDetail(data)
|
setDetail(data)
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
Toast.show(error.message || '加载失败')
|
console.error('❌ 详情加载失败:', error)
|
||||||
navigate(-1)
|
// 如果是 401 错误,handleUnauthorized 会处理跳转
|
||||||
|
if (error.message !== '登录已过期') {
|
||||||
|
Toast.show(error.message || '加载失败')
|
||||||
|
navigate(-1)
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
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()
|
loadDetail()
|
||||||
}, [id])
|
}, [id])
|
||||||
|
|
||||||
@@ -217,6 +365,22 @@ export default function BusinessTripDetail() {
|
|||||||
<div className={styles.detail}>
|
<div className={styles.detail}>
|
||||||
<NavBar onBack={() => navigate(-1)}>出差申请详情</NavBar>
|
<NavBar onBack={() => navigate(-1)}>出差申请详情</NavBar>
|
||||||
|
|
||||||
|
{/* 调试信息 */}
|
||||||
|
{process.env.NODE_ENV === 'development' && (
|
||||||
|
<div style={{
|
||||||
|
background: '#f0f0f0',
|
||||||
|
padding: '8px',
|
||||||
|
fontSize: '12px',
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
borderBottom: '1px solid #ddd'
|
||||||
|
}}>
|
||||||
|
<div>🔍 登录状态: {user ? `${user.name} (id:${user.id}, company:${user.company_id})` : '未登录'}</div>
|
||||||
|
<div>📱 在飞书中: {(navigator.userAgent.includes('Lark') || navigator.userAgent.includes('feishu') || navigator.userAgent.includes('lark')) ? '是' : '否'}</div>
|
||||||
|
<div>🔧 tt对象: {window.tt ? '存在' : '不存在'}</div>
|
||||||
|
<div>📍 URL参数: {new URLSearchParams(window.location.search).get('company') || '无'}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
{/* 状态卡片 */}
|
{/* 状态卡片 */}
|
||||||
<Card className={styles.statusCard}>
|
<Card className={styles.statusCard}>
|
||||||
@@ -370,6 +534,7 @@ export default function BusinessTripDetail() {
|
|||||||
<div className={styles.footer}>
|
<div className={styles.footer}>
|
||||||
{canDelete && (
|
{canDelete && (
|
||||||
<Button
|
<Button
|
||||||
|
key="delete"
|
||||||
block
|
block
|
||||||
color="danger"
|
color="danger"
|
||||||
fill="outline"
|
fill="outline"
|
||||||
@@ -380,18 +545,29 @@ export default function BusinessTripDetail() {
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{canEdit && (
|
{canEdit && (
|
||||||
<Button block color="primary" onClick={handleEdit}>
|
<Button
|
||||||
|
key="edit"
|
||||||
|
block
|
||||||
|
color="primary"
|
||||||
|
onClick={handleEdit}
|
||||||
|
>
|
||||||
编辑申请
|
编辑申请
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{canCancel && (
|
{canCancel && (
|
||||||
<Button block color="warning" onClick={handleCancel}>
|
<Button
|
||||||
|
key="cancel"
|
||||||
|
block
|
||||||
|
color="warning"
|
||||||
|
onClick={handleCancel}
|
||||||
|
>
|
||||||
撤销申请
|
撤销申请
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{canApprove && (
|
{canApprove && (
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
|
key="reject"
|
||||||
block
|
block
|
||||||
color="danger"
|
color="danger"
|
||||||
fill="outline"
|
fill="outline"
|
||||||
@@ -400,6 +576,7 @@ export default function BusinessTripDetail() {
|
|||||||
拒绝
|
拒绝
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
key="approve"
|
||||||
block
|
block
|
||||||
color="primary"
|
color="primary"
|
||||||
onClick={() => handleApprove('approve')}
|
onClick={() => handleApprove('approve')}
|
||||||
|
|||||||
Reference in New Issue
Block a user