feat: 完善审批功能
- 新增通用审批列表页面(我发起的/待我审批/我已审批) - 新增通用审批详情页面(支持审批通过/拒绝操作) - 修复审批列表过滤逻辑("我已审批"只显示已完成订单) - 修复登录页面未设置 Zustand store 的问题 - 修复 User 类型定义(id 改为 number 类型) - 修复草稿恢复时的日期转换问题 - 创建测试数据脚本(test_data.php) - 安装 webman 官方依赖包(database, redis, pagination 等) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { List, Card, Tag, Tabs, PullToRefresh, InfiniteScroll } from 'antd-mobile'
|
||||
import {
|
||||
ClockCircleOutline,
|
||||
CheckCircleOutline,
|
||||
CloseCircleOutline,
|
||||
ExclamationCircleOutline,
|
||||
} from 'antd-mobile-icons'
|
||||
import { approvalApi } from '../../services/api'
|
||||
import styles from './list.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: '已撤销' },
|
||||
}
|
||||
|
||||
interface ApprovalItem {
|
||||
id: number
|
||||
title: string
|
||||
business_type: string
|
||||
status: string
|
||||
created_at: string
|
||||
applicant?: {
|
||||
name: string
|
||||
}
|
||||
flow?: {
|
||||
name: string
|
||||
}
|
||||
}
|
||||
|
||||
// 业务类型映射
|
||||
const businessTypeConfig: Record<string, string> = {
|
||||
business_trip: '出差申请',
|
||||
expense: '费用报销',
|
||||
leave: '请假申请',
|
||||
purchase: '采购申请',
|
||||
}
|
||||
|
||||
export default function ApprovalList() {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const [activeTab, setActiveTab] = useState<'my' | 'pending' | 'approved'>('my')
|
||||
const [list, setList] = useState<ApprovalItem[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [hasMore, setHasMore] = useState(true)
|
||||
const [page, setPage] = useState(1)
|
||||
|
||||
// 加载数据
|
||||
const loadData = async (pageNum: number = 1, isRefresh: boolean = false) => {
|
||||
if (loading) return
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
const result = await approvalApi.getList({
|
||||
type: activeTab,
|
||||
page: pageNum,
|
||||
page_size: 20,
|
||||
})
|
||||
|
||||
const newList = result.list || []
|
||||
|
||||
if (isRefresh || pageNum === 1) {
|
||||
setList(newList)
|
||||
} else {
|
||||
setList([...list, ...newList])
|
||||
}
|
||||
|
||||
setHasMore(newList.length >= 20)
|
||||
setPage(pageNum)
|
||||
} catch (error: any) {
|
||||
console.error('Failed to load approvals:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 切换标签
|
||||
const handleTabChange = (key: string) => {
|
||||
setActiveTab(key as 'my' | 'pending' | 'approved')
|
||||
setList([])
|
||||
setHasMore(true)
|
||||
setPage(1)
|
||||
loadData(1, true)
|
||||
}
|
||||
|
||||
// 刷新
|
||||
const handleRefresh = async () => {
|
||||
await loadData(1, true)
|
||||
}
|
||||
|
||||
// 加载更多
|
||||
const handleLoadMore = async () => {
|
||||
await loadData(page + 1, false)
|
||||
}
|
||||
|
||||
// 初始加载
|
||||
useEffect(() => {
|
||||
loadData(1, true)
|
||||
}, [])
|
||||
|
||||
// 查看详情
|
||||
const handleViewDetail = (id: number) => {
|
||||
navigate(`/approval/${id}`)
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateStr: string) => {
|
||||
const date = new Date(dateStr)
|
||||
return `${date.getMonth() + 1}/${date.getDate()}`
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (dateStr: string) => {
|
||||
const date = new Date(dateStr)
|
||||
const now = new Date()
|
||||
const diff = now.getTime() - date.getTime()
|
||||
const days = Math.floor(diff / (1000 * 60 * 60 * 24))
|
||||
|
||||
if (days === 0) {
|
||||
const hours = Math.floor(diff / (1000 * 60 * 60))
|
||||
if (hours === 0) {
|
||||
const minutes = Math.floor(diff / (1000 * 60))
|
||||
return minutes < 1 ? '刚刚' : `${minutes}分钟前`
|
||||
}
|
||||
return `${hours}小时前`
|
||||
}
|
||||
if (days === 1) return '昨天'
|
||||
if (days < 7) return `${days}天前`
|
||||
return formatDate(dateStr)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.list}>
|
||||
<Tabs activeKey={activeTab} onChange={handleTabChange}>
|
||||
<Tabs.Tab title="我发起的" key="my" />
|
||||
<Tabs.Tab title="待我审批" key="pending" />
|
||||
<Tabs.Tab title="我已审批" key="approved" />
|
||||
</Tabs>
|
||||
|
||||
<PullToRefresh onRefresh={handleRefresh}>
|
||||
<List>
|
||||
{list.map((item) => {
|
||||
const config = statusConfig[item.status] || statusConfig.draft
|
||||
|
||||
return (
|
||||
<List.Item
|
||||
key={item.id}
|
||||
onClick={() => handleViewDetail(item.id)}
|
||||
className={styles.item}
|
||||
>
|
||||
<Card className={styles.card}>
|
||||
<div className={styles.header}>
|
||||
<span className={styles.title}>{item.title}</span>
|
||||
<Tag color={config.color} className={styles.tag}>
|
||||
{config.icon}
|
||||
{config.text}
|
||||
</Tag>
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.info}>
|
||||
<span className={styles.businessType}>
|
||||
{businessTypeConfig[item.business_type] || item.business_type}
|
||||
</span>
|
||||
<span className={styles.flow}>{item.flow?.name}</span>
|
||||
</div>
|
||||
<div className={styles.time}>{formatTime(item.created_at)}</div>
|
||||
{item.applicant && activeTab !== 'my' && (
|
||||
<div className={styles.applicant}>申请人: {item.applicant.name}</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</List.Item>
|
||||
)
|
||||
})}
|
||||
</List>
|
||||
</PullToRefresh>
|
||||
|
||||
{list.length === 0 && !loading && (
|
||||
<div className={styles.empty}>
|
||||
<ExclamationCircleOutline fontSize={48} color="#cccccc" />
|
||||
<p>暂无审批记录</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<InfiniteScroll loadMore={handleLoadMore} hasMore={hasMore} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user