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
+3 -4
View File
@@ -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>
}
+37
View File
@@ -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
}
+48
View File
@@ -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 /> },
],
},
])
+19
View File
@@ -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
}