Hair Keeper v1.0.0:一个高度集成、深度定制、约定优于配置的全栈Web应用模板,旨在保持灵活性的同时提供一套基于成熟架构的开发底座,自带身份认证、权限控制、丰富前端组件、文件上传、后台任务、智能体开发等丰富功能,提供AI开发辅助,免于纠结功能如何实现,可快速上手专注于业务逻辑

This commit is contained in:
2025-11-13 15:24:54 +08:00
commit 42be39b343
249 changed files with 38843 additions and 0 deletions

View 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>
)
}