| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314 |
- 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 TextInteraction from './pages/TextInteraction';
- import ImageGeneration from './pages/ImageGeneration';
- import AudioWorkshop from './pages/AudioWorkshop';
- import VideoSynthesis from './pages/VideoSynthesis';
- import ModelSquare from './pages/ModelSquare';
- import ModelPricingDetail from './pages/ModelPricingDetail';
- import Toolbox from './pages/Toolbox';
- import QwenOCR from './pages/QwenOCR';
- import ImageTranslation from './pages/ImageTranslation';
- import TingwuWorkshop from './pages/TingwuWorkshop';
- import TingwuResult from './pages/TingwuResult';
- import DataMining from './pages/DataMining';
- import DeepResearch from './pages/DeepResearch';
- import ResearchDetail from './pages/ResearchDetail';
- import QwenMT from './pages/QwenMT';
- import PhotoAnswer from './pages/PhotoAnswer';
- import MultimodalTranslation from './pages/MultimodalTranslation';
- import Profile from './pages/Profile';
- import Billing from './pages/Billing';
- import Login from './pages/Login';
- import Register from './pages/Register';
- import Recharge from './pages/Recharge';
- import RechargeReturn from './pages/RechargeReturn';
- import RechargeHistory from './pages/RechargeHistory';
- import InvoiceManagement from './pages/InvoiceManagement';
- import InvoiceApply from './pages/InvoiceApply';
- import InvoiceResult from './pages/InvoiceResult';
- import OpenPlatform from './pages/OpenPlatform';
- import OpenClaw from './pages/OpenClaw';
- import UserAgreement from './pages/UserAgreement';
- import PrivacyPolicy from './pages/PrivacyPolicy';
- import SSOCallback from './pages/SSOCallback';
- import { authService } from './services/authService';
- import { Loader2 } from './icons/commonIcons';
- import BindPhoneReminder from './components/BindPhoneReminder';
- import { NotificationProvider } from './contexts/NotificationContext';
- import './styles/markdown.css';
- // 品牌配置 Context
- export interface BrandingConfig { system_name: string; system_logo: string; icp_number: string }
- export const BrandingContext = React.createContext<BrandingConfig>({ system_name: '智创空间', system_logo: '', icp_number: '' })
- const API_BASE = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8010'
- const pagePathMap: Record<PageId, string> = {
- home: '/',
- text: '/text',
- image: '/image',
- audio: '/audio',
- video: '/video',
- models: '/models',
- toolbox: '/toolbox',
- platform: '/platform',
- openclaw: '/openclaw',
- profile: '/profile',
- billing: '/billing',
- invoice: '/invoice',
- };
- // 公开路由(不需要登录)
- const publicRoutes = ['/login', '/register', '/sso-callback', '/recharge-return', '/user-agreement', '/privacy-policy'];
- const CasLoginRedirect: React.FC<{ targetPath: string }> = ({ targetPath }) => {
- useEffect(() => {
- authService.startCasLogin(targetPath || '/');
- }, [targetPath]);
- 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>
- );
- };
- // 认证状态检查组件
- 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;
- }
- // 检查本地是否有Token
- if (!authService.isAuthenticated()) {
- setIsAuthenticated(false);
- setIsChecking(false);
- return;
- }
- // 有Token,验证并刷新
- 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>
- );
- }
- // 未登录且不是公开路由
- 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-16">
- <Sidebar />
- <main className="flex-1 ml-[11rem] h-[calc(100vh-4rem)] flex flex-col overflow-y-auto">
- <div className="max-w-[1800px] mx-auto px-2 py-2 w-full flex-1 flex flex-col min-h-0">
- {children}
- {location.pathname === '/' && (
- <footer className="mt-6 h-[32px] px-2 border-t border-gray-100 text-center flex items-center justify-center">
- <p className="text-xs text-gray-400">
- © 2026 {branding.system_name}
- </p>
- </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: '智创空间', system_logo: '', icp_number: '' })
- useEffect(() => {
- fetch(`${API_BASE}/api/public/branding`)
- .then(r => r.json())
- .then(d => { if (d.system_name) setBranding(d) })
- .catch(() => {})
- }, [])
- // States to keep track of selected models across the app
- // 默认模型ID会在模型加载后自动更新为第一个可用模型(如果当前ID不存在)
- const [textModelId, setTextModelId] = useState('gemini-pro');
- const [imageModelId, setImageModelId] = useState('dall-e-3');
- const [audioModelId, setAudioModelId] = useState('elevenlabs');
- const [videoModelId, setVideoModelId] = useState('runway-gen3');
- const handleModelNavigation = (page: PageId, modelId: string) => {
- if (page === 'text') setTextModelId(modelId);
- if (page === 'image') setImageModelId(modelId);
- if (page === 'audio') setAudioModelId(modelId);
- if (page === 'video') setVideoModelId(modelId);
- const path = pagePathMap[page];
- navigate(path);
- };
- // 判断是否是认证页面(登录/注册)
- const isAuthPage = publicRoutes.includes(location.pathname);
- return (
- <BrandingContext.Provider value={branding}>
- <NotificationProvider>
- <AuthGuard>
- <BindPhoneReminder />
- {isAuthPage ? (
- <AuthLayout>
- <Routes>
- <Route path="/login" element={<Login />} />
- <Route path="/register" element={<Register />} />
- <Route path="/sso-callback" element={<SSOCallback />} />
- <Route path="/user-agreement" element={<UserAgreement />} />
- <Route path="/privacy-policy" element={<PrivacyPolicy />} />
- {/* 支付回调页也属于公开路由,需要在无主布局下渲染 */}
- <Route path="/recharge-return" element={<RechargeReturn />} />
- </Routes>
- </AuthLayout>
- ) : (
- <MainLayout>
- <Routes>
- <Route path="/" element={<Home onUseModel={handleModelNavigation} />} />
- <Route
- path="/text"
- element={
- <TextInteraction
- selectedId={textModelId}
- onModelChange={setTextModelId}
- />
- }
- />
- <Route
- path="/image"
- element={
- <ImageGeneration
- selectedId={imageModelId}
- onModelChange={setImageModelId}
- />
- }
- />
- <Route
- path="/audio"
- element={
- <AudioWorkshop
- selectedId={audioModelId}
- onModelChange={setAudioModelId}
- />
- }
- />
- <Route
- path="/video"
- element={
- <VideoSynthesis
- selectedId={videoModelId}
- onModelChange={setVideoModelId}
- />
- }
- />
- <Route
- path="/models"
- element={<ModelSquare onUseModel={handleModelNavigation} />}
- />
- <Route path="/models/pricing/:modelCode" element={<ModelPricingDetail />} />
- <Route path="/toolbox" element={<Toolbox />} />
- <Route path="/toolbox/qwen-ocr" element={<QwenOCR />} />
- <Route path="/toolbox/image-translation" element={<ImageTranslation />} />
- <Route path="/toolbox/tingwu" element={<TingwuWorkshop />} />
- <Route path="/toolbox/tingwu/result/:taskId" element={<TingwuResult />} />
- <Route path="/toolbox/data-mining" element={<DataMining />} />
- <Route path="/toolbox/qwen-deep-research" element={<DeepResearch />} />
- <Route path="/toolbox/qwen-deep-research/:taskId" element={<ResearchDetail />} />
- <Route path="/toolbox/qwen-mt" element={<QwenMT />} />
- <Route path="/toolbox/photo-answer" element={<PhotoAnswer />} />
- <Route path="/toolbox/multimodal-translation" element={<MultimodalTranslation />} />
- <Route path="/profile" element={<Profile />} />
- <Route path="/billing" element={<Billing />} />
- <Route path="/invoice" element={<InvoiceManagement />} />
- <Route path="/invoice/result" element={<InvoiceResult />} />
- <Route path="/invoice/apply" element={<InvoiceApply />} />
- <Route path="/platform" element={<OpenPlatform />} />
- <Route path="/openclaw" element={<OpenClaw />} />
- <Route path="/user-agreement" element={<UserAgreement />} />
- <Route path="/recharge" element={<Recharge />} />
- <Route path="/recharge-return" element={<RechargeReturn />} />
- <Route path="/recharge-history" element={<RechargeHistory />} />
- <Route path="*" element={<Navigate to="/" replace />} />
- </Routes>
- </MainLayout>
- )}
- </AuthGuard>
- </NotificationProvider>
- </BrandingContext.Provider>
- );
- };
- export default App;
|