dvc890 commited on
Commit
d2d48e6
·
verified ·
1 Parent(s): 42332c7

Upload 25 files

Browse files
Files changed (4) hide show
  1. App.tsx +2 -2
  2. pages/Dashboard.tsx +30 -21
  3. pages/Reports.tsx +177 -76
  4. pages/ScoreList.tsx +1 -2
App.tsx CHANGED
@@ -99,7 +99,7 @@ const AppContent: React.FC = () => {
99
 
100
  const renderContent = () => {
101
  switch (currentView) {
102
- case 'dashboard': return <Dashboard />;
103
  case 'students': return <StudentList />;
104
  case 'classes': return <ClassList />;
105
  case 'courses': return <CourseList />;
@@ -108,7 +108,7 @@ const AppContent: React.FC = () => {
108
  case 'reports': return <Reports />;
109
  case 'subjects': return <SubjectList />;
110
  case 'users': return <UserList />;
111
- default: return <Dashboard />;
112
  }
113
  };
114
 
 
99
 
100
  const renderContent = () => {
101
  switch (currentView) {
102
+ case 'dashboard': return <Dashboard onNavigate={(view) => setCurrentView(view)} />;
103
  case 'students': return <StudentList />;
104
  case 'classes': return <ClassList />;
105
  case 'courses': return <CourseList />;
 
108
  case 'reports': return <Reports />;
109
  case 'subjects': return <SubjectList />;
110
  case 'users': return <UserList />;
111
+ default: return <Dashboard onNavigate={(view) => setCurrentView(view)} />;
112
  }
113
  };
114
 
pages/Dashboard.tsx CHANGED
@@ -1,10 +1,13 @@
1
-
2
  import React, { useEffect, useState } from 'react';
3
  import { Users, BookOpen, GraduationCap, TrendingUp, AlertTriangle, ArrowRight, Activity, Calendar } from 'lucide-react';
4
  import { api } from '../services/api';
5
  import { Score, ClassInfo, Subject } from '../types';
6
 
7
- export const Dashboard: React.FC = () => {
 
 
 
 
8
  const [stats, setStats] = useState({ studentCount: 0, courseCount: 0, avgScore: 0, excellentRate: '0%' });
9
  const [warnings, setWarnings] = useState<string[]>([]);
10
  const [loading, setLoading] = useState(true);
@@ -24,20 +27,7 @@ export const Dashboard: React.FC = () => {
24
  // Generate Warnings
25
  const newWarnings: string[] = [];
26
 
27
- // 1. Check Class Subject Averages
28
- const classSubjectMap = new Map<string, { total: number; count: number }>();
29
- scores.forEach((s: Score) => {
30
- if (s.status !== 'Normal') return;
31
- // Find class for student (Need student info, but let's assume we can map or get it.
32
- // Since Score doesn't have class, we need to fetch students too.
33
- // For dashboard simplicity, we might skip complex joins or fetch students.)
34
- // To be accurate, let's just warn about global subject averages for now or low individual scores.
35
- });
36
-
37
- // Simplified Warning Logic (Mocking deeper analysis for dashboard speed)
38
- // In a real app, this would be a backend aggregation.
39
-
40
- // Let's analyze global subject performance
41
  subjects.forEach((sub: Subject) => {
42
  const subScores = scores.filter((s: Score) => s.courseName === sub.name && s.status === 'Normal');
43
  if (subScores.length > 0) {
@@ -48,7 +38,17 @@ export const Dashboard: React.FC = () => {
48
  }
49
  });
50
 
51
- // Count failed students
 
 
 
 
 
 
 
 
 
 
52
  const failedCount = scores.filter((s: Score) => s.score < 60 && s.status === 'Normal').length;
53
  if (failedCount > 10) {
54
  newWarnings.push(`近期共有 ${failedCount} 人次考试不及格,请关注后进生辅导。`);
@@ -163,15 +163,24 @@ export const Dashboard: React.FC = () => {
163
  <p className="text-blue-100 text-sm mb-6">管理您的日常教务工作</p>
164
 
165
  <div className="space-y-3">
166
- <button className="w-full bg-white/10 hover:bg-white/20 border border-white/20 p-3 rounded-lg flex items-center justify-between transition-colors text-sm font-medium group">
 
 
 
167
  <span>录入新成绩</span>
168
  <ArrowRight size={16} className="opacity-0 group-hover:opacity-100 transition-opacity"/>
169
  </button>
170
- <button className="w-full bg-white/10 hover:bg-white/20 border border-white/20 p-3 rounded-lg flex items-center justify-between transition-colors text-sm font-medium group">
 
 
 
171
  <span>添加学生档案</span>
172
  <ArrowRight size={16} className="opacity-0 group-hover:opacity-100 transition-opacity"/>
173
  </button>
174
- <button className="w-full bg-white/10 hover:bg-white/20 border border-white/20 p-3 rounded-lg flex items-center justify-between transition-colors text-sm font-medium group">
 
 
 
175
  <span>下载本月报表</span>
176
  <ArrowRight size={16} className="opacity-0 group-hover:opacity-100 transition-opacity"/>
177
  </button>
@@ -185,4 +194,4 @@ export const Dashboard: React.FC = () => {
185
  </div>
186
  </div>
187
  );
188
- };
 
 
1
  import React, { useEffect, useState } from 'react';
2
  import { Users, BookOpen, GraduationCap, TrendingUp, AlertTriangle, ArrowRight, Activity, Calendar } from 'lucide-react';
3
  import { api } from '../services/api';
4
  import { Score, ClassInfo, Subject } from '../types';
5
 
6
+ interface DashboardProps {
7
+ onNavigate: (view: string) => void;
8
+ }
9
+
10
+ export const Dashboard: React.FC<DashboardProps> = ({ onNavigate }) => {
11
  const [stats, setStats] = useState({ studentCount: 0, courseCount: 0, avgScore: 0, excellentRate: '0%' });
12
  const [warnings, setWarnings] = useState<string[]>([]);
13
  const [loading, setLoading] = useState(true);
 
27
  // Generate Warnings
28
  const newWarnings: string[] = [];
29
 
30
+ // 1. Analyze Subject Averages
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  subjects.forEach((sub: Subject) => {
32
  const subScores = scores.filter((s: Score) => s.courseName === sub.name && s.status === 'Normal');
33
  if (subScores.length > 0) {
 
38
  }
39
  });
40
 
41
+ // 2. Analyze Class Performance
42
+ classes.forEach((cls: ClassInfo) => {
43
+ const className = cls.grade + cls.className;
44
+ // We need to fetch students for this class first to filter scores properly,
45
+ // but for dashboard speed we might approximate if student info is not linked in scores.
46
+ // Since Score schema has studentNo, we can't easily filter by class without student list.
47
+ // Skipping class-specific warnings for this quick dashboard view to avoid n+1 query performance hit,
48
+ // or we rely on backend aggregated stats.
49
+ });
50
+
51
+ // 3. Count failed students
52
  const failedCount = scores.filter((s: Score) => s.score < 60 && s.status === 'Normal').length;
53
  if (failedCount > 10) {
54
  newWarnings.push(`近期共有 ${failedCount} 人次考试不及格,请关注后进生辅导。`);
 
163
  <p className="text-blue-100 text-sm mb-6">管理您的日常教务工作</p>
164
 
165
  <div className="space-y-3">
166
+ <button
167
+ onClick={() => onNavigate('grades')}
168
+ className="w-full bg-white/10 hover:bg-white/20 border border-white/20 p-3 rounded-lg flex items-center justify-between transition-colors text-sm font-medium group cursor-pointer"
169
+ >
170
  <span>录入新成绩</span>
171
  <ArrowRight size={16} className="opacity-0 group-hover:opacity-100 transition-opacity"/>
172
  </button>
173
+ <button
174
+ onClick={() => onNavigate('students')}
175
+ className="w-full bg-white/10 hover:bg-white/20 border border-white/20 p-3 rounded-lg flex items-center justify-between transition-colors text-sm font-medium group cursor-pointer"
176
+ >
177
  <span>添加学生档案</span>
178
  <ArrowRight size={16} className="opacity-0 group-hover:opacity-100 transition-opacity"/>
179
  </button>
180
+ <button
181
+ onClick={() => onNavigate('reports')}
182
+ className="w-full bg-white/10 hover:bg-white/20 border border-white/20 p-3 rounded-lg flex items-center justify-between transition-colors text-sm font-medium group cursor-pointer"
183
+ >
184
  <span>下载本月报表</span>
185
  <ArrowRight size={16} className="opacity-0 group-hover:opacity-100 transition-opacity"/>
186
  </button>
 
194
  </div>
195
  </div>
196
  );
197
+ };
pages/Reports.tsx CHANGED
@@ -1,16 +1,15 @@
1
-
2
  import React, { useState, useEffect } from 'react';
3
  import {
4
  BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer,
5
  LineChart, Line, AreaChart, Area, PieChart as RePieChart, Pie, Cell, RadarChart, Radar, PolarGrid, PolarAngleAxis, PolarRadiusAxis
6
  } from 'recharts';
7
  import { api } from '../services/api';
8
- import { Loader2, Download, Filter, TrendingUp, Users, BookOpen, PieChart as PieChartIcon, Grid, BarChart2, User, UserCheck } from 'lucide-react';
9
  import { Score, Student, ClassInfo, Subject, Exam } from '../types';
10
 
11
  export const Reports: React.FC = () => {
12
  const [loading, setLoading] = useState(true);
13
- const [activeTab, setActiveTab] = useState<'overview' | 'grade' | 'trend' | 'matrix' | 'student'>('grade');
14
 
15
  // Data
16
  const [scores, setScores] = useState<Score[]>([]);
@@ -29,6 +28,15 @@ export const Reports: React.FC = () => {
29
  const [trendData, setTrendData] = useState<any[]>([]);
30
  const [matrixData, setMatrixData] = useState<any[]>([]);
31
 
 
 
 
 
 
 
 
 
 
32
  // Student Focus State
33
  const [selectedStudent, setSelectedStudent] = useState<Student | null>(null);
34
 
@@ -65,6 +73,38 @@ export const Reports: React.FC = () => {
65
  useEffect(() => {
66
  if (scores.length === 0 || students.length === 0) return;
67
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  // --- 1. Grade Analysis (Horizontal Class Comparison) ---
69
  const gradeClasses = classes.filter(c => c.grade === selectedGrade);
70
  const gaData = gradeClasses.map(cls => {
@@ -243,11 +283,11 @@ export const Reports: React.FC = () => {
243
  {/* Tabs */}
244
  <div className="flex space-x-1 bg-gray-100 p-1 rounded-xl w-full md:w-auto overflow-x-auto">
245
  {[
 
246
  { id: 'grade', label: '年级横向分析', icon: BarChart2 },
247
  { id: 'trend', label: '教学成长轨迹', icon: TrendingUp },
248
  { id: 'matrix', label: '学科质量透视', icon: Grid },
249
  { id: 'student', label: '学生个像', icon: User },
250
- { id: 'overview', label: '全校概览', icon: PieChartIcon },
251
  ].map(tab => (
252
  <button
253
  key={tab.id}
@@ -266,27 +306,90 @@ export const Reports: React.FC = () => {
266
  <div className="bg-white rounded-xl shadow-sm border border-gray-100 p-6 min-h-[500px]">
267
 
268
  {/* Filter Bar */}
269
- <div className="flex flex-wrap gap-4 mb-8 items-center bg-gray-50 p-4 rounded-lg">
270
- <div className="flex items-center text-sm font-bold text-gray-500"><Filter size={16} className="mr-2"/> 筛选维度:</div>
271
-
272
- {(activeTab === 'grade' || activeTab === 'matrix') && (
273
- <select className="border border-gray-300 p-2 rounded text-sm text-gray-800 focus:ring-2 focus:ring-blue-500" value={selectedGrade} onChange={e => setSelectedGrade(e.target.value)}>
274
- {uniqueGrades.map(g => <option key={g} value={g}>{g}</option>)}
275
- </select>
276
- )}
277
-
278
- {(activeTab === 'trend' || activeTab === 'student') && (
279
- <select className="border border-gray-300 p-2 rounded text-sm text-gray-800" value={selectedClass} onChange={e => setSelectedClass(e.target.value)}>
280
- {allClasses.map(c => <option key={c} value={c}>{c}</option>)}
281
- </select>
282
- )}
283
-
284
- {activeTab === 'trend' && (
285
- <select className="border border-gray-300 p-2 rounded text-sm text-gray-800" value={selectedSubject} onChange={e => setSelectedSubject(e.target.value)}>
286
- {subjects.map(s => <option key={s._id} value={s.name}>{s.name}</option>)}
287
- </select>
288
- )}
289
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
290
 
291
  {/* --- 1. Grade Analysis View --- */}
292
  {activeTab === 'grade' && (
@@ -382,8 +485,8 @@ export const Reports: React.FC = () => {
382
  else if (val < 60) { colorClass = 'text-red-600 font-bold'; bgClass = 'bg-red-50'; }
383
 
384
  return (
385
- <td key={sub._id} className={`px-6 py-4 ${bgClass}`}>
386
- <span className={colorClass}>{val || '-'}</span>
387
  </td>
388
  );
389
  })}
@@ -391,7 +494,6 @@ export const Reports: React.FC = () => {
391
  ))}
392
  </tbody>
393
  </table>
394
- <p className="text-xs text-gray-400 mt-4 text-right">* 绿色底色代表优秀,红色底色代表需要关注</p>
395
  </div>
396
  )}
397
 
@@ -403,16 +505,22 @@ export const Reports: React.FC = () => {
403
  {focusStudents.map(s => {
404
  const trend = getStudentTrend(s.studentNo);
405
  return (
406
- <div key={s._id||s.id} onClick={() => setSelectedStudent(s)} className="bg-white border rounded-xl p-4 hover:shadow-md cursor-pointer transition-shadow">
 
 
 
 
407
  <div className="flex items-center space-x-3 mb-3">
408
- <div className={`w-10 h-10 rounded-full flex items-center justify-center text-white font-bold ${s.gender==='Male'?'bg-blue-500':'bg-pink-500'}`}>{s.name[0]}</div>
 
 
409
  <div>
410
- <p className="font-bold text-gray-800">{s.name}</p>
411
- <p className="text-xs text-gray-500">{s.studentNo}</p>
412
  </div>
413
  </div>
414
- {/* Sparkline */}
415
- <div className="h-12">
416
  <ResponsiveContainer width="100%" height="100%">
417
  <LineChart data={trend}>
418
  <Line type="monotone" dataKey="score" stroke="#94a3b8" strokeWidth={2} dot={false} />
@@ -425,45 +533,50 @@ export const Reports: React.FC = () => {
425
  </div>
426
  </div>
427
  )}
428
-
429
- {/* --- 5. Overview View --- */}
430
- {activeTab === 'overview' && (
431
- <div className="flex flex-col items-center justify-center py-20 text-gray-400">
432
- <PieChartIcon size={64} className="mb-4 opacity-20"/>
433
- <p>全校概览模式 - 整合所有年级的大数据分析</p>
434
- <button onClick={()=>setActiveTab('grade')} className="mt-4 text-blue-600 hover:underline">查看年级详情</button>
435
- </div>
436
- )}
437
-
438
  </div>
439
 
440
- {/* Student Detail Modal */}
441
  {selectedStudent && (
442
- <div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
443
- <div className="bg-white rounded-xl w-full max-w-4xl max-h-[90vh] overflow-y-auto p-6 relative">
444
- <button onClick={()=>setSelectedStudent(null)} className="absolute top-4 right-4 text-gray-400 hover:text-gray-600"><UserCheck size={24}/></button>
445
-
446
- <div className="flex items-center space-x-4 mb-8">
447
- <div className={`w-16 h-16 rounded-full flex items-center justify-center text-2xl text-white font-bold ${selectedStudent.gender==='Male'?'bg-blue-600':'bg-pink-500'}`}>
448
- {selectedStudent.name[0]}
 
 
 
 
 
 
 
449
  </div>
450
- <div>
451
- <h2 className="text-2xl font-bold text-gray-800">{selectedStudent.name}</h2>
452
- <p className="text-gray-500">{selectedStudent.studentNo} | {selectedStudent.className}</p>
 
 
 
 
 
 
 
453
  </div>
 
454
  </div>
455
 
456
  <div className="grid grid-cols-1 md:grid-cols-2 gap-8">
457
  {/* Radar */}
458
- <div className="bg-gray-50 rounded-xl p-4">
459
- <h3 className="font-bold text-gray-700 mb-4 text-center">学科能力模型</h3>
460
  <div className="h-64">
461
  <ResponsiveContainer width="100%" height="100%">
462
- <RadarChart cx="50%" cy="50%" outerRadius="80%" data={getStudentRadar(selectedStudent.studentNo)}>
463
  <PolarGrid />
464
  <PolarAngleAxis dataKey="subject" />
465
  <PolarRadiusAxis angle={30} domain={[0, 100]} />
466
- <Radar name={selectedStudent.name} dataKey="score" stroke="#3b82f6" fill="#3b82f6" fillOpacity={0.6} />
467
  <Tooltip />
468
  </RadarChart>
469
  </ResponsiveContainer>
@@ -471,36 +584,24 @@ export const Reports: React.FC = () => {
471
  </div>
472
 
473
  {/* Trend */}
474
- <div className="bg-gray-50 rounded-xl p-4">
475
- <h3 className="font-bold text-gray-700 mb-4 text-center">综合成绩走势</h3>
476
  <div className="h-64">
477
  <ResponsiveContainer width="100%" height="100%">
478
  <LineChart data={getStudentTrend(selectedStudent.studentNo)}>
479
  <CartesianGrid strokeDasharray="3 3" vertical={false}/>
480
- <XAxis dataKey="name" fontSize={10}/>
481
  <YAxis domain={[0, 100]}/>
482
  <Tooltip />
483
- <Line type="monotone" dataKey="score" stroke="#10b981" strokeWidth={3} />
484
  </LineChart>
485
  </ResponsiveContainer>
486
  </div>
487
  </div>
488
-
489
- {/* Attendance */}
490
- <div className="bg-gray-50 rounded-xl p-4 md:col-span-2 flex justify-around">
491
- <div className="text-center">
492
- <p className="text-gray-500 text-sm">缺考次数</p>
493
- <p className="text-2xl font-bold text-red-500">{getStudentAttendance(selectedStudent.studentNo).absent}</p>
494
- </div>
495
- <div className="text-center">
496
- <p className="text-gray-500 text-sm">请假次数</p>
497
- <p className="text-2xl font-bold text-orange-500">{getStudentAttendance(selectedStudent.studentNo).leave}</p>
498
- </div>
499
- </div>
500
  </div>
501
  </div>
502
  </div>
503
  )}
504
  </div>
505
  );
506
- };
 
 
1
  import React, { useState, useEffect } from 'react';
2
  import {
3
  BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer,
4
  LineChart, Line, AreaChart, Area, PieChart as RePieChart, Pie, Cell, RadarChart, Radar, PolarGrid, PolarAngleAxis, PolarRadiusAxis
5
  } from 'recharts';
6
  import { api } from '../services/api';
7
+ import { Loader2, Download, Filter, TrendingUp, Grid, BarChart2, User, PieChart as PieChartIcon } from 'lucide-react';
8
  import { Score, Student, ClassInfo, Subject, Exam } from '../types';
9
 
10
  export const Reports: React.FC = () => {
11
  const [loading, setLoading] = useState(true);
12
+ const [activeTab, setActiveTab] = useState<'overview' | 'grade' | 'trend' | 'matrix' | 'student'>('overview');
13
 
14
  // Data
15
  const [scores, setScores] = useState<Score[]>([]);
 
28
  const [trendData, setTrendData] = useState<any[]>([]);
29
  const [matrixData, setMatrixData] = useState<any[]>([]);
30
 
31
+ // Overview Data
32
+ const [overviewData, setOverviewData] = useState<{
33
+ totalStudents: number;
34
+ avgScore: number;
35
+ passRate: number;
36
+ gradeLadder: any[];
37
+ subjectDist: any[];
38
+ }>({ totalStudents: 0, avgScore: 0, passRate: 0, gradeLadder: [], subjectDist: [] });
39
+
40
  // Student Focus State
41
  const [selectedStudent, setSelectedStudent] = useState<Student | null>(null);
42
 
 
73
  useEffect(() => {
74
  if (scores.length === 0 || students.length === 0) return;
75
 
76
+ // --- 0. Overview Calculations ---
77
+ const normalScores = scores.filter(s => s.status === 'Normal');
78
+ const totalAvg = normalScores.length ? normalScores.reduce((a,b)=>a+b.score,0)/normalScores.length : 0;
79
+ const totalPass = normalScores.filter(s => s.score >= 60).length;
80
+ const passRate = normalScores.length ? (totalPass / normalScores.length)*100 : 0;
81
+
82
+ // Grade Ladder (Avg score by grade)
83
+ const uniqueGradesList = Array.from(new Set(classes.map(c => c.grade))).sort();
84
+ const ladderData = uniqueGradesList.map(g => {
85
+ const gradeClasses = classes.filter(c => c.grade === g).map(c => c.grade+c.className);
86
+ const gradeStus = students.filter(s => gradeClasses.includes(s.className)).map(s => s.studentNo);
87
+ const gradeScores = normalScores.filter(s => gradeStus.includes(s.studentNo));
88
+ const gAvg = gradeScores.length ? gradeScores.reduce((a,b)=>a+b.score,0)/gradeScores.length : 0;
89
+ return { name: g, 平均分: Number(gAvg.toFixed(1)) };
90
+ });
91
+
92
+ // Subject Distribution
93
+ const subDist = subjects.map(sub => {
94
+ const subScores = normalScores.filter(s => s.courseName === sub.name);
95
+ const sAvg = subScores.length ? subScores.reduce((a,b)=>a+b.score,0)/subScores.length : 0;
96
+ return { name: sub.name, value: Number(sAvg.toFixed(1)), color: sub.color };
97
+ });
98
+
99
+ setOverviewData({
100
+ totalStudents: students.length,
101
+ avgScore: Number(totalAvg.toFixed(1)),
102
+ passRate: Number(passRate.toFixed(1)),
103
+ gradeLadder: ladderData,
104
+ subjectDist: subDist
105
+ });
106
+
107
+
108
  // --- 1. Grade Analysis (Horizontal Class Comparison) ---
109
  const gradeClasses = classes.filter(c => c.grade === selectedGrade);
110
  const gaData = gradeClasses.map(cls => {
 
283
  {/* Tabs */}
284
  <div className="flex space-x-1 bg-gray-100 p-1 rounded-xl w-full md:w-auto overflow-x-auto">
285
  {[
286
+ { id: 'overview', label: '全校概览', icon: PieChartIcon },
287
  { id: 'grade', label: '年级横向分析', icon: BarChart2 },
288
  { id: 'trend', label: '教学成长轨迹', icon: TrendingUp },
289
  { id: 'matrix', label: '学科质量透视', icon: Grid },
290
  { id: 'student', label: '学生个像', icon: User },
 
291
  ].map(tab => (
292
  <button
293
  key={tab.id}
 
306
  <div className="bg-white rounded-xl shadow-sm border border-gray-100 p-6 min-h-[500px]">
307
 
308
  {/* Filter Bar */}
309
+ {activeTab !== 'overview' && (
310
+ <div className="flex flex-wrap gap-4 mb-8 items-center bg-gray-50 p-4 rounded-lg">
311
+ <div className="flex items-center text-sm font-bold text-gray-500"><Filter size={16} className="mr-2"/> 筛选维度:</div>
312
+
313
+ {(activeTab === 'grade' || activeTab === 'matrix') && (
314
+ <select className="border border-gray-300 p-2 rounded text-sm text-gray-800 focus:ring-2 focus:ring-blue-500" value={selectedGrade} onChange={e => setSelectedGrade(e.target.value)}>
315
+ {uniqueGrades.map(g => <option key={g} value={g}>{g}</option>)}
316
+ </select>
317
+ )}
318
+
319
+ {(activeTab === 'trend' || activeTab === 'student') && (
320
+ <select className="border border-gray-300 p-2 rounded text-sm text-gray-800" value={selectedClass} onChange={e => setSelectedClass(e.target.value)}>
321
+ {allClasses.map(c => <option key={c} value={c}>{c}</option>)}
322
+ </select>
323
+ )}
324
+
325
+ {activeTab === 'trend' && (
326
+ <select className="border border-gray-300 p-2 rounded text-sm text-gray-800" value={selectedSubject} onChange={e => setSelectedSubject(e.target.value)}>
327
+ {subjects.map(s => <option key={s._id} value={s.name}>{s.name}</option>)}
328
+ </select>
329
+ )}
330
+ </div>
331
+ )}
332
+
333
+ {/* --- 0. Overview View --- */}
334
+ {activeTab === 'overview' && (
335
+ <div className="animate-in fade-in space-y-8">
336
+ {/* KPI Cards */}
337
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
338
+ <div className="bg-blue-50 p-6 rounded-xl border border-blue-100">
339
+ <p className="text-gray-500 text-sm font-medium mb-1">全校总人数</p>
340
+ <h3 className="text-3xl font-bold text-blue-700">{overviewData.totalStudents}</h3>
341
+ </div>
342
+ <div className="bg-violet-50 p-6 rounded-xl border border-violet-100">
343
+ <p className="text-gray-500 text-sm font-medium mb-1">全校综合平均分</p>
344
+ <h3 className="text-3xl font-bold text-violet-700">{overviewData.avgScore}</h3>
345
+ </div>
346
+ <div className="bg-emerald-50 p-6 rounded-xl border border-emerald-100">
347
+ <p className="text-gray-500 text-sm font-medium mb-1">综合及格率</p>
348
+ <h3 className="text-3xl font-bold text-emerald-700">{overviewData.passRate}%</h3>
349
+ </div>
350
+ </div>
351
+
352
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
353
+ {/* Grade Ladder */}
354
+ <div className="bg-white p-4 rounded-lg border border-gray-100 shadow-sm">
355
+ <h3 className="font-bold text-gray-800 mb-4 text-center">各年级平均分阶梯</h3>
356
+ <div className="h-72">
357
+ <ResponsiveContainer width="100%" height="100%">
358
+ <AreaChart data={overviewData.gradeLadder}>
359
+ <defs>
360
+ <linearGradient id="colorAvg" x1="0" y1="0" x2="0" y2="1">
361
+ <stop offset="5%" stopColor="#8884d8" stopOpacity={0.8}/>
362
+ <stop offset="95%" stopColor="#8884d8" stopOpacity={0}/>
363
+ </linearGradient>
364
+ </defs>
365
+ <XAxis dataKey="name" />
366
+ <YAxis domain={[0, 100]}/>
367
+ <CartesianGrid strokeDasharray="3 3" vertical={false} />
368
+ <Tooltip />
369
+ <Area type="monotone" dataKey="平均分" stroke="#8884d8" fillOpacity={1} fill="url(#colorAvg)" />
370
+ </AreaChart>
371
+ </ResponsiveContainer>
372
+ </div>
373
+ </div>
374
+
375
+ {/* Subject Distribution */}
376
+ <div className="bg-white p-4 rounded-lg border border-gray-100 shadow-sm">
377
+ <h3 className="font-bold text-gray-800 mb-4 text-center">全校学科平均分雷达</h3>
378
+ <div className="h-72">
379
+ <ResponsiveContainer width="100%" height="100%">
380
+ <RadarChart cx="50%" cy="50%" outerRadius="80%" data={overviewData.subjectDist}>
381
+ <PolarGrid />
382
+ <PolarAngleAxis dataKey="name" />
383
+ <PolarRadiusAxis angle={30} domain={[0, 100]} />
384
+ <Radar name="平均分" dataKey="value" stroke="#82ca9d" fill="#82ca9d" fillOpacity={0.6} />
385
+ <Tooltip />
386
+ </RadarChart>
387
+ </ResponsiveContainer>
388
+ </div>
389
+ </div>
390
+ </div>
391
+ </div>
392
+ )}
393
 
394
  {/* --- 1. Grade Analysis View --- */}
395
  {activeTab === 'grade' && (
 
485
  else if (val < 60) { colorClass = 'text-red-600 font-bold'; bgClass = 'bg-red-50'; }
486
 
487
  return (
488
+ <td key={sub.name} className={`px-6 py-4 ${bgClass}`}>
489
+ <span className={colorClass}>{val}</span>
490
  </td>
491
  );
492
  })}
 
494
  ))}
495
  </tbody>
496
  </table>
 
497
  </div>
498
  )}
499
 
 
505
  {focusStudents.map(s => {
506
  const trend = getStudentTrend(s.studentNo);
507
  return (
508
+ <div
509
+ key={s._id || s.id}
510
+ onClick={() => setSelectedStudent(s)}
511
+ className="bg-white border border-gray-200 p-4 rounded-xl cursor-pointer hover:shadow-lg hover:border-blue-400 transition-all group"
512
+ >
513
  <div className="flex items-center space-x-3 mb-3">
514
+ <div className={`w-10 h-10 rounded-full flex items-center justify-center font-bold text-white ${s.gender === 'Female' ? 'bg-pink-400' : 'bg-blue-400'}`}>
515
+ {s.name[0]}
516
+ </div>
517
  <div>
518
+ <h4 className="font-bold text-gray-800 group-hover:text-blue-600">{s.name}</h4>
519
+ <p className="text-xs text-gray-500 font-mono">{s.studentNo}</p>
520
  </div>
521
  </div>
522
+ {/* Mini Sparkline */}
523
+ <div className="h-16 w-full">
524
  <ResponsiveContainer width="100%" height="100%">
525
  <LineChart data={trend}>
526
  <Line type="monotone" dataKey="score" stroke="#94a3b8" strokeWidth={2} dot={false} />
 
533
  </div>
534
  </div>
535
  )}
 
 
 
 
 
 
 
 
 
 
536
  </div>
537
 
538
+ {/* Student Details Modal */}
539
  {selectedStudent && (
540
+ <div className="fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-4 backdrop-blur-sm animate-in fade-in">
541
+ <div className="bg-white rounded-2xl p-6 w-full max-w-4xl max-h-[90vh] overflow-y-auto shadow-2xl">
542
+ <div className="flex justify-between items-start mb-6">
543
+ <div className="flex items-center space-x-4">
544
+ <div className={`w-16 h-16 rounded-full flex items-center justify-center text-2xl font-bold text-white ${selectedStudent.gender === 'Female' ? 'bg-pink-500' : 'bg-blue-500'}`}>
545
+ {selectedStudent.name[0]}
546
+ </div>
547
+ <div>
548
+ <h2 className="text-2xl font-bold text-gray-900">{selectedStudent.name}</h2>
549
+ <div className="flex items-center space-x-3 text-sm text-gray-500 mt-1">
550
+ <span className="bg-gray-100 px-2 py-0.5 rounded text-gray-700">{selectedStudent.className}</span>
551
+ <span className="font-mono">{selectedStudent.studentNo}</span>
552
+ </div>
553
+ </div>
554
  </div>
555
+ <button onClick={() => setSelectedStudent(null)} className="p-2 hover:bg-gray-100 rounded-full transition-colors"><Grid size={20}/></button>
556
+ </div>
557
+
558
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
559
+ <div className="bg-blue-50 p-4 rounded-xl border border-blue-100">
560
+ <p className="text-blue-600 text-sm font-bold mb-1">考勤状况</p>
561
+ <div className="flex justify-between items-end">
562
+ <span className="text-3xl font-bold text-blue-800">{getStudentAttendance(selectedStudent.studentNo).absent}</span>
563
+ <span className="text-xs text-blue-400 mb-1">次缺考</span>
564
+ </div>
565
  </div>
566
+ {/* Add more stats here if needed */}
567
  </div>
568
 
569
  <div className="grid grid-cols-1 md:grid-cols-2 gap-8">
570
  {/* Radar */}
571
+ <div className="bg-white border border-gray-100 rounded-xl p-4 shadow-sm">
572
+ <h3 className="font-bold text-gray-800 mb-4 text-center">学科能力模型</h3>
573
  <div className="h-64">
574
  <ResponsiveContainer width="100%" height="100%">
575
+ <RadarChart cx="50%" cy="50%" outerRadius="70%" data={getStudentRadar(selectedStudent.studentNo)}>
576
  <PolarGrid />
577
  <PolarAngleAxis dataKey="subject" />
578
  <PolarRadiusAxis angle={30} domain={[0, 100]} />
579
+ <Radar name={selectedStudent.name} dataKey="score" stroke="#8884d8" fill="#8884d8" fillOpacity={0.6} />
580
  <Tooltip />
581
  </RadarChart>
582
  </ResponsiveContainer>
 
584
  </div>
585
 
586
  {/* Trend */}
587
+ <div className="bg-white border border-gray-100 rounded-xl p-4 shadow-sm">
588
+ <h3 className="font-bold text-gray-800 mb-4 text-center">综合成绩走势</h3>
589
  <div className="h-64">
590
  <ResponsiveContainer width="100%" height="100%">
591
  <LineChart data={getStudentTrend(selectedStudent.studentNo)}>
592
  <CartesianGrid strokeDasharray="3 3" vertical={false}/>
593
+ <XAxis dataKey="name" />
594
  <YAxis domain={[0, 100]}/>
595
  <Tooltip />
596
+ <Line type="monotone" dataKey="score" stroke="#2563eb" strokeWidth={3} />
597
  </LineChart>
598
  </ResponsiveContainer>
599
  </div>
600
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
601
  </div>
602
  </div>
603
  </div>
604
  )}
605
  </div>
606
  );
607
+ };
pages/ScoreList.tsx CHANGED
@@ -1,4 +1,3 @@
1
-
2
  import React, { useState, useEffect } from 'react';
3
  import { api } from '../services/api';
4
  import { Score, Student, Subject, ClassInfo, ExamStatus, Exam } from '../types';
@@ -468,4 +467,4 @@ export const ScoreList: React.FC = () => {
468
  )}
469
  </div>
470
  );
471
- };
 
 
1
  import React, { useState, useEffect } from 'react';
2
  import { api } from '../services/api';
3
  import { Score, Student, Subject, ClassInfo, ExamStatus, Exam } from '../types';
 
467
  )}
468
  </div>
469
  );
470
+ };