import { NavLink, Outlet, useNavigate, useLocation } from 'react-router-dom' import { AnimatePresence, motion } from 'framer-motion' import CommandPalette from '@/components/CommandPalette' import { LayoutDashboard, Users, Shield, MessageSquare, FolderTree, Leaf, LogOut, ChevronDown, Menu, FileText, Settings, Book, Home, Monitor, Hash, Folder, ChevronRight, Search, Bell, ChevronLeft, Bot, Radio, Image, Music, ScrollText, Sun, Moon, Trophy, Award, Star, Gift, List, } from 'lucide-react' import { useState, useMemo, useEffect } from 'react' import { useAuthStore } from '@/store/auth' import { useAppStore } from '@/store/app' import type { SystemMenu } from '@/api/system' import { Button } from '@/components/ui/button' import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' import { ScrollArea } from '@/components/ui/scroll-area' import TabBar from '@/components/TabBar' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu' import { cn } from '@/lib/utils' // Icon mapping const iconMap: Record = { dashboard: , home: , user: , users: , role: , shield: , system: , settings: , topic: , message: , post: , category: , tree: , plant: , leaf: , wiki: , book: , menu: , monitor: , client: , hash: , 'file-text': , folder: , bot: , ai: , radio: , image: , music: , scroll: , log: , trophy: , award: , star: , gift: , list: , } function getIcon(iconName?: string): React.ReactNode { if (!iconName) return const lower = iconName.toLowerCase() for (const [key, icon] of Object.entries(iconMap)) { if (lower.includes(key)) return icon } return } interface NavItem { title: string; href: string; icon: React.ReactNode permission?: string; children?: NavItem[] } function convertMenuToNavItem(menu: SystemMenu): NavItem { return { title: menu.title || menu.name, href: menu.path || menu.code || `/${menu.name.toLowerCase()}`, icon: getIcon(menu.icon), permission: menu.permission, children: menu.children?.filter(c => c.category !== 2).length ? menu.children.filter(c => c.category !== 2).map(convertMenuToNavItem) : undefined, } } // ==================== Nav Item ==================== function NavItemComponent({ item, collapsed, level = 0 }: { item: NavItem; collapsed: boolean; level?: number }) { const [open, setOpen] = useState(false) const hasPermission = useAuthStore(s => s.hasPermission) const location = useLocation() const hasActiveChild = item.children?.some(c => location.pathname.startsWith(c.href)) useEffect(() => { if (hasActiveChild && !collapsed) setOpen(true) }, [hasActiveChild, collapsed]) if (item.permission && !hasPermission(item.permission)) return null if (item.children?.length) { const visible = item.children.filter(c => !c.permission || hasPermission(c.permission)) if (!visible.length) return null return (
{open && !collapsed && (
{visible.map(child => )}
)}
) } return ( cn( 'flex items-center gap-3 rounded-xl px-3 py-2.5 text-sidebar-foreground transition-all duration-300 hover:bg-sidebar-accent/60 mb-1 group relative overflow-hidden', isActive && 'bg-primary/10 text-primary font-semibold', level > 0 && 'text-[13px] py-2' )}> {level === 0 && {item.icon}} {level > 0 && } {!collapsed && {item.title}} ) } // ==================== Breadcrumb ==================== function Breadcrumb() { const location = useLocation() const menus = useAuthStore(s => s.menus) const breadcrumbs = useMemo(() => { const segments = location.pathname.split('/').filter(Boolean) const crumbs: { title: string; href: string }[] = [] let currentPath = '' const findMenuItem = (items: SystemMenu[], target: string): SystemMenu | undefined => { for (const item of items) { if (item.path === target || item.code === target) return item if (item.children) { const found = findMenuItem(item.children, target); if (found) return found } } } segments.forEach(seg => { currentPath += `/${seg}` const menuItem = menus ? findMenuItem(menus, currentPath) : null crumbs.push({ title: menuItem ? (menuItem.title || menuItem.name) : seg.charAt(0).toUpperCase() + seg.slice(1), href: currentPath }) }) return crumbs }, [location.pathname, menus]) if (!breadcrumbs.length) return null return (
{breadcrumbs.length > 0 && } {breadcrumbs.map((crumb, i) => (
{i > 0 && } {i === breadcrumbs.length - 1 ? crumb.title : {crumb.title}}
))}
) } // ==================== Layout ==================== export default function AdminLayout() { const { sidebarOpen, mobileMenuOpen, toggleSidebar, setMobileMenu } = useAppStore() const { user, logout, menus } = useAuthStore() const navigate = useNavigate() const navItems = useMemo(() => { if (menus?.length) return menus.filter(m => m.category !== 2).map(convertMenuToNavItem) return [] }, [menus]) const handleLogout = async () => { await logout(); navigate('/login') } return } // ==================== Shell Render ==================== import { useRef } from 'react' function LayoutShell({ sidebarOpen, mobileMenuOpen, toggleSidebar, setMobileMenu, navItems, user, onLogout }: { sidebarOpen: boolean; mobileMenuOpen: boolean; toggleSidebar: () => void; setMobileMenu: (v: boolean) => void navItems: NavItem[]; user: import('@/api/system').SystemUser | null; onLogout: () => void }) { const { setCmdKOpen } = useAppStore() const location = useLocation() const spotlightRef = useRef(null) useEffect(() => { const handleMouseMove = (e: MouseEvent) => { if (spotlightRef.current) { spotlightRef.current.style.background = `radial-gradient(600px circle at ${e.clientX}px ${e.clientY}px, rgba(6,182,212,0.25), transparent 40%)` } } window.addEventListener('mousemove', handleMouseMove) return () => window.removeEventListener('mousemove', handleMouseMove) }, []) return (
{/* Interactive Cursor Spotlight (Illuminates the glass as you move the mouse) */}
{/* Global Ambient Background */}
{/* Cyan Glow Top Left */}
{/* Purple Glow Bottom Right */}
{/* Subtle Tech Grid */}
{/* Sidebar */} {/* Mobile overlay */} {mobileMenuOpen &&
setMobileMenu(false)} />} {/* Main */}
{/* Header */}
setCmdKOpen(true)}>
搜索菜单... ⌘K
{/* TabBar */} {/* Content */}
) }