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

10 KiB
Raw Blame History

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

使用方式:

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

使用方式:

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

环境变量

FEISHU_APP_ID=cli_xxxxxxxxxxxxx
FEISHU_APP_SECRET=xxxxxxxxxxxxxxxxxxxx
FEISHU_ENCRYPT_KEY=xxxxxxxxxxxxxxxxxxxx
FEISHU_VERIFICATION_TOKEN=xxxxxxxxxxxxxxxxxxxx

常用命令

前端

cd /www/x-server-mobile/frontend
npm install
npm run dev
npm run build

后端

cd /www/x-server-mobile/backend
composer install
php start.php start
php start.php stop

Docker

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(提交、保存等)

正确实现:

// 选择页面 - 使用独立的 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

解决方案: 在导航处理函数中立即保存草稿

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 属性手动控制弹框显示

// ❌ 错误用法 - 点击无反应
<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 全局样式导入

必须导入全局样式:

import 'antd-mobile/es/global'

否则弹框、遮罩等组件将无法正常显示。

Selector 组件对号图标隐藏

问题: Selector 组件选中时显示对号图标和背景色,想要只保留背景色

解决方案: 使用 showCheckMark 属性

<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. 完善权限控制