# 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()( 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`。