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

17 KiB
Raw Permalink Blame History

证照管理子应用 — 本地开发指南

目录


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 安装依赖

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 中的关键配置项:

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} 的容器,负责嵌套子页面

    function ContainerPage(props) {
      return <div>{props.children}</div>;
    }
    export default ContainerPage;
    
  2. 叶子页面(终端页面):实际的业务功能页面,通过 Connect 连接 DVA 数据

    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 菜单数据结构

{
  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

function NewFeature(props) {
  return <div>{props.children}</div>;
}
export default NewFeature;

步骤 3编写列表页面

src/pages/Container/Supervision/PersonnelLicense/NewFeature/List/index.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 中添加:

{
  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

菜单:

{
  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 语法

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 的 dispatchConnect 来调用。declareRequest 会自动生成对应的 dva effect

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

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 高阶组件

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 已知问题


8. 共享组件

业务组件位于 src/components/ 目录下,目前包括:

src/components/
├── PersonInChargeInfo/     # 负责人详情组件
├── PersonInChargeList/     # 负责人列表组件含搜索、分页、CRUD
├── SpecialCertificateInfo/ # 特种证书详情组件
└── SpecialCertificateList/ # 特种证书列表组件

组件通过 @/components/xxx 或别名 ~/components/xxx 引入:

import PersonInChargeList from "~/components/PersonInChargeList";

8.1 组件参数传递

列表组件通过 props 接收业务参数:

<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 组件检测运行环境:

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 菜单配置注意事项

10.3 重命名项目目录后需重新安装依赖

由于 pnpm 使用基于绝对路径的符号链接管理 node_modules,重命名项目根目录后需要:

Remove-Item -Recurse -Force node_modules
pnpm install

10.4 权限控制

页面使用 @cqsjjb/jjb-common-decorator/permissionPermission 装饰器控制访问:

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 等依赖版本,避免多副本冲突:

{
  "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