Files
NextEdu/src/modules/layout/components/app-sidebar.tsx
SpecialX 8c2fe14c20 refactor(modules): update classes, course-plans, diagnostic, questions, settings, student, layout
- Update classes data-access (invitations, main) for invitation management

- Update course-plans actions, data-access, and types

- Update diagnostic data-access for report queries

- Update questions data-access for question bank queries

- Update settings actions, ai-provider-settings-card, data-access, and types

- Update student course-filters, student-courses-view, student-schedule-filters, student-schedule-view

- Update layout app-sidebar, site-header, and navigation config
2026-06-24 12:03:35 +08:00

188 lines
7.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client"
import * as React from "react"
import Link from "next/link"
import { usePathname } from "next/navigation"
import { ChevronRight } from "lucide-react"
import { useTranslations } from "next-intl"
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/shared/components/ui/collapsible"
import { ScrollArea } from "@/shared/components/ui/scroll-area"
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/shared/components/ui/tooltip"
import { cn } from "@/shared/lib/utils"
import { usePermission } from "@/shared/hooks"
import { UnreadMessageBadge } from "@/modules/messaging/components/unread-message-badge"
import { useSidebar } from "./sidebar-provider"
import { NAV_CONFIG, Role } from "../config/navigation"
interface AppSidebarProps {
mode?: "mobile" | "desktop"
}
export function AppSidebar({ mode }: AppSidebarProps) {
const { expanded, toggleSidebar, isMobile } = useSidebar()
const pathname = usePathname()
const { permissions, hasRole } = usePermission()
const tNav = useTranslations("nav")
const tCommon = useTranslations("common")
// 自动检测当前角色(优先级 admin > student > parent > teacher
// 注意grade_head / teaching_head 统一归入 teacher因为 teacher 导航已通过
// 权限点GRADE_MANAGE 等)动态显示班主任专属功能,无需切换角色。
function detectAutoRole(): Role {
if (hasRole("admin")) return "admin"
if (hasRole("student")) return "student"
if (hasRole("parent")) return "parent"
return "teacher"
}
const effectiveRole: Role = detectAutoRole()
const allNavItems = NAV_CONFIG[effectiveRole] ?? NAV_CONFIG.teacher ?? []
// Filter nav items by permission
const navItems = allNavItems.filter((item) => {
if (!item.permission) return true
return permissions.includes(item.permission)
}).map((item) => ({
...item,
items: item.items?.filter((subItem) => {
if (!subItem.permission) return true
return permissions.includes(subItem.permission)
}),
}))
// Ensure consistent state for hydration
if (!expanded && mode === 'mobile') return null
return (
<div className="flex h-full flex-col gap-4">
{/* Sidebar Header */}
<div className={cn("flex h-16 items-center border-b px-4 transition-all duration-300", !expanded && !isMobile ? "justify-center px-2" : "justify-between")}>
{expanded || isMobile ? (
<Link href="/" className="flex items-center gap-2 font-bold">
<div className="bg-primary text-primary-foreground flex size-8 items-center justify-center rounded-lg">
NE
</div>
<span className="truncate text-lg">Next_Edu</span>
</Link>
) : (
<div className="bg-primary text-primary-foreground flex size-8 items-center justify-center rounded-lg">
NE
</div>
)}
</div>
{/* Navigation */}
<ScrollArea className="flex-1 px-3">
<nav className="flex flex-col gap-2 py-4">
<TooltipProvider delayDuration={0}>
{navItems.map((item, index) => {
const isActive = pathname.startsWith(item.href)
const hasChildren = item.items && item.items.length > 0
const title = tNav(item.title)
if (!expanded && !isMobile) {
// Collapsed Mode (Icon Only + Tooltip)
return (
<Tooltip key={index}>
<TooltipTrigger asChild>
<Link
href={item.href}
className={cn(
"hover:bg-sidebar-accent hover:text-sidebar-accent-foreground flex size-10 items-center justify-center rounded-md transition-colors",
isActive && "bg-sidebar-accent text-sidebar-accent-foreground"
)}
>
<item.icon className="size-5" />
<span className="sr-only">{title}</span>
</Link>
</TooltipTrigger>
<TooltipContent side="right">{title}</TooltipContent>
</Tooltip>
)
}
// Expanded Mode
if (hasChildren) {
return (
<Collapsible key={index} defaultOpen={isActive} className="group/collapsible">
<CollapsibleTrigger asChild>
<button
className={cn(
"hover:bg-sidebar-accent hover:text-sidebar-accent-foreground flex w-full items-center justify-between rounded-md p-2 text-sm font-medium transition-colors",
isActive && "text-sidebar-accent-foreground"
)}
>
<div className="flex items-center gap-2">
<item.icon className="size-4" />
<span>{title}</span>
</div>
<ChevronRight className="text-muted-foreground size-4 transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
</button>
</CollapsibleTrigger>
<CollapsibleContent className="data-[state=open]:animate-accordion-down data-[state=closed]:animate-accordion-up overflow-hidden">
<div className="ml-6 mt-1 flex flex-col gap-1 border-l pl-2">
{item.items?.map((subItem, subIndex) => (
<Link
key={subIndex}
href={subItem.href}
className={cn(
"text-muted-foreground hover:text-foreground block rounded-md px-2 py-1 text-sm transition-colors",
pathname === subItem.href && "text-foreground font-medium"
)}
>
{tNav(subItem.title)}
</Link>
))}
</div>
</CollapsibleContent>
</Collapsible>
)
}
return (
<Link
key={index}
href={item.href}
className={cn(
"hover:bg-sidebar-accent hover:text-sidebar-accent-foreground flex items-center gap-2 rounded-md p-2 text-sm font-medium transition-colors",
isActive && "bg-sidebar-accent text-sidebar-accent-foreground"
)}
>
<item.icon className="size-4" />
<span>{title}</span>
{item.href === "/messages" ? <UnreadMessageBadge /> : null}
</Link>
)
})}
</TooltipProvider>
</nav>
</ScrollArea>
{/* Sidebar Footer */}
<div className="p-4">
{!isMobile && (
<button
onClick={toggleSidebar}
className="hover:bg-sidebar-accent text-sidebar-foreground flex w-full items-center justify-center rounded-md border p-2 text-sm transition-colors"
>
{expanded ? tCommon("sidebar.collapse") : <ChevronRight className="size-4" />}
</button>
)}
</div>
</div>
)
}
AppSidebar.displayName = "AppSidebar"