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:
@@ -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')}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user