# 管理端页面切换性能分析与优化任务

> 分析日期：2026-06-18  
> 范围：`apps/admin` 侧边栏切换路由时的响应速度（非玩家端）。

---

## 任务清单

- [x] **measure-baseline**：DevTools 验收步骤见下文「九、度量验收」；目标：guard 无阻塞 /me、切回列表无重复全量 API、hover 预取后 RouteChanged < 100ms
- [x] **instant-nav-phase1**：KeepAlive name 对齐、非阻塞 guard、侧边栏 chunk 预取（2026-06-18）
- [x] **instant-nav-phase2**：`useStaleListLifecycle` + 列表页 v-loading 壳、AgentManager 按 tab 延迟 API（2026-06-18）
- [x] **keepalive-layout**：`ManageLayout` 的 `RouterView` 增加 `KeepAlive` + 列表页 `defineOptions({ name })`（2026-06-18）
- [x] **list-stale-cache**：高频列表页改 `onActivated` + stale-while-revalidate，避免 remount 全量 refetch（2026-06-18）
- [x] **fix-deposit-tabs**：`DepositManage` 的 `v-if` 改 `v-show`（2026-06-18）
- [ ] **lighten-agent-manager**：`AgentManager` 挂载 API 合并或按 tab 延迟加载（onActivated + 按 tab 延迟已做，拆分 SFC 待后续）
- [x] **guard-session**：`beforeEach` 去阻塞式 `ensureStaffSession`（TTL 内同步快速路径）；`api` 拦截器减少 per-request reconcile（2026-06-18）
- [x] **bundle-i18n-ep**：Element Plus 按需引入 + 落地 `split-i18n` + `App.vue` CSS 瘦身（2026-06-18）

---

## 现象定义

用户感知的「切换慢」通常包含三段延迟叠加：

```mermaid
sequenceDiagram
  participant User
  participant RouterGuard
  participant ChunkLoader
  participant PageView
  participant API

  User->>RouterGuard: 点击侧边栏
  RouterGuard->>RouterGuard: ensureStaffSession (可能 HTTP)
  RouterGuard->>ChunkLoader: 动态 import 页面 chunk
  ChunkLoader->>PageView: mount 组件
  PageView->>API: onMounted 并发拉列表
  API-->>PageView: 渲染表格
  PageView-->>User: 页面可交互
```

---

## 一、架构结论

| 层级 | 现状 | 对切页的影响 |
|------|------|-------------|
| 路由 | 全部 lazy load（`apps/admin/src/router/index.ts`） | 首次进入某页需下载 chunk |
| 布局 | `ManageLayout.vue` 常驻 | 壳层不重载，合理 |
| **页面缓存** | **全项目无 `<KeepAlive>`** | **切走即销毁，回来必 remount + 重拉数据** |
| 数据层 | 无 Pinia；仅少数 composable 有模块级缓存 | 绝大多数列表页无跨访问缓存 |
| 首屏 bundle | Element Plus 全量 + 三语 i18n 打进主包 | 影响首访/冷启动，对已登录切页影响次之 |

**最大根因：没有 KeepAlive + 页面 `onMounted` 全量 refetch。**

---

## 二、切页时实际发生什么

### 2.1 路由守卫（每条鉴权路由）

`apps/admin/src/router/index.ts` 的 `beforeEach`：

- 有 token 时 **`await ensureStaffSession()`**
- `apps/admin/src/utils/session-hydrate.ts` 有 60s TTL，过期后会 **`GET /manage/auth/me`**，**阻塞导航完成**
- 访问 smoke-tests 路由时额外 `await ensureLoaded()`

### 2.2 页面生命周期（无缓存）

`ManageLayout.vue` 内为裸 `<RouterView />`：

- 旧页面 **unmount**
- 新页面 chunk **动态 import** → **mount**
- 典型模式：`onMounted(load)` / 顶层 `void load()`

仅 **Dashboard 子页** 体验较好：`useAdminDashboard.ts` 模块级 `stats` 缓存，同 session 内切 `/` ↔ `/dashboard/players` 可跳过 API（但 `HomeEntry` 仍可能闪 boot 屏）。

### 2.3 布局层常驻副作用

`ManageLayout.vue` `onMounted`：

- `useDepositPendingCount`：立即请求 + **每 30s 轮询** `pending-count`
- `useSmokeTestsAllowed`：一次性权限探测

不直接阻塞切页，但增加后台并发请求。

### 2.4 每个 API 请求的同步开销

