2026-02-26 09:12:21 +08:00
|
|
|
import { useState, useEffect, useRef } from 'react'
|
2026-02-25 23:54:38 +08:00
|
|
|
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)
|
2026-02-26 09:12:21 +08:00
|
|
|
const [hasMore, setHasMore] = useState(false) // 改为 false,避免挂载时立即触发加载更多
|
2026-02-25 23:54:38 +08:00
|
|
|
const [page, setPage] = useState(1)
|
2026-02-26 09:12:21 +08:00
|
|
|
const currentTabRef = useRef(activeTab) // 用于追踪当前有效的 tab
|
|
|
|
|
const loadingRef = useRef(false) // 用于同步追踪 loading 状态
|
|
|
|
|
const requestIdRef = useRef<string | null>(null) // 用于追踪当前请求 ID
|
|
|
|
|
const mountedRef = useRef(true) // 组件挂载时立即设为 true
|
|
|
|
|
|
|
|
|
|
// 更新 ref 当 activeTab 变化时
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
currentTabRef.current = activeTab
|
|
|
|
|
}, [activeTab])
|
2026-02-25 23:54:38 +08:00
|
|
|
|
2026-02-26 00:10:08 +08:00
|
|
|
// 加载数据 - 接收 tab 参数避免使用旧的 activeTab
|
|
|
|
|
const loadData = async (pageNum: number = 1, isRefresh: boolean = false, tab?: string) => {
|
2026-02-26 09:12:21 +08:00
|
|
|
// 使用 ref 检查 loading,避免 React 18 StrictMode 双重调用
|
|
|
|
|
if (loadingRef.current) {
|
|
|
|
|
console.log('⏸️ Request blocked - already loading')
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-02-25 23:54:38 +08:00
|
|
|
|
2026-02-26 00:10:08 +08:00
|
|
|
// 使用传入的 tab 参数,如果没有则使用当前 activeTab
|
|
|
|
|
const currentTab = tab || activeTab
|
2026-02-26 09:12:21 +08:00
|
|
|
const requestId = `${currentTab}-${pageNum}-${Date.now()}`
|
|
|
|
|
requestIdRef.current = requestId
|
|
|
|
|
|
|
|
|
|
console.log('🔍 Loading approval list START:', {
|
|
|
|
|
requestId,
|
|
|
|
|
currentTab,
|
|
|
|
|
activeTab,
|
|
|
|
|
tab,
|
|
|
|
|
pageNum,
|
|
|
|
|
isRefresh,
|
|
|
|
|
currentTabRef: currentTabRef.current
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
loadingRef.current = true
|
2026-02-25 23:54:38 +08:00
|
|
|
setLoading(true)
|
|
|
|
|
try {
|
|
|
|
|
const result = await approvalApi.getList({
|
2026-02-26 00:10:08 +08:00
|
|
|
type: currentTab,
|
2026-02-25 23:54:38 +08:00
|
|
|
page: pageNum,
|
|
|
|
|
page_size: 20,
|
|
|
|
|
})
|
|
|
|
|
|
2026-02-26 09:12:21 +08:00
|
|
|
console.log('📦 API response received:', {
|
|
|
|
|
requestId,
|
|
|
|
|
type: currentTab,
|
|
|
|
|
count: result?.list?.length,
|
|
|
|
|
currentTabRefNow: currentTabRef.current
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 检查是否是最新的请求
|
|
|
|
|
if (requestIdRef.current !== requestId) {
|
|
|
|
|
console.log('⏭️ Skipping outdated response:', { requestId, latestRequestId: requestIdRef.current })
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log('📦 API response validated - processing data')
|
2026-02-26 00:09:36 +08:00
|
|
|
|
2026-02-26 00:10:08 +08:00
|
|
|
const newList = result?.list || []
|
2026-02-25 23:54:38 +08:00
|
|
|
|
2026-02-26 09:12:21 +08:00
|
|
|
// 只在当前 tab 与请求的 tab 一致时才更新数据
|
|
|
|
|
if (currentTabRef.current === currentTab) {
|
|
|
|
|
console.log('✅ Updating list for tab:', currentTab, 'items:', newList.length)
|
2026-02-25 23:54:38 +08:00
|
|
|
setList(newList)
|
2026-02-26 09:12:21 +08:00
|
|
|
setHasMore(newList.length >= 20)
|
|
|
|
|
setPage(pageNum)
|
2026-02-25 23:54:38 +08:00
|
|
|
} else {
|
2026-02-26 09:12:21 +08:00
|
|
|
console.log('⏭️ Skipping update - tab changed from', currentTab, 'to', currentTabRef.current)
|
2026-02-25 23:54:38 +08:00
|
|
|
}
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
console.error('Failed to load approvals:', error)
|
|
|
|
|
} finally {
|
2026-02-26 09:12:21 +08:00
|
|
|
loadingRef.current = false
|
2026-02-25 23:54:38 +08:00
|
|
|
setLoading(false)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 切换标签
|
|
|
|
|
const handleTabChange = (key: string) => {
|
2026-02-26 00:10:08 +08:00
|
|
|
console.log('🔄 Tab changing from', activeTab, 'to', key)
|
2026-02-26 09:12:21 +08:00
|
|
|
currentTabRef.current = key as 'my' | 'pending' | 'approved' // 立即更新 ref
|
2026-02-25 23:54:38 +08:00
|
|
|
setActiveTab(key as 'my' | 'pending' | 'approved')
|
|
|
|
|
setList([])
|
|
|
|
|
setHasMore(true)
|
|
|
|
|
setPage(1)
|
2026-02-26 00:10:08 +08:00
|
|
|
// 直接传入新的 tab 值,避免使用旧的 activeTab
|
|
|
|
|
loadData(1, true, key)
|
2026-02-25 23:54:38 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 刷新
|
|
|
|
|
const handleRefresh = async () => {
|
2026-02-26 09:12:21 +08:00
|
|
|
await loadData(1, true, activeTab)
|
2026-02-25 23:54:38 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 加载更多
|
|
|
|
|
const handleLoadMore = async () => {
|
2026-02-26 09:12:21 +08:00
|
|
|
await loadData(page + 1, false, activeTab)
|
2026-02-25 23:54:38 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 初始加载
|
|
|
|
|
useEffect(() => {
|
2026-02-26 09:12:21 +08:00
|
|
|
mountedRef.current = true
|
|
|
|
|
// 强制使用页码 1,忽略当前 page 状态
|
|
|
|
|
console.log('🚀 Initial load effect, forcing page=1')
|
2026-02-25 23:54:38 +08:00
|
|
|
loadData(1, true)
|
2026-02-26 09:12:21 +08:00
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
console.log('🧹 Cleanup function called')
|
|
|
|
|
mountedRef.current = false
|
|
|
|
|
// 不清空 requestIdRef,让异步请求能够正常完成处理
|
|
|
|
|
}
|
2026-02-25 23:54:38 +08:00
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
// 查看详情
|
|
|
|
|
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>
|
|
|
|
|
)
|
|
|
|
|
}
|