# 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() │ └── 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/`。