Files
workspace/code/g3soft-erp/docs/前端架构设计.md
T
2026-10-10 17:33:59 +08:00

11 KiB
Raw Blame History

g3soft-erp 前端架构设计(v0.1)

状态:前端骨架已落地并验证通过。后端与元数据引擎未开始。

1. 定位与范围

本文档解决:g3soft-erp 前端的项目骨架怎么搭。

不在范围内(后续单独立项):

项 状态
元数据驱动引擎(通用查询 / 保存 / 权限) 未设计
后端设计与 Java 分包 未开始
stk-table-vue(虚拟滚动表格) 待定
LogicFlow(审批流设计器) 待定
登录 / 权限 / 用户模块 未开始(骨架已留位)

2. 技术栈(已定)

项 选型
框架 Vue 3.5 + Vite 8
语言 TypeScript(strict)
路由 vue-router 5,静态路由 + 常量菜单
状态 Pinia 4 + pinia-plugin-persistedstate
样式 Sass + @g3soft/tokens
UI 库 @g3soft/ui(本地 link 开发,私库版本发版)
HTTP axios(统一拦截器)
图标 @lucide/vue
规范化 oxlint + oxfmt
测试 Vitest + happy-dom
日期 dayjs

后端(未开始,已定):Spring Boot 4.1 + Java 21 + spring-boot-starter-jdbc + Druid + SQL Server + JJWT + Apache Fesod。无 ORM,纯 SQL 驱动。

3. 目录结构

仓库层

g3soft-erp/
├── pnpm-workspace.yaml      # 仓库唯一配置文件:overrides + allowBuilds
├── web/                     # 前端应用(前端唯一的 package.json 在此)
├── server/                  # 后端占位(Maven,未开始)
├── docs/                    # 本文档
└── README.md

根目录刻意不放 package.json。 理由:

  • 前端可交付物只有 web/ 一个包,那个 package.json 就是 web/package.json,身份唯一。
  • 后端 server/ 由 Maven 管理,不受 Node 管;一个成员的 workspace 套 monorepo 的壳只是形式主义。
  • 代价:命令要 cd web 再跑,根目录敲不了 pnpm dev。这是有意的取舍。

pnpm-workspace.yaml 仍需保留,因为它承担两件必须在 workspace 根生效的事:overrides(把 @g3soft/ui 指向 g3soft-libs)和 allowBuilds(pnpm 11 构建脚本白名单)。已验证:根目录没有 package.json 时 overrides 依然生效。

前端层

web/src/
├── main.ts                  # 入口:Pinia / Router / G3UI.config / 样式
├── App.vue
├── router/
│   ├── index.ts             # createRouter + 守卫 + NProgress
│   └── modules/             # 每业务域一个路由文件
│       ├── ocean.ts  air.ts  container.ts  finance.ts  crm.ts  system.ts
├── layouts/
│   ├── DefaultLayout.vue    # 主布局:侧栏 + 顶栏 + 页签 + 内容区
│   ├── LayoutSidebar.vue    # 侧栏(含折叠)
│   ├── LayoutHeader.vue     # 顶栏(含面包屑标题、用户区)
│   ├── LayoutTabbar.vue     # 页签栏
│   ├── useSidebarMenu.ts    # 菜单逻辑(组合式)
│   └── icons.ts             # 菜单图标注册表
├── views/                   # 页面,与 router/modules 一一对应
│   ├── ocean/  air/  container/  finance/  crm/  system/
│   └── error/NotFound.vue
├── components/              # 跨域共享组件(初始只有 DomainPlaceholder)
├── stores/
│   ├── pinia.ts             # createPinia + 持久化插件
│   ├── app.ts               # 壳层状态:侧栏、页签
│   ├── user.ts              # 用户与会话
│   └── index.ts             # 统一导出
├── services/
│   ├── request.ts           # axios 实例 + 拦截器 + http<T>()
│   └── api/                 # 按域划分的接口封装(暂空)
├── composables/             # 跨域组合式函数(暂空)
├── types/                   # env.d.ts、menu.ts
├── styles/
│   ├── index.scss           # 入口:tokens → ui → global
│   ├── variables.scss       # 转发 tokens 的 scss API(不 emit CSS)
│   └── global.scss          # reset + 全局
├── constants/
│   ├── menu.ts              # 菜单常量(唯一数据源)
│   └── common.ts            # 存储前缀、标题、API_BASE
└── utils/                   # 全局工具(暂空)

关键约定

  1. components/、composables/、utils/ 刻意留空 遵循「为真实出现的第二个使用方而抽象」:只有一个业务域用的东西放该域内(如 views/ocean/utils.ts),出现第二个真实调用方才上提。不要因为"以后可能复用"提前抽一层。

  2. 业务域内部自包含

    views/ocean/
    ├── Index.vue         # 页面
    ├── components/       # 只属于海运的组件
    └── utils.ts          # 只属于海运的逻辑
    
  3. 样式:组件库用 g3- 前缀 + BEM;业务侧页面样式用 scoped;颜色尺寸一律走 --g3-* 变量,不自定义色值。

  4. 布局壳的样式不加 scoped(LayoutSidebar.vue 等),因为需要被子元素感知尺寸;业务组件必须加 scoped。

  5. 不兼容历史持久化数据 localStorage 里存过的偏好设置(g3erp:preferences)、页签(g3erp:app)等,不做版本迁移、不做字段兜底。 删字段或改字段名时直接删,不要写 if (oldField) 这类兼容分支——store.ts 的 mergePreferences 只遍历 defaultPreferences 的键,旧数据里多出来的键自然被忽略。 理由:开发期数据结构还在变,为一次性数据写迁移是纯负债。用户清一次 localStorage 即可。