`apps/admin/src/api.ts` 请求拦截器对每个请求调用 `reconcileStaffSessionFromToken()`（JWT decode + localStorage）。列表页 mount 时常 **并发 3–10 个请求**，同步开销被放大。

---

## 三、按影响排序的瓶颈清单

### P0 — 切换体验（每次切页都痛）

**1. 无 KeepAlive，列表页反复 remount + refetch**

受影响页面（模式相同）：

- `Bets.vue`、`Cashback.vue`
- `Matches.vue`、`MatchesOutrights.vue`
- `DepositOrders.vue`、`StaffManage.vue`
- `Contents.vue`、`FinanceLogs.vue` 等

**2. 重型页面挂载 API burst**

`AgentManager.vue`（`/users`）— 约 2900 行 SFC，`onMounted` 并行：

- `GET /admin/users/page-init`
- `GET /admin/users`（全量玩家）
- `GET /admin/agents?level=1`
- page-init 后再按层级 **N+1** 拉子代理

每次从其他页回到 `/users` 都会重复上述 burst。

**3. Tab 用 `v-if` 导致子页销毁**

`DepositManage.vue`：

```vue
<DepositOrders v-if="activeTab === 'orders'" />
<PaymentMethods v-if="activeTab === 'methods'" />
```

同页内切换 tab 也会 destroy + `onMounted(fetchList)`。

**4. Matches 展开面板扇出请求**

`LeagueMatchesPanel.vue` `watch(..., { immediate: true })`：恢复 session 展开最多 3 个联赛时，**最多 3 路** `GET /admin/matches`；折叠再展开会 remount 重拉。

### P1 — 间歇性卡顿（特定路径 / 时间）

**5. Session hydrate 阻塞导航**

60s TTL 过期后，**每次切页**先等 `/manage/auth/me`。弱网或后端慢时，侧边栏点击后「卡住」数秒。

**6. HomeEntry 回 Dashboard 闪屏**

`HomeEntry.vue`：`onBeforeMount` 再次 `ensureStaffSession` + `booting` 全屏 loading（router 已做过 hydrate）。

**7. 首次进入大 chunk 的 JS 解析**

| 页面 | 风险 |
|------|------|
| `AgentManager.vue` | 巨型 SFC + 多子组件 |
| `Settlement.vue` | ~1500 行 + async echarts |
| `Contents.vue` | `ContentRichEditor.vue` |

### P2 — 首屏 / 冷启动

**8. Element Plus 全量注册**

`main.ts` 全量 `app.use(ElementPlus)` + `element-plus/dist/index.css`，无按需引入。

**9. i18n 三语未真正拆包**

`admin-messages.ts` 仍静态 import zh/en/ms 全文；`split-i18n.mjs` 未落地，首屏携带全部语言文案。

**10. App.vue 全局 CSS ~1900 行**

无 scoped 的暗色/浅色双套 Element 覆盖，与全量 EP CSS 叠加。

---

## 四、问题分层矩阵

| 症状 | 最可能原因 | 验证方式 |
|------|-----------|----------|
| 任意页切回上一页都慢 | 无 KeepAlive + onMounted refetch | Network：切回同页重复相同 API |
| 仅 `/users` 特别慢 | AgentManager 多路并行 API + 大 chunk | mount 时 3+ 请求；Performance 看 JS |
| 偶尔点菜单无反应数秒 | `ensureStaffSession` 阻塞 | 切页瞬间是否有 `/manage/auth/me` |
| 充值页 tab 切换慢 | DepositManage `v-if` | 切 tab 是否重复 `deposit-orders` 请求 |
| 首次进某页慢、之后再进仍慢 | 大 chunk + 仍无缓存 | 对比首次/二次 Network |
| 整体首次打开就慢 | EP 全量 + i18n 三语 + 全局 CSS | `pnpm --filter @thebet365/admin build:analyze` |

---

## 五、优化路径（分阶段）

### 阶段 A — 切页体验（1–2 天，收益最大）

1. `ManageLayout.vue` 的 `<RouterView>` 外包 `<KeepAlive :max="8">`，列表页 `defineOptions({ name })`
2. 列表页改 `onActivated` + stale-while-revalidate（有缓存先展示，后台静默刷新）
3. `DepositManage.vue`：`v-if` → `v-show` 或 keep-alive 两个 tab
4. `HomeEntry.vue`：去掉重复 hydrate / 仅首次 boot

