import { memo, useState, useEffect, useCallback, Suspense, } from 'react' import { Link, useLocation, useNavigate } from 'react-router' import { LogOut, PanelLeftClose, PanelLeftOpen, Shield, ChevronRight, Activity, Menu, Sun, Moon, } from 'lucide-react' import { useTheme } from 'next-themes' import { useTranslation } from 'react-i18next' import { cn } from '@/lib/utils' import { useIsMobile } from '@/hooks/use-mobile' import { menuItems, findRouteMeta, findGroupKey } from '@/router/routes' import { GroupTabs } from './GroupTabs' import { getUser, clearAuth } from '@/lib/auth-storage' import { isThemeAuto, setThemeAuto } from '@/lib/theme-schedule' import { useScheduledTheme } from '@/hooks/useScheduledTheme' import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet' import { Skeleton } from '@/components/ui/skeleton' // 侧栏折叠状态持久化 key(命名空间见开发标准 08 章) const LAYOUT_COLLAPSED_KEY = 'lokistack.layout.collapsed' // 侧栏导航 — 桌面 aside 与移动端抽屉共用;菜单数据来自路由配置单源派生 function SidebarNav({ collapsed, onNavigate }: { collapsed: boolean; onNavigate?: () => void }) { const location = useLocation() const { t } = useTranslation() const isAdmin = getUser()?.role === 'admin' // adminOnly 项(用户管理)仅管理员可见;后端 AdminAuth 兜底 const visibleItems = menuItems.filter((item) => !item.adminOnly || isAdmin) return ( ) } // 侧栏 Logo 区 function SidebarLogo({ collapsed }: { collapsed: boolean }) { return (
{collapsed ? (
) : (
LokiStack
)}
) } // 主题二态切换:浅色 ↔ 深色(key: lokistack.theme.mode;resolvedTheme 兼容"跟随系统/定时"偏好) // 定时模式开启时点击 = 关闭定时并切到相反主题(用户手动覆盖调度)。 function ThemeToggle() { const { resolvedTheme, setTheme } = useTheme() const { t } = useTranslation() const isDark = resolvedTheme === 'dark' const label = isDark ? t('layout.toLight') : t('layout.toDark') return ( ) } // 页面级加载骨架(挂在 main 内,切页时布局不闪) function PageSkeleton() { const { t } = useTranslation() return (
{[1, 2, 3].map((i) => ( ))}
) } /** * Layout 组件 — 布局骨架,业务页只挂在内容区 * - 菜单/顶栏标题来自路由 meta 单源(router/routes.tsx) * - 桌面端折叠状态持久化;移动端(<768px)侧栏为抽屉 + 遮罩 * - 页面懒加载的 Suspense 边界在 main 内部 */ const Layout = memo(function Layout({ children }: { children: React.ReactNode }) { const isMobile = useIsMobile() const [collapsed, setCollapsed] = useState(() => localStorage.getItem(LAYOUT_COLLAPSED_KEY) === '1') const [mobileOpen, setMobileOpen] = useState(false) const location = useLocation() const navigate = useNavigate() const { t } = useTranslation() const user = getUser() // 主题定时调度(「定时」模式开启时每分钟按时区解析浅/深色) useScheduledTheme() // 顶栏标题来自路由 meta // meta.title 为 i18n key(common 命名空间),展示前翻译 const pageTitleKey = findRouteMeta(location.pathname)?.title const pageTitle = pageTitleKey ? t(pageTitleKey) : 'LokiStack' // 折叠状态持久化(仅桌面端语义) useEffect(() => { localStorage.setItem(LAYOUT_COLLAPSED_KEY, collapsed ? '1' : '0') }, [collapsed]) const toggleSidebar = useCallback(() => { setCollapsed((prev) => !prev) }, []) const handleLogout = useCallback(() => { clearAuth() navigate('/login', { replace: true }) }, [navigate]) return (
{/* 桌面侧栏 */} {!isMobile && ( )} {/* 移动端抽屉侧栏 */} {isMobile && ( {t('layout.sheetTitle')}
setMobileOpen(false)} />
)} {/* Main Content */}
{/* Top Bar */}
{isMobile && ( )}

{pageTitle}

{user && (
{user.username} {user.role === 'admin' ? t('layout.admin') : user.role}
)}
{/* 分组 tab(资源接入 / 日志服务 / 日志 / 系统管理);不属于分组时不渲染 */} {/* Page Content — 页面级 Suspense 边界,懒加载时不闪布局 */}
}>{children}
) }) export default Layout