52 lines
1.9 KiB
TypeScript
52 lines
1.9 KiB
TypeScript
import { NavLink, Outlet, useNavigate } from 'react-router-dom'
|
|
import { Badge, Button } from 'antd'
|
|
import { useQuery } from '@tanstack/react-query'
|
|
import { useAuth } from '../auth/AuthProvider'
|
|
import { ThemeToggle } from '../components/ThemeToggle'
|
|
import { services } from '../services'
|
|
|
|
const links = [
|
|
['/overview', '概览'],
|
|
['/runs/new', '新建测试'],
|
|
['/runs', '测试运行'],
|
|
['/reports', '汇总报告'],
|
|
['/providers', '模型配置'],
|
|
] as const
|
|
|
|
export function AppShell() {
|
|
const auth = useAuth()
|
|
const navigate = useNavigate()
|
|
const activeRuns = useQuery({
|
|
queryKey: ['runs', 'active-count'],
|
|
queryFn: () => services.runs.list(50),
|
|
refetchInterval: 5000,
|
|
select: (runs) => runs.filter((run) => !run.terminal).length,
|
|
})
|
|
return (
|
|
<div className="app-shell">
|
|
<aside className="sidebar">
|
|
<div className="brand"><span className="brand-mark">AS</span><div><strong>Safety Console</strong><small>AI 模型安全</small></div></div>
|
|
<nav aria-label="主导航">
|
|
{links.map(([to, label]) => <NavLink key={to} to={to}>{label}</NavLink>)}
|
|
{auth.user?.is_admin && <NavLink to="/users">用户管理</NavLink>}
|
|
<NavLink to="/account">账户设置</NavLink>
|
|
</nav>
|
|
<div className="sidebar-footer">
|
|
<div><strong>{auth.user?.username}</strong><small>{auth.user?.is_admin ? '管理员' : '操作员'}</small></div>
|
|
<Button type="text" onClick={() => void auth.logout()}>退出</Button>
|
|
</div>
|
|
</aside>
|
|
<div className="workspace">
|
|
<header className="topbar">
|
|
<button className="running-indicator" onClick={() => navigate('/runs')}>
|
|
<Badge status={activeRuns.data ? 'processing' : 'default'} />
|
|
{activeRuns.data || 0} 个任务正在运行
|
|
</button>
|
|
<ThemeToggle />
|
|
</header>
|
|
<main className="content"><Outlet /></main>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|