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 +}