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

View File

@@ -0,0 +1,175 @@
/**
* 持久化中间件基础设施。
*
* 为了平滑迁移,这里把旧实现直接读写 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<string, unknown> {
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 };