feat: 实现无感快速审批操作
问题: - 用户点击飞书消息按钮需要跳转到web界面 - 需要登录才能操作,体验不流畅 解决方案: 1. 创建快速审批页面 (quick-action.tsx) 2. 用户点击按钮后自动执行审批操作 3. 操作成功后自动关闭页面或返回 4. 全流程无需用户手动跳转 工作流程: 1. 用户在飞书中点击"通过"/"拒绝"按钮 2. 打开快速审批页面 3. 自动执行审批操作(如果已登录) 4. 显示操作成功/失败 5. 自动关闭页面(飞书客户端)或返回列表 优势: - 一步完成审批,无需跳转 - 已登录用户无感操作 - 未登录用户会引导登录后返回 相关文件: - frontend/src/pages/approval/quick-action.tsx (新建) - frontend/src/router/index.tsx - backend/app/service/FeishuNotificationService.php Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -310,9 +310,9 @@ class FeishuNotificationService
|
||||
// 操作按钮区域
|
||||
$actions = [];
|
||||
|
||||
// 如果有审批单ID,添加审批操作按钮
|
||||
// 如果有审批单ID,添加审批操作按钮(使用快速审批页面,无感操作)
|
||||
if ($orderId) {
|
||||
// 通过按钮(绿色)
|
||||
// 通过按钮(绿色)- 快速审批页面
|
||||
$actions[] = [
|
||||
'tag' => 'button',
|
||||
'text' => [
|
||||
@@ -321,10 +321,10 @@ class FeishuNotificationService
|
||||
],
|
||||
'type' => 'primary',
|
||||
'size' => 'medium',
|
||||
'url' => "https://xsm.dev.hnxinyouhe.com/approval/detail/{$orderId}?action=approve",
|
||||
'url' => "https://xsm.dev.hnxinyouhe.com/approval/quick-action?action=approve&order_id={$orderId}",
|
||||
];
|
||||
|
||||
// 拒绝按钮(红色)
|
||||
// 拒绝按钮(红色)- 快速审批页面
|
||||
$actions[] = [
|
||||
'tag' => 'button',
|
||||
'text' => [
|
||||
@@ -333,7 +333,7 @@ class FeishuNotificationService
|
||||
],
|
||||
'type' => 'danger',
|
||||
'size' => 'medium',
|
||||
'url' => "https://xsm.dev.hnxinyouhe.com/approval/detail/{$orderId}?action=reject",
|
||||
'url' => "https://xsm.dev.hnxinyouhe.com/approval/quick-action?action=reject&order_id={$orderId}",
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useNavigate, useLocation } from 'react-router-dom'
|
||||
import { SpinLoading, Toast } from 'antd-mobile'
|
||||
import { approvalApi } from '@/services/api'
|
||||
|
||||
export default function QuickAction() {
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [status, setStatus] = useState('processing')
|
||||
|
||||
useEffect(() => {
|
||||
const processAction = async () => {
|
||||
try {
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
const action = urlParams.get('action') as 'approve' | 'reject' | null
|
||||
const orderId = urlParams.get('order_id')
|
||||
const token = localStorage.getItem('token')
|
||||
|
||||
console.log('🚀 快速审批:', { action, orderId, hasToken: !!token })
|
||||
|
||||
if (!action || !orderId) {
|
||||
setStatus('error')
|
||||
Toast.show('参数错误')
|
||||
setTimeout(() => navigate('/login', { replace: true }), 1500)
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否登录
|
||||
if (!token) {
|
||||
console.log('⚠️ 未登录,跳转到登录页')
|
||||
setStatus('unauthorized')
|
||||
Toast.show('请先登录')
|
||||
setTimeout(() => {
|
||||
window.location.href = '/login?redirect=' + encodeURIComponent(window.location.pathname + window.location.search)
|
||||
}, 1500)
|
||||
return
|
||||
}
|
||||
|
||||
setStatus('processing')
|
||||
|
||||
// 执行审批操作
|
||||
if (action === 'approve') {
|
||||
console.log('✅ 执行通过操作')
|
||||
await approvalApi.approve(Number(orderId), '')
|
||||
Toast.show('已通过')
|
||||
} else if (action === 'reject') {
|
||||
console.log('❌ 执行拒绝操作')
|
||||
await approvalApi.reject(Number(orderId), '已拒绝')
|
||||
Toast.show('已拒绝')
|
||||
}
|
||||
|
||||
setStatus('success')
|
||||
|
||||
// 操作成功后关闭页面(在飞书客户端中)
|
||||
setTimeout(() => {
|
||||
console.log('✅ 操作完成,关闭页面')
|
||||
// 在飞书客户端中,关闭当前页面
|
||||
if ((window as any).tt && (window as any).tt.close) {
|
||||
(window as any).tt.close()
|
||||
} else {
|
||||
// 其他情况跳转到审批列表
|
||||
navigate('/approval', { replace: true })
|
||||
}
|
||||
}, 1000)
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('❌ 操作失败:', error)
|
||||
setStatus('error')
|
||||
Toast.show(error.message || '操作失败')
|
||||
setTimeout(() => navigate('/approval', { replace: true }), 2000)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
processAction()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: '100vh',
|
||||
background: '#f5f5f5'
|
||||
}}>
|
||||
{loading && (
|
||||
<>
|
||||
<SpinLoading color='primary' />
|
||||
<p style={{ marginTop: '16px', color: '#999' }}>处理中...</p>
|
||||
</>
|
||||
)}
|
||||
|
||||
{status === 'success' && (
|
||||
<>
|
||||
<div style={{ fontSize: '48px' }}>✅</div>
|
||||
<p style={{ marginTop: '16px', color: '#52c41a' }}>操作成功</p>
|
||||
</>
|
||||
)}
|
||||
|
||||
{status === 'error' && (
|
||||
<>
|
||||
<div style={{ fontSize: '48px' }}>❌</div>
|
||||
<p style={{ marginTop: '16px', color: '#ff4d4f' }}>操作失败</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import SelectApprover from '@/pages/business-trip/select-approver'
|
||||
import SelectCc from '@/pages/business-trip/select-cc'
|
||||
import ApprovalList from '@/pages/approval/list'
|
||||
import ApprovalDetail from '@/pages/approval/detail'
|
||||
import QuickAction from '@/pages/approval/quick-action'
|
||||
import Login from '@/pages/login'
|
||||
import SimpleLogin from '@/pages/login-simple'
|
||||
import DebugFeishu from '@/pages/debug-feishu'
|
||||
@@ -32,6 +33,7 @@ const router = createBrowserRouter([
|
||||
{ path: 'business-trip/:id', element: <BusinessTripDetail /> },
|
||||
{ path: 'approval', element: <ApprovalList /> },
|
||||
{ path: 'approval/:id', element: <ApprovalDetail /> },
|
||||
{ path: 'approval/quick-action', element: <QuickAction /> },
|
||||
],
|
||||
},
|
||||
{ path: '/login', element: <Login /> },
|
||||
|
||||
Reference in New Issue
Block a user