'use client'; import * as React from 'react'; import Link from 'next/link'; import { usePathname, useRouter } from 'next/navigation'; import { APP_BUILD_DATE, APP_VERSION } from '@/lib/app-info'; import { toast } from 'sonner'; import { Button } from '@/components/ui/button'; import { Spinner } from '@/components/ui/spinner'; import { AnimateIcon } from '@/components/animate-ui/icons/icon'; import { ChevronLeft } from '@/components/animate-ui/icons/chevron-left'; import { ChevronRight } from '@/components/animate-ui/icons/chevron-right'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { getCurrentTheme } from '@/components/layout/avater-style/registry'; import { Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupContent, SidebarGroupLabel, SidebarHeader, SidebarMenu, SidebarMenuButton, SidebarMenuItem, useSidebar, } from '@/components/ui/sidebar'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog'; import { ArrowUpRight, Bell, ChevronDown, Code, CreditCard, Database, FileQuestionMark, FileText, FolderOpen, Home, Layers, LogOut, MessagesSquare, Settings, ShieldCheck, Terminal, UserRound, } from 'lucide-react'; import { useTranslations } from 'next-intl'; import { useUser } from '@/contexts/user-context'; import { usePublicConfig } from '@/hooks/use-public-config'; type NavItem = { titleKey: string; url: string; icon: React.ComponentType; external?: boolean; }; const navMainItems: NavItem[] = [ { titleKey: 'home', url: '/home', icon: Home }, { titleKey: 'myFiles', url: '/files', icon: FolderOpen }, ]; const adminItems: NavItem[] = [ { titleKey: 'users', url: '/admin/users', icon: UserRound }, { titleKey: 'tasks', url: '/admin/tasks', icon: Layers }, { titleKey: 'storage', url: '/admin/files', icon: FolderOpen }, { titleKey: 'database', url: '/admin/database', icon: Database }, { titleKey: 'push', url: '/admin/push', icon: Bell }, { titleKey: 'messageGateway', url: '/admin/message-gateway', icon: MessagesSquare }, { titleKey: 'logs', url: '/admin/logs', icon: Terminal }, { titleKey: 'system', url: '/admin/system', icon: ShieldCheck }, { titleKey: 'adminSettings', url: '/admin/settings', icon: Settings }, ]; const documentItems: NavItem[] = [ { titleKey: 'demo', url: '/admin/demo', icon: Code }, { titleKey: 'apiDocs', url: '/docs/api', icon: CreditCard, external: true }, { titleKey: 'usageDocs', url: '/docs/how-to-use', icon: FileText, external: true, }, ]; function parseMenuDisplayConfig( raw: string | undefined, ): Record { if (!raw) return {}; try { const parsed: unknown = JSON.parse(raw); if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return {}; return Object.entries(parsed).reduce>( (result, [key, value]) => { if (typeof value === 'boolean') { result[key] = value; } return result; }, {}, ); } catch { return {}; } } /** * 应用侧边栏组件 * 显示应用侧边栏 * * @example * ```tsx * * ``` * @returns {React.ReactNode} 应用侧边栏组件 */ export function AppSidebar({ ...props }: React.ComponentProps) { const { toggleSidebar, state, isMobile, setOpenMobile } = useSidebar(); const { user, logout, loading: userLoading } = useUser(); const { config } = usePublicConfig(); const t = useTranslations('layout'); const tCommon = useTranslations('common'); const [showLogoutDialog, setShowLogoutDialog] = React.useState(false); const [isLoggingOut, setIsLoggingOut] = React.useState(false); const pathname = usePathname(); const router = useRouter(); const currentTheme = getCurrentTheme(); const { MainDecoration, InteractionDecoration, BackgroundEffect, smallMainDecorationConfig, largeMainDecorationConfig, interactionDecorationConfig, } = currentTheme; const hasAvatarDecoration = currentTheme.key !== 'none'; const [showEffect, setShowEffect] = React.useState(false); const [userMenuOpen, setUserMenuOpen] = React.useState(false); const userMenuLastToggle = React.useRef(0); const handleUserMenuOpenChange = React.useCallback((open: boolean) => { const now = Date.now(); if (now - userMenuLastToggle.current < 65) return; userMenuLastToggle.current = now; setUserMenuOpen(open); }, []); const handleCloseSidebar = React.useCallback(() => { if (isMobile) { setOpenMobile(false); } }, [isMobile, setOpenMobile]); const handleLogout = async () => { setIsLoggingOut(true); try { await logout(); setShowLogoutDialog(false); } catch (error) { toast.error(t('logoutDialog.failed'), { description: error instanceof Error ? error.message : t('logoutDialog.failedDescription'), }); setIsLoggingOut(false); } }; const navMainFiltered = React.useMemo(() => { const displayConfig = parseMenuDisplayConfig(config?.menu_display_config); return navMainItems.filter((item) => displayConfig[item.url] !== false); }, [config]); const adminFiltered = React.useMemo(() => { const displayConfig = parseMenuDisplayConfig(config?.menu_display_config); return adminItems.filter((item) => displayConfig[item.url] !== false); }, [config]); const documentFiltered = React.useMemo(() => { const displayConfig = parseMenuDisplayConfig(config?.menu_display_config); return documentItems.filter((item) => displayConfig[item.url] !== false); }, [config]); return ( <> {hasAvatarDecoration && showEffect && BackgroundEffect && ( )}
setShowEffect(!showEffect) : undefined } > {hasAvatarDecoration && ( )}
{hasAvatarDecoration && ( )} {user?.nickname?.charAt(0)?.toUpperCase() || 'L'}
{userLoading ? tCommon('loading') : user?.nickname || user?.username || t('user.notLoggedIn')} {userLoading ? t('user.syncing') : user?.is_admin ? t('user.adminRole') : t('user.userRole')}
{ router.push('/settings/profile'); handleCloseSidebar(); }} > {t('user.profile')} { router.push('/settings'); handleCloseSidebar(); }} > {t('user.settings')} { router.push('/docs/how-to-use'); handleCloseSidebar(); }} > {t('user.help')} setShowLogoutDialog(true)} > {t('user.logout')}
{navMainFiltered.length > 0 && ( {navMainFiltered.map((item) => { const title = t(`nav.${item.titleKey}`); return ( {item.icon && } {title} ); })} )} {user?.is_admin && adminFiltered.length > 0 && ( {t('groups.admin')} {adminFiltered.map((item) => { const title = t(`nav.${item.titleKey}`); return ( {item.icon && } {title} ); })} )} {documentFiltered.length > 0 && ( {t('groups.docs')} {documentFiltered.map((item) => { const title = t(`nav.${item.titleKey}`); return ( {item.external ? ( {item.icon && } {title} ) : ( {item.icon && } {title} )} ); })} )}
Version {APP_VERSION}
{APP_BUILD_DATE &&
Build At {APP_BUILD_DATE}
}
!isLoggingOut && setShowLogoutDialog(open)} > {t('logoutDialog.title')} {isLoggingOut ? t('logoutDialog.loggingOut') : t('logoutDialog.description')} {tCommon('cancel')} {isLoggingOut && } {isLoggingOut ? t('logoutDialog.inProgress') : t('logoutDialog.confirm')} ); }