Spaces:
Sleeping
Sleeping
| import React, { useEffect, useState } from 'react'; | |
| import { Users, BookOpen, GraduationCap, TrendingUp, AlertTriangle, Activity, Calendar, X, CheckCircle, Plus } from 'lucide-react'; | |
| import { api } from '../services/api'; | |
| import { Score, ClassInfo, Subject, Schedule, User, Exam } from '../types'; | |
| import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'; | |
| import { StudentDashboard } from './StudentDashboard'; | |
| import { TeacherDashboard } from './TeacherDashboard'; | |
| import { TodoList } from '../components/TodoList'; | |
| interface DashboardProps { | |
| onNavigate: (view: string) => void; | |
| } | |
| export const gradeOrder: Record<string, number> = { | |
| '一年级': 1, '二年级': 2, '三年级': 3, '四年级': 4, '五年级': 5, '六年级': 6, | |
| '初一': 7, '七年级': 7, '初二': 8, '八年级': 8, '初三': 9, '九年级': 9, | |
| '高一': 10, '高二': 11, '高三': 12 | |
| }; | |
| export const sortGrades = (a: string, b: string) => (gradeOrder[a] || 99) - (gradeOrder[b] || 99); | |
| const extractGrade = (s: string) => { | |
| if (!s) return ''; | |
| const keys = Object.keys(gradeOrder); | |
| return keys.find(g => s.startsWith(g)) || ''; | |
| }; | |
| export const sortClasses = (a: ClassInfo | string, b: ClassInfo | string) => { | |
| const nameA = typeof a === 'string' ? a : (a.grade + a.className); | |
| const nameB = typeof b === 'string' ? b : (b.grade + b.className); | |
| if (!nameA || !nameB) return 0; | |
| const gradeA = extractGrade(nameA); | |
| const gradeB = extractGrade(nameB); | |
| if (gradeA && gradeB && gradeA !== gradeB) { return sortGrades(gradeA, gradeB); } | |
| const getNum = (str: string) => { const match = str.match(/(\d+)/); return match ? parseInt(match[1]) : 0; }; | |
| return getNum(nameA) - getNum(nameB); | |
| }; | |
| export const Dashboard: React.FC<DashboardProps> = ({ onNavigate }) => { | |
| const [stats, setStats] = useState({ studentCount: 0, courseCount: 0, avgScore: 0, excellentRate: '0%' }); | |
| const [warnings, setWarnings] = useState<string[]>([]); | |
| const [loading, setLoading] = useState(true); | |
| const [trendData, setTrendData] = useState<any[]>([]); | |
| const currentUser = api.auth.getCurrentUser(); | |
| const isAdmin = currentUser?.role === 'ADMIN' || currentUser?.role === 'PRINCIPAL'; | |
| const isTeacher = currentUser?.role === 'TEACHER'; | |
| const isStudent = currentUser?.role === 'STUDENT'; | |
| useEffect(() => { | |
| if (isStudent || isTeacher) { | |
| setLoading(false); | |
| return; | |
| } | |
| const loadStats = async () => { | |
| try { | |
| const [summary, scoresData, examsData] = await Promise.all([ | |
| api.stats.getSummary(), | |
| api.scores.getAll(), | |
| api.exams.getAll() | |
| ]); | |
| setStats(summary); | |
| // Calculate Real Trend Data | |
| const examMap = new Map<string, { total: number, count: number, date: string }>(); | |
| // 1. Initialize map with known exams to get dates | |
| (examsData as Exam[]).forEach((e) => { | |
| if (e.name) { | |
| examMap.set(e.name, { total: 0, count: 0, date: e.date || '9999-99-99' }); | |
| } | |
| }); | |
| // 2. Aggregate scores | |
| (scoresData as Score[]).forEach((s) => { | |
| if (s.status === 'Normal') { | |
| const name = s.examName || s.type; | |
| if (!examMap.has(name)) { | |
| // Fallback for exams not in Exam definitions | |
| examMap.set(name, { total: 0, count: 0, date: '9999-99-99' }); | |
| } | |
| const entry = examMap.get(name)!; | |
| entry.total += s.score; | |
| entry.count += 1; | |
| } | |
| }); | |
| // 3. Convert to array and sort | |
| const trend = Array.from(examMap.entries()) | |
| .filter(([_, data]) => data.count > 0) // Filter out exams with no scores | |
| .map(([name, data]) => ({ | |
| name: name, | |
| score: Math.round(data.total / data.count), | |
| date: data.date | |
| })) | |
| .sort((a, b) => a.date.localeCompare(b.date)); | |
| // If no real data, keep empty to show empty state or fallback? | |
| // We will just set whatever we found. | |
| setTrendData(trend); | |
| } catch (e) { | |
| console.error(e); | |
| } finally { | |
| setLoading(false); | |
| } | |
| }; | |
| loadStats(); | |
| }, [isStudent, isTeacher]); | |
| if (isStudent) return <StudentDashboard />; | |
| if (isTeacher) return <TeacherDashboard />; | |
| // Admin Dashboard View | |
| return ( | |
| <div className="space-y-6"> | |
| <h1 className="text-2xl font-bold text-gray-800">全校概览</h1> | |
| <div className="grid grid-cols-1 md:grid-cols-4 gap-6"> | |
| <div className="bg-white p-6 rounded-xl shadow-sm border border-gray-100 flex items-center"> | |
| <div className="p-3 bg-blue-100 rounded-full text-blue-600 mr-4"> | |
| <Users size={24} /> | |
| </div> | |
| <div> | |
| <p className="text-gray-500 text-sm">在校学生</p> | |
| <p className="text-2xl font-bold text-gray-800">{stats.studentCount}</p> | |
| </div> | |
| </div> | |
| <div className="bg-white p-6 rounded-xl shadow-sm border border-gray-100 flex items-center"> | |
| <div className="p-3 bg-green-100 rounded-full text-green-600 mr-4"> | |
| <BookOpen size={24} /> | |
| </div> | |
| <div> | |
| <p className="text-gray-500 text-sm">开设课程</p> | |
| <p className="text-2xl font-bold text-gray-800">{stats.courseCount}</p> | |
| </div> | |
| </div> | |
| <div className="bg-white p-6 rounded-xl shadow-sm border border-gray-100 flex items-center"> | |
| <div className="p-3 bg-yellow-100 rounded-full text-yellow-600 mr-4"> | |
| <GraduationCap size={24} /> | |
| </div> | |
| <div> | |
| <p className="text-gray-500 text-sm">平均分</p> | |
| <p className="text-2xl font-bold text-gray-800">{stats.avgScore}</p> | |
| </div> | |
| </div> | |
| <div className="bg-white p-6 rounded-xl shadow-sm border border-gray-100 flex items-center"> | |
| <div className="p-3 bg-purple-100 rounded-full text-purple-600 mr-4"> | |
| <Activity size={24} /> | |
| </div> | |
| <div> | |
| <p className="text-gray-500 text-sm">优秀率</p> | |
| <p className="text-2xl font-bold text-gray-800">{stats.excellentRate}</p> | |
| </div> | |
| </div> | |
| </div> | |
| <div className="grid grid-cols-1 lg:grid-cols-2 gap-6"> | |
| <div className="bg-white p-6 rounded-xl shadow-sm border border-gray-100"> | |
| <h3 className="font-bold text-gray-800 mb-4 flex items-center"><TrendingUp size={20} className="mr-2 text-blue-600"/> 成绩走势</h3> | |
| <div className="h-64"> | |
| {trendData.length > 0 ? ( | |
| <ResponsiveContainer width="100%" height="100%"> | |
| <LineChart data={trendData}> | |
| <CartesianGrid strokeDasharray="3 3" vertical={false}/> | |
| <XAxis dataKey="name" tick={{fontSize: 12}} /> | |
| <YAxis domain={[0, 100]} /> | |
| <Tooltip /> | |
| <Line type="monotone" dataKey="score" stroke="#3b82f6" strokeWidth={3} dot={{r: 4}} activeDot={{r: 6}} /> | |
| </LineChart> | |
| </ResponsiveContainer> | |
| ) : ( | |
| <div className="flex flex-col items-center justify-center h-full text-gray-400"> | |
| <Activity size={48} className="mb-2 opacity-20"/> | |
| <p>暂无考试数据</p> | |
| <p className="text-xs mt-1">请在“成绩管理”中录入成绩</p> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| <div className="bg-white p-6 rounded-xl shadow-sm border border-gray-100"> | |
| <h3 className="font-bold text-gray-800 mb-4">快捷入口</h3> | |
| <div className="grid grid-cols-2 gap-4"> | |
| <button onClick={() => onNavigate('students')} className="p-4 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors flex flex-col items-center justify-center text-gray-700"> | |
| <Users className="mb-2 text-blue-500" /> | |
| <span>学生管理</span> | |
| </button> | |
| <button onClick={() => onNavigate('classes')} className="p-4 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors flex flex-col items-center justify-center text-gray-700"> | |
| <BookOpen className="mb-2 text-green-500" /> | |
| <span>班级管理</span> | |
| </button> | |
| <button onClick={() => onNavigate('reports')} className="p-4 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors flex flex-col items-center justify-center text-gray-700"> | |
| <Activity className="mb-2 text-purple-500" /> | |
| <span>报表统计</span> | |
| </button> | |
| <button onClick={() => onNavigate('settings')} className="p-4 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors flex flex-col items-center justify-center text-gray-700"> | |
| <Activity className="mb-2 text-gray-500" /> | |
| <span>系统设置</span> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| }; | |