u
This commit is contained in:
1 parent
0be0b0767a
commit
5f28a10d89
852 files changed
+8962
-111755
No files matched your search
@@ -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/`。
|
||||
Reference in new issue
Block a user