ba41b340fc
- 移除 loadingRef,解决 React 18 StrictMode 下请求阻塞问题 - 提交申请时根据选择的审批人动态创建/更新审批节点 - 修改申请提交后跳转到审批列表而非出差列表 - 更新 PROJECT_ISSUES.md 记录所有问题和解决方案 修复问题: 1. 首次进入审批列表"我发起的"不显示数据 2. 发起人也是审批人时"待我审批"不显示 3. 申请提交后跳转到错误的页面 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
468 lines
14 KiB
Markdown
468 lines
14 KiB
Markdown
# X-Server Mobile 项目问题汇总
|
||
|
||
本文档记录项目开发过程中遇到的问题和解决方案,便于 CLAUDE 后续参考。
|
||
|
||
---
|
||
|
||
## 1. 审批列表数据混乱问题
|
||
|
||
**问题**:审批列表的三个 tab(我发起的、待我审批、我已审批)显示的数据不正确
|
||
|
||
**原因**:
|
||
- 前端状态管理问题:`setActiveTab` 是异步的,`loadData` 函数使用旧状态值
|
||
- React 18 StrictMode 导致 useEffect 执行两次,造成重复请求和状态混乱
|
||
|
||
**解决方案**:
|
||
```typescript
|
||
// 在 loadData 中添加 tab 参数,直接传递目标 tab 值
|
||
const loadData = async (pageNum: number = 1, isRefresh: boolean = false, tab?: string) => {
|
||
const currentTab = tab || activeTab // 使用传入的 tab 参数
|
||
// ...
|
||
}
|
||
|
||
// 切换 tab 时直接传递新值
|
||
const handleTabChange = (key: string) => {
|
||
setActiveTab(key as 'my' | 'pending' | 'approved')
|
||
loadData(1, true, key) // 直接传递 key
|
||
}
|
||
```
|
||
|
||
**相关文件**:`frontend/src/pages/approval/list.tsx`
|
||
|
||
---
|
||
|
||
## 2. 列表首次加载不显示数据问题(多次修复)
|
||
|
||
**问题**:首次进入审批列表时,"我发起的"不显示数据,需要切换其他 tab 再切换回来才显示
|
||
|
||
**原因(版本1)**:
|
||
- React 18 StrictMode 导致组件快速 mount/unmount
|
||
- useEffect cleanup 函数中清空了 requestIdRef,导致正在进行的 API 请求响应被跳过
|
||
|
||
**原因(版本2 - 最终问题)**:
|
||
- 使用 `loadingRef.current` 检查是否正在加载
|
||
- React 18 StrictMode 导致 useEffect 执行两次
|
||
- 第一次请求设置 `loadingRef.current = true`
|
||
- 第二次请求检查 `loadingRef.current` 为 true,被阻塞
|
||
- 导致第二次请求没有发送,数据无法加载
|
||
|
||
**解决方案(最终版本)**:
|
||
```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(() => {
|
||
loadData(1, true)
|
||
return () => {
|
||
// 不清空任何 ref,让异步请求能够正常完成处理
|
||
}
|
||
}, [])
|
||
```
|
||
|
||
**关键点**:
|
||
- ❌ 不要使用 `loadingRef` 检查来阻塞请求
|
||
- ❌ 不要在 cleanup 中清空 `requestIdRef`
|
||
- ✅ 只使用 `requestId` 来标识和忽略过期响应
|
||
- ✅ 在 requestId 中加入随机数避免 React 18 StrictMode 下的冲突
|
||
|
||
**相关文件**:`frontend/src/pages/approval/list.tsx`
|
||
|
||
---
|
||
|
||
## 3. 页码初始值导致请求错误数据
|
||
|
||
**问题**:前端请求 page=2 而不是 page=1
|
||
|
||
**原因**:
|
||
- `InfiniteScroll` 组件在挂载时触发 `loadMore`,因为 `hasMore` 初始值是 `true`
|
||
|
||
**解决方案**:
|
||
```typescript
|
||
const [hasMore, setHasMore] = useState(false) // 改为 false,避免挂载时立即触发加载更多
|
||
```
|
||
|
||
**相关文件**:`frontend/src/pages/approval/list.tsx`
|
||
|
||
---
|
||
|
||
## 4. 后端响应序列化问题
|
||
|
||
**问题**:后端返回数据时发生错误,进程不断重启
|
||
|
||
**原因**:
|
||
- Eloquent 模型对象存在循环引用,JSON 序列化失败
|
||
|
||
**解决方案**:
|
||
```php
|
||
// 在 ApprovalService::getList() 中将模型对象转换为纯数组
|
||
$listArray = $list->map(function ($item) {
|
||
return [
|
||
'id' => $item->id,
|
||
'title' => $item->title,
|
||
'business_type' => $item->business_type,
|
||
'status' => $item->status,
|
||
'created_at' => $item->created_at ? $item->created_at->toDateTimeString() : null,
|
||
'applicant' => $item->applicant ? [
|
||
'id' => $item->applicant->id,
|
||
'name' => $item->applicant->name,
|
||
] : null,
|
||
'flow' => $item->flow ? [
|
||
'id' => $item->flow->id,
|
||
'name' => $item->flow->name,
|
||
] : null,
|
||
];
|
||
})->toArray();
|
||
|
||
return [
|
||
'list' => $listArray,
|
||
'total' => $total,
|
||
'page' => $page,
|
||
'page_size' => $pageSize,
|
||
];
|
||
```
|
||
|
||
**相关文件**:`backend/app/service/ApprovalService.php`
|
||
|
||
---
|
||
|
||
## 5. 审批人和抄送人重复校验
|
||
|
||
**问题**:提交申请时,审批人和抄送人可能存在重复
|
||
|
||
**解决方案**:
|
||
```php
|
||
// 在 BusinessTripService::submit() 中添加校验
|
||
$approverIds = array_column($approvers, 'id');
|
||
$ccUserIds = array_column($ccUsers, 'id');
|
||
|
||
// 检查审批人列表中是否有重复
|
||
if (count($approverIds) !== count(array_unique($approverIds))) {
|
||
throw new \Exception('审批人列表中有重复用户');
|
||
}
|
||
|
||
// 检查抄送人列表中是否有重复
|
||
if (count($ccUserIds) !== count(array_unique($ccUserIds))) {
|
||
throw new \Exception('抄送人列表中有重复用户');
|
||
}
|
||
|
||
// 检查审批人和抄送人是否有重叠
|
||
$overlap = array_intersect($approverIds, $ccUserIds);
|
||
if (!empty($overlap)) {
|
||
throw new \Exception('审批人和抄送人不能是同一个人');
|
||
}
|
||
```
|
||
|
||
**相关文件**:`backend/app/service/BusinessTripService.php`
|
||
|
||
---
|
||
|
||
## 6. 测试数据节点配置错误
|
||
|
||
**问题**:测试数据与实际审批节点配置不匹配
|
||
|
||
**解决方案**:
|
||
- 修正数据库中订单的 `current_nodes` 字段,使用正确的节点 ID
|
||
- 确保节点 ID 与实际审批人匹配
|
||
|
||
**相关数据库表**:`approval_node`, `approval_order`
|
||
|
||
---
|
||
|
||
## 7. 双 header 问题
|
||
|
||
**问题**:部分页面有两个 header(MobileLayout 的 Header + 页面的 NavBar)
|
||
|
||
**解决方案**:
|
||
```typescript
|
||
// 在 MobileLayout.tsx 中全局隐藏 Header
|
||
const showHeader = false
|
||
```
|
||
|
||
**相关文件**:`frontend/src/components/layout/MobileLayout.tsx`
|
||
|
||
---
|
||
|
||
## 8. 申请页面 padding-bottom 过大
|
||
|
||
**问题**:申请页面底部留白太大
|
||
|
||
**解决方案**:
|
||
```css
|
||
.apply {
|
||
padding-bottom: 1.5rem; /* 从 80px 改为 1.5rem */
|
||
}
|
||
```
|
||
|
||
**相关文件**:`frontend/src/pages/business-trip/apply.module.css`
|
||
|
||
---
|
||
|
||
## 9. 前端文件路径大小写问题
|
||
|
||
**问题**:容器内使用小写目录 `service`,而本地编辑器可能是大写 `Service`
|
||
|
||
**解决方案**:
|
||
- 注意容器内路径为 `/app/app/service/`(小写)
|
||
- 编辑文件后需要复制到容器并重启服务
|
||
|
||
---
|
||
|
||
## 10. React 状态管理最佳实践
|
||
|
||
### 使用 ref 追踪异步状态
|
||
```typescript
|
||
const loadingRef = useRef(false) // 同步状态,避免闭包陷阱
|
||
const requestIdRef = useRef<string | null>(null) // 追踪请求 ID
|
||
```
|
||
|
||
### useEffect 依赖清理
|
||
```typescript
|
||
useEffect(() => {
|
||
// 不要在 cleanup 中清空正在使用的 ref
|
||
return () => {
|
||
mountedRef.current = false
|
||
// requestIdRef.current = null // ❌ 不要清空,让异步请求正常完成
|
||
}
|
||
}, [])
|
||
```
|
||
|
||
### 函数参数优先于状态
|
||
```typescript
|
||
// ❌ 错误:使用可能过期的状态
|
||
const loadData = async () => {
|
||
await api.get('/list', { type: activeTab }) // activeTab 可能是旧值
|
||
}
|
||
|
||
// ✅ 正确:直接传递参数
|
||
const loadData = async (tab?: string) => {
|
||
const currentTab = tab || activeTab
|
||
await api.get('/list', { type: currentTab })
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 开发注意事项
|
||
|
||
1. **数据库密码**:mysql_xEsdPa
|
||
2. **数据库名称**:xserver(不是 x_server_mobile)
|
||
3. **MySQL 容器名**:1Panel-mysql-409s
|
||
4. **后端容器内路径**:/app/(不是 /www/x-server-mobile/backend/)
|
||
5. **调试后端**:
|
||
- 编辑文件后需要复制到容器:`docker cp host_file container:/app/path`
|
||
- 重启后端:`docker compose restart backend`
|
||
- 或重载 workers:`docker exec x-server-backend php /app/webman reload`
|
||
6. **查看日志**:`docker logs x-server-backend -f --tail 50`
|
||
7. **React 18 StrictMode**:开发模式下 useEffect 会执行两次,需要注意副作用
|
||
|
||
---
|
||
|
||
## 历史问题汇总(早期开发阶段)
|
||
|
||
### 1. 缺少 webman/database 包
|
||
**解决方案**:`composer require webman/database:^2.0`
|
||
|
||
### 2. 登录状态没有正确设置到 Zustand store
|
||
**解决方案**:在登录成功后调用 `login(result.data.user, result.data.token)`
|
||
|
||
### 3. User 类型定义中 id 类型错误
|
||
**解决方案**:将 `id: string` 改为 `id: number`
|
||
|
||
### 4. 选择审批人后表单数据丢失
|
||
**原因**:每次导航时组件重新挂载,sessionStorage 在第一次读取后被清除
|
||
**解决方案**:使用独立的 sessionStorage key,不在 useEffect 中清除
|
||
|
||
### 5. DatePicker 点击无反应
|
||
**原因**:antd-mobile 5.x 的 DatePicker children render prop 点击事件绑定有问题
|
||
**解决方案**:使用 `visible` 属性手动控制弹框显示
|
||
|
||
### 6. 审批列表"我已审批"过滤逻辑错误
|
||
**问题**:显示了所有已审批的订单,包括还在审批中的
|
||
**解决方案**:只显示已完成状态的订单(approved、rejected、cancelled)
|
||
|
||
### 7. Dialog 按钮不显示
|
||
**问题**:使用 `confirmText`/`cancelText` 参数无效
|
||
**解决方案**:使用 `actions` prop 自定义按钮
|
||
|
||
### 8. React key 重复警告
|
||
**问题**:审批人和抄送人的 avatar 使用相同 key
|
||
**解决方案**:添加前缀区分 `approver-${id}` 和 `cc-${id}`
|
||
|
||
### 9. 飞书登录回调地址
|
||
**解决方案**:更新为 https://xsm.dev.hnxinyouhe.com/login
|
||
|
||
### 10. 审批节点数据库重复
|
||
**解决方案**:删除重复节点
|
||
|
||
### 11. 页面间状态丢失
|
||
**问题**:`navigate(-1)` 不传递 state,`location.state` 为 null
|
||
**解决方案**:使用 sessionStorage 传递数据
|
||
|
||
### 12. sessionStorage 使用规范
|
||
**问题**:多次选择导致状态丢失
|
||
**关键点**:
|
||
- 不同类型数据使用独立的 sessionStorage key
|
||
- 不在 useEffect 中清除 sessionStorage
|
||
- 每次挂载时从 sessionStorage 恢复状态
|
||
- 只在业务操作完成后清除 sessionStorage(提交、保存等)
|
||
|
||
### 13. 导航前草稿保存不及时
|
||
**解决方案**:在导航处理函数中立即保存草稿
|
||
|
||
### 14. 申请页面完整数据流
|
||
|
||
申请页面(如出差申请)需要处理三种场景:
|
||
|
||
**场景1:首次进入(新建申请)**
|
||
- 清除旧的 sessionStorage 数据
|
||
- 加载默认审批人/抄送人
|
||
- 初始化空表单
|
||
|
||
**场景2:从选择页面返回**
|
||
- 从 sessionStorage 恢复草稿数据
|
||
- 从 sessionStorage 恢复人员数据(审批人/抄送人)
|
||
- 日期字符串需要转换为 Date 对象
|
||
|
||
**场景3:提交/保存成功**
|
||
- 清除所有 sessionStorage 数据
|
||
- 导航回列表页
|
||
|
||
**实现要点:**
|
||
- 使用 `location.state?.fromSelection` 标记返回状态
|
||
- 使用独立的 sessionStorage key:`businessTripDraft`, `selectedApprovers`, `selectedCc`
|
||
- 导航前立即保存草稿,不依赖 useEffect
|
||
- 日期对象序列化/反序列化处理
|
||
|
||
**相关文件:**
|
||
- `frontend/src/pages/business-trip/apply.tsx` - 申请页面主逻辑
|
||
- `frontend/src/pages/business-trip/select-approver.tsx` - 审批人选择
|
||
- `frontend/src/pages/business-trip/select-cc.tsx` - 抄送人选择
|
||
|
||
### 16. Ant Design Mobile 全局样式
|
||
**必须导入**:`import 'antd-mobile/es/global'`,否则弹框、遮罩等组件无法正常显示
|
||
|
||
### 17. Selector 组件隐藏对号图标
|
||
**解决方案**:使用 `showCheckMark={false}` 属性
|
||
|
||
### 18. Bootstrap 接口签名
|
||
**方法签名**:`public static function start(?\Workerman\Worker $worker): void`
|
||
|
||
### 19. PSR-4 自动加载
|
||
**目录名必须是 PascalCase**(Controller、Model、Service、Util)
|
||
|
||
### 20. Docker 容器 CMD
|
||
**使用前台运行**:`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`
|
||
|
||
---
|
||
|
||
## 文件修改记录
|
||
|
||
### 前端文件
|
||
- `frontend/src/pages/approval/list.tsx` - 修复状态管理和请求逻辑
|
||
- `frontend/src/components/layout/MobileLayout.tsx` - 隐藏 Header
|
||
- `frontend/src/pages/business-trip/apply.module.css` - 调整 padding-bottom
|
||
- `frontend/src/pages/business-trip/apply.tsx` - 修复 key 重复、草稿保存
|
||
- `frontend/src/pages/approval/detail.tsx` - Dialog 按钮修复、字段标签中文显示
|
||
- `frontend/src/services/api.ts` - 添加请求日志
|
||
- `frontend/src/main.tsx` - 导入 antd-mobile 全局样式
|
||
|
||
### 后端文件
|
||
- `backend/app/service/BusinessTripService.php` - 添加重复校验
|
||
- `backend/app/service/ApprovalService.php` - 数据序列化修复
|
||
- `backend/app/controller/ApprovalController.php` - 响应返回修复
|
||
- `backend/app/model/ApprovalOrder.php` - 模型定义
|
||
- `backend/config/database.php` - 数据库配置
|
||
- `backend/config/route.php` - 路由配置
|
||
|
||
### 数据库
|
||
- `approval_node` 表 - 删除重复节点
|
||
- `approval_order` 表 - 修正 current_nodes 字段
|