Files
LokiStack/frontend/src/App.tsx
T
opsx 9641503007
CI / Backend (go vet + test) (push) Waiting to run
CI / Frontend (lint + build + test) (push) Waiting to run
feat(license): Pro 授权机制 — 离线许可码 + 免费版功能闸门
免费版:日志查询/部署管理/采集/用户管理/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 的页面显示裸键)
2026-10-31 23:08:06 +08:00

89 lines
3.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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