diff --git a/frontend-web/src/components/page-sidebar-v1.tsx b/frontend-web/src/components/page-sidebar-v1.tsx new file mode 100644 index 00000000..58e59a72 --- /dev/null +++ b/frontend-web/src/components/page-sidebar-v1.tsx @@ -0,0 +1,303 @@ +import { useEffect, useState } from "react"; +import { useNavigate, useLocation } from "react-router-dom"; +import { + Bell, + ChevronLeft, + ChevronRight, + ChevronsUpDown, + Folder, + FolderOpen, + Settings, + User, + Wrench, +} from "lucide-react"; + +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { NotificationDialog } from "@/components/workspace/notification-dialog"; +import { SettingsDialog } from "@/components/workspace/settings"; +import { useTheme } from "@/contexts/ThemeContext"; +import { getStoredAuth } from "@/core/auth"; +import { useI18n } from "@/core/i18n/hooks"; +import { useUnreadCount } from "@/core/notifications"; +import { + defaultExpandedIds, + type MenuItem, +} from "@/core/page-layout/sidebar-menu"; +import { useSidebarMenuItems } from "@/core/page-layout/use-sidebar-menu"; + +type PageSidebarProps = { + collapsed: boolean; + onToggleCollapsed: () => void; +}; + +type SettingsSection = "account" | "appearance" | "memory" | "tools" | "about"; + +export function PageSidebar({ + collapsed, + onToggleCollapsed, +}: PageSidebarProps) { + const { theme } = useTheme(); + const isDark = theme === "dark"; + const isAdmin = getStoredAuth()?.system_role === "admin"; + const visibleMenuItems = useSidebarMenuItems(!!isAdmin); + const { t } = useI18n(); + const { data: unreadCount = 0 } = useUnreadCount(); + // defaultExpandedIds 是共享的 ReadonlySet,复制一份给 useState(避免外部突变) + const [expandedIds, setExpandedIds] = useState>(() => new Set(defaultExpandedIds)); + const [settingsOpen, setSettingsOpen] = useState(false); + const [settingsDefaultSection, setSettingsDefaultSection] = + useState("appearance"); + const [notificationOpen, setNotificationOpen] = useState(false); + const [mounted, setMounted] = useState(false); + const navigate = useNavigate(); + const { pathname } = useLocation(); + + useEffect(() => { + setMounted(true); + }, []); + + const openSettings = (section: SettingsSection) => { + setSettingsDefaultSection(section); + setSettingsOpen(true); + }; + + const iconButtonClass = isDark + ? "rounded-lg p-2 text-slate-400 transition-all duration-300 ease-out hover:scale-105 hover:bg-slate-800/50 hover:text-white" + : "rounded-lg p-2 text-gray-500 transition-all duration-300 ease-out hover:scale-105 hover:bg-red-50 hover:text-red-600"; + const panelClass = isDark + ? "ml-4 space-y-1 border-l border-blue-800/30 pl-4" + : "ml-4 space-y-1 border-l border-red-100 pl-4"; + const labelClass = (compact: boolean, small = false) => + `overflow-hidden whitespace-nowrap transition-all duration-300 ease-out ${ + compact ? "max-w-0 opacity-0" : "max-w-[220px] opacity-100" + } ${small ? "text-xs" : "text-sm"} flex-1 font-medium`; + const menuButtonClass = (isActive: boolean, depth = 0) => + `group relative flex w-full items-center rounded-[2px] py-2 text-left transition-all duration-300 ease-out theme-transition ${ + collapsed && depth === 0 ? "justify-center px-2" : "gap-3 px-3" + } ${ + isActive + ? isDark + ? "bg-blue-500/15 text-cyan-300 font-semibold" + : "bg-red-50 text-red-600 font-semibold" + : isDark + ? "text-slate-400 hover:bg-slate-800/50 hover:text-white" + : "text-gray-600 hover:bg-red-50 hover:text-red-600" + }`; + const footerButtonClass = menuButtonClass(false, 0); + + const toggleExpanded = (id: string) => { + setExpandedIds((prev) => { + const next = new Set(prev); + if (next.has(id)) { + next.delete(id); + } else { + next.add(id); + } + return next; + }); + }; + + const renderLeaf = (item: MenuItem, depth = 0) => { + const Icon = item.icon; + const isActive = !!item.path && pathname === item.path; + + return ( + + ); + }; + + const renderBranch = (item: MenuItem, depth = 0) => { + const expanded = expandedIds.has(item.id); + const isTopLevel = depth === 0; + const Icon = depth > 0 && item.icon === Folder && expanded ? FolderOpen : item.icon; + + return ( +
+ + + {(!collapsed || !isTopLevel) && item.children && ( +
+ {item.children.map((child) => + child.children ? renderBranch(child, depth + 1) : renderLeaf(child, depth + 1), + )} +
+ )} +
+ ); + }; + + return ( + <> + + + + + ); +}