This commit is contained in:
oneao committed 2026-10-09 22:05:02 +08:00
1 parent 0be0b0767a
commit 5f28a10d89
852 files changed
+8962 -111755

No files matched your search

+215
View File
@@ -0,0 +1,215 @@
# 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`。
## 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/`。