Hair Keeper v1.0.0:一个高度集成、深度定制、约定优于配置的全栈Web应用模板,旨在保持灵活性的同时提供一套基于成熟架构的开发底座,自带身份认证、权限控制、丰富前端组件、文件上传、后台任务、智能体开发等丰富功能,提供AI开发辅助,免于纠结功能如何实现,可快速上手专注于业务逻辑
This commit is contained in:
157
src/components/layout/app-sidebar.tsx
Normal file
157
src/components/layout/app-sidebar.tsx
Normal file
@@ -0,0 +1,157 @@
|
||||
'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)
|
||||
}, 300)
|
||||
}
|
||||
}, [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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user