18 changed files with 380 additions and 167 deletions
@ -1,10 +1,13 @@ |
|||
import { BrowserRouter } from 'react-router-dom'; |
|||
import { RecoilRoot } from 'recoil'; |
|||
import { AppRouter } from './routes/AppRouter'; |
|||
|
|||
export function App() { |
|||
return ( |
|||
<BrowserRouter> |
|||
<AppRouter /> |
|||
</BrowserRouter> |
|||
<RecoilRoot> |
|||
<BrowserRouter> |
|||
<AppRouter /> |
|||
</BrowserRouter> |
|||
</RecoilRoot> |
|||
); |
|||
} |
@ -0,0 +1,33 @@ |
|||
import { Navigate, useLocation } from 'react-router-dom'; |
|||
import { useAuth } from '@/hooks/useAuth'; |
|||
import { hasPermission, Permission } from '@/constants/menuConfig'; |
|||
|
|||
interface ProtectedRouteProps { |
|||
children: React.ReactNode; |
|||
requiredPermission?: Permission; |
|||
} |
|||
|
|||
export function ProtectedRoute({ children, requiredPermission }: ProtectedRouteProps) { |
|||
const { isAuthenticated, userPermissions, loading } = useAuth(); |
|||
const location = useLocation(); |
|||
|
|||
if (loading) { |
|||
return ( |
|||
<div className="flex items-center justify-center min-h-screen"> |
|||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div> |
|||
</div> |
|||
); |
|||
} |
|||
|
|||
if (!isAuthenticated) { |
|||
// 将用户重定向到登录页面,但保存他们尝试访问的URL
|
|||
return <Navigate to="/login" state={{ from: location }} replace />; |
|||
} |
|||
|
|||
if (requiredPermission && !hasPermission(userPermissions, requiredPermission)) { |
|||
// 如果用户没有所需权限,重定向到403页面
|
|||
return <Navigate to="/403" replace />; |
|||
} |
|||
|
|||
return <>{children}</>; |
|||
} |
@ -1,43 +0,0 @@ |
|||
import { LucideIcon, LayoutDashboard, Users, Settings } from 'lucide-react'; |
|||
|
|||
interface MenuItem { |
|||
title: string; |
|||
icon: LucideIcon; |
|||
href: string; |
|||
children?: { |
|||
title: string; |
|||
href: string; |
|||
}[]; |
|||
} |
|||
|
|||
export const menuItems: MenuItem[] = [ |
|||
{ |
|||
title: '仪表盘', |
|||
icon: LayoutDashboard, |
|||
href: '/dashboard', |
|||
}, |
|||
{ |
|||
title: '用户管理', |
|||
icon: Users, |
|||
href: '/dashboard/users', |
|||
children: [ |
|||
{ |
|||
title: '用户列表', |
|||
href: '/dashboard/users/list', |
|||
}, |
|||
{ |
|||
title: '角色管理', |
|||
href: '/dashboard/users/roles', |
|||
}, |
|||
{ |
|||
title: '权限管理', |
|||
href: '/dashboard/users/permissions', |
|||
}, |
|||
], |
|||
}, |
|||
{ |
|||
title: '系统设置', |
|||
icon: Settings, |
|||
href: '/dashboard/settings', |
|||
}, |
|||
]; |
@ -0,0 +1,69 @@ |
|||
import { LucideIcon, LayoutDashboard, Users, Settings } from 'lucide-react'; |
|||
|
|||
// 定义权限类型
|
|||
export type Permission = |
|||
| 'dashboard.view' |
|||
| 'users.view' |
|||
| 'users.manage' |
|||
| 'roles.view' |
|||
| 'roles.manage' |
|||
| 'permissions.view' |
|||
| 'permissions.manage' |
|||
| 'settings.view' |
|||
| 'settings.manage'; |
|||
|
|||
export interface MenuItem { |
|||
title: string; |
|||
icon: LucideIcon; |
|||
href: string; |
|||
permission?: Permission; |
|||
children?: { |
|||
title: string; |
|||
href: string; |
|||
permission?: Permission; |
|||
}[]; |
|||
} |
|||
|
|||
export const menuItems: MenuItem[] = [ |
|||
{ |
|||
title: '仪表盘', |
|||
icon: LayoutDashboard, |
|||
href: '/dashboard', |
|||
permission: 'dashboard.view', |
|||
}, |
|||
{ |
|||
title: '用户管理', |
|||
icon: Users, |
|||
href: '/dashboard/users', |
|||
permission: 'users.view', |
|||
children: [ |
|||
{ |
|||
title: '用户列表', |
|||
href: '/dashboard/users/list', |
|||
permission: 'users.view', |
|||
}, |
|||
{ |
|||
title: '角色管理', |
|||
href: '/dashboard/users/roles', |
|||
permission: 'roles.view', |
|||
}, |
|||
{ |
|||
title: '权限管理', |
|||
href: '/dashboard/users/permissions', |
|||
permission: 'permissions.view', |
|||
}, |
|||
], |
|||
}, |
|||
{ |
|||
title: '系统设置', |
|||
icon: Settings, |
|||
href: '/dashboard/settings', |
|||
permission: 'settings.view', |
|||
}, |
|||
]; |
|||
|
|||
// 导出权限检查工具函数
|
|||
export const hasPermission = (userPermissions: Permission[], requiredPermission?: Permission): boolean => { |
|||
if (!requiredPermission) return true; // 如果没有设置权限要求,则默认允许访问
|
|||
return userPermissions.includes(requiredPermission); |
|||
}; |
@ -0,0 +1,53 @@ |
|||
import { useState, useEffect } from 'react'; |
|||
import type { Permission } from '@/constants/menuConfig'; |
|||
|
|||
interface User { |
|||
id: string; |
|||
name: string; |
|||
email: string; |
|||
permissions: Permission[]; |
|||
} |
|||
|
|||
export function useAuth() { |
|||
const [user, setUser] = useState<User | null>(null); |
|||
const [loading, setLoading] = useState(true); |
|||
|
|||
useEffect(() => { |
|||
// 这里应该从你的认证系统获取用户信息
|
|||
// 例如从localStorage、API或状态管理库中获取
|
|||
const fetchUser = async () => { |
|||
try { |
|||
// 示例:从localStorage获取用户信息
|
|||
const userData = localStorage.getItem('user'); |
|||
if (userData) { |
|||
setUser(JSON.parse(userData)); |
|||
} |
|||
} catch (error) { |
|||
console.error('Failed to fetch user:', error); |
|||
} finally { |
|||
setLoading(false); |
|||
} |
|||
}; |
|||
|
|||
fetchUser(); |
|||
}, []); |
|||
|
|||
const login = async (userData: User) => { |
|||
setUser(userData); |
|||
localStorage.setItem('user', JSON.stringify(userData)); |
|||
}; |
|||
|
|||
const logout = () => { |
|||
setUser(null); |
|||
localStorage.removeItem('user'); |
|||
}; |
|||
|
|||
return { |
|||
user, |
|||
loading, |
|||
userPermissions: user?.permissions || [], |
|||
isAuthenticated: !!user, |
|||
login, |
|||
logout |
|||
}; |
|||
} |
@ -1,11 +1,19 @@ |
|||
import { create } from 'zustand'; |
|||
import { atom, useRecoilState } from 'recoil'; |
|||
|
|||
interface DrawerState { |
|||
isOpen: boolean; |
|||
toggle: () => void; |
|||
} |
|||
const drawerState = atom({ |
|||
key: 'drawerState', |
|||
default: false, |
|||
}); |
|||
|
|||
export const useDrawer = create<DrawerState>((set) => ({ |
|||
isOpen: false, |
|||
toggle: () => set((state) => ({ isOpen: !state.isOpen })), |
|||
})); |
|||
export const useDrawer = () => { |
|||
const [isOpen, setIsOpen] = useRecoilState(drawerState); |
|||
|
|||
const toggle = () => { |
|||
setIsOpen((prev) => !prev); |
|||
}; |
|||
|
|||
return { |
|||
isOpen, |
|||
toggle, |
|||
}; |
|||
}; |
@ -1,19 +1,19 @@ |
|||
import { create } from 'zustand'; |
|||
import { persist } from 'zustand/middleware'; |
|||
import { atom, useRecoilState } from 'recoil'; |
|||
|
|||
interface SidebarState { |
|||
isCollapsed: boolean; |
|||
toggleSidebar: () => void; |
|||
} |
|||
const sidebarState = atom({ |
|||
key: 'sidebarState', |
|||
default: false, |
|||
}); |
|||
|
|||
export const useSidebarToggle = create<SidebarState>()( |
|||
persist( |
|||
(set) => ({ |
|||
isCollapsed: false, |
|||
toggleSidebar: () => set((state) => ({ isCollapsed: !state.isCollapsed })), |
|||
}), |
|||
{ |
|||
name: 'sidebar-state', |
|||
} |
|||
) |
|||
); |
|||
export const useSidebarToggle = () => { |
|||
const [isCollapsed, setIsCollapsed] = useRecoilState(sidebarState); |
|||
|
|||
const toggleSidebar = () => { |
|||
setIsCollapsed((prev) => !prev); |
|||
}; |
|||
|
|||
return { |
|||
isCollapsed, |
|||
toggleSidebar, |
|||
}; |
|||
}; |
@ -0,0 +1,29 @@ |
|||
import { useNavigate } from 'react-router-dom'; |
|||
import { Button } from '@/components/ui/Button'; |
|||
|
|||
export default function ForbiddenPage() { |
|||
const navigate = useNavigate(); |
|||
|
|||
return ( |
|||
<div className="flex min-h-screen flex-col items-center justify-center"> |
|||
<div className="text-center"> |
|||
<h1 className="text-6xl font-bold text-gray-900">403</h1> |
|||
<p className="mt-4 text-xl text-gray-600">抱歉,您没有权限访问此页面</p> |
|||
<div className="mt-8"> |
|||
<Button |
|||
onClick={() => navigate(-1)} |
|||
variant="outline" |
|||
className="mr-4" |
|||
> |
|||
返回上一页 |
|||
</Button> |
|||
<Button |
|||
onClick={() => navigate('/dashboard')} |
|||
> |
|||
返回首页 |
|||
</Button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
); |
|||
} |
Loading…
Reference in new issue