feat(react19-migration): 阶段3 - 状态管理与路由

- T3.1: 创建 useAuthStore (Zustand) - login/logout/initFromStorage
- T3.2: 创建 usePermissionStore (Zustand) - fetchUserMenus/hasPermission/hasRole
- T3.3: 创建 useAppStore (Zustand) - collapsed 状态管理
- T3.4: 创建 React Router v7 数据路由配置 (createBrowserRouter)
- T3.5: 创建 authLoader 路由守卫 (token校验→初始化→权限加载)
- 创建占位页面组件 (13个路由页面 + DefaultLayout)
- 更新 App.tsx 使用 RouterProvider
- 安装 jwt-decode 依赖

验证: npx tsc --noEmit 通过, npm run dev 启动成功
This commit is contained in:
张翔
2026-05-03 15:34:09 +08:00
committed by zhangxiang
parent 01ddf0a5f6
commit 434a81dc71
21 changed files with 378 additions and 4 deletions
@@ -0,0 +1,13 @@
import { create } from 'zustand'
interface AppState {
collapsed: boolean
toggleCollapsed: () => void
setCollapsed: (val: boolean) => void
}
export const useAppStore = create<AppState>((set) => ({
collapsed: false,
toggleCollapsed: () => set((state) => ({ collapsed: !state.collapsed })),
setCollapsed: (val: boolean) => set({ collapsed: val }),
}))
@@ -0,0 +1,103 @@
import { create } from 'zustand'
import { jwtDecode } from 'jwt-decode'
import { authApi } from '@/api/auth.api'
import type { JwtPayload } from '@/types/user'
interface AuthState {
token: string | null
userId: number | null
username: string | null
nickname: string | null
roles: string[]
permissions: string[]
isAuthenticated: boolean
initialized: boolean
login: (username: string, password: string) => Promise<void>
logout: () => void
initFromStorage: () => void
setInitialized: (val: boolean) => void
}
export const useAuthStore = create<AuthState>((set, get) => ({
token: null,
userId: null,
username: null,
nickname: null,
roles: [],
permissions: [],
isAuthenticated: false,
initialized: false,
login: async (username: string, password: string) => {
const res = await authApi.login({ username, password })
const data = res as any
const token = data.token
const decoded = jwtDecode<JwtPayload>(token)
localStorage.setItem('token', token)
localStorage.setItem('userId', String(decoded.sub))
localStorage.setItem('username', decoded.username)
localStorage.setItem('roles', JSON.stringify(decoded.roles))
set({
token,
userId: Number(decoded.sub),
username: decoded.username,
roles: decoded.roles || [],
isAuthenticated: true,
})
const { usePermissionStore } = await import('./usePermissionStore')
await usePermissionStore.getState().fetchUserMenus()
},
logout: () => {
localStorage.removeItem('token')
localStorage.removeItem('userId')
localStorage.removeItem('username')
localStorage.removeItem('roles')
localStorage.removeItem('permission')
set({
token: null,
userId: null,
username: null,
nickname: null,
roles: [],
permissions: [],
isAuthenticated: false,
initialized: false,
})
import('./usePermissionStore').then(({ usePermissionStore }) => {
usePermissionStore.getState().clearPermissionData()
})
},
initFromStorage: () => {
const token = localStorage.getItem('token')
if (!token) return
try {
const decoded = jwtDecode<JwtPayload>(token)
if (decoded.exp * 1000 < Date.now()) {
get().logout()
return
}
set({
token,
userId: Number(decoded.sub),
username: decoded.username,
roles: decoded.roles || [],
isAuthenticated: true,
initialized: true,
})
} catch {
get().logout()
}
},
setInitialized: (val: boolean) => set({ initialized: val }),
}))
@@ -0,0 +1,95 @@
import { create } from 'zustand'
import { menuApi } from '@/api/menu'
import type { MenuItem } from '@/api/menu'
interface PermissionState {
roles: string[]
permissions: string[]
menus: MenuItem[]
loaded: boolean
fetchUserMenus: () => Promise<void>
hasPermission: (permission: string) => boolean
hasRole: (role: string) => boolean
clearPermissionData: () => void
initFromStorage: () => boolean
}
export const usePermissionStore = create<PermissionState>((set, get) => ({
roles: [],
permissions: [],
menus: [],
loaded: false,
fetchUserMenus: async () => {
try {
const res = await menuApi.getTree()
const menus = res as unknown as MenuItem[]
const permissions = extractPermissions(menus)
const stored = localStorage.getItem('roles')
const roles = stored ? JSON.parse(stored) : []
localStorage.setItem('permission', JSON.stringify({ permissions, menus }))
set({ menus, permissions, roles, loaded: true })
} catch (error) {
console.error('获取菜单失败:', error)
throw error
}
},
hasPermission: (permission: string) => {
const { permissions } = get()
return permissions.includes(permission) || permissions.includes('*')
},
hasRole: (role: string) => {
const { roles } = get()
return roles.includes(role) || roles.includes('admin')
},
clearPermissionData: () => {
localStorage.removeItem('permission')
set({ roles: [], permissions: [], menus: [], loaded: false })
},
initFromStorage: () => {
const stored = localStorage.getItem('permission')
if (!stored) return false
try {
const data = JSON.parse(stored)
const rolesStr = localStorage.getItem('roles')
const roles = rolesStr ? JSON.parse(rolesStr) : []
set({
permissions: data.permissions || [],
menus: data.menus || [],
roles,
loaded: true,
})
return true
} catch {
return false
}
},
}))
function extractPermissions(menus: MenuItem[]): string[] {
const permissions: string[] = []
function traverse(items: MenuItem[]) {
for (const item of items) {
if (item.permission) {
permissions.push(item.permission)
}
if (item.children?.length) {
traverse(item.children)
}
}
}
traverse(menus)
return permissions
}