import { Outlet, useLocation, useNavigate } from '@tanstack/react-router' import { useEffect, useRef, useState } from 'react' import { Activity, Bell, ChevronLeft, ChevronRight, FileCode, Globe, KeyRound, LayoutDashboard, ListTodo, LogOut, Menu, Moon, Network, PanelLeftClose, PanelLeftOpen, ScrollText, Server, Settings, Sun, Terminal, UserRound, Variable, X, } from 'lucide-react' import { useTheme } from '@/context/ThemeContext' import { api } from '@/api' import { usePublicSite, useSentence, useUser } from '@/api/hooks' import { NodeSwitcher } from '@/components/business/NodeSwitcher' import { SystemNotice } from '@/components/business/SystemNotice' import { ToastHost } from '@/components/business/ToastHost' import { resetAuthCache } from '@/lib/auth' import { eventBus } from '@/lib/event-bus' import { cn } from '@/components/ui' const NAV_ITEMS = [ { path: '/', icon: LayoutDashboard, label: '数据仪表', exact: true }, { path: '/tasks', icon: ListTodo, label: '定时任务', exact: true }, { path: '/agents', icon: Server, label: '远程执行', exact: true }, { path: '/editor', icon: FileCode, label: '脚本编辑', exact: false }, { path: '/history', icon: ScrollText, label: '执行历史', exact: true }, { path: '/environments', icon: Variable, label: '变量机密', exact: true }, { path: '/languages', icon: Globe, label: '语言依赖', exact: false }, { path: '/terminal', icon: Terminal, label: '终端命令', exact: true }, { path: '/notify', icon: Bell, label: '消息推送', exact: true }, { path: '/logs', icon: KeyRound, label: '运行日志', exact: true }, { path: '/monitor', icon: Activity, label: '系统监控', exact: true }, { path: '/interconnect', icon: Network, label: '互联管理', exact: true }, { path: '/settings', icon: Settings, label: '系统设置', exact: true }, ] as const const COLLAPSE_KEY = 'taskpool.sidebar.collapsed' function isActive(pathname: string, path: string, exact: boolean) { if (exact) return pathname === path return pathname === path || pathname.startsWith(path + '/') } export default function Layout() { const location = useLocation() const navigate = useNavigate() const { theme, toggleTheme } = useTheme() const { data: site } = usePublicSite() const { data: sentenceData } = useSentence() const { data: user } = useUser() const [menuOpen, setMenuOpen] = useState(false) const [isMobile, setIsMobile] = useState(false) const [collapsed, setCollapsed] = useState(() => { try { return localStorage.getItem(COLLAPSE_KEY) === '1' } catch { return false } }) const [userMenuOpen, setUserMenuOpen] = useState(false) const userMenuRef = useRef(null) const title = site?.title || '任务池' const sentence = sentenceData?.sentence || '欢迎使用任务池' const username = user?.username || '管理员' const sidebarCollapsed = !isMobile && collapsed useEffect(() => { document.title = title const icon = site?.icon if (icon) { let link = document.querySelector("link[rel='icon']") as HTMLLinkElement | null if (!link) { link = document.createElement('link') link.rel = 'icon' document.head.appendChild(link) } link.href = icon } }, [title, site?.icon]) useEffect(() => { const check = () => setIsMobile(window.innerWidth < 1024) check() window.addEventListener('resize', check) return () => window.removeEventListener('resize', check) }, []) useEffect(() => { setMenuOpen(false) setUserMenuOpen(false) }, [location.pathname]) useEffect(() => { eventBus.init() }, []) useEffect(() => { try { localStorage.setItem(COLLAPSE_KEY, collapsed ? '1' : '0') } catch { // ignore } }, [collapsed]) useEffect(() => { function onDocClick(e: MouseEvent) { if (!userMenuRef.current) return if (!userMenuRef.current.contains(e.target as Node)) { setUserMenuOpen(false) } } document.addEventListener('mousedown', onDocClick) return () => document.removeEventListener('mousedown', onDocClick) }, []) async function logout() { try { await api.auth.logout() } catch { // ignore } resetAuthCache() window.location.href = '/login' } return (
{isMobile && menuOpen && (
setMenuOpen(false)} /> )}
{isMobile ? ( ) : ( )}

{sentence}

{/* 顶栏工具统一 h-8,避免通知/主题/用户高度不一致 */}
{userMenuOpen && (
{username}
已登录
)}
) }