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:
root
2026-02-27 17:57:56 +08:00
parent b1ca2c58b7
commit 1004aea725
5 changed files with 329 additions and 31 deletions
+22 -6
View File
@@ -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) && (
<div className={styles.footer}>
{canCancel && (
<Button block color="warning" onClick={handleCancel}>
<Button
key="cancel"
block
color="warning"
onClick={handleCancel}
>
撤销申请
</Button>
)}
{canApprove && (
<>
<Button
key="reject"
block
color="danger"
fill="outline"
@@ -691,6 +706,7 @@ export default function ApprovalDetail() {
拒绝
</Button>
<Button
key="approve"
block
color="primary"
onClick={() => handleApprove('approve')}
+104
View File
@@ -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>
)
}