safety-eval-service-frontend/docs/dev-guide.md

573 lines
17 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 证照管理子应用 — 本地开发指南
## 目录
- [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 <div>{props.children}</div>;
}
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 <div>业务内容</div>;
}
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: <SomeIcon />, // 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 <div>{props.children}</div>;
}
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 (
<div>
<h1>新功能列表</h1>
</div>
);
}
export default Connect([NS_USER_CERTIFICATE], true)(List);
```
**步骤 4添加菜单配置**
`src/pages/Container/Layout/menuConfig.js` 中添加:
```js
{
key: "/certificate/container/Supervision/PersonnelLicense",
label: "人员证照",
icon: <TeamOutlined />,
children: [
// ... 已有菜单项
{
key: "/certificate/container/Supervision/PersonnelLicense/NewFeature/List",
label: "新功能",
icon: <SomeIcon />,
},
],
}
```
### 5.2 新增测试页面(独立于业务)
`src/pages/Container/` 下创建目录,并在菜单中添加即可:
```
src/pages/Container/MyDebug/
└── index.js
```
菜单:
```js
{
key: "/certificate/container/MyDebug",
label: "调试页面",
icon: <ExperimentOutlined />,
}
```
### 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 <div>{/* ... */}</div>;
}
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
<PersonInChargeList
props={props} // dva props 透传
certificatePhotoType={161} // 证照照片类型
personnelType="zyfzr" // 人员类型标识
permissionAdd="gfd-xxx-add" // 新增权限码
permissionEdit="gfd-xxx-edit" // 编辑权限码
permissionView="gfd-xxx-info" // 查看权限码
permissionDel="gfd-xxx-del" // 删除权限码
dictionaryType="zyfzrgwmc0000" // 字典类型
/>
```
---
## 9. 本地模拟布局
### 9.1 工作原理
`src/pages/Container/index.js` 中的 `AppMiddle` 组件检测运行环境:
```js
function AppMiddle(props) {
const content = process.env.NODE_ENV === "development" && !window.__POWERED_BY_QIANKUN__
? <SimulatedLayout>{props.children}</SimulatedLayout>
: 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
```