4. 菜单与路由:单一数据源

这是骨架的核心设计。

constants/menu.ts 是菜单与路由的唯一数据源,新增业务域只改这一处:

constants/menu.ts
      │
      ├──→ 侧栏菜单渲染(useSidebarMenu)
      ├──→ 首页重定向目标(router/index.ts 的 firstMenuPath)
      └──→ 页签标题来源(DefaultLayout 的 watch,取 route.meta.title)

新增一个业务域的完整步骤:

  1. constants/menu.ts 加一项(key / title / path / icon)
  2. layouts/icons.ts 注册图标(若用了新图标)
  3. router/modules/<域>.ts 新增路由文件,在 router/index.ts 里展开
  4. views/<域>/Index.vue 建页面
  5. 在 DefaultLayout 的 children 里已自动生效

页签的标题取自 route.meta.title,所以路由里写了 title 的页面会自动出现在页签栏;写 meta: { affix: true } 则不可关闭。

未来接入后端动态菜单时:constants/menu.ts 退化为「无权限时的兜底」或直接删除,useSidebarMenu 改为从 store 读。当前不做动态路由——引擎未设计,现在做等于写死一套假契约。

5. 状态管理

初始只有两个 store,业务域 store 用到再建。

store 职责 持久化
app.ts 侧栏折叠、页签 只持久化 sidebarCollapsed(pick 显式声明)
user.ts token、用户信息 token 存 localStorage

约定(沿用 fms 已验证的规则):

  1. 统一 Setup Store:defineStore('name', () => {}, options)
  2. 一个 store 一个领域,避免重复状态源
  3. persist 必须写 pick;加载态、页签态不持久化

6. 样式链路

styles/index.scss
  ├─ @use '@g3soft/tokens'        → :root 的 --g3-* 变量(全局只引一次)
  ├─ @use '@g3soft/ui/style.css'  → 组件库样式
  └─ @use './global'              → reset + 全局

局部样式需要变量/mixin 时,用 styles/variables.scss(转发 @g3soft/tokens/scss,不 emit CSS)。

⚠️ 不要在每个 .vue 里 @use '@g3soft/tokens'——那会让 :root 在每个组件样式里重复输出。只有 styles/index.scss 引一次。

7. 验证结果

下列验证在「拍平为 web/ 且删除根 package.json」之后重跑过,全部通过:

项 结果
pnpm typecheck(vue-tsc strict) ✅ 0 错误
pnpm lint(oxlint --deny-warnings) ✅ 0 warnings / 0 errors
pnpm fmt:check(oxfmt) ✅ 全部符合
pnpm build ✅ 3946 modules,路由级分包生效
pnpm dev ✅ localhost:5090,/ 与 /src/* 均 200
@g3soft/ui link 解析 ✅ Junction → g3soft-libs/packages/ui,dist/index.js 真实存在
@g3soft/tokens link 解析 ✅ Junction → g3soft-libs/packages/tokens
Vue 去重 ✅ 仅一份 vue@3.5.43
根目录无 package.json 时 overrides 是否生效 ✅ 生效(install 无警告,路径解析正确)
G3UI / zhCN 具名导出 ✅ 存在于 dist

搭建中实际踩到并修掉的坑(供参考):

  1. pnpm.overrides 写在包级无效 —— 只在 workspace 根生效,已移到 pnpm-workspace.yaml。
  2. @lucide/vue@1.53.0 的图标名与旧版不同 —— MenuFold/MenuUnfold 不存在,应使用 PanelLeftClose/PanelLeftOpen;FileQuestion 不存在,应为 FileQuestionMark。图标不确定时先查 node_modules/@lucide/vue/dist/lucide-vue.d.ts。
  3. link 方案必须配 dedupe: ['vue', ...],否则响应式失效。

另:验证过程中曾用沙盒推断「无根 package.json 时 link: 路径会错位一层」,该结论是错的——沙盒的目录层级与真实项目不同。真实项目实测路径解析正确。教训:相对路径解析这类问题必须用真实项目验证,沙盒结论不可直接采信。

8. 待确认事项

# 事项 影响 阻塞什么
1 私库是否存在 决定 CI 中 @g3soft/ui 的引用方式(当前是相对路径 link) CI 搭建
2 g3soft-libs/packages/framework 是否删除 若保留,布局壳可复用,本次手写的 layouts/* 可简化 无(骨架已自建)
3 stk-table-vue / LogicFlow 列表页与审批流设计器选型 对应功能开发
4 登录 / 权限模型 影响 router 守卫与 user store 用户模块
5 后端启动 server/ 目前仅占位 联调
6 H5 端规划 h5/ 与 web/ 平级、互相独立;@g3soft/ui 不给 H5 用 H5 立项

9. 后续建议

  1. 先做业务域页面,还是先做引擎? 建议先做 1~2 个真实业务页面(如海运委托单列表 + 明细),用真实需求反推引擎该提供什么能力。直接设计引擎容易做成「想象中通用、实际不好用」。
  2. SQL Server 方言要单独抽层 保留 SQL Server 已定,但元数据引擎的动态 SQL 生成器会被方言绑死(分页 OFFSET/FETCH、字符串拼接用 + 而非 ||、JSON 函数弱)。把 SQL 构建抽成一层方言适配,不要让 TOP 10 这类语法散落在业务代码里——将来真要换库只改那一层。
  3. 提交信息带端前缀 同仓建议用 feat(web): / fix(api):,方便区分改动影响哪端。
  4. H5 端不要急着抽共享层 @g3soft/ui 不给 H5 用,两端就是独立应用。等 H5 真正落地、且两处出现形态接近的重复代码时,再考虑抽共享包——不要为「以后可能要共用」提前建 packages/。