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