'use client'; import * as Collapsible from '@radix-ui/react-collapsible'; import Link from 'next/link'; import type { MenuNode } from '@/lib/api/types/menu'; import { NavTooltip } from '@/components/layout/nav-tooltip'; /** 布局固定「概览」,与 IAM nav 拼成同一棵树(不入库) */ export const LAYOUT_OVERVIEW_MENU_ID = '__layout_overview__'; export function isLayoutOverviewNode(node: MenuNode): boolean { return node.id === LAYOUT_OVERVIEW_MENU_ID; } export const layoutOverviewMenuNode: MenuNode = { id: LAYOUT_OVERVIEW_MENU_ID, parent_id: '', menu_name: '概览', menu_type: 2, path: '/dashboard', icon: '📊', }; /** 将布局「概览」与 IAM nav 合并为侧栏顶层列表,便于组好完整结构再一次性渲染 */ export function layoutNavRootsFromApi(items: MenuNode[]): MenuNode[] { return [layoutOverviewMenuNode, ...items.filter((n) => n.menu_type !== 3)]; } export function normalizeHref(node: MenuNode): string { if (node.external_link) { return node.external_link; } const p = node.path?.trim(); if (!p) { return '#'; } if (p.startsWith('http://') || p.startsWith('https://')) { return p; } return p.startsWith('/') ? p : `/${p}`; } export function isActivePath(pathname: string, href: string): boolean { if (!href || href === '#' || href.startsWith('http://') || href.startsWith('https://')) { return false; } if (pathname === href) { return true; } if (href !== '/' && pathname.startsWith(`${href}/`)) { return true; } return false; } export function linkClass(active: boolean): string { return [ 'block rounded-md px-2 py-2 text-sm', active ? 'bg-neutral-200 font-medium text-neutral-900' : 'text-neutral-800 hover:bg-neutral-100', ].join(' '); } /** 经典树:目录可展开,叶子为链接(用于经典侧栏与图标模式右侧浮层) */ export function NavTreeItem({ node, depth, pathname, onInternalNavigate, onMenuNavigate, /** 图标模式浮层:二级及以下项之间额外增加 2px(space-y-0.5) */ iconFlyout = false, }: { node: MenuNode; depth: number; pathname: string; onInternalNavigate?: () => void; onMenuNavigate?: (path: string, title: string) => void; iconFlyout?: boolean; }) { if (node.menu_type === 3) { return null; } const children = (node.children ?? []).filter((c) => c.menu_type !== 3); const hasChildren = children.length > 0; const pad = 10 + depth * 10; const label = ( {node.icon ? ( {node.icon} ) : null} {node.menu_name} ); if (hasChildren) { return (
{children.map((c) => ( ))}
); } 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); onInternalNavigate?.(); }} aria-current={active ? 'page' : undefined} > {label} ); } export function visibleChildren(node: MenuNode): MenuNode[] { return (node.children ?? []).filter((c) => c.menu_type !== 3); } /** 当前路径是否落在该节点子树内(用于图标模式自动展开浮层) */ export function subtreeContainsActivePath(node: MenuNode, pathname: string): boolean { if (isLayoutOverviewNode(node)) { const p = pathname.replace(/\/$/, '') || '/'; return p === '/dashboard'; } const href = normalizeHref(node); if (!node.external_link && href !== '#' && isActivePath(pathname, href)) { return true; } for (const c of visibleChildren(node)) { if (subtreeContainsActivePath(c, pathname)) { return true; } } return false; } /** 窄轨图标/文字切换:有 icon 显示 icon,否则显示菜单名 */ export function NavIconOrLabel({ node, className }: { node: MenuNode; className?: string }) { if (node.icon) { return ( {node.icon} ); } return {node.menu_name}; } /** 侧栏未认证/加载中/出错三态守卫 */ export function NavStateGuard({ authed, loading, error, onMenuNavigate, children, }: { authed: boolean; loading: boolean; error: string | null; onMenuNavigate: (path: string, title: string) => void; children?: React.ReactNode; }) { if (!authed) { return
登录后加载菜单
; } if (loading) { return
菜单加载中…
; } if (error) { return (
onMenuNavigate('/dashboard', '概览')} > 📊 概览

菜单失败

); } return children; }