Files
smart-go/web/components/layout/ClassicCollapsedSidebar.tsx
2026-04-23 18:58:13 +08:00

191 lines
7.2 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import { useMemo } from 'react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import type { MenuNode } from '@/lib/api/types/menu';
import { ClassicCascadeFlyoutPortal } from '@/components/layout/ClassicCascadeFlyout';
import { NavTooltip } from '@/components/layout/nav-tooltip';
import {
NavIconOrLabel,
NavStateGuard,
normalizeHref,
layoutNavRootsFromApi,
subtreeContainsActivePath,
visibleChildren,
} from '@/components/layout/nav-shared';
import { useFlyoutState } from '@/lib/hooks/use-flyout-state';
import { useMenuNavigation } from '@/lib/hooks/use-menu-navigation';
import { useLayoutStore } from '@/stores/layout-store';
const RAIL_W = 'w-[72px]';
export function ClassicCollapsedSidebar({
items,
loading,
error,
authed,
}: {
items: MenuNode[];
loading: boolean;
error: string | null;
authed: boolean;
onMenuNavigate?: (path: string, title: string) => void;
}) {
const pathname = usePathname() ?? '/';
const roots = useMemo(() => layoutNavRootsFromApi(items), [items]);
const toggleClassicNavRail = useLayoutStore((s) => s.toggleClassicNavRail);
const onMenuNavigate = useMenuNavigation();
const {
flyoutRoot,
l1AnchorRect,
openFlyout,
scheduleCloseFlyout,
clearCloseTimer,
closeFlyoutNow,
toggleFlyoutClick,
railScrollRef,
} = useFlyoutState(pathname);
const railShell = (inner: React.ReactNode) => (
<div className={`relative flex h-full min-h-0 shrink-0 flex-col border-r border-neutral-100 bg-white ${RAIL_W}`}>
{inner}
<div className="pointer-events-none absolute inset-0 z-[60] hidden md:block" aria-hidden>
<NavTooltip label="展开侧栏" side="right">
<button
type="button"
className="pointer-events-auto absolute left-full top-1/2 z-[60] flex h-14 w-3.5 -translate-y-1/2 cursor-pointer items-center justify-center rounded-r-full border border-neutral-200 border-l-0 bg-white text-xs text-neutral-500 shadow-sm transition-transform duration-200 ease-out hover:bg-neutral-50 active:scale-95 motion-reduce:transition-none"
aria-label="展开侧栏"
onClick={() => toggleClassicNavRail()}
>
<span aria-hidden></span>
</button>
</NavTooltip>
</div>
</div>
);
if (!authed || loading || error) {
return (
<div className="relative h-full min-h-0 w-[72px] shrink-0 flex-1">
{railShell(
<NavStateGuard authed={authed} loading={loading} error={error} onMenuNavigate={onMenuNavigate} />,
)}
</div>
);
}
return (
<div className="relative h-full min-h-0 w-[72px] shrink-0 flex-1">
{railShell(
<div ref={railScrollRef} className="min-h-0 flex-1 overflow-y-auto px-1 py-1">
<nav className="flex flex-col items-stretch gap-0.5" aria-label="主导航(收起)">
{roots.map((node) => {
const kids = visibleChildren(node);
if (kids.length > 0) {
const flyoutHere = flyoutRoot?.id === node.id;
const routeHere = subtreeContainsActivePath(node, pathname);
const railHighlight = flyoutHere || (routeHere && flyoutRoot === null);
return (
<div
key={node.id}
className="relative"
data-l1-cascade=""
onMouseEnter={(e) => openFlyout(node, e.currentTarget)}
onMouseLeave={scheduleCloseFlyout}
>
<NavTooltip label={node.menu_name}>
<button
type="button"
aria-expanded={flyoutHere}
aria-haspopup="true"
className={[
'flex h-10 w-full items-center justify-center rounded-md text-sm',
railHighlight
? flyoutHere
? 'bg-neutral-200 font-medium text-neutral-900'
: 'bg-neutral-100 text-neutral-900'
: 'text-neutral-700 hover:bg-neutral-100',
].join(' ')}
onClick={(e) =>
toggleFlyoutClick(
node,
(e.currentTarget as HTMLElement).closest('[data-l1-cascade]') as HTMLDivElement | null,
)
}
>
<span className="truncate px-1 text-center leading-tight">
<NavIconOrLabel node={node} />
</span>
</button>
</NavTooltip>
</div>
);
}
const href = normalizeHref(node);
const external = Boolean(node.external_link);
if (href === '#') {
return (
<div key={node.id} className="w-full" onMouseEnter={() => closeFlyoutNow()}>
<NavTooltip label={node.menu_name} delayDuration={0}>
<div className="flex h-10 items-center justify-center rounded-md px-1 text-center text-[11px] text-neutral-400">
<NavIconOrLabel node={node} className="text-base" />
</div>
</NavTooltip>
</div>
);
}
if (external) {
return (
<div key={node.id} className="w-full" onMouseEnter={() => closeFlyoutNow()}>
<NavTooltip label={node.menu_name} delayDuration={0}>
<a
href={href}
target="_blank"
rel="noopener noreferrer"
className="flex h-10 items-center justify-center rounded-md text-neutral-700 hover:bg-neutral-100"
>
<NavIconOrLabel node={node} />
</a>
</NavTooltip>
</div>
);
}
return (
<div key={node.id} className="w-full" onMouseEnter={() => closeFlyoutNow()}>
<NavTooltip label={node.menu_name} delayDuration={0}>
<Link
href={href}
className="flex h-10 items-center justify-center rounded-md text-neutral-700 hover:bg-neutral-100"
onClick={() => {
onMenuNavigate(href, node.menu_name);
closeFlyoutNow();
}}
>
<NavIconOrLabel node={node} />
</Link>
</NavTooltip>
</div>
);
})}
</nav>
</div>,
)}
{flyoutRoot && l1AnchorRect && visibleChildren(flyoutRoot).length > 0 ? (
<ClassicCascadeFlyoutPortal
rootNode={flyoutRoot}
anchorRect={l1AnchorRect}
pathname={pathname}
onMenuNavigate={onMenuNavigate}
onHoverEnter={clearCloseTimer}
onHoverLeave={scheduleCloseFlyout}
/>
) : null}
</div>
);
}