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:
@@ -1,13 +1,12 @@
|
||||
import { ConfigProvider } from 'antd'
|
||||
import zhCN from 'antd/locale/zh_CN'
|
||||
import { RouterProvider } from 'react-router'
|
||||
import { router } from '@/router'
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<ConfigProvider locale={zhCN}>
|
||||
<div style={{ padding: 24 }}>
|
||||
<h1>Novalon Manage System</h1>
|
||||
<p>React 19 迁移进行中...</p>
|
||||
</div>
|
||||
<RouterProvider router={router} />
|
||||
</ConfigProvider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Outlet } from 'react-router'
|
||||
|
||||
export default function DefaultLayout() {
|
||||
return (
|
||||
<div style={{ padding: 24 }}>
|
||||
<h1>DefaultLayout (TODO)</h1>
|
||||
<Outlet />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Result, Button } from 'antd'
|
||||
import { useNavigate } from 'react-router'
|
||||
|
||||
export default function Forbidden() {
|
||||
const navigate = useNavigate()
|
||||
return (
|
||||
<Result
|
||||
status="403"
|
||||
title="403"
|
||||
subTitle="抱歉,您没有权限访问此页面。"
|
||||
extra={<Button type="primary" onClick={() => navigate('/dashboard')}>返回首页</Button>}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export default function ExceptionLog() {
|
||||
return <div>ExceptionLog Page (TODO)</div>
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export default function LoginLog() {
|
||||
return <div>LoginLog Page (TODO)</div>
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export default function OperationLog() {
|
||||
return <div>OperationLog Page (TODO)</div>
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export default function ConfigManagement() {
|
||||
return <div>ConfigManagement Page (TODO)</div>
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export default function DictManagement() {
|
||||
return <div>DictManagement Page (TODO)</div>
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export default function Dashboard() {
|
||||
return <div>Dashboard Page (TODO)</div>
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export default function FileManagement() {
|
||||
return <div>FileManagement Page (TODO)</div>
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export default function Login() {
|
||||
return <div>Login Page (TODO)</div>
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export default function NoticeManagement() {
|
||||
return <div>NoticeManagement Page (TODO)</div>
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export default function MenuManagement() {
|
||||
return <div>MenuManagement Page (TODO)</div>
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export default function RoleManagement() {
|
||||
return <div>RoleManagement Page (TODO)</div>
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export default function UserManagement() {
|
||||
return <div>UserManagement Page (TODO)</div>
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { redirect } from 'react-router'
|
||||
import { useAuthStore } from '@/stores/useAuthStore'
|
||||
import { usePermissionStore } from '@/stores/usePermissionStore'
|
||||
|
||||
export async function authLoader() {
|
||||
const token = localStorage.getItem('token')
|
||||
|
||||
if (!token) {
|
||||
return redirect('/login')
|
||||
}
|
||||
|
||||
const authState = useAuthStore.getState()
|
||||
|
||||
if (!authState.initialized) {
|
||||
authState.initFromStorage()
|
||||
}
|
||||
|
||||
if (!authState.isAuthenticated) {
|
||||
return redirect('/login')
|
||||
}
|
||||
|
||||
const permState = usePermissionStore.getState()
|
||||
|
||||
if (!permState.loaded) {
|
||||
const restored = permState.initFromStorage()
|
||||
if (!restored) {
|
||||
try {
|
||||
await permState.fetchUserMenus()
|
||||
} catch {
|
||||
authState.logout()
|
||||
return redirect('/login')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { createBrowserRouter, Navigate } from 'react-router'
|
||||
import { authLoader } from './guards'
|
||||
import {
|
||||
DefaultLayout,
|
||||
Login,
|
||||
Dashboard,
|
||||
UserManagement,
|
||||
RoleManagement,
|
||||
MenuManagement,
|
||||
ConfigManagement,
|
||||
DictManagement,
|
||||
FileManagement,
|
||||
NoticeManagement,
|
||||
LoginLog,
|
||||
OperationLog,
|
||||
ExceptionLog,
|
||||
Forbidden,
|
||||
} from './routes'
|
||||
|
||||
export const router = createBrowserRouter([
|
||||
{
|
||||
path: '/login',
|
||||
element: <Login />,
|
||||
},
|
||||
{
|
||||
path: '/403',
|
||||
element: <Forbidden />,
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
element: <DefaultLayout />,
|
||||
loader: authLoader,
|
||||
children: [
|
||||
{ index: true, element: <Navigate to="/dashboard" replace /> },
|
||||
{ path: 'dashboard', element: <Dashboard /> },
|
||||
{ path: 'users', element: <UserManagement /> },
|
||||
{ path: 'roles', element: <RoleManagement /> },
|
||||
{ path: 'menus', element: <MenuManagement /> },
|
||||
{ path: 'sys/config', element: <ConfigManagement /> },
|
||||
{ path: 'dict', element: <DictManagement /> },
|
||||
{ path: 'files', element: <FileManagement /> },
|
||||
{ path: 'notice', element: <NoticeManagement /> },
|
||||
{ path: 'loginlog', element: <LoginLog /> },
|
||||
{ path: 'oplog', element: <OperationLog /> },
|
||||
{ path: 'exceptionlog', element: <ExceptionLog /> },
|
||||
],
|
||||
},
|
||||
])
|
||||
@@ -0,0 +1,19 @@
|
||||
import { lazy } from 'react'
|
||||
|
||||
const Login = lazy(() => import('@/pages/login'))
|
||||
const Dashboard = lazy(() => import('@/pages/dashboard'))
|
||||
const UserManagement = lazy(() => import('@/pages/system/user'))
|
||||
const RoleManagement = lazy(() => import('@/pages/system/role'))
|
||||
const MenuManagement = lazy(() => import('@/pages/system/menu'))
|
||||
const ConfigManagement = lazy(() => import('@/pages/config/config'))
|
||||
const DictManagement = lazy(() => import('@/pages/config/dict'))
|
||||
const FileManagement = lazy(() => import('@/pages/file'))
|
||||
const NoticeManagement = lazy(() => import('@/pages/notify'))
|
||||
const LoginLog = lazy(() => import('@/pages/audit/login-log'))
|
||||
const OperationLog = lazy(() => import('@/pages/audit/operation-log'))
|
||||
const ExceptionLog = lazy(() => import('@/pages/audit/exception-log'))
|
||||
const Forbidden = lazy(() => import('@/pages/403'))
|
||||
const DefaultLayout = lazy(() => import('@/layouts/DefaultLayout'))
|
||||
|
||||
export { authLoader } from './guards'
|
||||
export { DefaultLayout, Login, Dashboard, UserManagement, RoleManagement, MenuManagement, ConfigManagement, DictManagement, FileManagement, NoticeManagement, LoginLog, OperationLog, ExceptionLog, Forbidden }
|
||||
@@ -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