arbitrage-engine/frontend/components/Sidebar.tsx

116 lines
4.9 KiB
TypeScript

"use client";
import { useState } from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import {
LayoutDashboard, TrendingUp, Bell, Info, LogIn, UserPlus,
ChevronLeft, ChevronRight, Menu, X, Zap
} from "lucide-react";
const navItems = [
{ href: "/", label: "仪表盘", icon: LayoutDashboard },
{ href: "/kline", label: "K线大图", icon: TrendingUp },
{ href: "/signals", label: "信号历史", icon: Bell },
{ href: "/about", label: "说明", icon: Info },
];
const authItems = [
{ href: "/login", label: "登录", icon: LogIn },
{ href: "/register", label: "注册", icon: UserPlus },
];
export default function Sidebar() {
const pathname = usePathname();
const [collapsed, setCollapsed] = useState(false);
const [mobileOpen, setMobileOpen] = useState(false);
const SidebarContent = ({ mobile = false }: { mobile?: boolean }) => (
<div className={`flex flex-col h-full ${mobile ? "w-64" : collapsed ? "w-16" : "w-60"} transition-all duration-200`}>
{/* Logo */}
<div className={`flex items-center gap-2 px-4 py-5 border-b border-slate-100 ${collapsed && !mobile ? "justify-center px-0" : ""}`}>
<Zap className="w-6 h-6 text-blue-600 shrink-0" />
{(!collapsed || mobile) && (
<span className="font-bold text-slate-800 text-base leading-tight">Arbitrage<br/>Engine</span>
)}
</div>
{/* Nav */}
<nav className="flex-1 py-4 space-y-1 px-2">
{navItems.map(({ href, label, icon: Icon }) => {
const active = pathname === href;
return (
<Link key={href} href={href}
onClick={() => setMobileOpen(false)}
className={`flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm transition-colors
${active ? "bg-blue-50 text-blue-700 font-medium" : "text-slate-600 hover:bg-slate-100 hover:text-slate-900"}
${collapsed && !mobile ? "justify-center px-0" : ""}`}>
<Icon className={`shrink-0 ${active ? "text-blue-600" : "text-slate-400"}`} size={18} />
{(!collapsed || mobile) && <span>{label}</span>}
</Link>
);
})}
</nav>
{/* Auth */}
<div className="border-t border-slate-100 py-3 px-2 space-y-1">
{authItems.map(({ href, label, icon: Icon }) => (
<Link key={href} href={href}
onClick={() => setMobileOpen(false)}
className={`flex items-center gap-3 px-3 py-2 rounded-lg text-sm text-slate-500 hover:bg-slate-100 hover:text-slate-800 transition-colors
${collapsed && !mobile ? "justify-center px-0" : ""}`}>
<Icon className="shrink-0 text-slate-400" size={16} />
{(!collapsed || mobile) && <span>{label}</span>}
</Link>
))}
</div>
{/* Collapse toggle (desktop only) */}
{!mobile && (
<button onClick={() => setCollapsed(!collapsed)}
className="flex items-center justify-center p-3 border-t border-slate-100 text-slate-400 hover:text-slate-600 hover:bg-slate-50 transition-colors">
{collapsed ? <ChevronRight size={16} /> : <ChevronLeft size={16} />}
</button>
)}
</div>
);
return (
<>
{/* Desktop sidebar */}
<aside className={`hidden md:flex flex-col bg-white border-r border-slate-200 min-h-screen sticky top-0 shrink-0 transition-all duration-200 ${collapsed ? "w-16" : "w-60"}`}>
<SidebarContent />
</aside>
{/* Mobile top bar */}
<div className="md:hidden fixed top-0 left-0 right-0 z-40 bg-white border-b border-slate-200 flex items-center justify-between px-4 py-3">
<div className="flex items-center gap-2">
<Zap className="w-5 h-5 text-blue-600" />
<span className="font-bold text-slate-800 text-sm">Arbitrage Engine</span>
</div>
<button onClick={() => setMobileOpen(!mobileOpen)} className="text-slate-600 p-1">
{mobileOpen ? <X size={22} /> : <Menu size={22} />}
</button>
</div>
{/* Mobile drawer */}
{mobileOpen && (
<div className="md:hidden fixed inset-0 z-30" onClick={() => setMobileOpen(false)}>
<div className="absolute inset-0 bg-black/30" />
<aside className="absolute top-0 left-0 bottom-0 bg-white border-r border-slate-200 z-40 flex flex-col"
onClick={e => e.stopPropagation()}>
<div className="flex items-center justify-between px-4 py-3 border-b border-slate-100">
<div className="flex items-center gap-2">
<Zap className="w-5 h-5 text-blue-600" />
<span className="font-bold text-slate-800 text-sm">Arbitrage Engine</span>
</div>
<button onClick={() => setMobileOpen(false)} className="text-slate-400 p-1"><X size={18} /></button>
</div>
<SidebarContent mobile />
</aside>
</div>
)}
</>
);
}