init
This commit is contained in:
199
.cursor/rules/admin-framework-routing-store.mdc
Normal file
199
.cursor/rules/admin-framework-routing-store.mdc
Normal file
@@ -0,0 +1,199 @@
|
||||
---
|
||||
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`** 完成核心流程?
|
||||
Reference in New Issue
Block a user