# Zustand 全局状态迁移方案

> 目标：将 `apps/admin` 中分散的全局状态管理统一迁移到 Zustand，建立清晰、可维护、单向数据流的状态架构。

## 一、现状分析（迁移前）

迁移前，全局状态散落在多个位置：

| 状态 | 原实现 | 问题 |
| --- | --- | --- |
| token / user | 各组件直接读写 `localStorage`（Login、MainLayout、api 拦截器、下载、SSE 等） | 无类型约束、无订阅、重复解析 |
| permissions | `auth/permission-store.ts` 模块单例 + `window` 自定义事件 + `usePermission` 手动订阅 | 事件驱动易遗漏、无法细粒度订阅 |
| 布局 UI（侧边栏/抽屉/AI 抽屉/菜单展开） | `MainLayout` 内多个 `useState` | 局部状态无法跨组件共享、不持久化 |
| 路由页签（RouteDock） | 组件内 `useState` + 直接写 `localStorage('gongxue-route-dock')` | 持久化逻辑与 UI 耦合 |
| AI 聊天偏好（深度思考） | `AiChatDrawer` 内 `useState(false)` | 每次打开重置 |

## 二、目标架构

```text
apps/admin/src/store/
 ├── index.ts                    # 统一出口
 ├── types.ts                    # 通用类型（StoreStatus 等）
 ├── middleware/
 │    └── persist.ts             # 持久化适配器（兼容旧 localStorage key）
 ├── user/
 │    ├── userTypes.ts           # UserInfo / UserState / UserActions
 │    ├── userActions.ts         # 会话 actions（与 state 分离）
 │    └── userStore.ts           # create()(devtools(persist(...)))
 ├── app/
 │    ├── appTypes.ts            # 布局/抽屉/路由页签状态
 │    └── appStore.ts
 ├── permission/
 │    ├── permissionTypes.ts     # permissions + status（fail-closed）
 │    └── permissionStore.ts
 └── settings/
      ├── settingsTypes.ts       # 用户偏好（AI 深度思考等）
      └── settingsStore.ts
```

统一写法（Zustand 官方推荐）：

```ts
export const useUserStore = create<UserStore>()(
  devtools(
    persist(
      (set) => ({ token: null, user: null, ...createUserActions(set) }),
      { name: 'gongxue-auth', storage: authPersistStorage, version: 1 },
    ),
    { name: 'user-store', enabled: import.meta.env.DEV },
  ),
);
```

设计要点：

- **state 与 actions 分离**：`userActions.ts` 独立成文件，其余 Store 的 actions 量小，随 Store 内联，避免无限膨胀。
- **单向数据流**：组件通过 selector 订阅；仅通过 action 修改状态；持久化由 middleware 统一处理。
- **devtools 仅在开发环境启用**，生产不产生额外开销。
- **持久化版本化**：所有 Store `version: 1`，后续 schema 变更通过 `migrate` 平滑升级。

## 三、持久化兼容策略

为了平滑迁移且不破坏已有浏览器缓存：

| Store | persist name | 兼容的旧 key | 说明 |
| --- | --- | --- | --- |
| user | `gongxue-auth` | `token`、`user` | 适配器继续读写旧 key，格式不变 |
| permission | `permissions` | `permissions`（原始 JSON 数组） | 同时兼容旧数组与 zustand 信封；rehydrate 后强制 `status: 'unknown'`，保持 fail-closed |
| app | `gongxue-app-ui` | `gongxue-route-dock` | 首次读取自动迁移旧页签数据 |
| settings | `gongxue-settings` | 无 | 新 key |

权限状态特别说明：旧实现“缓存权限但未校验前不使用”。迁移后依然如此——持久化只保存权限码，`status` 恢复后一律为 `unknown`，必须等待 `/auth/profile` 校验成功后（`writePermissions`）才变为 `ready`。

## 四、已迁移的消费方

- `pages/Login`：登录后写入 user Store，权限写入 permission Store。
- `App.tsx`：`PrivateRoute` 从 user Store 读取 token（响应式）。
- `api/index.ts`：请求拦截器与 401 处理改读 Store / 调用 `logout()`。
- `layouts/MainLayout`：用户信息、权限校验、布局 UI、AI 抽屉开关全部迁移到 Store。
- `hooks/usePermission`：改为 Zustand selector 订阅，删除自定义事件。
- `components/RouteDock`：页签状态迁移到 app Store（持久化仍生效）。
- `components/AiChat/AiChatDrawer`：深度思考偏好迁移到 settings Store（持久化）。
- 下载、SSE、AiChat provider 等所有 token 读取统一走 `useUserStore.getState().token`。
- `auth/permission-store.ts` 已删除；测试 helper/setup 同步迁移。

## 五、验证

- `npx tsc -b apps/admin/tsconfig.app.json --noEmit` ✅
- `npx vitest run --root apps/admin`（142 个浏览器集成测试）✅
- `npm run build --workspace @gongxue/admin` ✅
- `npm run lint --workspace @gongxue/admin` ✅（无新增告警）

## 六、后续可选优化（本次未纳入）

- **页面级缓存**：`integration-config-cache.ts`、`schedule-visibility.ts`、`unavailable-dates-cache.ts`、`inspection-state.ts` 等仍是模块级单例，属于页面内缓存，可按需迁移为 Zustand（或保持现状，配合 React Query/SWR）。
- **请求去重**：若多个页面出现同一资源重复请求，可引入 SWR 统一缓存（当前各页数据获取均为单次请求，收益有限）。
- **包体积**：`@ant-design/icons` 为 barrel 导出但 `sideEffects: false`，Vite 构建可正确 tree-shake；如需进一步压缩 dev 冷启动，可改为深路径导入。`lucide-react` 已安装但未被引用，可择机移除。
- **大列表渲染**：消息列表/大表格可补充 `content-visibility` 或虚拟滚动；搜索大列表时可用 `useDeferredValue`。
