Add HeXi bot codebase: custom plugins, web frontends, tests
- hexi core: message handling, rate limiting, cooldown, plugin manager - Custom plugins: BF stats, daily check-in, quotes, persona cards, etc. - Community plugins vendored under hexi/plugins with local fixes - Web admin frontends (learning-chat, persona-admin), unified hexi/web - Tests for rate_limit/cooldown/memes/persona; poetry.lock Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
import { NavLink, Outlet, useLocation, useNavigate } from 'react-router-dom'
|
||||
import { Avatar, Button, Separator } from '@heroui/react'
|
||||
import { House, Rocket, Gear } from '@gravity-ui/icons'
|
||||
import { useAuth } from '../auth/auth'
|
||||
|
||||
interface NavItem { id: string; label: string; description: string; icon: any }
|
||||
|
||||
export function AppLayout() {
|
||||
const { token, username, logout } = useAuth()
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
|
||||
const navItems: NavItem[] = [
|
||||
{ id: 'home', label: '首页', description: '主机与服务状态', icon: House },
|
||||
{ id: 'plugins', label: '插件管理', description: '插件总览与分群控制', icon: Rocket },
|
||||
{ id: 'settings', label: '设置', description: '账号与系统设置', icon: Gear },
|
||||
]
|
||||
const isHome = location.pathname === '/hub'
|
||||
const isSettings = location.pathname.startsWith('/hub/settings')
|
||||
const isPluginList = location.pathname.startsWith('/hub/plugins')
|
||||
const isPlugins = isPluginList || (!isHome && !isSettings && location.pathname.startsWith('/hub/'))
|
||||
const isPluginSub = isPlugins && !isPluginList
|
||||
const activeId = isPlugins ? 'plugins' : isSettings ? 'settings' : 'home'
|
||||
const pageMeta = navItems.find(it => it.id === activeId) || navItems[0]
|
||||
|
||||
if (!token) return null
|
||||
|
||||
return (
|
||||
<div className="flex h-screen w-screen overflow-hidden bg-slate-50 text-slate-900">
|
||||
<aside className="flex h-full w-60 shrink-0 flex-col overflow-hidden bg-white border-r border-slate-200">
|
||||
{/* 品牌头 */}
|
||||
<div className="flex h-16 shrink-0 items-center px-4">
|
||||
<div className="grid size-9 place-items-center rounded-xl bg-indigo-100 text-indigo-600"><Gear className="size-5" /></div>
|
||||
<div className="ml-3 min-w-0 flex-1">
|
||||
<div className="truncate text-sm font-bold">HeXi 管理系统</div>
|
||||
<div className="truncate text-xs text-slate-400">统一管理台</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 导航 */}
|
||||
<nav className="flex-1 min-h-0 space-y-1 overflow-y-auto px-2 py-2">
|
||||
{navItems.filter(it => it.id !== 'settings').map(it => {
|
||||
const to = it.id === 'home' ? '/hub' : '/hub/' + it.id
|
||||
return (
|
||||
<NavLink
|
||||
key={it.id}
|
||||
to={to}
|
||||
end={it.id === 'home'}
|
||||
className={({ isActive }) => {
|
||||
const base = 'group relative flex items-center rounded-lg py-2.5 text-sm font-medium transition-colors cursor-pointer'
|
||||
return base + (isActive ? ' bg-indigo-50 text-indigo-700' : ' text-slate-600 hover:bg-slate-100 hover:text-slate-800')
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<>
|
||||
{isActive && <span className="absolute left-0 top-1/2 h-5 w-0.5 -translate-y-1/2 rounded-full bg-indigo-500" />}
|
||||
<span className="relative z-10 grid w-11 shrink-0 place-items-center"><it.icon className="size-4" /></span>
|
||||
<span className="relative z-10 flex min-w-0 flex-1 flex-col items-start overflow-hidden pr-3">
|
||||
<span className="w-full truncate leading-tight">{it.label}</span>
|
||||
<span className="w-full truncate text-xs font-normal text-slate-400">{it.description}</span>
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</NavLink>
|
||||
)
|
||||
})}
|
||||
</nav>
|
||||
|
||||
{/* 底部设置 */}
|
||||
<nav className="shrink-0 space-y-1 border-t border-slate-100 px-2 py-2">
|
||||
{navItems.filter(it => it.id === 'settings').map(it => {
|
||||
const to = '/hub/' + it.id
|
||||
return (
|
||||
<NavLink
|
||||
key={it.id}
|
||||
to={to}
|
||||
className={({ isActive }) => {
|
||||
const base = 'group relative flex items-center rounded-lg py-2.5 text-sm font-medium transition-colors cursor-pointer'
|
||||
return base + (isActive ? ' bg-indigo-50 text-indigo-700' : ' text-slate-600 hover:bg-slate-100 hover:text-slate-800')
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<>
|
||||
{isActive && <span className="absolute left-0 top-1/2 h-5 w-0.5 -translate-y-1/2 rounded-full bg-indigo-500" />}
|
||||
<span className="relative z-10 grid w-11 shrink-0 place-items-center"><it.icon className="size-4" /></span>
|
||||
<span className="relative z-10 flex min-w-0 flex-1 flex-col items-start overflow-hidden pr-3">
|
||||
<span className="w-full truncate leading-tight">{it.label}</span>
|
||||
<span className="w-full truncate text-xs font-normal text-slate-400">{it.description}</span>
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</NavLink>
|
||||
)
|
||||
})}
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<div className="flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden">
|
||||
<header className="flex h-16 shrink-0 items-center justify-between border-b border-slate-200 bg-white px-6">
|
||||
<div className="flex items-center gap-3">
|
||||
{isPluginSub && (
|
||||
<Button size="sm" variant="tertiary" onPress={() => navigate('/hub/plugins')}>← 返回插件管理</Button>
|
||||
)}
|
||||
<pageMeta.icon className="size-4 text-slate-500" />
|
||||
<span className="text-sm font-semibold text-slate-800">{pageMeta.label}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Avatar size="sm"><Avatar.Fallback>{(username || 'U').charAt(0).toUpperCase()}</Avatar.Fallback></Avatar>
|
||||
<span className="text-sm text-slate-600">{username || '未登录'}</span>
|
||||
<Separator orientation="vertical" className="h-5" />
|
||||
<Button size="sm" variant="tertiary" onPress={logout}>退出</Button>
|
||||
</div>
|
||||
</header>
|
||||
<main className="min-h-0 flex-1 overflow-y-auto p-6"><Outlet /></main>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user