diff --git a/novalon-manage-web/src/App.tsx b/novalon-manage-web/src/App.tsx
index e490ad3..bb5d2f9 100644
--- a/novalon-manage-web/src/App.tsx
+++ b/novalon-manage-web/src/App.tsx
@@ -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 (
-
-
Novalon Manage System
-
React 19 迁移进行中...
-
+
)
}
diff --git a/novalon-manage-web/src/layouts/DefaultLayout.tsx b/novalon-manage-web/src/layouts/DefaultLayout.tsx
new file mode 100644
index 0000000..a1adf1d
--- /dev/null
+++ b/novalon-manage-web/src/layouts/DefaultLayout.tsx
@@ -0,0 +1,10 @@
+import { Outlet } from 'react-router'
+
+export default function DefaultLayout() {
+ return (
+
+
DefaultLayout (TODO)
+
+
+ )
+}
diff --git a/novalon-manage-web/src/pages/403/index.tsx b/novalon-manage-web/src/pages/403/index.tsx
new file mode 100644
index 0000000..5a2f051
--- /dev/null
+++ b/novalon-manage-web/src/pages/403/index.tsx
@@ -0,0 +1,14 @@
+import { Result, Button } from 'antd'
+import { useNavigate } from 'react-router'
+
+export default function Forbidden() {
+ const navigate = useNavigate()
+ return (
+ navigate('/dashboard')}>返回首页}
+ />
+ )
+}
diff --git a/novalon-manage-web/src/pages/audit/exception-log/index.tsx b/novalon-manage-web/src/pages/audit/exception-log/index.tsx
new file mode 100644
index 0000000..f56cacf
--- /dev/null
+++ b/novalon-manage-web/src/pages/audit/exception-log/index.tsx
@@ -0,0 +1,3 @@
+export default function ExceptionLog() {
+ return ExceptionLog Page (TODO)
+}
diff --git a/novalon-manage-web/src/pages/audit/login-log/index.tsx b/novalon-manage-web/src/pages/audit/login-log/index.tsx
new file mode 100644
index 0000000..9ea25b5
--- /dev/null
+++ b/novalon-manage-web/src/pages/audit/login-log/index.tsx
@@ -0,0 +1,3 @@
+export default function LoginLog() {
+ return LoginLog Page (TODO)
+}
diff --git a/novalon-manage-web/src/pages/audit/operation-log/index.tsx b/novalon-manage-web/src/pages/audit/operation-log/index.tsx
new file mode 100644
index 0000000..8254cc9
--- /dev/null
+++ b/novalon-manage-web/src/pages/audit/operation-log/index.tsx
@@ -0,0 +1,3 @@
+export default function OperationLog() {
+ return OperationLog Page (TODO)
+}
diff --git a/novalon-manage-web/src/pages/config/config/index.tsx b/novalon-manage-web/src/pages/config/config/index.tsx
new file mode 100644
index 0000000..04e2412
--- /dev/null
+++ b/novalon-manage-web/src/pages/config/config/index.tsx
@@ -0,0 +1,3 @@
+export default function ConfigManagement() {
+ return ConfigManagement Page (TODO)
+}
diff --git a/novalon-manage-web/src/pages/config/dict/index.tsx b/novalon-manage-web/src/pages/config/dict/index.tsx
new file mode 100644
index 0000000..3f9ecf2
--- /dev/null
+++ b/novalon-manage-web/src/pages/config/dict/index.tsx
@@ -0,0 +1,3 @@
+export default function DictManagement() {
+ return DictManagement Page (TODO)
+}
diff --git a/novalon-manage-web/src/pages/dashboard/index.tsx b/novalon-manage-web/src/pages/dashboard/index.tsx
new file mode 100644
index 0000000..89f19aa
--- /dev/null
+++ b/novalon-manage-web/src/pages/dashboard/index.tsx
@@ -0,0 +1,3 @@
+export default function Dashboard() {
+ return Dashboard Page (TODO)
+}
diff --git a/novalon-manage-web/src/pages/file/index.tsx b/novalon-manage-web/src/pages/file/index.tsx
new file mode 100644
index 0000000..ab11ae8
--- /dev/null
+++ b/novalon-manage-web/src/pages/file/index.tsx
@@ -0,0 +1,3 @@
+export default function FileManagement() {
+ return FileManagement Page (TODO)
+}
diff --git a/novalon-manage-web/src/pages/login/index.tsx b/novalon-manage-web/src/pages/login/index.tsx
new file mode 100644
index 0000000..a4a88dc
--- /dev/null
+++ b/novalon-manage-web/src/pages/login/index.tsx
@@ -0,0 +1,3 @@
+export default function Login() {
+ return Login Page (TODO)
+}
diff --git a/novalon-manage-web/src/pages/notify/index.tsx b/novalon-manage-web/src/pages/notify/index.tsx
new file mode 100644
index 0000000..bff150b
--- /dev/null
+++ b/novalon-manage-web/src/pages/notify/index.tsx
@@ -0,0 +1,3 @@
+export default function NoticeManagement() {
+ return NoticeManagement Page (TODO)
+}
diff --git a/novalon-manage-web/src/pages/system/menu/index.tsx b/novalon-manage-web/src/pages/system/menu/index.tsx
new file mode 100644
index 0000000..17ffb7f
--- /dev/null
+++ b/novalon-manage-web/src/pages/system/menu/index.tsx
@@ -0,0 +1,3 @@
+export default function MenuManagement() {
+ return MenuManagement Page (TODO)
+}
diff --git a/novalon-manage-web/src/pages/system/role/index.tsx b/novalon-manage-web/src/pages/system/role/index.tsx
new file mode 100644
index 0000000..c27956d
--- /dev/null
+++ b/novalon-manage-web/src/pages/system/role/index.tsx
@@ -0,0 +1,3 @@
+export default function RoleManagement() {
+ return RoleManagement Page (TODO)
+}
diff --git a/novalon-manage-web/src/pages/system/user/index.tsx b/novalon-manage-web/src/pages/system/user/index.tsx
new file mode 100644
index 0000000..c2ad5ef
--- /dev/null
+++ b/novalon-manage-web/src/pages/system/user/index.tsx
@@ -0,0 +1,3 @@
+export default function UserManagement() {
+ return UserManagement Page (TODO)
+}
diff --git a/novalon-manage-web/src/router/guards.tsx b/novalon-manage-web/src/router/guards.tsx
new file mode 100644
index 0000000..78894eb
--- /dev/null
+++ b/novalon-manage-web/src/router/guards.tsx
@@ -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
+}
diff --git a/novalon-manage-web/src/router/index.tsx b/novalon-manage-web/src/router/index.tsx
new file mode 100644
index 0000000..5d425ba
--- /dev/null
+++ b/novalon-manage-web/src/router/index.tsx
@@ -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: ,
+ },
+ {
+ path: '/403',
+ element: ,
+ },
+ {
+ path: '/',
+ element: ,
+ loader: authLoader,
+ children: [
+ { index: true, element: },
+ { path: 'dashboard', element: },
+ { path: 'users', element: },
+ { path: 'roles', element: },
+ { path: 'menus', element: },
+ { path: 'sys/config', element: },
+ { path: 'dict', element: },
+ { path: 'files', element: },
+ { path: 'notice', element: },
+ { path: 'loginlog', element: },
+ { path: 'oplog', element: },
+ { path: 'exceptionlog', element: },
+ ],
+ },
+])
diff --git a/novalon-manage-web/src/router/routes.ts b/novalon-manage-web/src/router/routes.ts
new file mode 100644
index 0000000..a1ca388
--- /dev/null
+++ b/novalon-manage-web/src/router/routes.ts
@@ -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 }
diff --git a/novalon-manage-web/src/stores/useAppStore.ts b/novalon-manage-web/src/stores/useAppStore.ts
new file mode 100644
index 0000000..6c49b3f
--- /dev/null
+++ b/novalon-manage-web/src/stores/useAppStore.ts
@@ -0,0 +1,13 @@
+import { create } from 'zustand'
+
+interface AppState {
+ collapsed: boolean
+ toggleCollapsed: () => void
+ setCollapsed: (val: boolean) => void
+}
+
+export const useAppStore = create((set) => ({
+ collapsed: false,
+ toggleCollapsed: () => set((state) => ({ collapsed: !state.collapsed })),
+ setCollapsed: (val: boolean) => set({ collapsed: val }),
+}))
diff --git a/novalon-manage-web/src/stores/useAuthStore.ts b/novalon-manage-web/src/stores/useAuthStore.ts
new file mode 100644
index 0000000..8dca1a6
--- /dev/null
+++ b/novalon-manage-web/src/stores/useAuthStore.ts
@@ -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
+ logout: () => void
+ initFromStorage: () => void
+ setInitialized: (val: boolean) => void
+}
+
+export const useAuthStore = create((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(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(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 }),
+}))
diff --git a/novalon-manage-web/src/stores/usePermissionStore.ts b/novalon-manage-web/src/stores/usePermissionStore.ts
new file mode 100644
index 0000000..1eb39bd
--- /dev/null
+++ b/novalon-manage-web/src/stores/usePermissionStore.ts
@@ -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
+ hasPermission: (permission: string) => boolean
+ hasRole: (role: string) => boolean
+ clearPermissionData: () => void
+ initFromStorage: () => boolean
+}
+
+export const usePermissionStore = create((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
+}