Files
framework_project_web/.cursor/rules/admin-framework-routing-store.mdc
张成 dee3a336ce init
2026-04-29 13:34:39 +08:00

200 lines
12 KiB
Plaintext
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.
---
description: Admin Framework 动态路由与 Vuex 使用约束(详尽版,对照 admin_core 源码)
globs: admin/src/**/*.{vue,js},admin_core/src/**/*.{vue,js}
alwaysApply: false
---
# Admin 路由与状态约束(详尽)
适用于 `admin` 端动态菜单、路由与 Vuex**实现以 `admin_core` 源码为准**,模板目录 `admin/src` 与之对齐。涉及文件主要包括:`src/index.js`、`src/router/index.js`、`src/utils/uiTool.js`、`src/utils/http.js`、`src/store/user.js`、`src/store/app.js`、`src/store/index.js`、`src/views/index.js``setupComponentMap`)。
---
## 1. `createApp` 生命周期与单例
### 1.1 执行顺序(与 `index.js` 中 `createApp` 一致)
1. 若未传 `uploadUrl` 且存在 `apiUrl`:按 `apiUrl` 是否以 `/` 结尾拼接 `upload` 或 `/upload`。
2. 若 `config.HomePage` 存在:写入框架实例 `this.HomePage`。
3. `this.config = config`。
4. `Vue.use(ViewUI)`、`Vue.use(VueRouter)`、`Vue.use(Vuex)`。
5. 原型注入:`$config`、`$http`、`$tools`、`$uiTool`、`$framework`。
6. `registerGlobalComponents(Vue)`:注册布局、表格、上传等全局组件。
7. `setupComponentMap(config.componentMap || {}, uiTool)`:合并内置系统页映射与业务映射(见第 3 节)。
8. **Store**:若 `!this.store`,则 `createStore(Vuex, {}, null)` 并 `http.init(config, this.store)`;若已有 `store` 则**不重建**、不重复 `http.init`。
9. **Router**:若 `!this.router`,则 `getRoutes` + `createRouter`hash 模式)并挂守卫;若已有 `router` 则**不重建**。
10. 根 Vue`render: h => h('router-view')``created` 内:`uiTool.setRem()`、`resize` 监听;若有 `token` + `localStorage.authorityMenus` 则 `dispatch('user/setAuthorityMenus', { Main, ParentView, Page404, HomePage, authorityMenus })` 再 `dispatch('app/getSysTitle', { defaultTitle, defaultLogo })`;否则 `document.title = config.title`;最后调用 `config.onReady`(若存在)。
### 1.2 单例与全局
- 默认导出为**同一** `AdminFramework` 实例;`createApp` 前后均可能在浏览器挂载 `window.framework`。
- **`store` / `router` 一旦创建即复用**:业务不得在页面里 `new Vuex.Store` 或 `new VueRouter` 替换框架主链路。
- `window.framework`、`window.rootVue` **仅供调试**业务代码不得依赖其存在SSR、单测、微前端场景可能无 `window` 或未挂载)。
### 1.3 根实例恢复菜单与标题
- 刷新后:读 `this.$store.state.user.token` 与 `localStorage.getItem('authorityMenus')`;二者齐全才恢复动态路由并拉标题。
- `authorityMenus` 在 `user` 模块里经 `setAuthorityMenus` mutation 写入 **`localStorage.authorityMenus`**(值为 **JSON 字符串**)。
---
## 2. 路由模式、基础路由与守卫
### 2.1 模式与基础表
- `createRouter` 使用 **`mode: 'hash'`**`router/index.js`)。
- `createBaseRoutes``/login`、`/401`、`/404`、`/500`、通配 `*` → 404。
### 2.2 `setupRouterGuards` 行为摘要
- 每次导航:`ViewUI.LoadingBar.start()``afterEach``LoadingBar.finish()` + `window.scrollTo(0,0)`。
- **`name === 'view_log'`**:直接 `next()`,不做登录校验(操作日志类独立入口)。
- **无 token 且目标非 `login`** → `next({ name: 'login' })`。
- **无 token 且目标为 `login`** → `next()`。
- **有 token 且目标为 `login`**:若 `from.name === 'home'` 则 `next(false)` 避免重复导航;否则 `next({ name: 'home' })`。
- **有 token、`to.matched.length === 0` 且 `to.path !== '/'`**:认为可能动态路由尚未就绪,**150ms** 后 `router.resolve` 再试;仍无匹配则 `next({ name: 'home', replace: true })`。
### 2.3 动态主路由与 `redirect`
- 初始 `customRoutes` 来自 `getRoutes`,内部为 `uiTool.getRoutes(Main, ParentView, Page404, HomePage)`,得到 `path: '/'` 的 **主路由** 对象(含 `children`、`redirect`)。
- `setAuthorityMenus` 成功生成 `mainMenu` 后:在 `router.options.routes` 中 **删除** 原 `path === '/'` 的项,再 **`router.addRoute(mainMenu)`**;并 `await` 约 **100ms** 等待路由表更新。
- **`mainMenu.redirect`**`getRoutes` 内若存在权限菜单,会 `findFirstRoute`:优先带 `/home` 或 `home` 的项;否则取第一个非「菜单」类型且有 `path` 的节点(菜单类型会递归子节点)。用于登录后首屏落地路径。
---
## 3. 菜单数据、类型与组件映射
### 3.1 菜单项 `type` 与 `menuToRoute``uiTool.js`
| `type` | 行为 |
|--------|------|
| `菜单` | `component` 固定为 **`ParentView`**(布局容器) |
| `页面` / `功能` | 用 `item.component` 调 **`uiTool.getComponent`**;命中则替换为真实组件;未命中则渲染占位 `Alert`(提示路径并保留 `item.componentPath``catch` 时用 **`Page404`** |
| 其他 | 视为 `ParentView` |
| 任意 | 设置 `meta``icon`、`isMenu`、`type`、`title`(来自 `name`);有 `children` 则递归 |
### 3.2 `getComponent` 与 `setupComponentMap`
- `getComponent`:去掉路径尾 `.vue` 后与表内键匹配;也支持直接用带 `.vue` 的键。
- `setupComponentMap`:先合并默认 `home/index` 与系统页,再展开 **`customMap`**;对每个键生成 **`cleanPath`** 与 **`cleanPath + '.vue'`** 两条映射指向同一组件。
- 后端菜单 **`component` 字符串必须与映射键一致**(或仅差 `.vue`,由双键覆盖)。
### 3.3 首页 `home` 与权限树合并(`getRoutes`
- 默认子路由含 **`/home`**`HomePage` 或内置欢迎 render
- 若 `localStorage.authorityMenus` 解析后非空:先 `transformTree` 再 `menuToRoute` 得 `curRoutes`。
- **`hasHomeInRoutes`**:任意层出现 `path === '/home'` 或 `'home'` 即视为权限自带首页。
- **有 home**`mainRoute.children = curRoutes`(权限树完全替换子级),`redirect` 先置 `/home` 再由 `findFirstRoute` 覆盖。
- **无 home**`mainRoute.children = [homeRoute, ...curRoutes]`,保证仍有框架默认首页。
---
## 4. Vuex`user` 模块(鉴权、菜单、租户)
### 4.1 `state` 与持久化
| 字段 | 说明 |
|------|------|
| `token` | 初始 `getToken()`Cookie `token`,见 `tools.js` 中 `TOKEN_KEY` |
| `authorityMenus` | 内存;实际持久化由 mutation 写 **localStorage `authorityMenus`** |
| `menuList` | 默认从 **localStorage `menuList`** JSON 解析 |
| `currentTenant` | 登录租户;**localStorage `currentTenant`** |
| `userName` / `avatorImgPath` | 展示用;`userName` 可与 **localStorage.userName** 同步 |
### 4.2 `setAuthorityMenus` 入参action
- **`Main`, `ParentView`, `Page404`**:必填语义;用于 `uiTool.getRoutes` / 动态 `addRoute`。
- **`HomePage`**:可选;缺省时用 `window.framework.HomePage`(框架不推荐业务依赖 `window`,调用方应显式传入与 `createApp` 一致的 `HomePage`)。
- **`authorityMenus`**:可选;若传入则不再请求接口;可为 **已 JSON.stringify 的树** 或对象数组(内部会 `JSON.parse` 字符串)。
- **`menuIds`**:可选;仅在 **未传入 `authorityMenus` 且 `authorityMenus()` 接口失败** 时,用于从 **`defaultMenus`**`menuConfig.js`)过滤。
### 4.3 `setAuthorityMenus` 数据流摘要
1. 无 `authorityMenus` → `userServer.authorityMenus()``code === 0` 取 `data`;失败则用 `menuIds` + `defaultMenus` 或全量 `defaultMenus`。
2. 字符串则 `JSON.parse`;失败或非法数组则回退 `defaultMenus`。
3. `commit('setAuthorityMenus', JSON.stringify(menus))`。
4. `mainMenu = uiTool.getRoutes(...)`;若有 `children``commit('setMenuList', mainMenu.children)`,再按第 2.3 节替换 `/` 路由。
### 4.4 `handleLogin`(与登录页配合)
- 期望 **`userServer.login`** 返回体:`{ code, message, data }`,且 `data` 含 **`token`**、**`user.name`**、可选 **`authorityMenus`**(菜单 ID 列表,字符串或数组)、可选 **`tenant`**。
- 成功后:`setUserName`、`setToken`、`setCurrentTenant`;解析 `menuIds``dispatch('setAuthorityMenus', { Main, ParentView, Page404, HomePage, menuIds })`;再 **`dispatch('app/getSysTitle', {}, { root: true })`**。
- 失败:抛错,由页面 `try/catch` 处理。
### 4.5 `handleLogOut`
- `setToken('')`、`setAuthorityMenus('[]')`、`setMenuList([])`、`setCurrentTenant(null)`、移除 **`menuList`** 的 localStorage、`location.reload()`。
---
## 5. Vuex`app` 模块(面包屑、站点标题)
### 5.1 `getSysTitle`**命名空间 `app`**
- 调用:**`this.$store.dispatch('app/getSysTitle', { defaultTitle, defaultLogo })`**;从其他 namespaced 模块内需 **`{ root: true }`**。
- 无 `defaultTitle` 时尝试 `window.framework.config.title`,再回退文案。
- 有 token请求 **`paramSetupServer.getOne('sys_title')`**、**`getOne('sys_logo')`**(与参数表约定一致);更新 `document.title` 与 `commit('setSysTitle', formModel)`。
- 无 token仅用默认标题写 `document.title`。
### 5.2 面包屑
- `setBreadCrumb` / `setHomeRoute` 由布局与路由变化驱动;业务侧改菜单后应通过 **`setAuthorityMenus`** 刷新路由与 `menuList`,而不是手写面包屑数组。
---
## 6. `createStore` 扩展
```js
createStore(Vuex, customModules, createPersistedState)
```
- **模块合并**`{ user, app, ...customModules }`。
- **第三个参数**:传入 **`vuex-persistedstate` 工厂** 则启用 `localStorage` 插件;**`null`**(框架默认)表示不启用持久化插件。
---
## 7. HTTP 与全局 Loading`http.js`
### 7.1 实例与 Token
- **`getHttpInstance`**`baseURL = config.apiUrl`,合并 `timeout`(默认 300000若存在 `store.state.user`,设置请求头 **`admin-token`** 为 **`store.state.user.token`**。
- Token 存 Cookie`tools.setToken`),请求头由 HTTP 层统一加;**不要**在业务里手写 `admin-token`。
### 7.2 响应拦截
- **HTTP 200** 且 **`response.data.code === 0`**resolve 整个 `response`(注意:`get`/`post` 的 Promise 里再取 **`response.data`** 作为业务返回值)。
- **`code !== 0`**`hideLoad`、`Message.error(message)`、`reject(message)`(字符串)。
- **`401`**`commit('user/setToken','')`;若存在 **`window.framework.router`** 则 `push({ path: '/login' })`。
- 其他网络错误:统一文案映射后 `showError`、`reject`。
### 7.3 各方法与 Loading
| 方法 | Loading |
|------|---------|
| `get` | 默认 `showLoad`**`config.hideLoad === true`** 时不展示 |
| `post` | **始终** `showLoad`(无 `hideLoad` 分支) |
| `postFormData` | 无单独 `showLoad`/`hideLoad`(与 `post` 不同,实现上依赖实例 post 行为,以源码为准联调) |
| `fileExport` | 独立 `axios.post` + `blob`;默认 `is_down=true` 时调 **`window.framework.uiTool.downloadFile`** |
### 7.4 参数预处理 `formatParamete`
- 深拷贝后对**对象**递归删除值为 **`''`** 的键;`Date` 格式化为 **`YYYY-MM-DD HH:mm:ss`**。
---
## 8. 组件复用优先级(与框架导出一致)
- 表单弹窗:**`editModal`**、**`AsyncModal`**;字段:**`FieldRenderer`**、**`fieldItem`**。
- 表格/树/上传:**`Tables`**、**`TreeGrid`**、**`UploadSingle`** / **`UploadMultiple`**。
- 布局:**`Main`**、**`ParentView`**;勿复制一套侧边栏 + 多页签逻辑。
---
## 9. 联调检查清单(路由 / Store
- [ ] 菜单里「页面/功能」的 `component` 路径是否能在映射表命中?
- [ ] 改菜单后是否调用 **`user/setAuthorityMenus`**(并传入布局组件),而非只改 DB
- [ ] 刷新后标题是否仍走 **`app/getSysTitle`**
- [ ] 新增无感轮询是否用 **`get` + `hideLoad`**,而非 `post`
- [ ] 是否避免在业务逻辑里依赖 **`window.framework`** 完成核心流程?