免费版:日志查询/部署管理/采集/用户管理/LDAP/设置;Pro(¥199 个人单实例永久 / ¥499 团队多实例永久): 日志分析、日志告警、智能运维(AI)、审计日志。 - 后端:HMAC-SHA256 离线许可码(签发 CLI / license --plan --edition --seats --customer --days; 永久=days 0),GET /system/license + POST activate;RequirePro 闸门挂 metrics/ai/audit; 过期自动回落免费;测试覆盖签发-校验/篡改/免费拒签 - 前端:菜单 Pro 项带锁标;四个 Pro 页面免费版渲染升级引导卡(功能清单+定价+页内激活); 设置页新增授权区(版本/席位/到期/激活入口);license 词典中英 - domains 注册提升到 main.tsx 全局(修复 license 命名空间在未引 domains 的页面显示裸键)
89 lines
3.1 KiB
TypeScript
89 lines
3.1 KiB
TypeScript
import { lazy, Suspense, memo, useEffect } from 'react'
|
||
import { Routes, Route, Navigate, useLocation } from 'react-router'
|
||
import { Toaster } from '@/components/ui/sonner'
|
||
import { isLoggedIn, getUser } from '@/lib/auth-storage'
|
||
import { routes, findRouteMeta } from '@/router/routes'
|
||
import ErrorBoundary from '@/components/ErrorBoundary'
|
||
import { LicenseProvider } from '@/hooks/useLicense'
|
||
import { useTranslation } from 'react-i18next'
|
||
|
||
const Layout = lazy(() => import('@/components/Layout'))
|
||
|
||
// 路由守卫 - 未登录跳登录页并携带来源路径(登录成功后回跳,见 lib/redirect.ts)
|
||
const RequireAuth = memo(function RequireAuth({ children }: { children: React.ReactNode }) {
|
||
const location = useLocation()
|
||
if (!isLoggedIn()) {
|
||
const from = location.pathname + location.search
|
||
return <Navigate to={`/login?redirect=${encodeURIComponent(from)}`} replace />
|
||
}
|
||
if (getUser()?.role !== 'admin' && location.pathname !== '/logs') return <Navigate to="/logs" replace />
|
||
// 动态导入Layout以避免在登录页面加载它
|
||
return <LazyLayout>{children}</LazyLayout>
|
||
})
|
||
|
||
// 懒加载布局包装器(Layout chunk 加载期间的整屏占位)
|
||
function LazyLayout({ children }: { children: React.ReactNode }) {
|
||
return (
|
||
<Suspense fallback={<AppLoadingFallback />}>
|
||
<Layout>{children}</Layout>
|
||
</Suspense>
|
||
)
|
||
}
|
||
|
||
// 整屏加载占位
|
||
function AppLoadingFallback() {
|
||
return (
|
||
<div className="h-screen w-screen flex items-center justify-center bg-muted dark:bg-slate-950">
|
||
<div className="w-6 h-6 border-2 border-slate-300 border-t-primary rounded-full animate-spin" />
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// document.title 由路由 meta 单源驱动(meta.title 为 i18n key,common 命名空间)
|
||
function useDocumentTitle() {
|
||
const location = useLocation()
|
||
const { t, i18n } = useTranslation()
|
||
useEffect(() => {
|
||
const titleKey = findRouteMeta(location.pathname)?.title
|
||
const title = titleKey ? t(titleKey) : ''
|
||
document.title = title && location.pathname !== '/login' ? `${title} · LokiStack` : 'LokiStack'
|
||
}, [location.pathname, t, i18n.language])
|
||
}
|
||
|
||
/**
|
||
* App 根组件
|
||
* - 路由配置集中在 router/routes.tsx(meta 驱动标题/菜单)
|
||
* - 页面全部 React.lazy 懒加载实现代码分割
|
||
* - 页面级 Suspense 在 Layout 的内容区,切换页面时布局不闪
|
||
*/
|
||
const App = memo(function App() {
|
||
useDocumentTitle()
|
||
return (
|
||
<LicenseProvider>
|
||
<Routes>
|
||
{routes.map((route) => (
|
||
<Route
|
||
key={route.path}
|
||
path={route.path}
|
||
element={
|
||
route.auth ? (
|
||
<RequireAuth>
|
||
{/* 页面级错误边界:单页崩溃/chunk 加载失败只影响当前页,侧栏与其它路由仍可用 */}
|
||
<ErrorBoundary key={route.path}>{<route.component />}</ErrorBoundary>
|
||
</RequireAuth>
|
||
) : (
|
||
<Suspense fallback={<AppLoadingFallback />}>
|
||
<route.component />
|
||
</Suspense>
|
||
)
|
||
}
|
||
/>
|
||
))}
|
||
</Routes>
|
||
<Toaster />
|
||
</LicenseProvider>
|
||
)
|
||
})
|
||
|
||
export default App
|