157 lines
6.2 KiB
TypeScript
157 lines
6.2 KiB
TypeScript
'use client'
|
|
|
|
import * as React from 'react'
|
|
import Link from 'next/link'
|
|
import { usePathname, useRouter } from 'next/navigation'
|
|
import { Package, ChevronRight } from 'lucide-react'
|
|
import { Badge } from '@/components/ui/badge'
|
|
import {
|
|
Sidebar,
|
|
SidebarContent,
|
|
SidebarGroup,
|
|
SidebarHeader,
|
|
SidebarMenu,
|
|
SidebarMenuButton,
|
|
SidebarMenuItem,
|
|
SidebarMenuSub,
|
|
SidebarMenuSubButton,
|
|
SidebarMenuSubItem,
|
|
SidebarRail,
|
|
useSidebar,
|
|
} from '@/components/ui/sidebar'
|
|
import {
|
|
Collapsible,
|
|
CollapsibleContent,
|
|
CollapsibleTrigger,
|
|
} from '@/components/ui/collapsible'
|
|
import type { MenuItem } from '@/constants/menu'
|
|
import { getMenuIcon } from '@/constants/menu-icons'
|
|
import { SITE_NAME, SITE_VERSION } from '@/constants/site'
|
|
|
|
interface AppSidebarProps extends React.ComponentProps<typeof Sidebar> {
|
|
menuItems: MenuItem[]
|
|
}
|
|
|
|
export function AppSidebar({ menuItems, ...props }: AppSidebarProps) {
|
|
const pathname = usePathname()
|
|
const router = useRouter()
|
|
const { isMobile, setOpenMobile } = useSidebar()
|
|
|
|
// 移动端点击菜单项后关闭侧边栏,延迟导航避免闪动
|
|
const handleMenuItemClick = React.useCallback((e: React.MouseEvent<HTMLAnchorElement>, href: string) => {
|
|
if (isMobile) {
|
|
e.preventDefault() // 阻止默认跳转
|
|
setOpenMobile(false) // 先关闭侧边栏
|
|
// 等待侧边栏关闭动画完成后再导航
|
|
setTimeout(() => {
|
|
router.push(href)
|
|
}, 350)
|
|
}
|
|
}, [isMobile, setOpenMobile, router])
|
|
|
|
return (
|
|
<Sidebar collapsible="icon" {...props}>
|
|
<SidebarHeader>
|
|
<SidebarMenu>
|
|
<SidebarMenuItem>
|
|
<SidebarMenuButton size="lg" asChild>
|
|
<Link href="/">
|
|
<div className="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg">
|
|
<Package className="size-4" />
|
|
</div>
|
|
<div className="grid flex-1 text-left text-sm leading-tight">
|
|
<span className="truncate font-semibold">{SITE_NAME}</span>
|
|
<span className="truncate text-xs">{SITE_VERSION}</span>
|
|
</div>
|
|
</Link>
|
|
</SidebarMenuButton>
|
|
</SidebarMenuItem>
|
|
</SidebarMenu>
|
|
</SidebarHeader>
|
|
<SidebarContent>
|
|
<SidebarGroup>
|
|
<SidebarMenu className="gap-2">
|
|
{menuItems.map((item) => {
|
|
const Icon = getMenuIcon(item.icon)
|
|
const isActive = pathname === item.href || pathname.startsWith(item.href + "/")
|
|
const hasActiveChild = item.children?.some(
|
|
(child) => pathname === child.href || pathname.startsWith(item.href + "/")
|
|
)
|
|
|
|
// 如果有子菜单,使用 Collapsible
|
|
if (item.children?.length) {
|
|
return (
|
|
<Collapsible
|
|
key={item.title}
|
|
asChild
|
|
defaultOpen={hasActiveChild}
|
|
className="group/collapsible"
|
|
>
|
|
<SidebarMenuItem>
|
|
<CollapsibleTrigger asChild>
|
|
<SidebarMenuButton
|
|
tooltip={item.title}
|
|
isActive={isActive || hasActiveChild}
|
|
>
|
|
{Icon && <Icon />}
|
|
<span className="truncate">{item.title}</span>
|
|
{item.badge && (
|
|
<Badge variant="secondary" className="ml-auto shrink-0">
|
|
{item.badge}
|
|
</Badge>
|
|
)}
|
|
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
|
|
</SidebarMenuButton>
|
|
</CollapsibleTrigger>
|
|
<CollapsibleContent>
|
|
<SidebarMenuSub className="mt-1">
|
|
{item.children.map((child) => {
|
|
const ChildIcon = getMenuIcon(child.icon)
|
|
const childIsActive = pathname === child.href || pathname.startsWith(child.href + "/")
|
|
return (
|
|
<SidebarMenuSubItem key={child.title}>
|
|
<SidebarMenuSubButton asChild isActive={childIsActive}>
|
|
<Link href={child.href || '#'} onClick={(e) => handleMenuItemClick(e, child.href || '#')}>
|
|
{ChildIcon && <ChildIcon className="h-4 w-4" />}
|
|
<span className="truncate">{child.title}</span>
|
|
{child.badge && (
|
|
<Badge variant="secondary" className="ml-auto shrink-0">
|
|
{child.badge}
|
|
</Badge>
|
|
)}
|
|
</Link>
|
|
</SidebarMenuSubButton>
|
|
</SidebarMenuSubItem>
|
|
)
|
|
})}
|
|
</SidebarMenuSub>
|
|
</CollapsibleContent>
|
|
</SidebarMenuItem>
|
|
</Collapsible>
|
|
)
|
|
}
|
|
|
|
// 没有子菜单的普通菜单项
|
|
return (
|
|
<SidebarMenuItem key={item.title}>
|
|
<SidebarMenuButton asChild isActive={isActive} tooltip={item.title}>
|
|
<Link href={item.href || '#'} onClick={(e) => handleMenuItemClick(e, item.href || '#')}>
|
|
{Icon && <Icon />}
|
|
<span className="truncate">{item.title}</span>
|
|
{item.badge && (
|
|
<Badge variant="secondary" className="ml-auto shrink-0">
|
|
{item.badge}
|
|
</Badge>
|
|
)}
|
|
</Link>
|
|
</SidebarMenuButton>
|
|
</SidebarMenuItem>
|
|
)
|
|
})}
|
|
</SidebarMenu>
|
|
</SidebarGroup>
|
|
</SidebarContent>
|
|
<SidebarRail />
|
|
</Sidebar>
|
|
)
|
|
} |