--- 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`** 完成核心流程?