'use client'; import Link from 'next/link'; import { createPortal } from 'react-dom'; import { useCallback, useEffect, useState } from 'react'; import type { MenuNode } from '@/lib/api/types/menu'; import { NavTooltip } from '@/components/layout/nav-tooltip'; import { isLayoutOverviewNode, normalizeHref, isActivePath, visibleChildren, linkClass, } from '@/components/layout/nav-shared'; function CascadeFolderPanel({ title, nodes, pathname, onMenuNavigate, depth = 0, }: { title: string; nodes: MenuNode[]; pathname: string; onMenuNavigate?: (path: string, title: string) => void; depth?: number; }) { const [openId, setOpenId] = useState(null); const handleFolderKey = useCallback( (e: React.KeyboardEvent, nodeId: string) => { if (e.key === 'Enter' || e.key === 'ArrowRight') { e.preventDefault(); setOpenId(nodeId); } else if (e.key === 'Escape') { setOpenId(null); } }, [], ); if (nodes.length === 0) { return null; } return (
{title}
{nodes.map((node) => { if (node.menu_type === 3) { return null; } const kids = visibleChildren(node); const hasKids = kids.length > 0; const pad = 8; const label = ( {node.icon ? ( {node.icon} ) : null} {node.menu_name} ); if (hasKids) { return (
setOpenId(node.id)} > {openId === node.id ? (
) : null}
); } const href = normalizeHref(node); const external = Boolean(node.external_link); const active = isLayoutOverviewNode(node) ? (pathname.replace(/\/$/, '') || '/') === '/dashboard' : isActivePath(pathname, href); if (href === '#') { return (
{label}
); } if (external) { return ( {label} ); } return ( onMenuNavigate?.(href, node.menu_name)} aria-current={active ? 'page' : undefined} > {label} ); })}
); } /** L2 起:fixed 根与一级行顶对齐;更深层级在面板内 absolute,与对应行顶对齐;无内部滚动 */ export function ClassicCascadeFlyoutPortal({ rootNode, anchorRect, pathname, onMenuNavigate, onHoverEnter, onHoverLeave, }: { rootNode: MenuNode; anchorRect: DOMRectReadOnly; pathname: string; onMenuNavigate?: (path: string, title: string) => void; onHoverEnter: () => void; onHoverLeave: () => void; }) { const [mounted, setMounted] = useState(false); useEffect(() => setMounted(true), []); if (!mounted || typeof document === 'undefined') { return null; } const nodes = visibleChildren(rootNode); if (nodes.length === 0) { return null; } /** 左缘与一级导航右缘对齐,不向左叠在窄轨上(不再用负 overlap) */ const left = anchorRect.right; return createPortal(
, document.body, ); }