2026-02-25 23:54:38 +08:00
|
|
|
|
import { useState, useEffect } from 'react'
|
|
|
|
|
|
import { useNavigate, useParams } from 'react-router-dom'
|
|
|
|
|
|
import { useTranslation } from 'react-i18next'
|
|
|
|
|
|
import { NavBar, Card, Tag, Button, Dialog, Toast, TextArea } from 'antd-mobile'
|
|
|
|
|
|
import {
|
|
|
|
|
|
LeftOutline,
|
|
|
|
|
|
ClockCircleOutline,
|
|
|
|
|
|
CheckCircleOutline,
|
|
|
|
|
|
CloseCircleOutline,
|
|
|
|
|
|
ExclamationCircleOutline,
|
|
|
|
|
|
} from 'antd-mobile-icons'
|
|
|
|
|
|
import { approvalApi } from '../../services/api'
|
|
|
|
|
|
import { useUserStore } from '@/stores/user'
|
|
|
|
|
|
import dayjs from 'dayjs'
|
|
|
|
|
|
import styles from './detail.module.css'
|
|
|
|
|
|
|
|
|
|
|
|
// 状态配置
|
|
|
|
|
|
const statusConfig: Record<string, { color: string; icon: React.ReactNode; text: string }> = {
|
|
|
|
|
|
draft: { color: 'default', icon: <ClockCircleOutline />, text: '草稿' },
|
|
|
|
|
|
pending: { color: 'warning', icon: <ExclamationCircleOutline />, text: '审批中' },
|
|
|
|
|
|
approved: { color: 'success', icon: <CheckCircleOutline />, text: '已通过' },
|
|
|
|
|
|
rejected: { color: 'danger', icon: <CloseCircleOutline />, text: '已拒绝' },
|
|
|
|
|
|
cancelled: { color: 'default', icon: <CloseCircleOutline />, text: '已撤销' },
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 操作配置
|
|
|
|
|
|
const actionConfig: Record<string, { color: string; icon: string; text: string }> = {
|
|
|
|
|
|
submit: { color: '#999', icon: '📝', text: '提交申请' },
|
|
|
|
|
|
approve: { color: '#52c41a', icon: '✅', text: '已同意' },
|
|
|
|
|
|
reject: { color: '#ff4d4f', icon: '❌', text: '已拒绝' },
|
|
|
|
|
|
cancel: { color: '#999', icon: '🔄', text: '已撤销' },
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 业务类型映射
|
|
|
|
|
|
const businessTypeConfig: Record<string, string> = {
|
|
|
|
|
|
business_trip: '出差申请',
|
|
|
|
|
|
expense: '费用报销',
|
|
|
|
|
|
leave: '请假申请',
|
|
|
|
|
|
purchase: '采购申请',
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-26 00:06:19 +08:00
|
|
|
|
// 表单字段映射(英文key -> 中文label)
|
|
|
|
|
|
const formFieldLabelMap: Record<string, string> = {
|
|
|
|
|
|
reason: '出差事由',
|
|
|
|
|
|
itineraries: '行程安排',
|
|
|
|
|
|
destination: '目的地',
|
|
|
|
|
|
start_date: '开始日期',
|
|
|
|
|
|
end_date: '结束日期',
|
|
|
|
|
|
estimated_cost: '预计费用',
|
|
|
|
|
|
approvers: '审批人',
|
|
|
|
|
|
cc_users: '抄送人',
|
|
|
|
|
|
attachment: '附件',
|
|
|
|
|
|
remark: '备注',
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-25 23:54:38 +08:00
|
|
|
|
interface ApprovalRecord {
|
|
|
|
|
|
id: number
|
2026-02-26 00:05:43 +08:00
|
|
|
|
node_id: number
|
2026-02-25 23:54:38 +08:00
|
|
|
|
node_type: string
|
|
|
|
|
|
action: string
|
2026-02-26 00:05:43 +08:00
|
|
|
|
approver_id: number
|
2026-02-25 23:54:38 +08:00
|
|
|
|
comment: string | null
|
|
|
|
|
|
created_at: string
|
|
|
|
|
|
approver: {
|
|
|
|
|
|
name: string
|
|
|
|
|
|
avatar?: string
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface ApprovalNode {
|
|
|
|
|
|
id: number
|
|
|
|
|
|
type: string
|
|
|
|
|
|
name: string
|
|
|
|
|
|
approvers: number[]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface ApprovalDetail {
|
|
|
|
|
|
id: number
|
|
|
|
|
|
title: string
|
|
|
|
|
|
business_type: string
|
|
|
|
|
|
business_id: number
|
|
|
|
|
|
status: string
|
|
|
|
|
|
created_at: string
|
|
|
|
|
|
current_nodes: number[]
|
|
|
|
|
|
applicant: {
|
|
|
|
|
|
name: string
|
|
|
|
|
|
avatar?: string
|
|
|
|
|
|
}
|
|
|
|
|
|
flow: {
|
|
|
|
|
|
name: string
|
|
|
|
|
|
nodes?: ApprovalNode[]
|
|
|
|
|
|
}
|
|
|
|
|
|
form_data: Record<string, any>
|
|
|
|
|
|
records: ApprovalRecord[]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export default function ApprovalDetail() {
|
|
|
|
|
|
const { t } = useTranslation()
|
|
|
|
|
|
const navigate = useNavigate()
|
|
|
|
|
|
const { id } = useParams()
|
|
|
|
|
|
const { user } = useUserStore()
|
|
|
|
|
|
|
|
|
|
|
|
const [detail, setDetail] = useState<ApprovalDetail | null>(null)
|
|
|
|
|
|
const [loading, setLoading] = useState(true)
|
|
|
|
|
|
const [commentVisible, setCommentVisible] = useState(false)
|
|
|
|
|
|
const [comment, setComment] = useState('')
|
|
|
|
|
|
const [actionType, setActionType] = useState<'approve' | 'reject' | 'cancel'>('approve')
|
|
|
|
|
|
|
|
|
|
|
|
// 加载详情
|
|
|
|
|
|
const loadDetail = async () => {
|
|
|
|
|
|
if (!id) return
|
|
|
|
|
|
|
|
|
|
|
|
setLoading(true)
|
|
|
|
|
|
try {
|
|
|
|
|
|
const data = await approvalApi.getDetail(Number(id))
|
|
|
|
|
|
setDetail(data)
|
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
Toast.show(error.message || '加载失败')
|
|
|
|
|
|
navigate(-1)
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setLoading(false)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
loadDetail()
|
|
|
|
|
|
}, [id])
|
|
|
|
|
|
|
|
|
|
|
|
// 撤销申请
|
|
|
|
|
|
const handleCancel = () => {
|
|
|
|
|
|
Dialog.confirm({
|
|
|
|
|
|
content: '确定要撤销此申请吗?',
|
|
|
|
|
|
onConfirm: async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await approvalApi.cancel(Number(id))
|
|
|
|
|
|
Toast.show('已撤销')
|
|
|
|
|
|
loadDetail()
|
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
Toast.show(error.message || '操作失败')
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 打开审批意见框
|
|
|
|
|
|
const handleApprove = (action: 'approve' | 'reject') => {
|
|
|
|
|
|
setActionType(action)
|
|
|
|
|
|
setComment('')
|
|
|
|
|
|
setCommentVisible(true)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 提交审批
|
|
|
|
|
|
const handleConfirmApprove = async () => {
|
|
|
|
|
|
if (document.activeElement instanceof HTMLElement) {
|
|
|
|
|
|
document.activeElement.blur()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (actionType === 'approve') {
|
|
|
|
|
|
await approvalApi.approve(Number(id), comment)
|
|
|
|
|
|
Toast.show('已同意')
|
|
|
|
|
|
} else if (actionType === 'reject') {
|
|
|
|
|
|
await approvalApi.reject(Number(id), comment)
|
|
|
|
|
|
Toast.show('已拒绝')
|
|
|
|
|
|
} else if (actionType === 'cancel') {
|
|
|
|
|
|
await approvalApi.cancel(Number(id))
|
|
|
|
|
|
Toast.show('已撤销')
|
|
|
|
|
|
}
|
|
|
|
|
|
setCommentVisible(false)
|
|
|
|
|
|
loadDetail()
|
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
Toast.show(error.message || '操作失败')
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 检查是否是当前审批人
|
|
|
|
|
|
const isCurrentApprover = () => {
|
2026-02-26 00:05:43 +08:00
|
|
|
|
console.log('🔍 isCurrentApprover 检查:', {
|
|
|
|
|
|
hasDetail: !!detail,
|
|
|
|
|
|
hasUser: !!user,
|
|
|
|
|
|
userId: user?.id,
|
|
|
|
|
|
status: detail?.status,
|
|
|
|
|
|
currentNodes: detail?.current_nodes,
|
|
|
|
|
|
nodesCount: detail?.flow?.nodes?.length,
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-02-25 23:54:38 +08:00
|
|
|
|
if (!detail || !user) return false
|
|
|
|
|
|
if (detail.status !== 'pending') return false
|
|
|
|
|
|
|
|
|
|
|
|
// 检查当前用户是否在 current_nodes 对应的节点审批人列表中
|
|
|
|
|
|
const currentNodeIds = detail.current_nodes || []
|
|
|
|
|
|
const nodes = detail.flow?.nodes || []
|
|
|
|
|
|
|
2026-02-26 00:05:43 +08:00
|
|
|
|
console.log('🔍 节点信息:', {
|
|
|
|
|
|
currentNodeIds,
|
|
|
|
|
|
nodes: nodes.map(n => ({ id: n.id, name: n.name, approvers: n.approvers })),
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-02-25 23:54:38 +08:00
|
|
|
|
for (const nodeId of currentNodeIds) {
|
|
|
|
|
|
const node = nodes.find((n) => n.id === nodeId)
|
2026-02-26 00:05:43 +08:00
|
|
|
|
console.log(`🔍 检查节点 ${nodeId}:`, {
|
|
|
|
|
|
node,
|
|
|
|
|
|
approvers: node?.approvers,
|
|
|
|
|
|
userId: user.id,
|
|
|
|
|
|
isInApprovers: node?.approvers?.includes(user.id),
|
|
|
|
|
|
})
|
2026-02-25 23:54:38 +08:00
|
|
|
|
if (node && node.approvers && node.approvers.includes(user.id)) {
|
|
|
|
|
|
// 检查是否已审批 - 使用 approver_id 而不是 approver.id
|
|
|
|
|
|
const hasApproved = detail.records.some(
|
|
|
|
|
|
(r) => r.node_id === nodeId && r.approver_id === user.id
|
|
|
|
|
|
)
|
2026-02-26 00:05:43 +08:00
|
|
|
|
console.log(`🔍 节点 ${nodeId} 审批状态:`, {
|
|
|
|
|
|
hasApproved,
|
|
|
|
|
|
records: detail.records.filter(r => r.node_id === nodeId),
|
|
|
|
|
|
})
|
|
|
|
|
|
if (!hasApproved) {
|
|
|
|
|
|
console.log('✅ 是当前审批人!')
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
2026-02-25 23:54:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-26 00:05:43 +08:00
|
|
|
|
console.log('❌ 不是当前审批人')
|
2026-02-25 23:54:38 +08:00
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (loading || !detail) {
|
|
|
|
|
|
return <div className={styles.loading}>加载中...</div>
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const config = statusConfig[detail.status] || statusConfig.draft
|
|
|
|
|
|
const isOwner = detail.applicant?.name === user?.name
|
|
|
|
|
|
const canCancel = detail.status === 'pending' && isOwner
|
|
|
|
|
|
const canApprove = isCurrentApprover()
|
|
|
|
|
|
|
|
|
|
|
|
// 格式化时间
|
|
|
|
|
|
const formatTime = (timeStr: string) => {
|
|
|
|
|
|
return dayjs(timeStr).format('YYYY-MM-DD HH:mm')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取节点类型名称
|
|
|
|
|
|
const getNodeTypeName = (nodeType: string) => {
|
|
|
|
|
|
const typeMap: Record<string, string> = {
|
|
|
|
|
|
approver: '审批人',
|
|
|
|
|
|
and_approve: '会签',
|
|
|
|
|
|
or_approve: '或签',
|
2026-02-26 00:14:11 +08:00
|
|
|
|
notify: '抄送人',
|
|
|
|
|
|
single_approve: '审批人',
|
2026-02-25 23:54:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
return typeMap[nodeType] || nodeType
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-26 00:14:11 +08:00
|
|
|
|
// 获取节点的审批人姓名
|
|
|
|
|
|
const getNodeApproversNames = (nodeId: number, nodeType: string): string => {
|
|
|
|
|
|
// 从审批记录中获取已审批的审批人姓名
|
|
|
|
|
|
const approverRecords = detail.records.filter(r => r.node_id === nodeId && r.action !== 'submit')
|
|
|
|
|
|
if (approverRecords.length > 0) {
|
|
|
|
|
|
return approverRecords.map(r => r.approver?.name).join('、')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-26 00:16:55 +08:00
|
|
|
|
// 未审批的节点,显示节点名称(人名)
|
|
|
|
|
|
const nodes = detail.flow?.nodes || []
|
|
|
|
|
|
const node = nodes.find((n) => n.id === nodeId)
|
|
|
|
|
|
return node?.name || '待审批'
|
2026-02-26 00:14:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-26 00:16:14 +08:00
|
|
|
|
// 获取节点显示名称(只显示人名)
|
2026-02-26 00:14:11 +08:00
|
|
|
|
const getNodeDisplayName = (node: any): string => {
|
2026-02-26 00:16:14 +08:00
|
|
|
|
return getNodeApproversNames(node.id, node.type)
|
2026-02-26 00:14:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-25 23:54:38 +08:00
|
|
|
|
// 获取当前审批人
|
|
|
|
|
|
const getCurrentApprovers = () => {
|
|
|
|
|
|
const currentNodeIds = detail.current_nodes || []
|
|
|
|
|
|
const nodes = detail.flow?.nodes || []
|
|
|
|
|
|
const approvers: any[] = []
|
|
|
|
|
|
|
|
|
|
|
|
for (const nodeId of currentNodeIds) {
|
|
|
|
|
|
const node = nodes.find((n) => n.id === nodeId)
|
|
|
|
|
|
if (node) {
|
|
|
|
|
|
approvers.push({
|
2026-02-26 00:14:11 +08:00
|
|
|
|
nodeId: node.id,
|
|
|
|
|
|
nodeType: node.type,
|
2026-02-25 23:54:38 +08:00
|
|
|
|
type: getNodeTypeName(node.type),
|
|
|
|
|
|
approvers: node.approvers,
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return approvers
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const currentApprovers = getCurrentApprovers()
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className={styles.detail}>
|
|
|
|
|
|
<NavBar onBack={() => navigate(-1)}>审批详情</NavBar>
|
|
|
|
|
|
|
|
|
|
|
|
<div className={styles.content}>
|
|
|
|
|
|
{/* 状态卡片 */}
|
|
|
|
|
|
<Card className={styles.statusCard}>
|
|
|
|
|
|
<div className={styles.statusHeader}>
|
|
|
|
|
|
<span className={styles.title}>{detail.title}</span>
|
|
|
|
|
|
<Tag color={config.color}>
|
|
|
|
|
|
{config.icon}
|
|
|
|
|
|
{config.text}
|
|
|
|
|
|
</Tag>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className={styles.statusInfo}>
|
|
|
|
|
|
<div className={styles.infoItem}>
|
|
|
|
|
|
<span className={styles.label}>业务类型:</span>
|
|
|
|
|
|
<span className={styles.value}>
|
|
|
|
|
|
{businessTypeConfig[detail.business_type] || detail.business_type}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className={styles.infoItem}>
|
|
|
|
|
|
<span className={styles.label}>申请人:</span>
|
|
|
|
|
|
<span className={styles.value}>{detail.applicant?.name}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className={styles.infoItem}>
|
|
|
|
|
|
<span className={styles.label}>申请时间:</span>
|
|
|
|
|
|
<span className={styles.value}>{formatTime(detail.created_at)}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className={styles.infoItem}>
|
|
|
|
|
|
<span className={styles.label}>审批流程:</span>
|
|
|
|
|
|
<span className={styles.value}>{detail.flow?.name}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 当前审批节点 */}
|
|
|
|
|
|
{detail.status === 'pending' && currentApprovers.length > 0 && (
|
|
|
|
|
|
<Card title="当前审批" className={styles.section}>
|
|
|
|
|
|
{currentApprovers.map((item, index) => (
|
|
|
|
|
|
<div key={index} className={styles.approverItem}>
|
2026-02-26 00:16:14 +08:00
|
|
|
|
<div className={styles.approverName}>{getNodeApproversNames(item.nodeId, item.nodeType)}</div>
|
2026-02-25 23:54:38 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* 表单数据 */}
|
|
|
|
|
|
{Object.keys(detail.form_data).length > 0 && (
|
|
|
|
|
|
<Card title="申请内容" className={styles.section}>
|
|
|
|
|
|
<div className={styles.formData}>
|
|
|
|
|
|
{Object.entries(detail.form_data)
|
|
|
|
|
|
.filter(([key]) => !['approvers', 'cc_users'].includes(key))
|
2026-02-26 00:06:19 +08:00
|
|
|
|
.map(([key, value]) => {
|
|
|
|
|
|
// 获取字段标签,优先使用映射表,没有则使用原始key
|
|
|
|
|
|
const label = formFieldLabelMap[key] || key
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div key={key} className={styles.formRow}>
|
|
|
|
|
|
<span className={styles.formLabel}>{label}:</span>
|
|
|
|
|
|
<span className={styles.formValue}>
|
|
|
|
|
|
{Array.isArray(value) ? value.join(', ') : String(value)}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
})}
|
2026-02-25 23:54:38 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* 审批流程 */}
|
|
|
|
|
|
{detail.flow?.nodes && detail.flow.nodes.length > 0 && (
|
|
|
|
|
|
<Card title="审批流程" className={styles.section}>
|
|
|
|
|
|
<div className={styles.flowNodes}>
|
|
|
|
|
|
{detail.flow.nodes.map((node, index) => {
|
|
|
|
|
|
const isCurrent = detail.current_nodes?.includes(node.id)
|
|
|
|
|
|
const record = detail.records.find((r) => r.node_id === node.id)
|
|
|
|
|
|
|
|
|
|
|
|
let status: 'pending' | 'done' = 'pending'
|
|
|
|
|
|
if (record) {
|
|
|
|
|
|
status = 'done'
|
|
|
|
|
|
} else if (!isCurrent && index > 0) {
|
|
|
|
|
|
// 检查前面的节点是否完成
|
|
|
|
|
|
const prevNode = detail.flow.nodes[index - 1]
|
|
|
|
|
|
const prevRecord = detail.records.find((r) => r.node_id === prevNode.id)
|
|
|
|
|
|
if (prevRecord) status = 'done'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div
|
|
|
|
|
|
key={node.id}
|
|
|
|
|
|
className={`${styles.flowNode} ${styles[status]} ${isCurrent ? styles.current : ''}`}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div className={styles.flowNodeDot}>
|
|
|
|
|
|
{status === 'done' ? '✓' : index + 1}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className={styles.flowNodeContent}>
|
2026-02-26 00:14:11 +08:00
|
|
|
|
<div className={styles.flowNodeName}>{getNodeDisplayName(node)}</div>
|
2026-02-25 23:54:38 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
})}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* 审批记录 */}
|
|
|
|
|
|
<Card title="审批记录" className={styles.section}>
|
|
|
|
|
|
<div className={styles.timeline}>
|
|
|
|
|
|
{detail.records && detail.records.length > 0 ? (
|
|
|
|
|
|
detail.records.map((record) => {
|
|
|
|
|
|
const recordConfig = actionConfig[record.action] || actionConfig.submit
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div key={record.id} className={styles.timelineItem}>
|
|
|
|
|
|
<div className={styles.timelineDot} style={{ background: recordConfig.color }}>
|
|
|
|
|
|
<span className={styles.timelineDotIcon}>{recordConfig.icon}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className={styles.timelineContent}>
|
|
|
|
|
|
<div className={styles.timelineTitle}>
|
|
|
|
|
|
{recordConfig.text}
|
|
|
|
|
|
<span className={styles.timelineName}>
|
|
|
|
|
|
{record.approver?.name}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className={styles.timelineTime}>
|
|
|
|
|
|
{formatTime(record.created_at)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{record.comment && (
|
|
|
|
|
|
<div className={styles.timelineComment}>备注: {record.comment}</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
})
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<div className={styles.empty}>暂无审批记录</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 底部操作栏 */}
|
|
|
|
|
|
{(canCancel || canApprove) && (
|
|
|
|
|
|
<div className={styles.footer}>
|
|
|
|
|
|
{canCancel && (
|
|
|
|
|
|
<Button block color="warning" onClick={handleCancel}>
|
|
|
|
|
|
撤销申请
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{canApprove && (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
block
|
|
|
|
|
|
color="danger"
|
|
|
|
|
|
fill="outline"
|
|
|
|
|
|
onClick={() => handleApprove('reject')}
|
|
|
|
|
|
>
|
|
|
|
|
|
拒绝
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
block
|
|
|
|
|
|
color="primary"
|
|
|
|
|
|
onClick={() => handleApprove('approve')}
|
|
|
|
|
|
>
|
|
|
|
|
|
同意
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* 审批意见弹框 */}
|
|
|
|
|
|
<Dialog
|
|
|
|
|
|
visible={commentVisible}
|
|
|
|
|
|
title={actionType === 'approve' ? '同意申请' : actionType === 'reject' ? '拒绝申请' : '撤销申请'}
|
|
|
|
|
|
content={
|
|
|
|
|
|
<div className={styles.commentBox}>
|
|
|
|
|
|
<TextArea
|
|
|
|
|
|
placeholder="请输入审批意见(可选)"
|
|
|
|
|
|
value={comment}
|
|
|
|
|
|
onChange={setComment}
|
|
|
|
|
|
rows={3}
|
|
|
|
|
|
maxLength={200}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
}
|
|
|
|
|
|
onConfirm={handleConfirmApprove}
|
|
|
|
|
|
onClose={() => setCommentVisible(false)}
|
|
|
|
|
|
onCancel={() => setCommentVisible(false)}
|
|
|
|
|
|
confirmText="确认"
|
|
|
|
|
|
cancelText="取消"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|