fix: 修复审批列表首次加载不显示和发起人审批人问题
- 移除 loadingRef,解决 React 18 StrictMode 下请求阻塞问题 - 提交申请时根据选择的审批人动态创建/更新审批节点 - 修改申请提交后跳转到审批列表而非出差列表 - 更新 PROJECT_ISSUES.md 记录所有问题和解决方案 修复问题: 1. 首次进入审批列表"我发起的"不显示数据 2. 发起人也是审批人时"待我审批"不显示 3. 申请提交后跳转到错误的页面 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+109
-8
@@ -31,27 +31,73 @@ const handleTabChange = (key: string) => {
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. 列表首次加载不显示数据问题
|
## 2. 列表首次加载不显示数据问题(多次修复)
|
||||||
|
|
||||||
**问题**:首次进入审批列表时,"我发起的"不显示数据,需要切换其他 tab 再切换回来才显示
|
**问题**:首次进入审批列表时,"我发起的"不显示数据,需要切换其他 tab 再切换回来才显示
|
||||||
|
|
||||||
**原因**:
|
**原因(版本1)**:
|
||||||
- React 18 StrictMode 导致组件快速 mount/unmount
|
- React 18 StrictMode 导致组件快速 mount/unmount
|
||||||
- useEffect cleanup 函数中 `requestIdRef.current = null` 清空了请求 ID
|
- useEffect cleanup 函数中清空了 requestIdRef,导致正在进行的 API 请求响应被跳过
|
||||||
- 正在进行的 API 请求返回时,`requestIdRef.current` 已经是 null,导致响应被跳过
|
|
||||||
|
|
||||||
**解决方案**:
|
**原因(版本2 - 最终问题)**:
|
||||||
|
- 使用 `loadingRef.current` 检查是否正在加载
|
||||||
|
- React 18 StrictMode 导致 useEffect 执行两次
|
||||||
|
- 第一次请求设置 `loadingRef.current = true`
|
||||||
|
- 第二次请求检查 `loadingRef.current` 为 true,被阻塞
|
||||||
|
- 导致第二次请求没有发送,数据无法加载
|
||||||
|
|
||||||
|
**解决方案(最终版本)**:
|
||||||
```typescript
|
```typescript
|
||||||
|
// 移除 loadingRef,只使用 requestId 追踪请求
|
||||||
|
const requestIdRef = useRef<string>('')
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
setLoading(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await approvalApi.getList({
|
||||||
|
type: currentTab,
|
||||||
|
page: pageNum,
|
||||||
|
page_size: 20,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 检查是否是最新的请求
|
||||||
|
if (requestIdRef.current !== requestId) {
|
||||||
|
return // 跳过过期响应
|
||||||
|
}
|
||||||
|
|
||||||
|
const newList = result?.list || []
|
||||||
|
setList(newList)
|
||||||
|
setHasMore(newList.length >= 20)
|
||||||
|
setPage(pageNum)
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Failed to load approvals:', error)
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// useEffect cleanup 不做任何操作
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadData(1, true)
|
loadData(1, true)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
mountedRef.current = false
|
// 不清空任何 ref,让异步请求能够正常完成处理
|
||||||
// 不清空 requestIdRef,让异步请求能够正常完成处理
|
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**关键点**:
|
||||||
|
- ❌ 不要使用 `loadingRef` 检查来阻塞请求
|
||||||
|
- ❌ 不要在 cleanup 中清空 `requestIdRef`
|
||||||
|
- ✅ 只使用 `requestId` 来标识和忽略过期响应
|
||||||
|
- ✅ 在 requestId 中加入随机数避免 React 18 StrictMode 下的冲突
|
||||||
|
|
||||||
**相关文件**:`frontend/src/pages/approval/list.tsx`
|
**相关文件**:`frontend/src/pages/approval/list.tsx`
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -340,6 +386,61 @@ const loadData = async (tab?: string) => {
|
|||||||
### 20. Docker 容器 CMD
|
### 20. Docker 容器 CMD
|
||||||
**使用前台运行**:`php start.php start`(不要用 `-d`),避免容器退出
|
**使用前台运行**:`php start.php start`(不要用 `-d`),避免容器退出
|
||||||
|
|
||||||
|
### 21. 发起人也是审批人时"待我审批"不显示
|
||||||
|
|
||||||
|
**问题**:用户提交申请后,既是发起人也是审批人,但"待我审批"中不显示该订单
|
||||||
|
|
||||||
|
**原因**:
|
||||||
|
- `BusinessTripService::submit()` 中硬编码 `current_nodes => [1]`
|
||||||
|
- 但数据库中节点 1 的审批人是其他用户(如用户 2)
|
||||||
|
- 提交申请时没有根据用户选择的审批人创建/更新节点
|
||||||
|
|
||||||
|
**解决方案**:
|
||||||
|
```php
|
||||||
|
// 根据审批人创建审批节点
|
||||||
|
$approverIds = array_column($approvers, 'id');
|
||||||
|
$node = ApprovalNode::where('flow_id', $flow->id)
|
||||||
|
->where('order', 0)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (!$node) {
|
||||||
|
// 创建第一个审批节点
|
||||||
|
$node = ApprovalNode::create([
|
||||||
|
'flow_id' => $flow->id,
|
||||||
|
'type' => count($approverIds) > 1 ? 'and_approve' : 'approver',
|
||||||
|
'name' => '审批',
|
||||||
|
'order' => 0,
|
||||||
|
'approvers' => $approverIds,
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
// 更新现有节点的审批人
|
||||||
|
$node->approvers = $approverIds;
|
||||||
|
$node->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用创建的节点 ID
|
||||||
|
$approvalOrder = ApprovalOrder::create([
|
||||||
|
// ...
|
||||||
|
'current_nodes' => [$node->id],
|
||||||
|
// ...
|
||||||
|
]);
|
||||||
|
```
|
||||||
|
|
||||||
|
**相关文件**:`backend/app/service/BusinessTripService.php`
|
||||||
|
|
||||||
|
### 22. 申请提交后跳转到错误的页面
|
||||||
|
|
||||||
|
**问题**:提交申请后跳转到出差列表 (`/business-trip`),而不是审批列表 (`/approval`)
|
||||||
|
|
||||||
|
**解决方案**:
|
||||||
|
```typescript
|
||||||
|
// 修改申请页面跳转逻辑
|
||||||
|
Toast.show(t('businessTrip.submitSuccess'))
|
||||||
|
navigate('/approval') // 改为跳转到审批列表
|
||||||
|
```
|
||||||
|
|
||||||
|
**相关文件**:`frontend/src/pages/business-trip/apply.tsx`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 文件修改记录
|
## 文件修改记录
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ class BusinessTripService
|
|||||||
throw new \Exception('审批人和抄送人不能是同一个人');
|
throw new \Exception('审批人和抄送人不能是同一个人');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取审批流程(简化版:直接使用第一个审批人作为单人审批)
|
// 获取审批流程
|
||||||
$flow = ApprovalFlow::where('business_type', 'business_trip')
|
$flow = ApprovalFlow::where('business_type', 'business_trip')
|
||||||
->where('status', 1)
|
->where('status', 1)
|
||||||
->first();
|
->first();
|
||||||
@@ -138,6 +138,49 @@ class BusinessTripService
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 根据审批人创建审批节点
|
||||||
|
$approverIds = array_column($approvers, 'id');
|
||||||
|
$node = ApprovalNode::where('flow_id', $flow->id)
|
||||||
|
->where('order', 0)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (!$node) {
|
||||||
|
// 创建第一个审批节点
|
||||||
|
$node = ApprovalNode::create([
|
||||||
|
'flow_id' => $flow->id,
|
||||||
|
'type' => count($approverIds) > 1 ? 'and_approve' : 'approver',
|
||||||
|
'name' => '审批',
|
||||||
|
'order' => 0,
|
||||||
|
'approvers' => $approverIds,
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
// 更新现有节点的审批人
|
||||||
|
$node->approvers = $approverIds;
|
||||||
|
$node->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建抄送节点(如果有抄送人)
|
||||||
|
$ccUserIds = array_column($ccUsers, 'id');
|
||||||
|
if (!empty($ccUserIds)) {
|
||||||
|
$ccNode = ApprovalNode::where('flow_id', $flow->id)
|
||||||
|
->where('type', 'notify')
|
||||||
|
->where('order', 1)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (!$ccNode) {
|
||||||
|
ApprovalNode::create([
|
||||||
|
'flow_id' => $flow->id,
|
||||||
|
'type' => 'notify',
|
||||||
|
'name' => '抄送',
|
||||||
|
'order' => 1,
|
||||||
|
'approvers' => $ccUserIds,
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
$ccNode->approvers = $ccUserIds;
|
||||||
|
$ccNode->save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 创建审批单
|
// 创建审批单
|
||||||
$approvalOrder = ApprovalOrder::create([
|
$approvalOrder = ApprovalOrder::create([
|
||||||
'business_type' => 'business_trip',
|
'business_type' => 'business_trip',
|
||||||
@@ -145,7 +188,7 @@ class BusinessTripService
|
|||||||
'title' => '出差申请 - ' . $businessTrip->user->name,
|
'title' => '出差申请 - ' . $businessTrip->user->name,
|
||||||
'applicant_id' => $userId,
|
'applicant_id' => $userId,
|
||||||
'status' => ApprovalOrder::STATUS_PENDING,
|
'status' => ApprovalOrder::STATUS_PENDING,
|
||||||
'current_nodes' => [1], // 简化:使用节点1
|
'current_nodes' => [$node->id], // 使用创建的节点 ID
|
||||||
'flow_id' => $flow->id,
|
'flow_id' => $flow->id,
|
||||||
'form_data' => [
|
'form_data' => [
|
||||||
'reason' => $businessTrip->reason,
|
'reason' => $businessTrip->reason,
|
||||||
@@ -165,8 +208,8 @@ class BusinessTripService
|
|||||||
// 记录提交操作
|
// 记录提交操作
|
||||||
ApprovalRecord::create([
|
ApprovalRecord::create([
|
||||||
'order_id' => $approvalOrder->id,
|
'order_id' => $approvalOrder->id,
|
||||||
'node_id' => 1,
|
'node_id' => $node->id,
|
||||||
'node_type' => 'single_approve',
|
'node_type' => $node->type,
|
||||||
'approver_id' => $userId,
|
'approver_id' => $userId,
|
||||||
'action' => 'submit',
|
'action' => 'submit',
|
||||||
'comment' => null,
|
'comment' => null,
|
||||||
|
|||||||
@@ -49,31 +49,19 @@ export default function ApprovalList() {
|
|||||||
const [activeTab, setActiveTab] = useState<'my' | 'pending' | 'approved'>('my')
|
const [activeTab, setActiveTab] = useState<'my' | 'pending' | 'approved'>('my')
|
||||||
const [list, setList] = useState<ApprovalItem[]>([])
|
const [list, setList] = useState<ApprovalItem[]>([])
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [hasMore, setHasMore] = useState(false) // 改为 false,避免挂载时立即触发加载更多
|
const [hasMore, setHasMore] = useState(false)
|
||||||
const [page, setPage] = useState(1)
|
const [page, setPage] = useState(1)
|
||||||
const currentTabRef = useRef(activeTab) // 用于追踪当前有效的 tab
|
|
||||||
const loadingRef = useRef(false) // 用于同步追踪 loading 状态
|
|
||||||
const requestIdRef = useRef<string | null>(null) // 用于追踪当前请求 ID
|
|
||||||
const mountedRef = useRef(true) // 组件挂载时立即设为 true
|
|
||||||
|
|
||||||
// 更新 ref 当 activeTab 变化时
|
const requestIdRef = useRef<string>('')
|
||||||
useEffect(() => {
|
|
||||||
currentTabRef.current = activeTab
|
|
||||||
}, [activeTab])
|
|
||||||
|
|
||||||
// 加载数据 - 接收 tab 参数避免使用旧的 activeTab
|
// 加载数据 - 接收 tab 参数
|
||||||
const loadData = async (pageNum: number = 1, isRefresh: boolean = false, tab?: string) => {
|
const loadData = async (pageNum: number = 1, isRefresh: boolean = false, tab?: string) => {
|
||||||
// 使用 ref 检查 loading,避免 React 18 StrictMode 双重调用
|
// 生成新的请求 ID
|
||||||
if (loadingRef.current) {
|
const requestId = `${tab || activeTab}-${pageNum}-${Date.now()}-${Math.random()}`
|
||||||
console.log('⏸️ Request blocked - already loading')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用传入的 tab 参数,如果没有则使用当前 activeTab
|
|
||||||
const currentTab = tab || activeTab
|
|
||||||
const requestId = `${currentTab}-${pageNum}-${Date.now()}`
|
|
||||||
requestIdRef.current = requestId
|
requestIdRef.current = requestId
|
||||||
|
|
||||||
|
const currentTab = tab || activeTab
|
||||||
|
|
||||||
console.log('🔍 Loading approval list START:', {
|
console.log('🔍 Loading approval list START:', {
|
||||||
requestId,
|
requestId,
|
||||||
currentTab,
|
currentTab,
|
||||||
@@ -81,11 +69,11 @@ export default function ApprovalList() {
|
|||||||
tab,
|
tab,
|
||||||
pageNum,
|
pageNum,
|
||||||
isRefresh,
|
isRefresh,
|
||||||
currentTabRef: currentTabRef.current
|
requestIdRefNow: requestIdRef.current
|
||||||
})
|
})
|
||||||
|
|
||||||
loadingRef.current = true
|
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await approvalApi.getList({
|
const result = await approvalApi.getList({
|
||||||
type: currentTab,
|
type: currentTab,
|
||||||
@@ -95,34 +83,30 @@ export default function ApprovalList() {
|
|||||||
|
|
||||||
console.log('📦 API response received:', {
|
console.log('📦 API response received:', {
|
||||||
requestId,
|
requestId,
|
||||||
type: currentTab,
|
currentTab,
|
||||||
count: result?.list?.length,
|
requestIdRefNow: requestIdRef.current,
|
||||||
currentTabRefNow: currentTabRef.current
|
count: result?.list?.length
|
||||||
})
|
})
|
||||||
|
|
||||||
// 检查是否是最新的请求
|
// 检查是否是最新的请求
|
||||||
if (requestIdRef.current !== requestId) {
|
if (requestIdRef.current !== requestId) {
|
||||||
console.log('⏭️ Skipping outdated response:', { requestId, latestRequestId: requestIdRef.current })
|
console.log('⏭️ Skipping outdated response:', {
|
||||||
|
requestId,
|
||||||
|
latestRequestId: requestIdRef.current
|
||||||
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('📦 API response validated - processing data')
|
|
||||||
|
|
||||||
const newList = result?.list || []
|
const newList = result?.list || []
|
||||||
|
|
||||||
// 只在当前 tab 与请求的 tab 一致时才更新数据
|
// 直接更新数据
|
||||||
if (currentTabRef.current === currentTab) {
|
|
||||||
console.log('✅ Updating list for tab:', currentTab, 'items:', newList.length)
|
console.log('✅ Updating list for tab:', currentTab, 'items:', newList.length)
|
||||||
setList(newList)
|
setList(newList)
|
||||||
setHasMore(newList.length >= 20)
|
setHasMore(newList.length >= 20)
|
||||||
setPage(pageNum)
|
setPage(pageNum)
|
||||||
} else {
|
|
||||||
console.log('⏭️ Skipping update - tab changed from', currentTab, 'to', currentTabRef.current)
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Failed to load approvals:', error)
|
console.error('❌ Failed to load approvals:', error)
|
||||||
} finally {
|
} finally {
|
||||||
loadingRef.current = false
|
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -130,12 +114,11 @@ export default function ApprovalList() {
|
|||||||
// 切换标签
|
// 切换标签
|
||||||
const handleTabChange = (key: string) => {
|
const handleTabChange = (key: string) => {
|
||||||
console.log('🔄 Tab changing from', activeTab, 'to', key)
|
console.log('🔄 Tab changing from', activeTab, 'to', key)
|
||||||
currentTabRef.current = key as 'my' | 'pending' | 'approved' // 立即更新 ref
|
|
||||||
setActiveTab(key as 'my' | 'pending' | 'approved')
|
setActiveTab(key as 'my' | 'pending' | 'approved')
|
||||||
setList([])
|
setList([])
|
||||||
setHasMore(true)
|
setHasMore(false)
|
||||||
setPage(1)
|
setPage(1)
|
||||||
// 直接传入新的 tab 值,避免使用旧的 activeTab
|
// 直接传入新的 tab 值
|
||||||
loadData(1, true, key)
|
loadData(1, true, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,15 +134,11 @@ export default function ApprovalList() {
|
|||||||
|
|
||||||
// 初始加载
|
// 初始加载
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
mountedRef.current = true
|
console.log('🚀 Initial load effect')
|
||||||
// 强制使用页码 1,忽略当前 page 状态
|
|
||||||
console.log('🚀 Initial load effect, forcing page=1')
|
|
||||||
loadData(1, true)
|
loadData(1, true)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
console.log('🧹 Cleanup function called')
|
console.log('🧹 Cleanup function called')
|
||||||
mountedRef.current = false
|
|
||||||
// 不清空 requestIdRef,让异步请求能够正常完成处理
|
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
|||||||
@@ -270,7 +270,7 @@ export default function BusinessTripApply() {
|
|||||||
sessionStorage.removeItem('businessTripDraft')
|
sessionStorage.removeItem('businessTripDraft')
|
||||||
|
|
||||||
Toast.show('草稿已保存')
|
Toast.show('草稿已保存')
|
||||||
navigate('/business-trip')
|
navigate('/approval')
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
Toast.show(error.message || '保存失败')
|
Toast.show(error.message || '保存失败')
|
||||||
}
|
}
|
||||||
@@ -336,7 +336,7 @@ export default function BusinessTripApply() {
|
|||||||
sessionStorage.removeItem('businessTripDraft')
|
sessionStorage.removeItem('businessTripDraft')
|
||||||
|
|
||||||
Toast.show(t('businessTrip.submitSuccess'))
|
Toast.show(t('businessTrip.submitSuccess'))
|
||||||
navigate('/business-trip')
|
navigate('/approval')
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
Toast.show(error.message || '提交失败')
|
Toast.show(error.message || '提交失败')
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user