p01 / frontend /src /components /Layout.tsx
lbb123's picture
多上游 Key 池 + 管理页 + 计费按Key汇总;模型名映射;暗色主题;日志自动刷新;仪表盘多指标;移动端卡片布局
45f0bb9
Raw History Blame Contribute Delete
4.09 kB
import { useState } from 'react'
import { NavLink, useNavigate } from 'react-router-dom'
import { LayoutDashboard, KeyRound, KeySquare, ScrollText, SlidersHorizontal, Receipt, LogOut, Zap, Menu, X } from 'lucide-react'
import { clearToken } from '../api'
import { ThemeToggle } from './Theme'
const nav = [
{ to: '/', label: '仪表盘', icon: LayoutDashboard, end: true },
{ to: '/keys', label: 'API Keys', icon: KeyRound, end: false },
{ to: '/upstream', label: '上游 Key', icon: KeySquare, end: false },
{ to: '/logs', label: '请求日志', icon: ScrollText, end: false },
{ to: '/settings', label: '设置', icon: SlidersHorizontal, end: false },
{ to: '/billing', label: '计费', icon: Receipt, end: false },
]
function Brand() {
return (
<div className="flex h-7 w-7 items-center justify-center rounded-lg bg-gradient-to-br from-indigo-500 to-violet-600 shadow-sm shadow-indigo-500/40">
<Zap size={15} className="text-white" fill="currentColor" />
</div>
)
}
export default function Layout({ children }: { children: JSX.Element }) {
const navigate = useNavigate()
const [open, setOpen] = useState(false)
const logout = () => {
clearToken()
navigate('/login', { replace: true })
}
return (
<div className="flex min-h-screen flex-col md:flex-row">
{/* 移动端顶栏 */}
<header className="sticky top-0 z-30 flex h-14 shrink-0 items-center justify-between border-b border-slate-800 bg-slate-900 px-4 text-white md:hidden">
<div className="flex items-center gap-2.5 font-semibold">
<Brand />
<span>Pasl Relay</span>
</div>
<button onClick={() => setOpen(true)} className="-mr-2 p-2 text-slate-400 hover:text-white" aria-label="打开菜单">
<Menu size={20} />
</button>
</header>
{/* 移动端遮罩 */}
{open && <div className="fixed inset-0 z-40 animate-fade-in bg-black/50 md:hidden" onClick={() => setOpen(false)} />}
{/* 侧边栏:桌面常驻,移动端抽屉 */}
<aside
className={`fixed md:static inset-y-0 left-0 z-50 flex w-64 shrink-0 flex-col bg-slate-900 transition-transform duration-200 ${
open ? 'translate-x-0' : '-translate-x-full md:translate-x-0'
}`}
>
<div className="flex h-14 shrink-0 items-center gap-2.5 border-b border-slate-800/80 px-4 text-white">
<Brand />
<div className="min-w-0 flex-1">
<div className="text-sm font-semibold leading-tight">Pasl Relay</div>
<div className="text-[10px] leading-tight text-slate-500">中转管理后台</div>
</div>
<ThemeToggle />
<button onClick={() => setOpen(false)} className="p-1 text-slate-400 hover:text-white md:hidden" aria-label="关闭菜单">
<X size={18} />
</button>
</div>
<nav className="flex-1 space-y-1 overflow-y-auto px-3 py-4">
{nav.map((n) => (
<NavLink key={n.to} to={n.to} end={n.end} onClick={() => setOpen(false)} className="block">
{({ isActive }) => (
<span
className={`flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors ${
isActive ? 'bg-indigo-500/15 text-white' : 'text-slate-400 hover:bg-white/5 hover:text-slate-100'
}`}
>
<n.icon size={16} className={isActive ? 'text-indigo-400' : ''} />
{n.label}
</span>
)}
</NavLink>
))}
</nav>
<div className="shrink-0 border-t border-slate-800/80 p-3">
<button
onClick={logout}
className="flex w-full items-center gap-3 rounded-lg px-3 py-2 text-sm text-slate-400 transition-colors hover:bg-white/5 hover:text-white"
>
<LogOut size={16} />
退出登录
</button>
</div>
</aside>
<main className="min-w-0 flex-1 overflow-x-hidden">{children}</main>
</div>
)
}