- 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
188 lines
7.4 KiB
TypeScript
188 lines
7.4 KiB
TypeScript
"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"
|