132 lines
4.0 KiB
TypeScript
132 lines
4.0 KiB
TypeScript
import React, { useState } from "react";
|
|
import { LayoutDashboard, ChevronDown, ChevronRight, X } from "lucide-react";
|
|
|
|
export interface NavItem {
|
|
key: string;
|
|
label: string;
|
|
icon?: React.ReactNode;
|
|
active?: boolean;
|
|
onClick?: () => void;
|
|
children?: NavItem[];
|
|
}
|
|
|
|
interface SidebarNavProps {
|
|
items?: NavItem[];
|
|
isOpen?: boolean;
|
|
onClose?: () => void;
|
|
}
|
|
|
|
export default function SidebarNav({ items = [] as NavItem[], isOpen = true, onClose }: SidebarNavProps) {
|
|
const [expandedKeys, setExpandedKeys] = useState<Set<string>>(new Set(["configuration", "monitoring", "artifact", "cluster"]));
|
|
|
|
const toggleExpand = (key: string) => {
|
|
setExpandedKeys((prev) => {
|
|
const next = new Set(prev);
|
|
if (next.has(key)) {
|
|
next.delete(key);
|
|
} else {
|
|
next.add(key);
|
|
}
|
|
return next;
|
|
});
|
|
};
|
|
|
|
const handleItemClick = (item: NavItem, hasChildren: boolean) => {
|
|
if (hasChildren) {
|
|
toggleExpand(item.key);
|
|
} else {
|
|
if (item.onClick) {
|
|
item.onClick();
|
|
}
|
|
// 移动端点击后关闭侧边栏
|
|
if (onClose) {
|
|
onClose();
|
|
}
|
|
}
|
|
};
|
|
|
|
const renderNavItem = (item: NavItem, level = 0) => {
|
|
const hasChildren = Boolean(item.children && item.children.length > 0);
|
|
const isExpanded = expandedKeys.has(item.key);
|
|
|
|
return (
|
|
<div key={item.key}>
|
|
<button
|
|
onClick={() => handleItemClick(item, hasChildren)}
|
|
className={`w-full text-left flex items-center gap-2 px-3 py-2 rounded-xl text-sm font-medium transition-colors duration-200 ${
|
|
item.active
|
|
? "bg-brand-accent/15 text-primary border border-brand-accent/40 shadow-glow"
|
|
: "text-secondary hover:text-primary hover:bg-dark-elevated/70"
|
|
}`}
|
|
style={{ paddingLeft: `${12 + level * 16}px` }}
|
|
>
|
|
{item.icon}
|
|
<span className="flex-1">{item.label}</span>
|
|
{hasChildren && (
|
|
isExpanded ? (
|
|
<ChevronDown className="w-4 h-4" />
|
|
) : (
|
|
<ChevronRight className="w-4 h-4" />
|
|
)
|
|
)}
|
|
</button>
|
|
{hasChildren && isExpanded && (
|
|
<div className="mt-1 space-y-1">
|
|
{item.children!.map((child) => renderNavItem(child, level + 1))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<>
|
|
{/* 移动端遮罩层 */}
|
|
{isOpen && (
|
|
<div
|
|
className="fixed inset-0 bg-black/50 z-40 md:hidden"
|
|
onClick={onClose}
|
|
/>
|
|
)}
|
|
|
|
{/* 侧边栏 */}
|
|
<aside
|
|
className={`
|
|
fixed md:static inset-y-0 left-0 z-50 md:z-0
|
|
flex flex-col w-64 sm:w-72 md:w-60 xl:w-64 bg-dark-lighter/85 backdrop-blur-xl border-r border-dark-border/80 shadow-soft
|
|
transform transition-transform duration-300 ease-in-out
|
|
${isOpen ? "translate-x-0" : "-translate-x-full md:translate-x-0"}
|
|
`}
|
|
>
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between px-4 py-4 border-b border-dark-border/70">
|
|
<div className="flex items-center gap-2">
|
|
<LayoutDashboard className="w-5 h-5 text-brand-accent" />
|
|
<span className="text-sm font-semibold text-secondary tracking-wide">Console</span>
|
|
</div>
|
|
{/* 移动端关闭按钮 */}
|
|
{onClose && (
|
|
<button
|
|
onClick={onClose}
|
|
className="md:hidden p-1.5 rounded-lg hover:bg-dark-elevated/70 text-secondary hover:text-primary transition"
|
|
aria-label="关闭菜单"
|
|
>
|
|
<X className="w-5 h-5" />
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
{/* Navigation */}
|
|
<nav className="flex-1 p-3 space-y-1 overflow-y-auto">
|
|
{items.map((item) => renderNavItem(item))}
|
|
</nav>
|
|
|
|
{/* Footer */}
|
|
<div className="p-3 text-xs text-muted border-t border-dark-border/70">
|
|
© {new Date().getFullYear()} OCDP
|
|
</div>
|
|
</aside>
|
|
</>
|
|
);
|
|
}
|