Files
x-server-mobile/frontend/src/pages/approval/list.tsx
T
root 8ce070bd77 fix: 使用 sessionStorage 保持 tab 状态
问题:navigate(-1) 浏览器后退不会传递 location.state

解决方案:
- 切换 tab 时保存到 sessionStorage
- 页面加载时优先从 sessionStorage 恢复 tab

修改文件:
- frontend/src/pages/business-trip/list.tsx
- frontend/src/pages/approval/list.tsx

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 18:38:42 +08:00

241 lines
7.4 KiB
TypeScript

import { useState, useEffect, useRef } from 'react'
import { useNavigate, useLocation } 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 location = useLocation()
// 优先从 sessionStorage 恢复 tab,其次从 location.state,最后默认为 'my'
const savedTab = sessionStorage.getItem('approval-tab') as string
const stateTab = ((location.state as any)?.fromTab as string) || undefined
const initialTab = (savedTab && ['my', 'pending', 'approved', 'cc'].includes(savedTab)) ? savedTab : (stateTab || 'my')
const [activeTab, setActiveTab] = useState<'my' | 'pending' | 'approved' | 'cc'>(initialTab)
const [list, setList] = useState<ApprovalItem[]>([])
const [loading, setLoading] = useState(false)
const [hasMore, setHasMore] = useState(false)
const [page, setPage] = useState(1)
const requestIdRef = useRef<string>('')
// 加载数据 - 接收 tab 参数
const loadData = async (pageNum: number = 1, isRefresh: boolean = false, tab?: string) => {
// 生成新的请求 ID
const requestId = `${tab || activeTab}-${pageNum}-${Date.now()}-${Math.random()}`
requestIdRef.current = requestId
const currentTab = tab || activeTab
console.log('🔍 Loading approval list START:', {
requestId,
currentTab,
activeTab,
tab,
pageNum,
isRefresh,
requestIdRefNow: requestIdRef.current
})
setLoading(true)
try {
const result = await approvalApi.getList({
type: currentTab,
page: pageNum,
page_size: 20,
})
console.log('📦 API response received:', {
requestId,
currentTab,
requestIdRefNow: requestIdRef.current,
count: result?.list?.length
})
// 检查是否是最新的请求
if (requestIdRef.current !== requestId) {
console.log('⏭️ Skipping outdated response:', {
requestId,
latestRequestId: requestIdRef.current
})
return
}
const newList = result?.list || []
// 直接更新数据
console.log('✅ Updating list for tab:', currentTab, 'items:', newList.length)
setList(newList)
setHasMore(newList.length >= 20)
setPage(pageNum)
} catch (error: any) {
console.error('❌ Failed to load approvals:', error)
} finally {
setLoading(false)
}
}
// 切换标签
const handleTabChange = (key: string) => {
console.log('🔄 Tab changing from', activeTab, 'to', key)
setActiveTab(key as 'my' | 'pending' | 'approved' | 'cc')
// 保存 tab 到 sessionStorage
sessionStorage.setItem('approval-tab', key)
setList([])
setHasMore(false)
setPage(1)
// 直接传入新的 tab 值
loadData(1, true, key)
}
// 刷新
const handleRefresh = async () => {
await loadData(1, true, activeTab)
}
// 加载更多
const handleLoadMore = async () => {
await loadData(page + 1, false, activeTab)
}
// 初始加载
useEffect(() => {
console.log('🚀 Initial load effect')
loadData(1, true)
return () => {
console.log('🧹 Cleanup function called')
}
}, [])
// 查看详情
const handleViewDetail = (id: number) => {
navigate(`/approval/${id}`, { state: { fromTab: activeTab } })
}
// 格式化日期
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.Tab title="抄送我的" key="cc" />
</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>
)
}