191 lines
7.2 KiB
TypeScript
191 lines
7.2 KiB
TypeScript
'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>
|
||
);
|
||
}
|