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:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user