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

221 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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/`。