Spaces:
Sleeping
Sleeping
| import React, { useState, useEffect, Suspense } from 'react'; | |
| import { Sidebar } from './components/Sidebar'; | |
| import { Header } from './components/Header'; | |
| import { Login } from './pages/Login'; | |
| import { User, UserRole } from './types'; | |
| import { api } from './services/api'; | |
| import { AlertTriangle, Loader2 } from 'lucide-react'; | |
| // --- Lazy Load Helper with Retry Strategy --- | |
| // This wrapper catches chunk loading errors (common after deployments) and reloads the page once. | |
| const lazyRetry = (importFn: () => Promise<any>, name: string): React.LazyExoticComponent<React.ComponentType<any>> => { | |
| return React.lazy(async () => { | |
| try { | |
| const module = await importFn(); | |
| return { default: module[name] }; | |
| } catch (error: any) { | |
| console.error(`Failed to load ${name}:`, error); | |
| // Identify if it's a fetch/chunk loading error | |
| const isLoadError = error.message?.includes('fetch') || error.message?.includes('chunk') || error.message?.includes('dynamically imported'); | |
| if (isLoadError) { | |
| const storageKey = `retry_load_${name}`; | |
| const hasRetried = sessionStorage.getItem(storageKey); | |
| if (!hasRetried) { | |
| console.warn(`Chunk load failed for ${name}. Reloading page to fetch new chunks...`); | |
| sessionStorage.setItem(storageKey, 'true'); | |
| window.location.reload(); | |
| // Return a pending promise to pause React rendering while page reloads | |
| return new Promise(() => {}); | |
| } else { | |
| // If we already reloaded and it still fails, cleanup and throw | |
| sessionStorage.removeItem(storageKey); | |
| } | |
| } | |
| throw error; | |
| } | |
| }); | |
| }; | |
| // Lazy load pages using the robust retry wrapper | |
| const Dashboard = lazyRetry(() => import('./pages/Dashboard'), 'Dashboard'); | |
| const StudentList = lazyRetry(() => import('./pages/StudentList'), 'StudentList'); | |
| const CourseList = lazyRetry(() => import('./pages/CourseList'), 'CourseList'); | |
| const ScoreList = lazyRetry(() => import('./pages/ScoreList'), 'ScoreList'); | |
| const ClassList = lazyRetry(() => import('./pages/ClassList'), 'ClassList'); | |
| const Settings = lazyRetry(() => import('./pages/Settings'), 'Settings'); | |
| const Reports = lazyRetry(() => import('./pages/Reports'), 'Reports'); | |
| const SubjectList = lazyRetry(() => import('./pages/SubjectList'), 'SubjectList'); | |
| const UserList = lazyRetry(() => import('./pages/UserList'), 'UserList'); | |
| const SchoolList = lazyRetry(() => import('./pages/SchoolList'), 'SchoolList'); | |
| const Games = lazyRetry(() => import('./pages/Games'), 'Games'); | |
| const AttendancePage = lazyRetry(() => import('./pages/Attendance'), 'AttendancePage'); | |
| const Profile = lazyRetry(() => import('./pages/Profile'), 'Profile'); | |
| const WishesAndFeedback = lazyRetry(() => import('./pages/WishesAndFeedback'), 'WishesAndFeedback'); | |
| const AIAssistant = lazyRetry(() => import('./pages/AIAssistant'), 'AIAssistant'); | |
| class ErrorBoundary extends React.Component<{children: React.ReactNode}, {hasError: boolean, error: Error | null}> { | |
| constructor(props: any) { | |
| super(props); | |
| this.state = { hasError: false, error: null }; | |
| } | |
| static getDerivedStateFromError(error: Error) { | |
| return { hasError: true, error }; | |
| } | |
| componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { | |
| console.error("Uncaught error:", error, errorInfo); | |
| } | |
| render() { | |
| if (this.state.hasError) { | |
| return ( | |
| <div className="min-h-screen flex items-center justify-center bg-gray-50 p-4"> | |
| <div className="bg-white p-8 rounded-xl shadow-lg max-w-lg w-full text-center"> | |
| <div className="mx-auto flex h-16 w-16 items-center justify-center rounded-full bg-red-100 mb-4"> | |
| <AlertTriangle className="h-8 w-8 text-red-600" /> | |
| </div> | |
| <h2 className="text-2xl font-bold text-gray-900 mb-2">哎呀,页面出错了</h2> | |
| <p className="text-gray-500 mb-6">系统遇到了一些非预期的问题。通常这可能是网络波动或缓存问题导致的。</p> | |
| <div className="bg-gray-50 p-4 rounded text-left text-xs text-red-500 overflow-auto max-h-40 mb-6 border border-red-100 font-mono"> | |
| {this.state.error?.message || 'Unknown Error'} | |
| </div> | |
| <button onClick={() => { sessionStorage.clear(); window.location.reload(); }} className="w-full px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors font-medium">刷新页面重试</button> | |
| <p className="mt-3 text-xs text-gray-400">如果问题持续存在,请尝试清除浏览器缓存。</p> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| return this.props.children; | |
| } | |
| } | |
| const PageLoading = () => ( | |
| <div className="flex h-full w-full items-center justify-center min-h-[400px]"> | |
| <div className="flex flex-col items-center space-y-3"> | |
| <Loader2 className="h-8 w-8 animate-spin text-blue-600" /> | |
| <p className="text-sm text-gray-400">资源加载中...</p> | |
| </div> | |
| </div> | |
| ); | |
| const AppContent: React.FC = () => { | |
| const [isAuthenticated, setIsAuthenticated] = useState(false); | |
| const [currentUser, setCurrentUser] = useState<User | null>(null); | |
| const [currentView, setCurrentView] = useState('dashboard'); | |
| const [loading, setLoading] = useState(true); | |
| const [sidebarOpen, setSidebarOpen] = useState(false); | |
| useEffect(() => { | |
| const initApp = async () => { | |
| api.init(); | |
| let storedUser = api.auth.getCurrentUser(); | |
| // Refresh session from server to get latest role/class info | |
| if (storedUser) { | |
| try { | |
| const refreshedUser = await api.auth.refreshSession(); | |
| if (refreshedUser) storedUser = refreshedUser; | |
| } catch (e) { | |
| console.warn('Session refresh failed'); | |
| } | |
| setCurrentUser(storedUser); | |
| setIsAuthenticated(true); | |
| } | |
| setLoading(false); | |
| }; | |
| initApp(); | |
| }, []); | |
| const handleLogin = (user: User) => { | |
| setCurrentUser(user); | |
| setIsAuthenticated(true); | |
| setCurrentView('dashboard'); | |
| }; | |
| const handleLogout = () => { | |
| api.auth.logout(); | |
| setIsAuthenticated(false); | |
| setCurrentUser(null); | |
| }; | |
| const renderContent = () => { | |
| switch (currentView) { | |
| case 'dashboard': return <Dashboard onNavigate={(view: string) => setCurrentView(view)} />; | |
| case 'students': return <StudentList />; | |
| case 'classes': return <ClassList />; | |
| case 'courses': return <CourseList />; | |
| case 'grades': return <ScoreList />; | |
| case 'settings': return <Settings />; | |
| case 'reports': return <Reports />; | |
| case 'subjects': return <SubjectList />; | |
| case 'users': return <UserList />; | |
| case 'schools': return <SchoolList />; | |
| case 'games': return <Games />; | |
| case 'attendance': return <AttendancePage />; | |
| case 'wishes': return <WishesAndFeedback />; | |
| case 'ai-assistant': return <AIAssistant />; | |
| case 'profile': return <Profile />; | |
| default: return <Dashboard onNavigate={(view: string) => setCurrentView(view)} />; | |
| } | |
| }; | |
| const viewTitles: Record<string, string> = { | |
| dashboard: '工作台', | |
| students: '学生档案管理', | |
| classes: '班级管理', | |
| courses: '课程安排', | |
| grades: '成绩管理', | |
| settings: '系统设置', | |
| reports: '统计报表', | |
| subjects: '学科设置', | |
| users: '用户权限管理', | |
| schools: '学校维度管理', | |
| games: '互动教学中心', | |
| attendance: '考勤管理', | |
| wishes: '心愿与反馈', | |
| 'ai-assistant': 'AI 智能助教', | |
| profile: '个人中心' | |
| }; | |
| if (loading) return <div className="min-h-screen flex items-center justify-center bg-gray-50 text-blue-600">加载中...</div>; | |
| if (!isAuthenticated) return <Login onLogin={handleLogin} />; | |
| return ( | |
| <div className="flex h-screen bg-gray-50 overflow-hidden"> | |
| <Sidebar | |
| currentView={currentView} | |
| onChangeView={(view) => { setCurrentView(view); setSidebarOpen(false); }} | |
| userRole={(currentUser?.role as UserRole) || UserRole.USER} | |
| onLogout={handleLogout} | |
| isOpen={sidebarOpen} | |
| onClose={() => setSidebarOpen(false)} | |
| /> | |
| <div className="flex-1 flex flex-col w-full"> | |
| <Header user={currentUser!} title={viewTitles[currentView] || '智慧校园'} onMenuClick={() => setSidebarOpen(true)}/> | |
| <main className="flex-1 overflow-x-hidden overflow-y-auto bg-gray-50 p-4 md:p-6 w-full"> | |
| <div className="max-w-7xl mx-auto w-full h-full"> | |
| <Suspense fallback={<PageLoading />}>{renderContent()}</Suspense> | |
| </div> | |
| </main> | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| const App: React.FC = () => { | |
| return ( | |
| <ErrorBoundary> | |
| <AppContent /> | |
| </ErrorBoundary> | |
| ); | |
| }; | |
| export default App; |