refactor: 前端登录/权限/界面状态迁移至 zustand

This commit is contained in:
2026-08-04 14:41:27 +08:00
parent ce1dcc07ea
commit f07ffdc64c
39 changed files with 970 additions and 211 deletions

98
docs/zustand-migration.md Normal file
View File

@@ -0,0 +1,98 @@
# 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 + statusfail-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`