feat: admin frontend - React + Vite, auth pages, user management, roles, permissions, webhooks, devices, logs
This commit is contained in:
329
frontend/admin/src/layouts/AdminLayout/AdminLayout.tsx
Normal file
329
frontend/admin/src/layouts/AdminLayout/AdminLayout.tsx
Normal file
@@ -0,0 +1,329 @@
|
||||
/**
|
||||
* AdminLayout - 管理后台布局
|
||||
*
|
||||
* 布局:侧栏 248px + 顶栏 64px + 内容区
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Layout, Menu, Avatar, Dropdown, Spin, Drawer, Button, type MenuProps } from 'antd'
|
||||
import {
|
||||
DashboardOutlined,
|
||||
SafetyOutlined,
|
||||
FileTextOutlined,
|
||||
ApiOutlined,
|
||||
UserOutlined,
|
||||
MenuFoldOutlined,
|
||||
MenuUnfoldOutlined,
|
||||
MenuOutlined,
|
||||
LogoutOutlined,
|
||||
SettingOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import type { ReactNode } from 'react'
|
||||
import { Outlet, useLocation, useNavigate } from 'react-router-dom'
|
||||
import { useAuth } from '@/app/providers/auth-context'
|
||||
import { useBreadcrumbs } from '@/lib/hooks/useBreadcrumbs'
|
||||
import styles from './AdminLayout.module.css'
|
||||
|
||||
const { Sider, Header, Content } = Layout
|
||||
|
||||
// 管理员菜单配置
|
||||
const adminMenuItems: MenuProps['items'] = [
|
||||
{
|
||||
key: '/dashboard',
|
||||
icon: <DashboardOutlined />,
|
||||
label: '总览',
|
||||
},
|
||||
{
|
||||
key: 'access-control',
|
||||
icon: <SafetyOutlined />,
|
||||
label: '访问控制',
|
||||
children: [
|
||||
{ key: '/users', label: '用户管理' },
|
||||
{ key: '/roles', label: '角色管理' },
|
||||
{ key: '/permissions', label: '权限管理' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'logs',
|
||||
icon: <FileTextOutlined />,
|
||||
label: '审计日志',
|
||||
children: [
|
||||
{ key: '/logs/login', label: '登录日志' },
|
||||
{ key: '/logs/operation', label: '操作日志' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'integration',
|
||||
icon: <ApiOutlined />,
|
||||
label: '集成能力',
|
||||
children: [
|
||||
{ key: '/webhooks', label: 'Webhooks' },
|
||||
{ key: '/import-export', label: '导入导出' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'profile',
|
||||
icon: <UserOutlined />,
|
||||
label: '我的账户',
|
||||
children: [
|
||||
{ key: '/profile', label: '个人资料' },
|
||||
{ key: '/profile/security', label: '安全设置' },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
// 非管理员菜单配置(只有 Webhooks 和个人中心)
|
||||
const userMenuItems: MenuProps['items'] = [
|
||||
{
|
||||
key: '/webhooks',
|
||||
icon: <ApiOutlined />,
|
||||
label: 'Webhooks',
|
||||
},
|
||||
{
|
||||
key: 'profile',
|
||||
icon: <UserOutlined />,
|
||||
label: '我的账户',
|
||||
children: [
|
||||
{ key: '/profile', label: '个人资料' },
|
||||
{ key: '/profile/security', label: '安全设置' },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
interface AdminLayoutProps {
|
||||
children?: ReactNode
|
||||
}
|
||||
|
||||
export function AdminLayout({ children }: AdminLayoutProps) {
|
||||
const [collapsed, setCollapsed] = useState(false)
|
||||
const [mobileDrawerOpen, setMobileDrawerOpen] = useState(false)
|
||||
const [isMobile, setIsMobile] = useState(false)
|
||||
const location = useLocation()
|
||||
const navigate = useNavigate()
|
||||
const { user, isAdmin, logout, isLoading } = useAuth()
|
||||
const breadcrumbItems = useBreadcrumbs()
|
||||
|
||||
// 检测移动端
|
||||
useEffect(() => {
|
||||
const checkMobile = () => {
|
||||
setIsMobile(window.innerWidth < 768)
|
||||
}
|
||||
checkMobile()
|
||||
window.addEventListener('resize', checkMobile)
|
||||
return () => window.removeEventListener('resize', checkMobile)
|
||||
}, [])
|
||||
|
||||
// 移动端切换侧边栏
|
||||
const toggleMobileDrawer = () => {
|
||||
setMobileDrawerOpen(!mobileDrawerOpen)
|
||||
}
|
||||
|
||||
// 移动端菜单点击后关闭抽屉
|
||||
const handleMobileMenuClick: MenuProps['onClick'] = (info) => {
|
||||
navigate(info.key)
|
||||
setMobileDrawerOpen(false)
|
||||
}
|
||||
|
||||
// 根据是否为管理员选择菜单
|
||||
const menuItems = isAdmin ? adminMenuItems : userMenuItems
|
||||
|
||||
// 当前选中的菜单
|
||||
const selectedKeys = [location.pathname]
|
||||
|
||||
// 当前展开的菜单组(根据路径决定哪个分组展开)
|
||||
const openKeys = collapsed
|
||||
? []
|
||||
: [
|
||||
...(location.pathname.startsWith('/users') ||
|
||||
location.pathname.startsWith('/roles') ||
|
||||
location.pathname.startsWith('/permissions')
|
||||
? ['access-control']
|
||||
: []),
|
||||
...(location.pathname.startsWith('/logs') ? ['logs'] : []),
|
||||
...(location.pathname.startsWith('/webhooks') ||
|
||||
location.pathname.startsWith('/import-export')
|
||||
? ['integration']
|
||||
: []),
|
||||
...(location.pathname.startsWith('/profile') ? ['profile'] : []),
|
||||
]
|
||||
|
||||
const handleMenuClick: MenuProps['onClick'] = (info) => {
|
||||
navigate(info.key)
|
||||
}
|
||||
|
||||
// 处理面包屑点击
|
||||
const handleBreadcrumbClick = (path: string) => {
|
||||
navigate(path)
|
||||
}
|
||||
|
||||
// 处理登出
|
||||
const handleLogout = () => {
|
||||
void logout()
|
||||
}
|
||||
|
||||
// 用户下拉菜单
|
||||
const userDropdownItems: MenuProps['items'] = [
|
||||
{
|
||||
key: 'profile',
|
||||
icon: <UserOutlined />,
|
||||
label: '个人资料',
|
||||
onClick: () => navigate('/profile'),
|
||||
},
|
||||
{
|
||||
key: 'security',
|
||||
icon: <SettingOutlined />,
|
||||
label: '安全设置',
|
||||
onClick: () => navigate('/profile/security'),
|
||||
},
|
||||
{ type: 'divider' },
|
||||
{
|
||||
key: 'logout',
|
||||
icon: <LogoutOutlined />,
|
||||
label: '退出登录',
|
||||
danger: true,
|
||||
onClick: handleLogout,
|
||||
},
|
||||
]
|
||||
|
||||
// 加载中状态
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className={styles.loadingContainer}>
|
||||
<Spin size="large" tip="正在恢复会话..." />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout className={styles.layout}>
|
||||
{/* 跳过链接 - 便于键盘用户快速跳转到主要内容 */}
|
||||
<a href="#main-content" className={styles.skipLink}>
|
||||
跳转到主要内容
|
||||
</a>
|
||||
|
||||
{/* 侧边栏 */}
|
||||
<Sider
|
||||
collapsible
|
||||
collapsed={collapsed}
|
||||
onCollapse={setCollapsed}
|
||||
width={248}
|
||||
collapsedWidth={84}
|
||||
className={styles.sider}
|
||||
trigger={null}
|
||||
>
|
||||
{/* Logo 区域 */}
|
||||
<div className={styles.logo}>
|
||||
{collapsed ? 'UMS' : '用户管理系统'}
|
||||
</div>
|
||||
|
||||
{/* 导航菜单 */}
|
||||
<Menu
|
||||
mode="inline"
|
||||
selectedKeys={selectedKeys}
|
||||
defaultOpenKeys={openKeys}
|
||||
items={menuItems}
|
||||
onClick={handleMenuClick}
|
||||
className={styles.menu}
|
||||
style={{ pointerEvents: 'auto' }}
|
||||
/>
|
||||
</Sider>
|
||||
|
||||
{/* 右侧主体 */}
|
||||
<Layout>
|
||||
{/* 顶栏 */}
|
||||
<Header className={styles.header}>
|
||||
<div className={styles.headerLeft}>
|
||||
{/* 折叠/菜单按钮 - 移动端显示菜单图标,桌面端显示折叠图标 */}
|
||||
{isMobile ? (
|
||||
<Button
|
||||
type="text"
|
||||
icon={<MenuOutlined />}
|
||||
onClick={toggleMobileDrawer}
|
||||
className={styles.collapseBtn}
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
className={styles.collapseBtn}
|
||||
onClick={() => setCollapsed(!collapsed)}
|
||||
>
|
||||
{collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* 面包屑 */}
|
||||
{breadcrumbItems && breadcrumbItems.length > 0 && (
|
||||
<div className={styles.breadcrumb}>
|
||||
{breadcrumbItems.map((item, index) => (
|
||||
<span key={index}>
|
||||
{item.path ? (
|
||||
<a
|
||||
className={styles.breadcrumbLink}
|
||||
onClick={() => handleBreadcrumbClick(item.path as string)}
|
||||
>
|
||||
{item.title}
|
||||
</a>
|
||||
) : (
|
||||
<span className={styles.breadcrumbCurrent}>
|
||||
{item.title}
|
||||
</span>
|
||||
)}
|
||||
{index < breadcrumbItems.length - 1 && (
|
||||
<span className={styles.breadcrumbSeparator}>/</span>
|
||||
)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={styles.headerRight}>
|
||||
{/* 用户信息 */}
|
||||
<Dropdown menu={{ items: userDropdownItems }} placement="bottomRight">
|
||||
<div className={styles.userTrigger}>
|
||||
<Avatar
|
||||
size={32}
|
||||
icon={<UserOutlined />}
|
||||
src={user?.avatar || null}
|
||||
style={{ backgroundColor: user?.avatar ? undefined : 'var(--color-primary)' }}
|
||||
/>
|
||||
<span className={styles.userName}>
|
||||
{user?.nickname || user?.username || '用户'}
|
||||
</span>
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</Header>
|
||||
|
||||
{/* 内容区 */}
|
||||
<Content id="main-content" className={styles.content}>
|
||||
{children || <Outlet />}
|
||||
</Content>
|
||||
</Layout>
|
||||
|
||||
{/* 移动端抽屉式导航 */}
|
||||
<Drawer
|
||||
title={
|
||||
<div className={styles.logo}>
|
||||
{collapsed ? 'UMS' : '用户管理系统'}
|
||||
</div>
|
||||
}
|
||||
placement="left"
|
||||
onClose={toggleMobileDrawer}
|
||||
open={mobileDrawerOpen}
|
||||
size="default"
|
||||
className={styles.mobileDrawer}
|
||||
styles={{ body: { padding: 0 } }}
|
||||
>
|
||||
<Menu
|
||||
mode="inline"
|
||||
selectedKeys={selectedKeys}
|
||||
defaultOpenKeys={openKeys}
|
||||
items={menuItems}
|
||||
onClick={handleMobileMenuClick}
|
||||
className={styles.menu}
|
||||
style={{ pointerEvents: 'auto' }}
|
||||
/>
|
||||
</Drawer>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user