f0a1552
ZhenYi
feat(chat): add chat module with room management, message handling, and session revocation
f0a1552
import {
Activity,
Database,
HardDrive,
LayoutDashboard,
ListChecks,
LogOut,
MessagesSquare,
Moon,
ScrollText,
Settings,
ShieldCheck,
Sun,
Users,
} from 'lucide-react'
import { NavLink, Navigate, Outlet, useNavigate } from 'react-router-dom'
import { useSession } from '@/app/session'
import { useTheme } from '@/app/theme'
import { Badge, Button, Loading } from '@/shared/ui'
const NAV_ITEMS = [
{ to: '/', label: '系统状态', icon: LayoutDashboard, end: true },
{ to: '/users', label: '用户', icon: Users },
{ to: '/spaces', label: 'Space', icon: Database },
{ to: '/chat', label: '聊天记录', icon: MessagesSquare },
{ to: '/storage', label: '存储', icon: HardDrive },
{ to: '/queues', label: '队列', icon: Activity },
{ to: '/tasks', label: '任务', icon: ListChecks },
{ to: '/logs', label: '日志', icon: ScrollText },
{ to: '/admins', label: '管理员', icon: ShieldCheck },
{ to: '/system', label: '系统配置', icon: Settings },
]
function roleLabel(role: string): string {
switch (role) {
case 'super_admin':
return '超级管理员'
case 'operator':
return '运维'
default:
return '只读'
}
}
export function AdminShell() {
const { me, loading, logout } = useSession()
const { dark, toggle } = useTheme()
const navigate = useNavigate()
if (loading) {
return <Loading label="正在校验管理会话…" />
}
if (!me) {
return <Navigate to="/login" replace />
}
if (me.must_change_password) {
return <Navigate to="/password" replace />
}
return (
<div className="flex min-h-full">
<aside className="hidden w-56 shrink-0 flex-col border-r border-slate-200 bg-white p-3 dark:border-slate-800 dark:bg-slate-950 md:flex">
<div className="mb-4 px-2">
<div className="text-sm font-semibold text-slate-900 dark:text-slate-100">GitData</div>
<div className="text-xs text-slate-500">管理后台</div>
</div>
<nav className="flex flex-1 flex-col gap-1">
{NAV_ITEMS.map((item) => (
<NavLink
key={item.to}
to={item.to}
end={item.end}
className={({ isActive }) =>
`flex items-center gap-2 rounded-md px-2 py-1.5 text-sm ${
isActive
? 'bg-slate-900 text-white dark:bg-slate-100 dark:text-slate-900'
: 'text-slate-600 hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-800'
}`
}
>
<item.icon size={16} />
{item.label}
</NavLink>
))}
</nav>
<div className="mt-4 border-t border-slate-200 pt-3 text-xs text-slate-500 dark:border-slate-800">
adminhost
</div>
</aside>
<div className="flex min-w-0 flex-1 flex-col">
<header className="flex items-center justify-between gap-3 border-b border-slate-200 bg-white px-4 py-3 dark:border-slate-800 dark:bg-slate-950">
<div className="flex items-center gap-2">
<span className="font-medium text-slate-800 dark:text-slate-100">{me.display_name}</span>
<Badge tone={me.role === 'super_admin' ? 'danger' : 'info'}>{roleLabel(me.role)}</Badge>
</div>
<div className="flex items-center gap-2">
<Button variant="ghost" size="sm" title="切换主题" onClick={toggle}>
{dark ? <Sun size={16} /> : <Moon size={16} />}
</Button>
<Button variant="secondary" size="sm" onClick={() => navigate('/password')}>
修改口令
</Button>
<Button
variant="secondary"
size="sm"
onClick={async () => {
await logout()
navigate('/login')
}}
>
<LogOut size={14} />
退出
</Button>
</div>
</header>
<main className="min-w-0 flex-1 overflow-y-auto p-4">
<Outlet />
</main>
</div>
</div>
)
}