"use client"; /** 应用外壳:桌面左侧导航栏 + 窄屏顶栏与抽屉(usePathname 高亮当前页)。 */ import { useEffect, useState } from "react"; import Link from "next/link"; import { usePathname } from "next/navigation"; import { Icon, type IconName } from "@/components/icons"; interface NavItem { href: string; label: string; icon: IconName; } /** * 导航分组:按「研究闭环」的真实使用顺序排列 —— * 找标的(股票池)→ 出候选(筛选)→ 定规则(策略库)→ 验规则(回测)→ 复盘(实验)。 * 「交易信号」「因子研究」是可选支路,放在同组但靠后;单独建组会让闭环读起来断裂。 */ const GROUPS: { title: string; items: NavItem[] }[] = [ { title: "研究", items: [ { href: "/", label: "总览", icon: "grid" }, { href: "/stocks", label: "股票池", icon: "candles" }, { href: "/selection", label: "股票筛选", icon: "target" }, ], }, { title: "策略", items: [ { href: "/strategies", label: "策略库", icon: "book" }, { href: "/backtest", label: "选股回测", icon: "gauge" }, { href: "/experiments", label: "实验对比", icon: "archive" }, ], }, { title: "因子与信号", items: [ { href: "/factors", label: "因子研究", icon: "flask" }, { href: "/factors/compose", label: "因子组合", icon: "layers" }, { href: "/signals", label: "交易信号", icon: "scale" }, ], }, ]; function isActive(href: string, pathname: string): boolean { if (href === "/") return pathname === "/"; if (href === "/factors") return pathname === "/factors"; if (pathname === href || pathname.startsWith(href + "/")) return true; return false; } function Brand() { return ( qlib-platform 个人 A 股量化研究台 ); } function NavLinks({ pathname, onNavigate }: { pathname: string; onNavigate?: () => void }) { return ( <> {GROUPS.map((g) => (
{g.title}
{g.items.map((item) => { const active = isActive(item.href, pathname); return ( {item.label} ); })}
))} ); } export function AppShell({ children }: { children: React.ReactNode }) { const pathname = usePathname(); const [open, setOpen] = useState(false); // 路由变化时收起抽屉 useEffect(() => { setOpen(false); }, [pathname]); useEffect(() => { if (!open) return; const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") setOpen(false); }; window.addEventListener("keydown", onKey); document.body.style.overflow = "hidden"; return () => { window.removeEventListener("keydown", onKey); document.body.style.overflow = ""; }; }, [open]); return (
{open ? ( ); }