App.tsx 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. import React, { useState, useEffect } from 'react';
  2. import { Routes, Route, useNavigate, useLocation, Navigate } from 'react-router-dom';
  3. import Sidebar, { PageId } from './components/Sidebar';
  4. import Navbar from './components/Navbar';
  5. import Home from './pages/Home';
  6. import ModelSquare from './pages/ModelSquare';
  7. import Profile from './pages/Profile';
  8. import Login from './pages/Login';
  9. import Register from './pages/Register';
  10. import OpenPlatform from './pages/OpenPlatform';
  11. import UserAgreement from './pages/UserAgreement';
  12. import PrivacyPolicy from './pages/PrivacyPolicy';
  13. import SSOCallback from './pages/SSOCallback';
  14. import ModelPricingDetail from './pages/ModelPricingDetail';
  15. import { authService } from './services/authService';
  16. import { Loader2 } from './icons/commonIcons';
  17. import { NotificationProvider } from './contexts/NotificationContext';
  18. import './styles/markdown.css';
  19. const API_BASE = import.meta.env.VITE_API_BASE_URL ?? 'http://localhost:8010';
  20. // 品牌配置 Context
  21. export interface BrandingConfig { system_name: string; system_logo: string; icp_number: string }
  22. export const BrandingContext = React.createContext<BrandingConfig>({ system_name: '四川路桥Maas算力平台', system_logo: '', icp_number: '' })
  23. const pagePathMap: Record<PageId, string> = {
  24. home: '/',
  25. models: '/models',
  26. platform: '/platform',
  27. profile: '/profile',
  28. };
  29. // 公开路由(不需要登录)
  30. const publicRoutes = ['/login', '/register', '/sso-callback', '/auth/callback', '/user-agreement', '/privacy-policy'];
  31. // 认证状态检查组件
  32. const AuthGuard: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  33. const location = useLocation();
  34. const [isChecking, setIsChecking] = useState(true);
  35. const [isAuthenticated, setIsAuthenticated] = useState(false);
  36. useEffect(() => {
  37. const checkAuth = async () => {
  38. if (publicRoutes.includes(location.pathname)) {
  39. setIsChecking(false);
  40. return;
  41. }
  42. if (!authService.isAuthenticated()) {
  43. setIsAuthenticated(false);
  44. setIsChecking(false);
  45. return;
  46. }
  47. try {
  48. const isValid = await authService.checkAndRefreshToken();
  49. setIsAuthenticated(isValid);
  50. } catch (error) {
  51. console.error('[AuthGuard] Auth check failed:', error);
  52. setIsAuthenticated(false);
  53. } finally {
  54. setIsChecking(false);
  55. }
  56. };
  57. checkAuth();
  58. }, [location.pathname]);
  59. // 订阅认证状态变化
  60. useEffect(() => {
  61. const unsubscribe = authService.subscribe(() => {
  62. const authenticated = authService.isAuthenticated();
  63. setIsAuthenticated(authenticated);
  64. });
  65. return () => unsubscribe();
  66. }, []);
  67. // 正在检查认证状态
  68. if (isChecking && !publicRoutes.includes(location.pathname)) {
  69. return (
  70. <div className="min-h-screen bg-gray-50 flex items-center justify-center">
  71. <div className="flex flex-col items-center gap-4">
  72. <Loader2 className="w-8 h-8 text-blue-600 animate-spin" />
  73. <span className="text-gray-600">验证登录状态...</span>
  74. </div>
  75. </div>
  76. );
  77. }
  78. // 未登录且不是公开路由 → 一律跳登录页(登录页上有 SSO 按钮供用户选择)
  79. if (!isAuthenticated && !publicRoutes.includes(location.pathname)) {
  80. return <Navigate to="/login" state={{ from: location }} replace />;
  81. }
  82. // 已登录且访问登录/注册页,重定向到首页
  83. if (isAuthenticated && (location.pathname === '/login' || location.pathname === '/register')) {
  84. return <Navigate to="/" replace />;
  85. }
  86. return <>{children}</>;
  87. };
  88. // 主布局(带侧边栏和导航栏)
  89. const MainLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  90. const location = useLocation();
  91. const branding = React.useContext(BrandingContext);
  92. return (
  93. <div className="min-h-screen flex flex-col bg-gray-50">
  94. <Navbar />
  95. <div className="flex flex-1 pt-14 sm:pt-16">
  96. <Sidebar />
  97. <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">
  98. <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">
  99. {children}
  100. {location.pathname === '/' && (
  101. <footer className="mt-6 px-2 border-t border-gray-100 text-center py-2 flex flex-col items-center gap-1">
  102. <p className="text-xs text-gray-400">© 2026 {branding.system_name}</p>
  103. <div className="flex items-center justify-center gap-4 text-xs text-gray-400">
  104. <a
  105. href="https://beian.mps.gov.cn/#/query/webSearch?code=51019002009294"
  106. target="_blank"
  107. rel="noopener noreferrer"
  108. className="flex items-center gap-1 hover:text-gray-600"
  109. >
  110. <img src="https://beian.mps.gov.cn/web/assets/logo01.6189a29f.png" alt="公安备案图标" className="h-4 w-4" />
  111. 川公网安备51019002009294号
  112. </a>
  113. <a
  114. href="https://beian.miit.gov.cn/"
  115. target="_blank"
  116. rel="noopener noreferrer"
  117. className="hover:text-gray-600"
  118. >
  119. 蜀ICP备2025168675号-3
  120. </a>
  121. </div>
  122. </footer>
  123. )}
  124. </div>
  125. </main>
  126. </div>
  127. </div>
  128. );
  129. };
  130. // 认证布局(无侧边栏和导航栏,用于登录/注册页面)
  131. const AuthLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  132. return <>{children}</>;
  133. };
  134. const App: React.FC = () => {
  135. const navigate = useNavigate();
  136. const location = useLocation();
  137. // 品牌配置
  138. const [branding, setBranding] = React.useState<BrandingConfig>({ system_name: '四川路桥Maas算力平台', system_logo: '', icp_number: '' });
  139. useEffect(() => {
  140. fetch(`${API_BASE}/api/public/branding`)
  141. .then(r => r.json())
  142. .then(d => {
  143. if (d.system_name) {
  144. setBranding(d);
  145. document.title = d.system_name;
  146. }
  147. // 动态更新 favicon
  148. if (d.system_logo) {
  149. let link = document.querySelector<HTMLLinkElement>("link[rel~='icon']");
  150. if (!link) {
  151. link = document.createElement('link');
  152. link.rel = 'icon';
  153. document.head.appendChild(link);
  154. }
  155. link.href = d.system_logo;
  156. }
  157. })
  158. .catch(() => {});
  159. }, []);
  160. // 判断是否是认证页面(登录/注册)
  161. const isAuthPage = publicRoutes.includes(location.pathname);
  162. return (
  163. <BrandingContext.Provider value={branding}>
  164. <NotificationProvider>
  165. <AuthGuard>
  166. {isAuthPage ? (
  167. <AuthLayout>
  168. <Routes>
  169. <Route path="/login" element={<Login />} />
  170. <Route path="/register" element={<Register />} />
  171. <Route path="/sso-callback" element={<SSOCallback />} />
  172. <Route path="/auth/callback" element={<SSOCallback />} />
  173. <Route path="/user-agreement" element={<UserAgreement />} />
  174. <Route path="/privacy-policy" element={<PrivacyPolicy />} />
  175. </Routes>
  176. </AuthLayout>
  177. ) : (
  178. <MainLayout>
  179. <Routes>
  180. <Route path="/" element={<Home />} />
  181. <Route path="/models" element={<ModelSquare />} />
  182. <Route path="/models/pricing/:modelCode" element={<ModelPricingDetail />} />
  183. <Route path="/profile" element={<Profile />} />
  184. <Route path="/platform" element={<OpenPlatform />} />
  185. <Route path="/user-agreement" element={<UserAgreement />} />
  186. <Route path="*" element={<Navigate to="/" replace />} />
  187. </Routes>
  188. </MainLayout>
  189. )}
  190. </AuthGuard>
  191. </NotificationProvider>
  192. </BrandingContext.Provider>
  193. );
  194. };
  195. export default App;