import React, { useState, useEffect, useRef } 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'; import { LiveAssistant } from './components/LiveAssistant'; // --- Page Loading Component --- const PageLoading = () => (

正在加载资源...

); // --- Manual Async Component Helper (Replaces React.lazy) --- // This avoids Suspense getting stuck by managing loading state explicitly. const lazyLoad = (importFn: () => Promise, name: string) => { return (props: any) => { const [Component, setComponent] = useState | null>(null); const [error, setError] = useState(null); const mountedRef = useRef(true); useEffect(() => { mountedRef.current = true; importFn() .then(module => { if (!mountedRef.current) return; // Support both named export (priority) and default export const Comp = module[name] || module.default; if (Comp) { // Use callback to set function component to state setComponent(() => Comp); } else { throw new Error(`Export "${name}" not found in module.`); } }) .catch(err => { if (!mountedRef.current) return; console.error(`Error loading page ${name}:`, err); setError(err); // Retry logic for chunks could go here, but usually reload is best if (err.message && (err.message.includes('fetch') || err.message.includes('chunk'))) { console.warn('Chunk load error, suggesting reload.'); } }); return () => { mountedRef.current = false; }; }, []); if (error) { return (

页面加载失败: {error.message}

); } if (!Component) return ; return ; }; }; // Lazy load pages using the manual helper const Dashboard = lazyLoad(() => import('./pages/Dashboard'), 'Dashboard'); const StudentList = lazyLoad(() => import('./pages/StudentList'), 'StudentList'); const CourseList = lazyLoad(() => import('./pages/CourseList'), 'CourseList'); const ScoreList = lazyLoad(() => import('./pages/ScoreList'), 'ScoreList'); const ClassList = lazyLoad(() => import('./pages/ClassList'), 'ClassList'); const Settings = lazyLoad(() => import('./pages/Settings'), 'Settings'); const Reports = lazyLoad(() => import('./pages/Reports'), 'Reports'); const SubjectList = lazyLoad(() => import('./pages/SubjectList'), 'SubjectList'); const UserList = lazyLoad(() => import('./pages/UserList'), 'UserList'); const SchoolList = lazyLoad(() => import('./pages/SchoolList'), 'SchoolList'); const Games = lazyLoad(() => import('./pages/Games'), 'Games'); const AttendancePage = lazyLoad(() => import('./pages/Attendance'), 'AttendancePage'); const Profile = lazyLoad(() => import('./pages/Profile'), 'Profile'); const WishesAndFeedback = lazyLoad(() => import('./pages/WishesAndFeedback'), 'WishesAndFeedback'); const AIAssistant = lazyLoad(() => import('./pages/AIAssistant'), 'AIAssistant'); const MyClass = lazyLoad(() => import('./pages/MyClass'), 'MyClass'); 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 (

页面运行遇到问题

请尝试刷新页面。

); } return this.props.children; } } const AppContent: React.FC = () => { const [isAuthenticated, setIsAuthenticated] = useState(false); const [currentUser, setCurrentUser] = useState(null); const [currentView, setCurrentView] = useState('dashboard'); const [loading, setLoading] = useState(true); const [sidebarOpen, setSidebarOpen] = useState(false); // --- Auto-Update Logic --- useEffect(() => { // 1. Force reload page when Service Worker updates (controller change) // This happens automatically due to 'registerType: autoUpdate' in vite config, // but the window needs to reload to use the new SW. const handleControllerChange = () => { console.log("🔄 App updated, reloading..."); window.location.reload(); }; // 2. Check for updates on visibility change (e.g. user comes back to tab) const checkForUpdates = () => { if ('serviceWorker' in navigator) { navigator.serviceWorker.ready.then(registration => { registration.update(); }).catch(() => {}); } }; if ('serviceWorker' in navigator) { navigator.serviceWorker.addEventListener('controllerchange', handleControllerChange); document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') { checkForUpdates(); } }); // Initial check checkForUpdates(); } return () => { if ('serviceWorker' in navigator) { navigator.serviceWorker.removeEventListener('controllerchange', handleControllerChange); } }; }, []); useEffect(() => { const initApp = async () => { api.init(); let storedUser = api.auth.getCurrentUser(); 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 setCurrentView(view)} />; case 'students': return ; case 'classes': return ; case 'courses': return ; case 'grades': return ; case 'settings': return ; case 'reports': return ; case 'subjects': return ; case 'users': return ; case 'schools': return ; case 'games': return ; case 'attendance': return ; case 'wishes': return ; case 'ai-assistant': return ; case 'profile': return ; case 'my-class': return ; default: return setCurrentView(view)} />; } }; const viewTitles: Record = { dashboard: '工作台', students: '学生档案管理', classes: '班级管理', courses: '课程安排', grades: '成绩管理', settings: '系统设置', reports: '统计报表', subjects: '学科设置', users: '用户权限管理', schools: '学校维度管理', games: '互动教学中心', attendance: '考勤管理', wishes: '心愿与反馈', 'ai-assistant': 'AI 智能助教', profile: '个人中心', 'my-class': '我的班级 (班主任)' }; if (loading) return
加载中...
; if (!isAuthenticated) return ; const showLiveAssistant = currentUser && (currentUser.role === UserRole.ADMIN || currentUser.aiAccess); // Layout Logic: // For 'ai-assistant' and 'games', we want full height without internal padding from Main, // so the component can manage its own scroll/flex layout (sticky headers/footers). // For other pages, we use the standard scrolling Main container. const isAppLikePage = currentView === 'ai-assistant' || currentView === 'games'; const mainClasses = isAppLikePage ? "flex-1 overflow-hidden relative bg-slate-50 w-full" : "flex-1 overflow-x-hidden overflow-y-auto bg-gray-50 p-4 md:p-6 w-full relative"; return ( // Fixed inset-0 prevents body scroll, everything happens inside
{ setCurrentView(view); setSidebarOpen(false); }} userRole={(currentUser?.role as UserRole) || UserRole.USER} onLogout={handleLogout} isOpen={sidebarOpen} onClose={() => setSidebarOpen(false)} />
setSidebarOpen(true)}/>
{isAppLikePage ? ( renderContent() ) : (
{renderContent()}
)}
{showLiveAssistant && }
); }; const App: React.FC = () => { return ( ); }; export default App;