| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219 |
- import React, { useState, useEffect } from 'react';
- import { Routes, Route, useNavigate, useLocation, Navigate } from 'react-router-dom';
- import Sidebar, { PageId } from './components/Sidebar';
- import Navbar from './components/Navbar';
- import Home from './pages/Home';
- import ModelSquare from './pages/ModelSquare';
- import Profile from './pages/Profile';
- import Login from './pages/Login';
- import Register from './pages/Register';
- import OpenPlatform from './pages/OpenPlatform';
- import UserAgreement from './pages/UserAgreement';
- import PrivacyPolicy from './pages/PrivacyPolicy';
- import SSOCallback from './pages/SSOCallback';
- import ModelPricingDetail from './pages/ModelPricingDetail';
- import { authService } from './services/authService';
- import { Loader2 } from './icons/commonIcons';
- import { NotificationProvider } from './contexts/NotificationContext';
- import './styles/markdown.css';
- const API_BASE = import.meta.env.VITE_API_BASE_URL ?? 'http://localhost:8010';
- // 品牌配置 Context
- export interface BrandingConfig { system_name: string; system_logo: string; icp_number: string }
- export const BrandingContext = React.createContext<BrandingConfig>({ system_name: '四川路桥Maas算力平台', system_logo: '', icp_number: '' })
- const pagePathMap: Record<PageId, string> = {
- home: '/',
- models: '/models',
- platform: '/platform',
- profile: '/profile',
- };
- // 公开路由(不需要登录)
- const publicRoutes = ['/login', '/register', '/sso-callback', '/auth/callback', '/user-agreement', '/privacy-policy'];
- // 认证状态检查组件
- const AuthGuard: React.FC<{ children: React.ReactNode }> = ({ children }) => {
- const location = useLocation();
- const [isChecking, setIsChecking] = useState(true);
- const [isAuthenticated, setIsAuthenticated] = useState(false);
- useEffect(() => {
- const checkAuth = async () => {
- if (publicRoutes.includes(location.pathname)) {
- setIsChecking(false);
- return;
- }
- if (!authService.isAuthenticated()) {
- setIsAuthenticated(false);
- setIsChecking(false);
- return;
- }
- try {
- const isValid = await authService.checkAndRefreshToken();
- setIsAuthenticated(isValid);
- } catch (error) {
- console.error('[AuthGuard] Auth check failed:', error);
- setIsAuthenticated(false);
- } finally {
- setIsChecking(false);
- }
- };
- checkAuth();
- }, [location.pathname]);
- // 订阅认证状态变化
- useEffect(() => {
- const unsubscribe = authService.subscribe(() => {
- const authenticated = authService.isAuthenticated();
- setIsAuthenticated(authenticated);
- });
- return () => unsubscribe();
- }, []);
- // 正在检查认证状态
- if (isChecking && !publicRoutes.includes(location.pathname)) {
- return (
- <div className="min-h-screen bg-gray-50 flex items-center justify-center">
- <div className="flex flex-col items-center gap-4">
- <Loader2 className="w-8 h-8 text-blue-600 animate-spin" />
- <span className="text-gray-600">验证登录状态...</span>
- </div>
- </div>
- );
- }
- // 未登录且不是公开路由 → 一律跳登录页(登录页上有 SSO 按钮供用户选择)
- if (!isAuthenticated && !publicRoutes.includes(location.pathname)) {
- return <Navigate to="/login" state={{ from: location }} replace />;
- }
- // 已登录且访问登录/注册页,重定向到首页
- if (isAuthenticated && (location.pathname === '/login' || location.pathname === '/register')) {
- return <Navigate to="/" replace />;
- }
- return <>{children}</>;
- };
- // 主布局(带侧边栏和导航栏)
- const MainLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
- const location = useLocation();
- const branding = React.useContext(BrandingContext);
- return (
- <div className="min-h-screen flex flex-col bg-gray-50">
- <Navbar />
- <div className="flex flex-1 pt-14 sm:pt-16">
- <Sidebar />
- <main className="flex-1 ml-0 md:ml-[11rem] h-[calc(100vh-3.5rem)] sm:h-[calc(100vh-4rem)] flex flex-col overflow-y-auto pb-14 md:pb-0">
- <div className="max-w-[1800px] mx-auto px-3 sm:px-4 md:px-2 py-2 w-full flex-1 flex flex-col min-h-0">
- {children}
- {location.pathname === '/' && (
- <footer className="mt-6 px-2 border-t border-gray-100 text-center py-2 flex flex-col items-center gap-1">
- <p className="text-xs text-gray-400">© 2026 {branding.system_name}</p>
- <div className="flex items-center justify-center gap-4 text-xs text-gray-400">
- <a
- href="https://beian.mps.gov.cn/#/query/webSearch?code=51019002009294"
- target="_blank"
- rel="noopener noreferrer"
- className="flex items-center gap-1 hover:text-gray-600"
- >
- <img src="https://beian.mps.gov.cn/web/assets/logo01.6189a29f.png" alt="公安备案图标" className="h-4 w-4" />
- 川公网安备51019002009294号
- </a>
- <a
- href="https://beian.miit.gov.cn/"
- target="_blank"
- rel="noopener noreferrer"
- className="hover:text-gray-600"
- >
- 蜀ICP备2025168675号-3
- </a>
- </div>
- </footer>
- )}
- </div>
- </main>
- </div>
- </div>
- );
- };
- // 认证布局(无侧边栏和导航栏,用于登录/注册页面)
- const AuthLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
- return <>{children}</>;
- };
- const App: React.FC = () => {
- const navigate = useNavigate();
- const location = useLocation();
- // 品牌配置
- const [branding, setBranding] = React.useState<BrandingConfig>({ system_name: '四川路桥Maas算力平台', system_logo: '', icp_number: '' });
- useEffect(() => {
- fetch(`${API_BASE}/api/public/branding`)
- .then(r => r.json())
- .then(d => {
- if (d.system_name) {
- setBranding(d);
- document.title = d.system_name;
- }
- // 动态更新 favicon
- if (d.system_logo) {
- let link = document.querySelector<HTMLLinkElement>("link[rel~='icon']");
- if (!link) {
- link = document.createElement('link');
- link.rel = 'icon';
- document.head.appendChild(link);
- }
- link.href = d.system_logo;
- }
- })
- .catch(() => {});
- }, []);
- // 判断是否是认证页面(登录/注册)
- const isAuthPage = publicRoutes.includes(location.pathname);
- return (
- <BrandingContext.Provider value={branding}>
- <NotificationProvider>
- <AuthGuard>
- {isAuthPage ? (
- <AuthLayout>
- <Routes>
- <Route path="/login" element={<Login />} />
- <Route path="/register" element={<Register />} />
- <Route path="/sso-callback" element={<SSOCallback />} />
- <Route path="/auth/callback" element={<SSOCallback />} />
- <Route path="/user-agreement" element={<UserAgreement />} />
- <Route path="/privacy-policy" element={<PrivacyPolicy />} />
- </Routes>
- </AuthLayout>
- ) : (
- <MainLayout>
- <Routes>
- <Route path="/" element={<Home />} />
- <Route path="/models" element={<ModelSquare />} />
- <Route path="/models/pricing/:modelCode" element={<ModelPricingDetail />} />
- <Route path="/profile" element={<Profile />} />
- <Route path="/platform" element={<OpenPlatform />} />
- <Route path="/user-agreement" element={<UserAgreement />} />
- <Route path="*" element={<Navigate to="/" replace />} />
- </Routes>
- </MainLayout>
- )}
- </AuthGuard>
- </NotificationProvider>
- </BrandingContext.Provider>
- );
- };
- export default App;
|