import { Popover } from "antd"; import { ChevronDown, ChevronRight, CircleUserRound, History as HistoryIcon, Infinity as InfinityIcon, PanelLeftClose, PanelLeftOpen, Plus } from "lucide-react"; import { LayoutGroup, motion, useReducedMotion } from "motion/react"; import { useEffect, useMemo, useRef, useState, type ComponentType, type CSSProperties } from "react"; import { Link, useLocation, useNavigate, useSearchParams } from "react-router"; import { BrandLogoFrame } from "@/components/brand/brand-logo"; import { UserAvatar } from "@/components/layout/user-avatar"; import { Kbd } from "@/components/ui/base/kbd"; import { navigationTools, type NavigationToolSlug } from "@/constant/navigation-tools"; import { useWorkspaceLogout } from "@/hooks/use-workspace-logout"; import { aceternityMotion } from "@/lib/aceternity-motion"; import { cn } from "@/lib/utils"; import { preloadWorkspaceRoute } from "@/lib/workspace-route-modules"; import { useUserStore, type FeatureAvailability } from "@/stores/use-user-store"; import { useAppearanceStore } from "@/stores/use-appearance-store"; import { WorkspaceAccountCard } from "./workspace-account-card"; import { WorkspaceSidebarCheckin } from "./workspace-sidebar-checkin"; import { WorkspaceSidebarStorageMeter } from "./workspace-sidebar-storage-meter"; import { openWorkspaceWallet } from "@/lib/workspace-wallet"; export type WorkspaceNavItem = { id: string; title: string; icon?: ComponentType<{ className?: string; strokeWidth?: number }>; to?: string; shortcut?: string; badge?: string | number; action?: "search" | "logout"; children?: WorkspaceNavItem[]; }; type WorkspaceNavGroup = { heading?: string; items: WorkspaceNavItem[]; }; function toolItem(slug: NavigationToolSlug, to: string): WorkspaceNavItem { const tool = navigationTools.find((item) => item.slug === slug); return { id: slug, title: tool?.label ?? slug, icon: tool?.icon, to }; } function buildNav(features: FeatureAvailability): { groups: WorkspaceNavGroup[]; footer: WorkspaceNavItem[] } { const groups: WorkspaceNavGroup[] = [ { items: [ { ...toolItem("create", "/"), id: "home", title: "创作" }, { ...toolItem("inspirations", "/inspirations"), title: "灵感" }, { ...toolItem("projects", "/projects"), title: "短剧 Agent" }, { ...toolItem("canvas", "/canvas"), title: "自由画布" }, ], }, { heading: "资源与工具", items: [{ ...toolItem("assets", "/assets"), title: "资产" }, { ...toolItem("skills", "/skills"), title: "技能" }, ...(features.pluginCenterEnabled ? [{ ...toolItem("plugins", "/plugins"), title: "插件" }] : [])], }, ...(features.taskCenterEnabled ? [{ items: [{ ...toolItem("tasks", "/tasks"), title: "创作历史", icon: HistoryIcon }] }] : []), ]; // 管理、设置和退出登录不再占据参考站式侧栏底部,而是通过用户卡片菜单进入。 // 路由和写操作仍保留,避免把用户端导航变成无法访问的装饰。 return { groups, footer: [] }; } function WorkspaceSidebarProfile({ collapsed, user, onExpand, onCollapse }: { collapsed: boolean; user: NonNullable["user"]> | null; onExpand: () => void; onCollapse: () => void }) { const [menuOpen, setMenuOpen] = useState(false); const profileName = user?.displayName || user?.username || "未登录"; if (!user) { return {!collapsed ? 游客模式 : null}; } const content = setMenuOpen(false)} onWallet={() => { setMenuOpen(false); openWorkspaceWallet(); }} />; return (
{!collapsed ? : null}
{collapsed ? : null}
); } function WorkspaceSwitcher({ collapsed, onNavigate }: { collapsed: boolean; onNavigate: () => void; onExpand: () => void; onCollapse: () => void }) { const appearance = useAppearanceStore((state) => state.appearance); if (collapsed) { return (
} />
); } return (
} /> {appearance.brandName} 创作工作台
); } function NavItem({ item, activeId, onSelect, onOpenSearch, onLogout, level = 0, collapsed = false, }: { item: WorkspaceNavItem; activeId: string; onSelect: (id: string) => void; onOpenSearch: () => void; onLogout: () => void; level?: number; collapsed?: boolean; }) { const isActive = activeId === item.id || (item.id === "settings" && activeId.startsWith("settings:")); const hasChildren = Boolean(item.children?.length); const [isOpen, setIsOpen] = useState(false); const reducedMotion = useReducedMotion(); // 激活分支自动展开(如设置分区子项),保证当前位置可见。 useEffect(() => { if (isActive && hasChildren) setIsOpen(true); }, [isActive, hasChildren]); const Icon = item.icon; const rowStyle = collapsed ? undefined : ({ paddingLeft: `${level * 12 + 10}px` } as CSSProperties); const collapsedTitle = item.id === "home" ? "创作" : item.id === "projects" ? "短剧" : item.id === "canvas" ? "画布" : item.id === "assets" ? "资产" : item.id === "skills" ? "技能" : item.id === "plugins" ? "插件" : item.id === "tasks" ? "历史" : item.title.slice(0, 2); const rowContent = ( <> {Icon ? ( ) : ( )} {collapsed ? collapsedTitle : item.title} {item.shortcut ? ( {item.shortcut} ) : null} {item.badge !== undefined ? {item.badge} : null} {hasChildren ? : null} ); const rowClassName = cn( "app-workspace-nav-link group relative isolate flex min-h-11 w-full items-center justify-between gap-2 rounded-[var(--r-md)] px-3 py-2 text-[var(--fs-body)] transition-[color,transform] duration-200 select-none", collapsed && "is-collapsed", isActive ? "is-active font-medium" : "text-foreground/62 hover:bg-surface-hover hover:text-foreground", ); const activePill = isActive ? ( ) : null; const handleClick = () => { if (item.action === "search") { onOpenSearch(); return; } if (item.action === "logout") { onLogout(); return; } if (hasChildren) { setIsOpen((open) => !open); return; } onSelect(item.id); }; // 局部 const:闭包内 TS 保留窄化,供 preload 回调使用。 const linkTo = item.to; return (
{linkTo ? ( preloadWorkspaceRoute(linkTo)} onPointerDown={() => preloadWorkspaceRoute(linkTo)} onPointerEnter={() => preloadWorkspaceRoute(linkTo)} > {activePill} {rowContent} ) : ( )} {hasChildren && !collapsed ? (
{item.children!.map((child) => ( ))}
) : null}
); } function NavGroup({ group, activeId, onNavigate, onOpenSearch, onLogout, collapsed }: { group: WorkspaceNavGroup; activeId: string; onNavigate: () => void; onOpenSearch: () => void; onLogout: () => void; collapsed: boolean }) { const [isOpen, setIsOpen] = useState(true); const hasActive = group.items.some((item) => item.id === activeId || (item.id === "settings" && activeId.startsWith("settings:"))); // 激活项所在分组自动展开,保证当前位置可见。 useEffect(() => { if (hasActive) setIsOpen(true); }, [hasActive]); const content = (
{group.items.map((item) => ( ))}
); // 无标题分组(核心导航入口)常驻展示,不做折叠。 if (!group.heading || collapsed) { return
{content}
; } return (
{content}
); } export function WorkspaceSidebarNav({ collapsed, onNavigate, onOpenSearch, onExpand, onCollapse }: { collapsed: boolean; onNavigate: () => void; onOpenSearch: () => void; onExpand: () => void; onCollapse: () => void }) { const { pathname } = useLocation(); const [searchParams] = useSearchParams(); const features = useUserStore((state) => state.features); const user = useUserStore((state) => state.user); const { handleLogout } = useWorkspaceLogout(); const { groups, footer } = useMemo(() => buildNav(features), [features]); const slug = pathname.split("/").filter(Boolean)[0] || "home"; const section = searchParams.get("section"); const activeId = slug === "settings" && section ? `settings:${section}` : slug; const scrollRef = useRef(null); const [scrollState, setScrollState] = useState({ hasTopFade: false, hasBottomFade: false }); const handleScroll = () => { const element = scrollRef.current; if (!element) return; const { scrollTop, scrollHeight, clientHeight } = element; setScrollState({ hasTopFade: scrollTop > 0, hasBottomFade: scrollTop + clientHeight < scrollHeight - 1, }); }; useEffect(() => { handleScroll(); }, [groups]); return (
{groups.map((group, index) => ( void handleLogout()} collapsed={collapsed} /> ))}
{footer.length ?
{footer.map((item) => ( void handleLogout()} collapsed={collapsed} /> ))}
: null}
); }