diff --git a/docs/dev-guide.md b/docs/dev-guide.md new file mode 100644 index 0000000..ffa9ab6 --- /dev/null +++ b/docs/dev-guide.md @@ -0,0 +1,572 @@ +# 证照管理子应用 — 本地开发指南 + +## 目录 + +- [1. 项目概述](#1-项目概述) +- [2. 环境准备与启动](#2-环境准备与启动) +- [3. 路由系统](#3-路由系统) +- [4. 菜单配置](#4-菜单配置) +- [5. 页面开发流程](#5-页面开发流程) +- [6. API 接口声明](#6-api-接口声明) +- [7. 状态管理(DVA)](#7-状态管理dva) +- [8. 共享组件](#8-共享组件) +- [9. 本地模拟布局](#9-本地模拟布局) +- [10. 已知问题与注意事项](#10-已知问题与注意事项) +- [11. 目录结构参考](#11-目录结构参考) + +--- + +## 1. 项目概述 + +本项目是基于 **qiankun 微前端** 框架的子应用,运行在 **GBS 底座平台** 内。使用以下核心技术栈: + +| 技术 | 说明 | +|------|------| +| React 18 | UI 组件框架 | +| Ant Design 5.x | UI 组件库 | +| @cqsjjb/jjb-dva-runtime | 底座 DVA 状态管理运行时 | +| @cqsjjb/jjb-common-decorator | 装饰器工具(权限、模块加载等) | +| rspack | 构建打包工具 | +| pnpm | 包管理器 | + +项目通过 `jjb.config.js` 中配置的 `appIdentifier: "certificate"` 注册到底座,所有页面路由前缀为 `/certificate/container/`。 + +--- + +## 2. 环境准备与启动 + +### 2.1 安装依赖 + +```bash +pnpm install +``` + +### 2.2 启动命令 + +| 命令 | 说明 | +|------|------| +| `pnpm run serve:development` | **开发环境启动**(推荐) | +| `pnpm run serve:production` | 生产环境配置启动 | +| `pnpm run build:development` | 开发环境构建 | +| `pnpm run build:production` | 生产环境构建 | + +### 2.3 访问地址 + +启动后开发服务器默认监听 `http://127.0.0.1:8081`,访问: + +``` +http://localhost:8081/certificate/container/ +http://localhost:8081/certificate/container/Test +``` + +> **注意**:本地独立启动时,应用处于 qiankun 独立运行模式(`window.__POWERED_BY_QIANKUN__` 为 false), +> 会使用内置的 `SimulatedLayout` 模拟侧边栏和标签页布局。 + +### 2.4 配置说明 + +`jjb.config.js` 中的关键配置项: + +```js +module.exports = { + appIdentifier: "certificate", // 应用唯一标识,对应路由前缀 + server: { + port: "8081", // 开发服务器端口 + host: "127.0.0.1", // 服务地址 + }, + environment: { + development: { + API_HOST: "https://gbs-gateway.qhdsafety.com", // 开发环境接口地址 + }, + }, +}; +``` + +--- + +## 3. 路由系统 + +### 3.1 约定式路由 + +本项目使用 **文件系统约定式路由**,由 `@cqsjjb/jjb-dva-runtime` 框架自动注册。页面对应的路径由目录结构决定: + +``` +src/pages/Container/<目录层级>/index.js → /certificate/container/<目录层级> +``` + +**示例:** + +| 文件路径 | 对应路由 | +|----------|----------| +| `src/pages/Container/Test/index.js` | `/certificate/container/Test` | +| `src/pages/Container/Supervision/index.js` | `/certificate/container/Supervision` | +| `src/pages/Container/Supervision/PersonnelLicense/PersonInCharge/List/index.js` | `/certificate/container/Supervision/PersonnelLicense/PersonInCharge/List` | + +### 3.2 页面组件类型 + +项目中有两类页面组件: + +1. **容器组件**(中间层级):作为 `{props.children}` 的容器,负责嵌套子页面 + ```js + function ContainerPage(props) { + return
{props.children}
; + } + export default ContainerPage; + ``` + +2. **叶子页面**(终端页面):实际的业务功能页面,通过 `Connect` 连接 DVA 数据 + ```js + import { Connect } from "@cqsjjb/jjb-dva-runtime"; + import { NS_USER_CERTIFICATE } from "~/enumerate/namespace"; + + function List(props) { + return
业务内容
; + } + + export default Connect([NS_USER_CERTIFICATE], true)(List); + ``` + +### 3.3 路由匹配规则 + +- 框架自动递归扫描 `src/pages/` 目录,每个 `index.js` 对应一个路由 +- 路由路径是将目录路径转换为小写并用 `/` 连接 +- 带参数的路由(如 `/View/:id`)需在目录中创建对应层级 + +--- + +## 4. 菜单配置 + +侧边栏菜单定义在 `src/pages/Container/Layout/menuConfig.js` 中。 + +### 4.1 菜单数据结构 + +```js +{ + key: "/certificate/container/<实际路由>", // 必须与文件目录对应的路由完全一致 + label: "菜单显示名称", + icon: , // Ant Design 图标组件 + children: [...] // 可选,子菜单项 +} +``` + +### 4.2 当前菜单结构 + +``` +📁 监管端 (Supervision) + ├── 📄 企业证照 (EnterpriseLicense) + │ ├── 企业证照管理 + │ ├── 分公司统计 + │ └── 干系人统计 + └── 📄 人员证照 (PersonnelLicense) + ├── 负责人 + ├── 安全管理员 + ├── 特种设备 + ├── 特种作业人员 + ├── 分公司人员统计 + └── 干系人人员统计 + +📁 分公司端 (BranchCompany) + └── 📄 企业证照 (EnterpriseLicense) + ├── 企业证照管理 + └── 📄 人员证照 (PersonnelLicense) + ├── 负责人 + ├── 安全管理员 + ├── 特种设备 + └── 特种作业人员 + +📁 干系人端 (Stakeholder) + └── 📄 企业证照 (EnterpriseLicense) + ├── 企业证照管理 + └── 📄 人员证照 (PersonnelLicense) + ├── 负责人 + ├── 安全管理员 + ├── 特种设备 + └── 特种作业人员 + +📁 测试页面 (Test) +``` + +### 4.3 菜单配置注意事项 + +> **关键规则**:菜单的 `key` 必须与文件系统的实际目录层级完全一致,否则会出现"页面不存在"。 + +以"干系人端 → 负责人"为例: + +- **文件位置**:`Stakeholder/EnterpriseLicense/PersonnelLicense/PersonInCharge/List/index.js` +- **正确路径**:`/certificate/container/Stakeholder/EnterpriseLicense/PersonnelLicense/PersonInCharge/List` +- **错误路径**(缺少 `EnterpriseLicense` 层级):`/certificate/container/Stakeholder/PersonnelLicense/PersonInCharge/List` + +### 4.4 工具函数 + +`menuConfig.js` 同时导出以下辅助函数: + +| 函数 | 参数 | 返回值 | 说明 | +|------|------|--------|------| +| `flattenMenu(items)` | 菜单树 | 扁平数组 | 将树形菜单拍平 | +| `findMenuPath(path)` | 当前路由 | 菜单项数组 | 从根到叶子节点的面包屑路径 | +| `getPageLabel(path)` | 当前路由 | 字符串 | 获取页面标签名 | +| `getOpenKeys(path)` | 当前路由 | 路径数组 | 默认展开的菜单项 | +| `getSelectedKeys(path)` | 当前路由 | 路径数组 | 默认选中的菜单项 | + +--- + +## 5. 页面开发流程 + +### 5.1 新增业务页面 + +以新增"监管端 → 人员证照 → 新增功能"为例: + +**步骤 1:创建目录结构** + +``` +src/pages/Container/Supervision/PersonnelLicense/NewFeature/ +├── index.js # 容器组件 +├── List/ +│ └── index.js # 列表页 +└── View/ + └── index.js # 详情页 +``` + +**步骤 2:编写容器组件** + +`src/pages/Container/Supervision/PersonnelLicense/NewFeature/index.js`: +```js +function NewFeature(props) { + return
{props.children}
; +} +export default NewFeature; +``` + +**步骤 3:编写列表页面** + +`src/pages/Container/Supervision/PersonnelLicense/NewFeature/List/index.js`: +```js +import { Connect } from "@cqsjjb/jjb-dva-runtime"; +import { NS_USER_CERTIFICATE } from "~/enumerate/namespace"; + +function List(props) { + return ( +
+

新功能列表

+
+ ); +} + +export default Connect([NS_USER_CERTIFICATE], true)(List); +``` + +**步骤 4:添加菜单配置** + +在 `src/pages/Container/Layout/menuConfig.js` 中添加: +```js +{ + key: "/certificate/container/Supervision/PersonnelLicense", + label: "人员证照", + icon: , + children: [ + // ... 已有菜单项 + { + key: "/certificate/container/Supervision/PersonnelLicense/NewFeature/List", + label: "新功能", + icon: , + }, + ], +} +``` + +### 5.2 新增测试页面(独立于业务) + +在 `src/pages/Container/` 下创建目录,并在菜单中添加即可: + +``` +src/pages/Container/MyDebug/ +└── index.js +``` + +菜单: +```js +{ + key: "/certificate/container/MyDebug", + label: "调试页面", + icon: , +} +``` + +### 5.3 验证路径是否正确 + +启动项目后,通过以下方式验证: + +1. 在浏览器直接访问完整 URL +2. 从侧边栏菜单点击导航 +3. 检查开发者工具 Network 面板,404 表示路径不匹配 + +--- + +## 6. API 接口声明 + +### 6.1 接口定义位置 + +API 接口声明在 `src/api/` 目录下,按模块划分: + +``` +src/api/ +├── userCertificate/index.js # 人员证书接口 +├── corpCertificate/index.js # 企业证书接口 +├── courseware/index.js # 课件接口 +└── global/index.js # 全局接口 +``` + +### 6.2 declareRequest 语法 + +```js +import { declareRequest } from "@cqsjjb/jjb-dva-runtime"; + +// 格式:declareRequest(loadingKey, "HttpMethod > 接口路径") +export const listApi = declareRequest( + "namespaceLoading", // loading 状态 key + "Post > @/certificate/userCertificate/list", // POST + 相对路径(带 @) +); + +export const getApi = declareRequest( + "namespaceLoading", + "Get > /certificate/userCertificate/getInfoById/{id}", // GET + 绝对路径 +); +``` + +**语法规则:** + +| 符号 | 说明 | 示例 | +|------|------|------| +| `@/` | 相对路径,使用 `jjb.config.js` 中配置的 `API_HOST` | `Post > @/certificate/xxx/list` | +| `/` | 绝对路径 | `Get > /config/dict-trees/list` | +| `{param}` | 路径参数 | `/certificate/xxx/delete/{id}` | +| `Post >` | POST 请求 | — | +| `Get >` | GET 请求 | — | +| `Put >` | PUT 请求 | — | +| `Delete >` | DELETE 请求 | — | + +### 6.3 在组件中使用 API + +API 通过 DVA 的 `dispatch` 和 `Connect` 来调用。`declareRequest` 会自动生成对应的 dva effect: + +```js +import { Connect } from "@cqsjjb/jjb-dva-runtime"; +import { NS_USER_CERTIFICATE } from "~/enumerate/namespace"; + +function List({ dispatch, userCertificate }) { + const fetchData = () => { + dispatch({ + type: `${NS_USER_CERTIFICATE}/userCertificateList`, // namespace/接口名 + payload: { page: 1, size: 10 }, + }); + }; + + return
{/* ... */}
; +} + +export default Connect([NS_USER_CERTIFICATE], true)(List); +``` + +--- + +## 7. 状态管理(DVA) + +### 7.1 Namespace 定义 + +`src/enumerate/namespace/index.js` 中定义了所有 DVA namespace: + +```js +export const NS_GLOBAL = defineNamespace("global"); +export const NS_PERSNONEL_CERTFICATE = defineNamespace("personnelCertificate"); +export const NS_CORP_CERTIFICATE = defineNamespace("corpCertificate"); +export const NS_USER_CERTIFICATE = defineNamespace("userCertificate"); +export const NS_COURSEWARE = defineNamespace("courseware"); +``` + +### 7.2 Connect 高阶组件 + +```js +Connect([NS_USER_CERTIFICATE], true)(MyComponent) +``` + +- 第一个参数:需要连接的 namespace 数组 +- 第二个参数:是否在组件挂载时自动加载数据(`true` 表示自动触发 `load` effect) + +### 7.3 数据流向 + +``` +dispatch(action) → DVA Effect → declareRequest → HTTP API → reducer → state 更新 → 组件重渲染 +``` + +### 7.4 本地开发时的限制 + +本地独立运行时(非底座环境),DVA 状态管理正常可用,但 `declareRequest` 发起的 HTTP 请求会因缺少认证 Token 而返回 **401**。详见 [§10 已知问题](#10-已知问题与注意事项)。 + +--- + +## 8. 共享组件 + +业务组件位于 `src/components/` 目录下,目前包括: + +``` +src/components/ +├── PersonInChargeInfo/ # 负责人详情组件 +├── PersonInChargeList/ # 负责人列表组件(含搜索、分页、CRUD) +├── SpecialCertificateInfo/ # 特种证书详情组件 +└── SpecialCertificateList/ # 特种证书列表组件 +``` + +组件通过 `@/components/xxx` 或别名 `~/components/xxx` 引入: + +```js +import PersonInChargeList from "~/components/PersonInChargeList"; +``` + +### 8.1 组件参数传递 + +列表组件通过 `props` 接收业务参数: + +```js + +``` + +--- + +## 9. 本地模拟布局 + +### 9.1 工作原理 + +`src/pages/Container/index.js` 中的 `AppMiddle` 组件检测运行环境: + +```js +function AppMiddle(props) { + const content = process.env.NODE_ENV === "development" && !window.__POWERED_BY_QIANKUN__ + ? {props.children} + : props.children; + // ... +} +``` + +- **本地开发**(`development` + 非 qiankun):使用 `SimulatedLayout` 包裹内容,提供模拟的侧边栏、面包屑和标签页 +- **底座环境**:由底座平台提供真实的布局 + +### 9.2 SimulatedLayout 功能 + +`src/pages/Container/Layout/index.jsx` 提供: + +| 功能 | 说明 | +|------|------| +| 侧边栏 | 使用 `menuConfig.js` 的菜单数据渲染,支持折叠 | +| 面包屑 | 根据当前路径自动计算导航路径 | +| 多标签页 | 支持打开多个页面标签,右键关闭操作 | +| 标签持久化 | 使用 sessionStorage 保存标签状态 | +| 主题适配 | 通过 `window.base?.themeConfig` 支持底座主题切换 | + +--- + +## 10. 已知问题与注意事项 + +### 10.1 API 401 错误 + +**现象**:本地启动后,点击侧边栏菜单(除 Test 页面外),控制台报 401 错误。 + +**原因**:本项目是 qiankun 微前端子应用,API 请求需要的认证 Token 由 GBS 底座注入。本地独立运行时没有底座,也就没有 Token,所有 API 调用被拒绝。 + +**处理方式**: +- **纯前端调试**:使用 Test 页面(`/certificate/container/Test`),该页面不发起任何 API 请求 +- **完整功能调试**:在 GBS 底座环境中打开应用 + +### 10.2 路由路径与菜单 key 必须匹配 + +菜单的 `key` 值必须和文件目录层级完全一致。错误示例见 [§4.3 菜单配置注意事项](#43-菜单配置注意事项)。 + +### 10.3 重命名项目目录后需重新安装依赖 + +由于 pnpm 使用基于绝对路径的符号链接管理 `node_modules`,重命名项目根目录后需要: + +```bash +Remove-Item -Recurse -Force node_modules +pnpm install +``` + +### 10.4 权限控制 + +页面使用 `@cqsjjb/jjb-common-decorator/permission` 的 `Permission` 装饰器控制访问: + +```js +import { Permission } from "@cqsjjb/jjb-common-decorator/permission"; + +export default Connect([NS_USER_CERTIFICATE], true)(Permission(List)); +``` + +权限码由底座平台管理,本地开发时权限校验可能不生效。 + +### 10.5 pnpm overrides + +`package.json` 中通过 `pnpm.overrides` 强制统一 React 等依赖版本,避免多副本冲突: + +```json +{ + "pnpm": { + "overrides": { + "react": "$react", + "react-dom": "$react-dom" + } + } +} +``` + +--- + +## 11. 目录结构参考 + +``` +safety-eval-service-frontend/ +├── public/ +│ └── index.html # HTML 模板 +├── src/ +│ ├── main.js # 应用入口(setup + 生命周期导出) +│ ├── api/ # API 接口声明 +│ │ ├── global/ +│ │ ├── userCertificate/ +│ │ ├── corpCertificate/ +│ │ └── courseware/ +│ ├── components/ # 共享业务组件 +│ │ ├── PersonInChargeList/ +│ │ ├── PersonInChargeInfo/ +│ │ ├── SpecialCertificateList/ +│ │ └── SpecialCertificateInfo/ +│ ├── enumerate/ # 枚举/常量定义 +│ │ ├── namespace/ # DVA namespace +│ │ ├── context/ # React Context +│ │ └── constant/ # 常量 +│ ├── pages/ +│ │ └── Container/ +│ │ ├── index.js # 容器入口(Antd 主题 + 布局切换) +│ │ ├── Entry/ # 底座入口页 +│ │ ├── Layout/ # 本地模拟布局 +│ │ │ ├── index.jsx # SimulatedLayout 组件 +│ │ │ └── menuConfig.js # ★ 菜单配置 +│ │ ├── Test/ # 测试页面(无 API 依赖) +│ │ ├── Supervision/ # 监管端 +│ │ │ ├── EnterpriseLicense/ +│ │ │ └── PersonnelLicense/ +│ │ ├── BranchCompany/ # 分公司端 +│ │ │ └── EnterpriseLicense/ +│ │ └── Stakeholder/ # 干系人端 +│ │ └── EnterpriseLicense/ +│ ├── assets/ # 静态资源 +│ └── utils/ # 工具函数 +├── jjb.config.js # 项目配置(端口、API_HOST、主题等) +├── package.json +├── pnpm-lock.yaml +└── pnpm-workspace.yaml +``` diff --git a/docs/startup.md b/docs/startup.md index fbf7058..6fb7745 100644 --- a/docs/startup.md +++ b/docs/startup.md @@ -1,38 +1,47 @@ -# 证照管理子应用 启动指南 +# 证照管理子应用 — 快速启动 -## 启动命令 +## 环境要求 + +- Node.js >= 18 +- pnpm >= 8 + +## 启动 ```bash +# 安装依赖(首次或重命名项目目录后) +pnpm install + +# 启动开发服务器 pnpm run serve:development ``` -## 其他命令 +## 访问 + +| 地址 | 说明 | +|------|------| +| `http://localhost:8081/certificate/container/` | 应用首页 | +| `http://localhost:8081/certificate/container/Test` | 测试页面(无 API 依赖,纯前端调试可用) | + +## 常用命令 | 命令 | 说明 | |------|------| -| `pnpm run serve` | 默认启动(不指定环境) | | `pnpm run serve:development` | 开发环境启动(推荐) | | `pnpm run serve:production` | 生产环境配置启动 | | `pnpm run build:development` | 开发环境构建 | | `pnpm run build:production` | 生产环境构建 | -## 访问地址 - -启动后访问:`http://127.0.0.1:8081/certificate/container/` - -启动后访问:`http://127.0.0.1:8081/certificate/container/Test` - ## 技术栈 -- React 18 + ReactDOM 18 -- Ant Design 5.x +- React 18 + Ant Design 5.x - @cqsjjb/jjb-dva-runtime(GBS 底座 DVA 运行时) -- rspack 构建 -- pnpm 包管理器 +- rspack 构建 / pnpm 包管理 - qiankun 微前端子应用 -## 注意事项 +## 重要提示 -- 本项目为 GBS 底座微前端子应用,独立启动时无侧边栏、Header 等底座布局 -- 需同时启动 GBS 底座主应用才能看到完整页面效果 -- 使用 pnpm overrides 强制统一 React 版本,避免多副本冲突 +- 本项目为 GBS 底座微前端子应用,独立启动时有内置模拟布局 +- 本地独立运行时 API 调用会返回 401(缺少底座注入的认证 Token) +- 仅 Test 页面可在本地无底座环境下正常访问 +- 重命名项目目录后需 `pnpm install` 重建 node_modules 符号链接 +- 完整开发指南请参考 [dev-guide.md](./dev-guide.md) diff --git a/src/pages/Container/Layout/menuConfig.js b/src/pages/Container/Layout/menuConfig.js index 2bfdea7..c571867 100644 --- a/src/pages/Container/Layout/menuConfig.js +++ b/src/pages/Container/Layout/menuConfig.js @@ -20,6 +20,11 @@ const menuItems = [ label: "监管端", icon: , children: [ + { + key: "/certificate/container/Supervision/test2", + label: "Test2 测试", + icon: , + }, { key: "/certificate/container/Supervision/EnterpriseLicense", label: "企业证照", @@ -92,27 +97,27 @@ const menuItems = [ icon: , }, { - key: "/certificate/container/BranchCompany/PersonnelLicense", + key: "/certificate/container/BranchCompany/EnterpriseLicense/PersonnelLicense", label: "人员证照", icon: , children: [ { - key: "/certificate/container/BranchCompany/PersonnelLicense/PersonInCharge/List", + key: "/certificate/container/BranchCompany/EnterpriseLicense/PersonnelLicense/PersonInCharge/List", label: "负责人", icon: , }, { - key: "/certificate/container/BranchCompany/PersonnelLicense/SecurityAdmini/List", + key: "/certificate/container/BranchCompany/EnterpriseLicense/PersonnelLicense/SecurityAdmini/List", label: "安全管理员", icon: , }, { - key: "/certificate/container/BranchCompany/PersonnelLicense/SpecialDevice/List", + key: "/certificate/container/BranchCompany/EnterpriseLicense/PersonnelLicense/SpecialDevice/List", label: "特种设备", icon: , }, { - key: "/certificate/container/BranchCompany/PersonnelLicense/SpecialPersonnel/List", + key: "/certificate/container/BranchCompany/EnterpriseLicense/PersonnelLicense/SpecialPersonnel/List", label: "特种作业人员", icon: , }, @@ -131,27 +136,27 @@ const menuItems = [ icon: , }, { - key: "/certificate/container/Stakeholder/PersonnelLicense", + key: "/certificate/container/Stakeholder/EnterpriseLicense/PersonnelLicense", label: "人员证照", icon: , children: [ { - key: "/certificate/container/Stakeholder/PersonnelLicense/PersonInCharge/List", + key: "/certificate/container/Stakeholder/EnterpriseLicense/PersonnelLicense/PersonInCharge/List", label: "负责人", icon: , }, { - key: "/certificate/container/Stakeholder/PersonnelLicense/SecurityAdmini/List", + key: "/certificate/container/Stakeholder/EnterpriseLicense/PersonnelLicense/SecurityAdmini/List", label: "安全管理员", icon: , }, { - key: "/certificate/container/Stakeholder/PersonnelLicense/SpecialDevice/List", + key: "/certificate/container/Stakeholder/EnterpriseLicense/PersonnelLicense/SpecialDevice/List", label: "特种设备", icon: , }, { - key: "/certificate/container/Stakeholder/PersonnelLicense/SpecialPersonnel/List", + key: "/certificate/container/Stakeholder/EnterpriseLicense/PersonnelLicense/SpecialPersonnel/List", label: "特种作业人员", icon: , }, diff --git a/src/pages/Container/Supervision/test2/index.js b/src/pages/Container/Supervision/test2/index.js new file mode 100644 index 0000000..78291bb --- /dev/null +++ b/src/pages/Container/Supervision/test2/index.js @@ -0,0 +1,41 @@ +import React from "react"; +import { Button, Card, Result, Space, Typography } from "antd"; + +const { Title, Paragraph, Text } = Typography; + +function Test2Page() { + const [count, setCount] = React.useState(0); + + return ( +
+ + +
+ 计数器 + + {count} + + + + + + +
+
+ + + + 当前路由:/certificate/container/Supervision/test2 + React 版本:{React.version} + 是否底座环境:{window.__IN_BASE__ ? "是" : "否(独立运行)"} + + +
+ ); +} + +export default Test2Page;