Files
x-server-mobile/CLAUDE.md
T
root 582f454a5a refactor: 重构后端目录结构和命名规范
- 将后端目录名从大写改为小写 (Controller->controller 等)
- 将命名空间从 App\ 改为 app\ (符合 webman 官方规范)
- 移除不必要的 bootstrap 目录
- 完善出差审批业务功能:
  - 添加删除草稿功能
  - 添加审批操作功能(同意/拒绝)
  - 完善审批详情页,显示审批人和审批记录
- 前端优化:
  - 调整列表页行高
  - 隐藏底部导航栏
  - 仪表盘只显示3个快捷入口

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 18:09:04 +08:00

372 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# X-Server Mobile 项目文档
## 项目概述
这是一个基于飞书的企业内部H5移动工具平台,采用多租户架构,支持审批流程引擎。
**关键特性:**
- 支持5种语言:中文、英文、俄语、西语、法语
- 多租户架构(用户全局唯一,关联多个公司)
- 审批流程引擎(单人审批、会签、或签、通知)
- 飞书免登集成
**技术栈:**
- 前端:React 18.3.1 + TypeScript + Vite + Ant Design Mobile
- 后端:PHP 8.5 + Webman 2.x + MySQL + Redis
- 部署:Docker
## 国际化 (i18n) - 重要!
### 前端
- 配置:`frontend/src/i18n/`
- 翻译文件:`frontend/src/i18n/locales/{zh,en,ru,es,fr}.json`
- 语言切换:
- 首页右上角国旗按钮
- 个人中心 -> 语言设置
- 存储位置:localStorage `i18nextLng`
使用方式:
```tsx
import { useTranslation } from 'react-i18next'
const { t } = useTranslation()
<div>{t('common.confirm')}</div>
```
### 后端
- 翻译文件:`backend/lang/{zh,en,ru,es,fr}.php`
- 工具类:`backend/app/Util/Lang.php`
使用方式:
```php
use App\Util\Lang;
$message = Lang::get('common.confirm');
Lang::setLocale('en');
```
## 目录结构
```
/www/x-server-mobile/
├── frontend/ # React 前端 (运行在 5173)
│ ├── src/
│ │ ├── i18n/ # 国际化配置
│ │ ├── components/ # 组件
│ │ ├── pages/ # 页面
│ │ ├── modules/ # 业务模块 (approval等)
│ │ └── router/ # 路由
│ └── package.json
├── backend/ # PHP 后端 (运行在 8787)
│ ├── app/
│ │ ├── Controller/ # 控制器 (PascalCase命名)
│ │ ├── Model/ # 模型 (PascalCase命名)
│ │ ├── Service/ # 服务层
│ │ ├── Util/ # 工具类
│ │ └── Middleware/ # 中间件
│ ├── config/ # 配置
│ ├── lang/ # 语言文件
│ └── composer.json
├── docker-compose.yml # Docker 编排
├── nginx/ # OpenResty 配置
├── frpc/ # frpc 配置
├── PROJECT.md # 项目详细文档
└── IMPLEMENTATION_PLAN.md # 实现计划
```
## 环境配置
### 端口
- Frontend (Vite): 5173
- Backend (Webman): 8787
- OpenResty: 8080
- MySQL: 3306
- Redis: 6379
### 数据库
- 数据库名:x_server_mobile
- 密码:mysql_xEsdPa
- 网络:1panel-network
### Docker 容器
- 后端容器:x-server-backend
- 使用 Dockerfile:`backend/Dockerfile`
- PHP 版本:8.5
## 关键文件路径
### 前端关键文件
- 入口:`frontend/src/main.tsx`
- 路由:`frontend/src/router/index.tsx`
- 布局:`frontend/src/components/layout/`
- 状态管理:`frontend/src/stores/`
- rem 适配:`frontend/src/utils/rem.ts`
### 后端关键文件
- 入口:`backend/start.php`
- 路由:`backend/config/route.php`
- 数据库配置:`backend/config/database.php`
- Redis 配置:`backend/config/redis.php`
- 容器配置:`backend/config/container.php`
### 重要配置文件
- Docker 编排:`docker-compose.yml`
- OpenResty 配置:`nginx/x-server-api.conf`
- frpc 配置:`frpc/frpc-dev.ini`
## 审批流程引擎
### 节点类型
- `single_approve` - 单人审批
- `and_sign` - 会签(所有人通过)
- `or_sign` - 或签(任一人通过)
- `notify` - 通知节点
### 状态
- pending: 待审批
- approved: 已通过
- rejected: 已拒绝
- cancelled: 已撤销
### 关键文件
- 服务:`backend/app/Service/ApprovalService.php`
- 模型:`backend/app/Model/ApprovalOrder.php`
- 前端模块:`frontend/src/modules/approval/`
## 飞书集成
### 免登接口
- 回调:`POST /api/v1/auth/feishu-login`
### 环境变量
```env
FEISHU_APP_ID=cli_xxxxxxxxxxxxx
FEISHU_APP_SECRET=xxxxxxxxxxxxxxxxxxxx
FEISHU_ENCRYPT_KEY=xxxxxxxxxxxxxxxxxxxx
FEISHU_VERIFICATION_TOKEN=xxxxxxxxxxxxxxxxxxxx
```
## 常用命令
### 前端
```bash
cd /www/x-server-mobile/frontend
npm install
npm run dev
npm run build
```
### 后端
```bash
cd /www/x-server-mobile/backend
composer install
php start.php start
php start.php stop
```
### Docker
```bash
cd /www/x-server-mobile
docker compose up -d
docker compose restart backend
docker logs x-server-backend -f
```
## 反向代理配置(开发环境)
用户已有 frpc 和 OpenResty,配置用于调试飞书登录。
1. OpenResty 监听 8080,代理到 8787
2. frpc 将 8080 暴露到公网
3. 飞书回调地址配置为公网域名
## 已知问题和解决方案
### 页面间状态丢失问题 ⚠️ 重要!
#### 问题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 自动保存作为备份机制(防止用户不导航就离开页面)
### 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 自动加载
- 目录名必须是 PascalCase(Controller、Model、Service、Util)
- 不能用小写(controller、model、service、util)
### React 版本
- 必须使用 React 18.3.1(不能用19)
- @types/react 和 @types/react-dom 也要用 v18
### 数据库连接
- 使用自定义 Db 类 (`backend/app/Util/Db.php`)
- 不使用 `support\Db`(Webman 2.x 没有)
### Bootstrap 接口
- 方法签名:`public static function start(?\Workerman\Worker $worker): void`
- 需要添加 `use Workerman\Worker;`
### Docker 容器重启
- CMD 使用 `php start.php start`(不要用 `-d`)
- 前台运行,避免容器退出
## 语言切换位置
1. **首页** - 右上角显示当前语言国旗,点击切换
2. **个人中心** -> 语言设置
## 当前开发状态
- ✅ 前端基础框架搭建完成
- ✅ 后端 API 框架搭建完成
- ✅ 5语言国际化支持完成
- ✅ Docker 开发环境配置完成
- ✅ 审批流程引擎基础框架完成
- 🚧 出差申请功能开发中
## 下一步工作
1. 完成出差申请前端页面
2. 实现审批列表和详情页
3. 集成飞书免登
4. 实现公司切换功能
5. 完善权限控制