monitor-ai/src/components/Sidebar.tsx

90 lines
3.7 KiB
TypeScript

'use client'
// src/components/Sidebar.tsx — 权限驱动侧边栏(遵循设计系统「统一管理布局」)
import { usePathname } from 'next/navigation'
import Link from 'next/link'
import { LayoutDashboard, Server, Bell, Clock, Shield, Users, FileText, Activity } from 'lucide-react'
import { hasPermission } from '@/lib/permissions'
interface NavItem { label: string; href: string; icon: React.ComponentType<{ size?: number }>; permission?: string }
interface NavSection { title?: string; items: NavItem[] }
export default function Sidebar() {
const pathname = usePathname()
const isActive = (href: string) => pathname === href || (href !== '/' && pathname.startsWith(href))
// 默认显示所有导航项(客户端权限在 API 层强制验证,此处仅 UI 过滤)
const userRole = 'admin' // TODO: 从 cookie 或 session 读取实际角色
const sections: NavSection[] = [
{
items: [
{ label: '仪表盘', href: '/', icon: LayoutDashboard },
],
},
{
title: '监控',
items: [
{ label: '服务管理', href: '/services', icon: Server, permission: 'services:view' },
],
},
{
title: '告警',
items: [
{ label: '告警设置', href: '/settings', icon: Bell, permission: 'alerts:manage' },
{ label: '告警历史', href: '/alerts', icon: Activity, permission: 'alerts:view' },
{ label: '状态历史', href: '/status-history', icon: Clock, permission: 'status_history:view' },
],
},
{
title: '管理',
items: [
{ label: '用户管理', href: '/admin/users', icon: Users, permission: 'users:view' },
{ label: '角色权限', href: '/admin/roles', icon: Shield, permission: 'roles:manage' },
{ label: '审计日志', href: '/admin/audit-logs', icon: FileText, permission: 'audit:view' },
],
},
]
return (
<aside className="fixed left-0 top-0 bottom-0 w-60 bg-white dark:bg-slate-900 border-r border-slate-200 dark:border-slate-800 z-[200] flex flex-col">
{/* 品牌区 */}
<div className="h-14 flex items-center px-6 border-b border-slate-200 dark:border-slate-800 shrink-0">
<Link href="/" className="flex items-center gap-3">
<div className="w-8 h-8 rounded-lg bg-indigo-600 flex items-center justify-center text-white font-bold text-sm">M</div>
<span className="font-semibold text-slate-900 dark:text-white">monitor-ai</span>
</Link>
</div>
{/* 导航区 */}
<nav className="flex-1 overflow-y-auto p-3 space-y-6">
{sections.map((section, i) => (
<div key={i}>
{section.title && (
<p className="px-3 mb-1 text-xs font-semibold text-slate-400 dark:text-slate-500 uppercase tracking-widest">
{section.title}
</p>
)}
{/* 客户端仅控制导航可见性,实际权限由 middleware + API 验证 */}
{section.items.filter(item => !item.permission || hasPermission(userRole, item.permission)).map(item => (
<Link
key={item.href}
href={item.href}
className={`flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-colors mb-0.5 ${
isActive(item.href)
? 'bg-indigo-50 text-indigo-700 dark:bg-indigo-500/10 dark:text-indigo-400'
: 'text-slate-700 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-800'
}`}
>
<item.icon size={18} />
<span>{item.label}</span>
</Link>
))}
</div>
))}
</nav>
{/* 用户信息和退出在 TopBar 中,此处不重复 */}
</aside>
)
}