feat: 优化web
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user