docs: 整理项目问题文档,修复审批列表数据混乱
- 新增 PROJECT_ISSUES.md 统一管理所有问题和解决方案 - 精简 CLAUDE.md,移除重复的问题记录部分 - 修复审批列表数据混乱问题(React 18 StrictMode + 状态管理) - 修复首次加载不显示数据问题(requestIdRef 清理时机) - 修复页码初始值错误(hasMore 默认值) - 修复后端响应序列化问题(Eloquent 模型转数组) - 添加审批人和抄送人重复校验 - 修复双 header 问题 - 调整申请页面 padding-bottom Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -197,203 +197,20 @@ docker logs x-server-backend -f
|
||||
2. frpc 将 8080 暴露到公网
|
||||
3. 飞书回调地址配置为公网域名
|
||||
|
||||
## 已知问题和解决方案
|
||||
### Webman 框架要点
|
||||
|
||||
### 页面间状态丢失问题 ⚠️ 重要!
|
||||
|
||||
#### 问题1:navigate(-1) 不传递 state
|
||||
**现象:** 使用 `navigate(-1, { state })` 返回上一页时,`location.state` 为 `null`
|
||||
|
||||
**原因:** React Router 在使用 `navigate(-1)` 时,`state` 参数不可靠
|
||||
|
||||
**解决方案:** 使用 sessionStorage 传递数据
|
||||
|
||||
#### 问题2:多次选择导致状态丢失
|
||||
**现象:**
|
||||
- 选择审批人后,再选择抄送人,审批人状态丢失
|
||||
- 先选择抄送人,再选择审批人,抄送人状态丢失
|
||||
|
||||
**原因:**
|
||||
- 每次导航时,目标页面组件会重新挂载
|
||||
- 重新挂载时,所有 useState 都会重置为初始值
|
||||
- sessionStorage 在第一次读取后被清除,第二次挂载时无数据可恢复
|
||||
|
||||
**解决方案:**
|
||||
1. **使用独立的 sessionStorage key**(不要共用一个 key)
|
||||
2. **不在 useEffect 中立即清除 sessionStorage**
|
||||
3. **每次挂载时从 sessionStorage 恢复状态**
|
||||
4. **只在业务操作完成后才清除 sessionStorage**(提交、保存等)
|
||||
|
||||
**正确实现:**
|
||||
|
||||
```tsx
|
||||
// 选择页面 - 使用独立的 key
|
||||
const handleConfirm = () => {
|
||||
if (type === 'approver') {
|
||||
sessionStorage.setItem('selectedApprovers', JSON.stringify(selectedUsers))
|
||||
} else if (type === 'cc') {
|
||||
sessionStorage.setItem('selectedCc', JSON.stringify(selectedUsers))
|
||||
}
|
||||
navigate(-1)
|
||||
}
|
||||
|
||||
// 申请页面 - 每次挂载时恢复状态
|
||||
useEffect(() => {
|
||||
const approversData = sessionStorage.getItem('selectedApprovers')
|
||||
const ccData = sessionStorage.getItem('selectedCc')
|
||||
|
||||
if (approversData) {
|
||||
setApprovers(JSON.parse(approversData))
|
||||
}
|
||||
if (ccData) {
|
||||
setCcUsers(JSON.parse(ccData))
|
||||
}
|
||||
}, [])
|
||||
|
||||
// 提交/保存成功后清除
|
||||
const handleSubmit = async () => {
|
||||
// ... 提交逻辑 ...
|
||||
sessionStorage.removeItem('selectedApprovers')
|
||||
sessionStorage.removeItem('selectedCc')
|
||||
navigate('/business-trip')
|
||||
}
|
||||
```
|
||||
|
||||
**关键点:**
|
||||
- ✅ 不同类型数据使用独立的 sessionStorage key
|
||||
- ✅ 不在 useEffect 中清除 sessionStorage
|
||||
- ✅ 每次挂载都恢复状态,确保组件重新挂载时状态不丢失
|
||||
- ✅ 只在业务操作完成后清除 sessionStorage
|
||||
|
||||
**相关文件:**
|
||||
- `frontend/src/pages/business-trip/select-approver.tsx`
|
||||
- `frontend/src/pages/business-trip/select-cc.tsx`
|
||||
- `frontend/src/pages/business-trip/apply.tsx`
|
||||
|
||||
#### 问题3:导航时草稿保存不及时
|
||||
**现象:** 用户输入内容后快速点击选择人员,返回时输入的内容丢失
|
||||
|
||||
**原因:** React 的 useEffect 是异步执行的,用户快速导航时,useEffect 可能还没来得及保存到 sessionStorage
|
||||
|
||||
**解决方案:** 在导航处理函数中立即保存草稿
|
||||
|
||||
```tsx
|
||||
const handleSelectApprovers = () => {
|
||||
// 导航前立即保存草稿
|
||||
const draftData = { reason, itineraries }
|
||||
sessionStorage.setItem('businessTripDraft', JSON.stringify(draftData))
|
||||
navigate('/business-trip/apply/select-approver', { state: { selected: approvers, type: 'approver' } })
|
||||
}
|
||||
|
||||
const handleSelectCc = () => {
|
||||
// 导航前立即保存草稿
|
||||
const draftData = { reason, itineraries }
|
||||
sessionStorage.setItem('businessTripDraft', JSON.stringify(draftData))
|
||||
navigate('/business-trip/apply/select-cc', { state: { selected: ccUsers, type: 'cc' } })
|
||||
}
|
||||
```
|
||||
|
||||
**关键点:**
|
||||
- ✅ 导航前同步保存草稿数据,不依赖 useEffect
|
||||
- ✅ 保留 useEffect 自动保存作为备份机制(防止用户不导航就离开页面)
|
||||
|
||||
### 申请页面完整数据流
|
||||
|
||||
申请页面(如出差申请)需要处理三种场景:
|
||||
|
||||
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` - 抄送人选择
|
||||
|
||||
### Ant Design Mobile DatePicker 点击无反应
|
||||
**问题:** DatePicker 组件使用 children render prop 方式时,点击无法弹出选择框
|
||||
|
||||
**原因:** antd-mobile 5.x 的 DatePicker children render prop 点击事件绑定存在问题
|
||||
|
||||
**解决方案:** 使用 `visible` 属性手动控制弹框显示
|
||||
|
||||
```tsx
|
||||
// ❌ 错误用法 - 点击无反应
|
||||
<DatePicker value={value} onConfirm={setValue}>
|
||||
{(value) => <button>{value ? '已选择' : '请选择'}</button>}
|
||||
</DatePicker>
|
||||
|
||||
// ✅ 正确用法 - 使用 visible 属性
|
||||
const [visible, setVisible] = useState(false)
|
||||
|
||||
<div onClick={() => setVisible(true)}>
|
||||
{value ? '已选择' : '请选择'}
|
||||
</div>
|
||||
<DatePicker
|
||||
visible={visible}
|
||||
onConfirm={(val) => {
|
||||
setValue(val)
|
||||
setVisible(false)
|
||||
}}
|
||||
onClose={() => setVisible(false)}
|
||||
onCancel={() => setVisible(false)}
|
||||
/>
|
||||
```
|
||||
|
||||
**相关文件:**
|
||||
- `frontend/src/main.tsx` - 需要导入 `import 'antd-mobile/es/global'`
|
||||
- `frontend/src/pages/business-trip/apply.tsx` - 正确使用示例
|
||||
|
||||
### Ant Design Mobile 全局样式导入
|
||||
**必须导入全局样式:**
|
||||
```tsx
|
||||
import 'antd-mobile/es/global'
|
||||
```
|
||||
|
||||
否则弹框、遮罩等组件将无法正常显示。
|
||||
|
||||
### Selector 组件对号图标隐藏
|
||||
**问题:** Selector 组件选中时显示对号图标和背景色,想要只保留背景色
|
||||
|
||||
**解决方案:** 使用 `showCheckMark` 属性
|
||||
```tsx
|
||||
<Selector showCheckMark={false} options={options} />
|
||||
```
|
||||
|
||||
### PSR-4 自动加载
|
||||
**PSR-4 自动加载**
|
||||
- 目录名必须是 PascalCase(Controller、Model、Service、Util)
|
||||
- 不能用小写(controller、model、service、util)
|
||||
|
||||
### React 版本
|
||||
- 必须使用 React 18.3.1(不能用19)
|
||||
- @types/react 和 @types/react-dom 也要用 v18
|
||||
|
||||
### 数据库连接
|
||||
**数据库连接**
|
||||
- 使用 Webman 官方 `webman/database` 包
|
||||
- 使用 `support\Db` facade 进行数据库操作
|
||||
- 使用 `Db::connection()` 获取数据库连接进行事务处理
|
||||
|
||||
### Bootstrap 接口
|
||||
**Bootstrap 接口**
|
||||
- 方法签名:`public static function start(?\Workerman\Worker $worker): void`
|
||||
- 需要添加 `use Workerman\Worker;`
|
||||
|
||||
### Webman 官方依赖包
|
||||
**官方依赖包**
|
||||
```bash
|
||||
composer require workerman/webman:^2.0
|
||||
composer require webman/database:^2.0
|
||||
@@ -403,20 +220,24 @@ composer require illuminate/events:^11.0
|
||||
composer require symfony/var-dumper:^7.0
|
||||
```
|
||||
|
||||
### User Store 状态管理
|
||||
- 使用 Zustand with persistence
|
||||
### 前端框架要点
|
||||
|
||||
**React 版本**
|
||||
- 必须使用 React 18.3.1(不能用19)
|
||||
- @types/react 和 @types/react-dom 也要用 v18
|
||||
|
||||
**Ant Design Mobile**
|
||||
- 必须导入全局样式:`import 'antd-mobile/es/global'`
|
||||
- DatePicker 使用 `visible` 属性控制弹框显示
|
||||
|
||||
**Zustand 状态管理**
|
||||
- 登录时必须调用 `login(user, token)` 方法设置状态
|
||||
- Token 存储在 localStorage,key 为 `token`
|
||||
- 登录页面必须正确初始化 Zustand store,否则后续页面获取不到用户信息
|
||||
|
||||
### 类型定义注意
|
||||
**类型定义**
|
||||
- `User.id` 是 `number` 类型(后端返回整型)
|
||||
- `User.company_id` 是 `number` 类型(数据库字段名)
|
||||
|
||||
### Docker 容器重启
|
||||
- CMD 使用 `php start.php start`(不要用 `-d`)
|
||||
- 前台运行,避免容器退出
|
||||
|
||||
## 语言切换位置
|
||||
|
||||
1. **首页** - 右上角显示当前语言国旗,点击切换
|
||||
@@ -442,3 +263,39 @@ composer require symfony/var-dumper:^7.0
|
||||
2. 实现公司切换功能
|
||||
3. 完善权限控制
|
||||
4. 添加更多业务类型(费用报销、请假申请、采购申请)
|
||||
|
||||
---
|
||||
|
||||
## 快速参考
|
||||
|
||||
### 查看问题汇总
|
||||
所有历史问题已整理在项目根目录的 `PROJECT_ISSUES.md` 文件中。
|
||||
|
||||
### 让 CLAUDE 加载问题文档
|
||||
```
|
||||
请参考 /www/x-server-mobile/PROJECT_ISSUES.md 中的问题汇总
|
||||
```
|
||||
|
||||
### 调试后端
|
||||
```bash
|
||||
# 查看日志
|
||||
docker logs x-server-backend -f --tail 50
|
||||
|
||||
# 重启后端
|
||||
docker compose restart backend
|
||||
|
||||
# 复制文件到容器
|
||||
docker cp host_file x-server-backend:/app/app/service/xxx.php
|
||||
|
||||
# 重载 workers(不重启整个服务)
|
||||
docker exec x-server-backend php /app/webman reload
|
||||
```
|
||||
|
||||
### 数据库操作
|
||||
```bash
|
||||
# 进入 MySQL 容器
|
||||
docker exec -it 1Panel-mysql-409s mysql -uroot -pmysql_xEsdPa xserver
|
||||
|
||||
# 运行测试数据脚本
|
||||
docker exec x-server-backend php /app/test_data.php
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user