App.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314
  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 TextInteraction from './pages/TextInteraction';
  7. import ImageGeneration from './pages/ImageGeneration';
  8. import AudioWorkshop from './pages/AudioWorkshop';
  9. import VideoSynthesis from './pages/VideoSynthesis';
  10. import ModelSquare from './pages/ModelSquare';
  11. import ModelPricingDetail from './pages/ModelPricingDetail';
  12. import Toolbox from './pages/Toolbox';
  13. import QwenOCR from './pages/QwenOCR';
  14. import ImageTranslation from './pages/ImageTranslation';
  15. import TingwuWorkshop from './pages/TingwuWorkshop';
  16. import TingwuResult from './pages/TingwuResult';
  17. import DataMining from './pages/DataMining';
  18. import DeepResearch from './pages/DeepResearch';
  19. import ResearchDetail from './pages/ResearchDetail';
  20. import QwenMT from './pages/QwenMT';
  21. import PhotoAnswer from './pages/PhotoAnswer';
  22. import MultimodalTranslation from './pages/MultimodalTranslation';
  23. import Profile from './pages/Profile';
  24. import Billing from './pages/Billing';
  25. import Login from './pages/Login';
  26. import Register from './pages/Register';
  27. import Recharge from './pages/Recharge';
  28. import RechargeReturn from './pages/RechargeReturn';
  29. import RechargeHistory from './pages/RechargeHistory';
  30. import InvoiceManagement from './pages/InvoiceManagement';
  31. import InvoiceApply from './pages/InvoiceApply';
  32. import InvoiceResult from './pages/InvoiceResult';
  33. import OpenPlatform from './pages/OpenPlatform';
  34. import OpenClaw from './pages/OpenClaw';
  35. import UserAgreement from './pages/UserAgreement';
  36. import PrivacyPolicy from './pages/PrivacyPolicy';
  37. import SSOCallback from './pages/SSOCallback';
  38. import { authService } from './services/authService';
  39. import { Loader2 } from './icons/commonIcons';
  40. import BindPhoneReminder from './components/BindPhoneReminder';
  41. import { NotificationProvider } from './contexts/NotificationContext';
  42. import './styles/markdown.css';
  43. // 品牌配置 Context
  44. export interface BrandingConfig { system_name: string; system_logo: string; icp_number: string }
  45. export const BrandingContext = React.createContext<BrandingConfig>({ system_name: '智创空间', system_logo: '', icp_number: '' })
  46. const API_BASE = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8010'
  47. const pagePathMap: Record<PageId, string> = {
  48. home: '/',
  49. text: '/text',
  50. image: '/image',
  51. audio: '/audio',
  52. video: '/video',
  53. models: '/models',
  54. toolbox: '/toolbox',
  55. platform: '/platform',
  56. openclaw: '/openclaw',
  57. profile: '/profile',
  58. billing: '/billing',
  59. invoice: '/invoice',
  60. };
  61. // 公开路由(不需要登录)
  62. const publicRoutes = ['/login', '/register', '/sso-callback', '/recharge-return', '/user-agreement', '/privacy-policy'];
  63. const CasLoginRedirect: React.FC<{ targetPath: string }> = ({ targetPath }) => {
  64. useEffect(() => {
  65. authService.startCasLogin(targetPath || '/');
  66. }, [targetPath]);
  67. return (
  68. <div className="min-h-screen bg-gray-50 flex items-center justify-center">
  69. <div className="flex flex-col items-center gap-4">
  70. <Loader2 className="w-8 h-8 text-blue-600 animate-spin" />
  71. <span className="text-gray-600">正在跳转统一认证登录...</span>
  72. </div>
  73. </div>
  74. );
  75. };
  76. // 认证状态检查组件
  77. const AuthGuard: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  78. const location = useLocation();
  79. const [isChecking, setIsChecking] = useState(true);
  80. const [isAuthenticated, setIsAuthenticated] = useState(false);
  81. useEffect(() => {
  82. const checkAuth = async () => {
  83. // 如果是公开路由,不需要检查
  84. if (publicRoutes.includes(location.pathname)) {
  85. setIsChecking(false);
  86. return;
  87. }
  88. // 检查本地是否有Token
  89. if (!authService.isAuthenticated()) {
  90. setIsAuthenticated(false);
  91. setIsChecking(false);
  92. return;
  93. }
  94. // 有Token,验证并刷新
  95. try {
  96. const isValid = await authService.checkAndRefreshToken();
  97. setIsAuthenticated(isValid);
  98. } catch (error) {
  99. console.error('[AuthGuard] Auth check failed:', error);
  100. setIsAuthenticated(false);
  101. } finally {
  102. setIsChecking(false);
  103. }
  104. };
  105. checkAuth();
  106. }, [location.pathname]);
  107. // 订阅认证状态变化
  108. useEffect(() => {
  109. const unsubscribe = authService.subscribe(() => {
  110. const authenticated = authService.isAuthenticated();
  111. setIsAuthenticated(authenticated);
  112. });
  113. return () => unsubscribe();
  114. }, []);
  115. // 正在检查认证状态
  116. if (isChecking && !publicRoutes.includes(location.pathname)) {
  117. return (
  118. <div className="min-h-screen bg-gray-50 flex items-center justify-center">
  119. <div className="flex flex-col items-center gap-4">
  120. <Loader2 className="w-8 h-8 text-blue-600 animate-spin" />
  121. <span className="text-gray-600">验证登录状态...</span>
  122. </div>
  123. </div>
  124. );
  125. }
  126. // 未登录且不是公开路由
  127. if (!isAuthenticated && !publicRoutes.includes(location.pathname)) {
  128. return <Navigate to="/login" state={{ from: location }} replace />;
  129. }
  130. // 已登录且访问登录/注册页,重定向到首页
  131. if (isAuthenticated && (location.pathname === '/login' || location.pathname === '/register')) {
  132. return <Navigate to="/" replace />;
  133. }
  134. return <>{children}</>;
  135. };
  136. // 主布局(带侧边栏和导航栏)
  137. const MainLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  138. const location = useLocation();
  139. const branding = React.useContext(BrandingContext);
  140. return (
  141. <div className="min-h-screen flex flex-col bg-gray-50">
  142. <Navbar />
  143. <div className="flex flex-1 pt-16">
  144. <Sidebar />
  145. <main className="flex-1 ml-[11rem] h-[calc(100vh-4rem)] flex flex-col overflow-y-auto">
  146. <div className="max-w-[1800px] mx-auto px-2 py-2 w-full flex-1 flex flex-col min-h-0">
  147. {children}
  148. {location.pathname === '/' && (
  149. <footer className="mt-6 h-[32px] px-2 border-t border-gray-100 text-center flex items-center justify-center">
  150. <p className="text-xs text-gray-400">
  151. © 2026 {branding.system_name}
  152. </p>
  153. </footer>
  154. )}
  155. </div>
  156. </main>
  157. </div>
  158. </div>
  159. );
  160. };
  161. // 认证布局(无侧边栏和导航栏,用于登录/注册页面)
  162. const AuthLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  163. return <>{children}</>;
  164. };
  165. const App: React.FC = () => {
  166. const navigate = useNavigate();
  167. const location = useLocation();
  168. // 品牌配置
  169. const [branding, setBranding] = React.useState<BrandingConfig>({ system_name: '智创空间', system_logo: '', icp_number: '' })
  170. useEffect(() => {
  171. fetch(`${API_BASE}/api/public/branding`)
  172. .then(r => r.json())
  173. .then(d => { if (d.system_name) setBranding(d) })
  174. .catch(() => {})
  175. }, [])
  176. // States to keep track of selected models across the app
  177. // 默认模型ID会在模型加载后自动更新为第一个可用模型(如果当前ID不存在)
  178. const [textModelId, setTextModelId] = useState('gemini-pro');
  179. const [imageModelId, setImageModelId] = useState('dall-e-3');
  180. const [audioModelId, setAudioModelId] = useState('elevenlabs');
  181. const [videoModelId, setVideoModelId] = useState('runway-gen3');
  182. const handleModelNavigation = (page: PageId, modelId: string) => {
  183. if (page === 'text') setTextModelId(modelId);
  184. if (page === 'image') setImageModelId(modelId);
  185. if (page === 'audio') setAudioModelId(modelId);
  186. if (page === 'video') setVideoModelId(modelId);
  187. const path = pagePathMap[page];
  188. navigate(path);
  189. };
  190. // 判断是否是认证页面(登录/注册)
  191. const isAuthPage = publicRoutes.includes(location.pathname);
  192. return (
  193. <BrandingContext.Provider value={branding}>
  194. <NotificationProvider>
  195. <AuthGuard>
  196. <BindPhoneReminder />
  197. {isAuthPage ? (
  198. <AuthLayout>
  199. <Routes>
  200. <Route path="/login" element={<Login />} />
  201. <Route path="/register" element={<Register />} />
  202. <Route path="/sso-callback" element={<SSOCallback />} />
  203. <Route path="/user-agreement" element={<UserAgreement />} />
  204. <Route path="/privacy-policy" element={<PrivacyPolicy />} />
  205. {/* 支付回调页也属于公开路由,需要在无主布局下渲染 */}
  206. <Route path="/recharge-return" element={<RechargeReturn />} />
  207. </Routes>
  208. </AuthLayout>
  209. ) : (
  210. <MainLayout>
  211. <Routes>
  212. <Route path="/" element={<Home onUseModel={handleModelNavigation} />} />
  213. <Route
  214. path="/text"
  215. element={
  216. <TextInteraction
  217. selectedId={textModelId}
  218. onModelChange={setTextModelId}
  219. />
  220. }
  221. />
  222. <Route
  223. path="/image"
  224. element={
  225. <ImageGeneration
  226. selectedId={imageModelId}
  227. onModelChange={setImageModelId}
  228. />
  229. }
  230. />
  231. <Route
  232. path="/audio"
  233. element={
  234. <AudioWorkshop
  235. selectedId={audioModelId}
  236. onModelChange={setAudioModelId}
  237. />
  238. }
  239. />
  240. <Route
  241. path="/video"
  242. element={
  243. <VideoSynthesis
  244. selectedId={videoModelId}
  245. onModelChange={setVideoModelId}
  246. />
  247. }
  248. />
  249. <Route
  250. path="/models"
  251. element={<ModelSquare onUseModel={handleModelNavigation} />}
  252. />
  253. <Route path="/models/pricing/:modelCode" element={<ModelPricingDetail />} />
  254. <Route path="/toolbox" element={<Toolbox />} />
  255. <Route path="/toolbox/qwen-ocr" element={<QwenOCR />} />
  256. <Route path="/toolbox/image-translation" element={<ImageTranslation />} />
  257. <Route path="/toolbox/tingwu" element={<TingwuWorkshop />} />
  258. <Route path="/toolbox/tingwu/result/:taskId" element={<TingwuResult />} />
  259. <Route path="/toolbox/data-mining" element={<DataMining />} />
  260. <Route path="/toolbox/qwen-deep-research" element={<DeepResearch />} />
  261. <Route path="/toolbox/qwen-deep-research/:taskId" element={<ResearchDetail />} />
  262. <Route path="/toolbox/qwen-mt" element={<QwenMT />} />
  263. <Route path="/toolbox/photo-answer" element={<PhotoAnswer />} />
  264. <Route path="/toolbox/multimodal-translation" element={<MultimodalTranslation />} />
  265. <Route path="/profile" element={<Profile />} />
  266. <Route path="/billing" element={<Billing />} />
  267. <Route path="/invoice" element={<InvoiceManagement />} />
  268. <Route path="/invoice/result" element={<InvoiceResult />} />
  269. <Route path="/invoice/apply" element={<InvoiceApply />} />
  270. <Route path="/platform" element={<OpenPlatform />} />
  271. <Route path="/openclaw" element={<OpenClaw />} />
  272. <Route path="/user-agreement" element={<UserAgreement />} />
  273. <Route path="/recharge" element={<Recharge />} />
  274. <Route path="/recharge-return" element={<RechargeReturn />} />
  275. <Route path="/recharge-history" element={<RechargeHistory />} />
  276. <Route path="*" element={<Navigate to="/" replace />} />
  277. </Routes>
  278. </MainLayout>
  279. )}
  280. </AuthGuard>
  281. </NotificationProvider>
  282. </BrandingContext.Provider>
  283. );
  284. };
  285. export default App;