feat: 飞书审批消息添加操作按钮

功能:
1. 审批通知卡片添加"通过"和"拒绝"按钮
2. 点击按钮跳转到审批详情页,自动弹出审批确认框
3. 保留"查看详情"按钮

实现:
- FeishuNotificationService.buildApprovalCard() - 添加审批操作按钮
- ApprovalService.notifyNextApprovers() - 传递 order_id 参数
- approval/detail.tsx - 处理 URL 中的 action 参数

按钮样式:
- 通过按钮:绿色 (primary)
- 拒绝按钮:红色 (danger)
- 查看详情:默认样式

相关文件:
- backend/app/service/FeishuNotificationService.php
- backend/app/service/ApprovalService.php
- frontend/src/pages/approval/detail.tsx

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
root
2026-02-26 18:57:59 +08:00
parent 32d19d56ee
commit df0d592ee4
3 changed files with 70 additions and 15 deletions
+1
View File
@@ -482,6 +482,7 @@ class ApprovalService
'type' => $businessTypeName,
'content' => "**申请标题**: {$order->title}\n**申请人**: {$order->applicant->name}",
'url' => $detailUrl,
'order_id' => $order->id,
]);
error_log("✅ 审批人通知成功: approver_id={$approverId}");
} catch (\Exception $e) {
@@ -118,8 +118,7 @@ class FeishuNotificationService
* - type: 申请类型
* - content: 申请内容摘要
* - url: 详情链接
* - approve_url: 通过按钮链接(可选)
* - reject_url: 拒绝按钮链接(可选)
* - order_id: 审批单ID(用于构建审批操作按钮)
* @return bool
*/
public function sendApprovalNotification(array $params): bool
@@ -284,10 +283,12 @@ class FeishuNotificationService
$type = $params['type'] ?? '申请';
$content = $params['content'] ?? '';
$url = $params['url'] ?? '';
$orderId = $params['order_id'] ?? null;
// 简化版卡片,先测试
// 构建卡片元素
$elements = [];
// 内容区域
if ($content) {
$elements[] = [
'tag' => 'div',
@@ -306,20 +307,54 @@ class FeishuNotificationService
];
}
// 操作按钮区域
$actions = [];
// 如果有审批单ID,添加审批操作按钮
if ($orderId) {
// 通过按钮(绿色)
$actions[] = [
'tag' => 'button',
'text' => [
'content' => '✓ 通过',
'tag' => 'plain_text',
],
'type' => 'primary',
'size' => 'medium',
'url' => "https://xsm.dev.hnxinyouhe.com/approval/detail/{$orderId}?action=approve",
];
// 拒绝按钮(红色)
$actions[] = [
'tag' => 'button',
'text' => [
'content' => '✗ 拒绝',
'tag' => 'plain_text',
],
'type' => 'danger',
'size' => 'medium',
'url' => "https://xsm.dev.hnxinyouhe.com/approval/detail/{$orderId}?action=reject",
];
}
// 查看详情按钮
if ($url) {
$actions[] = [
'tag' => 'button',
'text' => [
'content' => '查看详情',
'tag' => 'plain_text',
],
'type' => 'default',
'size' => 'medium',
'url' => $url,
];
}
if (!empty($actions)) {
$elements[] = [
'tag' => 'action',
'actions' => [
[
'tag' => 'button',
'text' => [
'content' => '查看详情',
'tag' => 'plain_text',
],
'type' => 'default',
'url' => $url,
],
],
'actions' => $actions,
];
}
+20 -1
View File
@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react'
import { useNavigate, useParams } from 'react-router-dom'
import { useNavigate, useParams, useLocation } from 'react-router-dom'
import { useTranslation } from 'react-i18next'
import { Card, Tag, Button, Dialog, Toast, TextArea } from 'antd-mobile'
import {
@@ -98,6 +98,7 @@ export default function ApprovalDetail() {
const { t } = useTranslation()
const navigate = useNavigate()
const { id } = useParams()
const location = useLocation()
const { user } = useUserStore()
const [detail, setDetail] = useState<ApprovalDetail | null>(null)
@@ -126,6 +127,24 @@ export default function ApprovalDetail() {
loadDetail()
}, [id])
// 处理 URL 中的 action 参数(来自飞书消息按钮)
useEffect(() => {
if (!detail || !canApprove) return
const searchParams = new URLSearchParams(location.search)
const action = searchParams.get('action')
if (action === 'approve' || action === 'reject') {
// 自动打开审批对话框
setActionType(action)
setComment('')
setCommentVisible(true)
// 清除 URL 参数,避免重复触发
window.history.replaceState({}, '', `/approval/detail/${id}`)
}
}, [detail, location.search])
// 撤销申请
const handleCancel = () => {
Dialog.confirm({