### 阶段 B — 重型页与 API（2–3 天）

1. 拆分 `AgentManager.vue`：按 tab lazy，或合并 bootstrap 为单一 `page-init` 接口
2. 提取通用 `useListCache(key, fetcher, ttl)` 给 Bets/Users/Deposit 等
3. `LeagueMatchesPanel`：对已加载 `leagueId` 短 TTL 缓存
4. `api.ts`：`reconcileStaffSessionFromToken` 移到 token 变更时，而非每请求

### 阶段 C — 守卫与 bundle（3–5 天）

1. `beforeEach` 改为同步 JWT/localStorage 校验；`/me` 仅登录/刷新时调用
2. Element Plus 改按需 + 去全量 CSS
3. 执行/合入 i18n `split-i18n.mjs`，首屏只加载当前语言
4. 瘦身 `App.vue` 全局样式

### 阶段 D — 度量与验收

```bash
pnpm --filter @thebet365/admin build:analyze
```

验收指标（Chrome DevTools，Fast 3G）：

- 切回已访问列表页：无重复全量列表 API（或仅 background refresh）
- `/users` 二次进入：API 数从 3+ 降到 0–1
- 侧边栏切换：guard 阶段无阻塞性 `/me`（60s 内）

---

## 六、当前不必优先动的部分

- **ECharts**：已 async chunk，仅 dashboard/settlement 加载
- **ContentRichEditor**：未全局引入，仅 contents 路由
- **deposit 30s 轮询**：后台流量，通常不是切页主因
- **ManageLayout computed 菜单**：开销相对小

---

## 七、推荐落地顺序

1. **只做一处**：KeepAlive + 列表页 activated 缓存（阶段 A）
2. **`/users` 最慢**：在 A 之后做 AgentManager API 合并/延迟加载（阶段 B）
3. **首屏整体慢**：再动 Element Plus / i18n 拆分（阶段 C）

---

## 八、关键文件索引

| 职责 | 路径 |
|------|------|
| 路由 + beforeEach | `apps/admin/src/router/index.ts` |
| 布局壳 | `apps/admin/src/layouts/ManageLayout.vue` |
| Session 水合 / TTL | `apps/admin/src/utils/session-hydrate.ts` |
| Auth store | `apps/admin/src/stores/auth.ts` |
| Axios 拦截器 | `apps/admin/src/api.ts` |
| Dashboard 入口 | `apps/admin/src/views/HomeEntry.vue` |
| Dashboard 数据缓存 | `apps/admin/src/composables/useAdminDashboard.ts` |
| Deposit 轮询 | `apps/admin/src/composables/useDepositPendingCount.ts` |
| Bootstrap | `apps/admin/src/main.ts` |
| Vite 分包 | `apps/admin/vite.config.ts` |
| 重型用户页 | `apps/admin/src/views/AgentManager.vue` |
| 充值 tab | `apps/admin/src/views/DepositManage.vue` |
| 路由 chunk 预取 | `apps/admin/src/utils/route-prefetch.ts` |
| 列表 stale 生命周期 | `apps/admin/src/composables/useStaleList.ts` |

---

## 九、度量验收（DevTools）

### Performance — 点击 → 路由切换

1. 打开 Chrome DevTools → **Performance**，勾选 Screenshots
2. 录制：hover 侧边栏「注单管理」约 0.5s → 点击 → 停止
3. 在 Main 线程找 `vue-router` / `Route` 相关事件；**hover 预取后** URL 与顶栏应在 **< 100ms** 内变化（无 session 阻塞时）

### Network — guard 不阻塞 /me

1. DevTools → **Network**，过滤 `auth/me`
2. 登录后 60s 内连续切换 3 个菜单：**不应**每次切页都出现 `/manage/auth/me`
3. TTL 过期后切页：路由应立即切换；`/me` 可在后台出现，**不应**出现在导航完成之前作为唯一请求

### Network — KeepAlive 切回

1. 进入 `/bets` 等待列表加载 → 切到 `/matches` → 再切回 `/bets`
2. 第二次进入：**不应**重复全量 `GET /admin/bets`（或仅 silent refresh，表格不白屏）
3. `/users` 二次进入：mount 时不应并行 3+ bootstrap API（仅当前 tab + 可选 silent refresh）

### 构建分析

```bash
pnpm --filter @thebet365/admin build:analyze
```

确认 `admin-users`、`admin-bets`、`admin-matches` 等独立 chunk 存在，主包不含完整列表页 SFC。
