/** * 持久化中间件基础设施。 * * 为了平滑迁移,这里把旧实现直接读写 localStorage 的 key * (token / user / permissions / gongxue-route-dock)包装成 zustand * persist 的 StateStorage,保证迁移前后数据格式兼容。 */ import { createJSONStorage, type StateStorage } from 'zustand/middleware'; import type { AppPersistedState, DockTab } from '../app/appTypes'; import type { PermissionPersistedState } from '../permission/permissionTypes'; import type { UserInfo, UserPersistedState } from '../user/userTypes'; export const AUTH_STORAGE_NAME = 'gongxue-auth'; export const LEGACY_TOKEN_KEY = 'token'; export const LEGACY_USER_KEY = 'user'; export const PERMISSION_STORAGE_NAME = 'permissions'; export const APP_UI_STORAGE_NAME = 'gongxue-app-ui'; export const LEGACY_DOCK_STORAGE_KEY = 'gongxue-route-dock'; export const SETTINGS_STORAGE_NAME = 'gongxue-settings'; function isString(value: unknown): value is string { return typeof value === 'string'; } function isRecord(value: unknown): value is Record { return typeof value === 'object' && value !== null; } function isDockTab(value: unknown): value is DockTab { return ( isRecord(value) && typeof value.key === 'string' && value.key.startsWith('/') && typeof value.label === 'string' ); } /** * 用户会话持久化:继续使用旧的 `token` / `user` 两个 key, * 保持与后端、既有代码及浏览器缓存格式一致。 */ const legacyAuthStorage: StateStorage = { getItem: () => { const token = localStorage.getItem(LEGACY_TOKEN_KEY); const rawUser = localStorage.getItem(LEGACY_USER_KEY); if (token === null && rawUser === null) return null; let user: UserInfo | null = null; if (rawUser !== null) { try { const parsed: unknown = JSON.parse(rawUser); user = isRecord(parsed) ? (parsed as UserInfo) : null; } catch { user = null; } } return JSON.stringify({ state: { token, user }, version: 1 }); }, setItem: (_name, value) => { try { const persisted = JSON.parse(value) as { state?: UserPersistedState }; const { token, user } = persisted.state ?? {}; if (token) { localStorage.setItem(LEGACY_TOKEN_KEY, token); } else { localStorage.removeItem(LEGACY_TOKEN_KEY); } if (user) { localStorage.setItem(LEGACY_USER_KEY, JSON.stringify(user)); } else { localStorage.removeItem(LEGACY_USER_KEY); } } catch { // 持久化写入失败不应影响应用运行 } }, removeItem: () => { localStorage.removeItem(LEGACY_TOKEN_KEY); localStorage.removeItem(LEGACY_USER_KEY); }, }; /** * 权限持久化:兼容旧格式(原始 JSON 数组)与 zustand persist 格式。 * 无论磁盘上是什么状态,恢复后一律为 `unknown`,保持 fail-closed, * 直到 `/auth/profile` 校验成功。 */ const legacyPermissionStorage: StateStorage = { getItem: () => { const raw = localStorage.getItem(PERMISSION_STORAGE_NAME); if (!raw) return null; try { const parsed: unknown = JSON.parse(raw); if (Array.isArray(parsed)) { return JSON.stringify({ state: { permissions: parsed.filter(isString), status: 'unknown' }, version: 1, }); } if (isRecord(parsed) && isRecord(parsed.state)) { const permissions = Array.isArray(parsed.state.permissions) ? parsed.state.permissions.filter(isString) : []; return JSON.stringify({ state: { permissions, status: 'unknown' }, version: 1, }); } } catch { // 损坏的缓存按无权限处理 } return null; }, setItem: (_name, value) => { try { const persisted = JSON.parse(value) as { state?: PermissionPersistedState }; const permissions = Array.isArray(persisted.state?.permissions) ? persisted.state.permissions.filter(isString) : []; localStorage.setItem(PERMISSION_STORAGE_NAME, JSON.stringify(permissions)); } catch { // 忽略损坏数据 } }, removeItem: () => { localStorage.removeItem(PERMISSION_STORAGE_NAME); }, }; /** * 应用 UI 状态持久化:新 key `gongxue-app-ui`, * 首次读取时自动迁移旧 key `gongxue-route-dock` 中已打开的页签。 */ const appUiStorage: StateStorage = { getItem: (name) => { const current = localStorage.getItem(name); if (current) return current; const legacy = localStorage.getItem(LEGACY_DOCK_STORAGE_KEY); if (!legacy) return null; try { const parsed: unknown = JSON.parse(legacy); const routeDockTabs = Array.isArray(parsed) ? parsed.filter(isDockTab) : []; return JSON.stringify({ state: { routeDockTabs, sidebarCollapsed: false }, version: 1 }); } catch { return null; } }, setItem: (name, value) => { try { localStorage.setItem(name, value); } catch { // 持久化失败不应影响应用运行 } }, removeItem: (name) => { try { localStorage.removeItem(name); } catch { // 持久化失败不应影响应用运行 } }, }; /** 会话 Store 使用的 persist storage(兼容旧 token/user key) */ export const authPersistStorage = createJSONStorage(() => legacyAuthStorage); /** 权限 Store 使用的 persist storage(兼容旧 permissions key) */ export const permissionPersistStorage = createJSONStorage(() => legacyPermissionStorage); /** 应用 UI Store 使用的 persist storage(含旧 RouteDock key 迁移) */ export const appUiPersistStorage = createJSONStorage(() => appUiStorage); export type { AppPersistedState, PermissionPersistedState, UserPersistedState };