# 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()
{t('common.confirm')}
``` ### 后端 - 翻译文件:`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 // ❌ 错误用法 - 点击无反应 {(value) => } // ✅ 正确用法 - 使用 visible 属性 const [visible, setVisible] = useState(false)
setVisible(true)}> {value ? '已选择' : '请选择'}
{ 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 ``` ### 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. 完善权限控制