dvc890 commited on
Commit
1e5e242
·
verified ·
1 Parent(s): ff09086

Upload 27 files

Browse files
pages/ClassList.tsx CHANGED
@@ -11,11 +11,16 @@ export const ClassList: React.FC = () => {
11
  const [submitting, setSubmitting] = useState(false);
12
 
13
  // Form
14
- const [grade, setGrade] = useState('六年级');
15
  const [className, setClassName] = useState('(1)班');
16
  const [teacherName, setTeacherName] = useState('');
17
 
18
- const grades = ['一年级', '二年级', '三年级', '四年级', '五年级', '六年级'];
 
 
 
 
 
19
 
20
  const loadClasses = async () => {
21
  setLoading(true);
@@ -111,7 +116,7 @@ export const ClassList: React.FC = () => {
111
  <form onSubmit={handleSubmit} className="space-y-4">
112
  <div>
113
  <label className="text-sm font-medium text-gray-700">年级</label>
114
- <select value={grade} onChange={e => setGrade(e.target.value)} className="w-full mt-1 px-3 py-2 border rounded-lg">
115
  {grades.map(g => <option key={g} value={g}>{g}</option>)}
116
  </select>
117
  </div>
@@ -121,7 +126,7 @@ export const ClassList: React.FC = () => {
121
  </div>
122
  <div>
123
  <label className="text-sm font-medium text-gray-700">班主任姓名</label>
124
- <input required type="text" value={teacherName} onChange={e => setTeacherName(e.target.value)} className="w-full mt-1 px-3 py-2 border rounded-lg" placeholder="张老师" />
125
  </div>
126
  <div className="flex space-x-3 pt-4">
127
  <button type="button" onClick={() => setIsModalOpen(false)} className="flex-1 py-2 border rounded-lg">取消</button>
 
11
  const [submitting, setSubmitting] = useState(false);
12
 
13
  // Form
14
+ const [grade, setGrade] = useState('一年级');
15
  const [className, setClassName] = useState('(1)班');
16
  const [teacherName, setTeacherName] = useState('');
17
 
18
+ // Expanded K12 Grades
19
+ const grades = [
20
+ '一年级', '二年级', '三年级', '四年级', '五年级', '六年级',
21
+ '初一/七年级', '初二/八年级', '初三/九年级',
22
+ '高一', '高二', '高三'
23
+ ];
24
 
25
  const loadClasses = async () => {
26
  setLoading(true);
 
116
  <form onSubmit={handleSubmit} className="space-y-4">
117
  <div>
118
  <label className="text-sm font-medium text-gray-700">年级</label>
119
+ <select value={grade} onChange={e => setGrade(e.target.value)} className="w-full mt-1 px-3 py-2 border rounded-lg max-h-40">
120
  {grades.map(g => <option key={g} value={g}>{g}</option>)}
121
  </select>
122
  </div>
 
126
  </div>
127
  <div>
128
  <label className="text-sm font-medium text-gray-700">班主任姓名</label>
129
+ <input type="text" value={teacherName} onChange={e => setTeacherName(e.target.value)} className="w-full mt-1 px-3 py-2 border rounded-lg" placeholder="可选填" />
130
  </div>
131
  <div className="flex space-x-3 pt-4">
132
  <button type="button" onClick={() => setIsModalOpen(false)} className="flex-1 py-2 border rounded-lg">取消</button>
pages/CourseList.tsx CHANGED
@@ -2,11 +2,12 @@
2
  import React, { useEffect, useState } from 'react';
3
  import { Plus, Clock, Loader2, Edit, Trash2, X } from 'lucide-react';
4
  import { api } from '../services/api';
5
- import { Course, Subject } from '../types';
6
 
7
  export const CourseList: React.FC = () => {
8
  const [courses, setCourses] = useState<Course[]>([]);
9
  const [subjects, setSubjects] = useState<Subject[]>([]);
 
10
  const [loading, setLoading] = useState(true);
11
  const [isModalOpen, setIsModalOpen] = useState(false);
12
  const [formData, setFormData] = useState({ courseCode: '', courseName: '', teacherName: '', credits: 2, capacity: 45 });
@@ -15,9 +16,14 @@ export const CourseList: React.FC = () => {
15
  const loadData = async () => {
16
  setLoading(true);
17
  try {
18
- const [c, s] = await Promise.all([api.courses.getAll(), api.subjects.getAll()]);
 
 
 
 
19
  setCourses(c);
20
  setSubjects(s);
 
21
  } catch (e) { console.error(e); } finally { setLoading(false); }
22
  };
23
 
@@ -69,7 +75,22 @@ export const CourseList: React.FC = () => {
69
  {subjects.map(s=><option key={s._id} value={s.name}>{s.name}</option>)}
70
  </select>
71
  </div>
72
- <input className="w-full border p-2 rounded" placeholder="教师姓名" value={formData.teacherName} onChange={e=>setFormData({...formData, teacherName:e.target.value})} required/>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  <button className="w-full bg-blue-600 text-white py-2 rounded">保存</button>
74
  <button type="button" onClick={()=>setIsModalOpen(false)} className="w-full border py-2 rounded">取消</button>
75
  </form>
 
2
  import React, { useEffect, useState } from 'react';
3
  import { Plus, Clock, Loader2, Edit, Trash2, X } from 'lucide-react';
4
  import { api } from '../services/api';
5
+ import { Course, Subject, User } from '../types';
6
 
7
  export const CourseList: React.FC = () => {
8
  const [courses, setCourses] = useState<Course[]>([]);
9
  const [subjects, setSubjects] = useState<Subject[]>([]);
10
+ const [teachers, setTeachers] = useState<User[]>([]); // New: List of teachers
11
  const [loading, setLoading] = useState(true);
12
  const [isModalOpen, setIsModalOpen] = useState(false);
13
  const [formData, setFormData] = useState({ courseCode: '', courseName: '', teacherName: '', credits: 2, capacity: 45 });
 
16
  const loadData = async () => {
17
  setLoading(true);
18
  try {
19
+ const [c, s, t] = await Promise.all([
20
+ api.courses.getAll(),
21
+ api.subjects.getAll(),
22
+ api.users.getAll({ role: 'TEACHER' }) // Fetch teachers
23
+ ]);
24
  setCourses(c);
25
  setSubjects(s);
26
+ setTeachers(t);
27
  } catch (e) { console.error(e); } finally { setLoading(false); }
28
  };
29
 
 
75
  {subjects.map(s=><option key={s._id} value={s.name}>{s.name}</option>)}
76
  </select>
77
  </div>
78
+
79
+ <div>
80
+ <label className="text-sm font-bold text-gray-500">任课教师</label>
81
+ <input
82
+ className="w-full border p-2 rounded"
83
+ placeholder="选择或输入教师姓名"
84
+ value={formData.teacherName}
85
+ onChange={e=>setFormData({...formData, teacherName:e.target.value})}
86
+ list="teacherList"
87
+ required
88
+ />
89
+ <datalist id="teacherList">
90
+ {teachers.map(t => <option key={t._id} value={t.trueName || t.username}>{t.username}</option>)}
91
+ </datalist>
92
+ </div>
93
+
94
  <button className="w-full bg-blue-600 text-white py-2 rounded">保存</button>
95
  <button type="button" onClick={()=>setIsModalOpen(false)} className="w-full border py-2 rounded">取消</button>
96
  </form>
pages/Dashboard.tsx CHANGED
@@ -1,5 +1,6 @@
 
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
 
@@ -11,6 +12,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);
 
 
 
 
14
 
15
  useEffect(() => {
16
  const loadData = async () => {
@@ -24,48 +29,20 @@ export const Dashboard: React.FC<DashboardProps> = ({ onNavigate }) => {
24
  ]);
25
  setStats(summary);
26
 
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) {
34
  const avg = subScores.reduce((a: number, b: Score) => a + b.score, 0) / subScores.length;
35
- if (avg < 60) {
36
- newWarnings.push(`全校 ${sub.name} 学科平均分过低 (${avg.toFixed(1)}分),请关注。`);
37
- }
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} 人次考试不及格,请关注后进生辅导。`);
55
- }
56
-
57
- const absentCount = scores.filter((s: Score) => s.status === 'Absent').length;
58
- if (absentCount > 5) {
59
- newWarnings.push(`近期有 ${absentCount} 人次缺考,请核实情况。`);
60
- }
61
-
62
  setWarnings(newWarnings);
63
 
64
- } catch (error) {
65
- console.error(error);
66
- } finally {
67
- setLoading(false);
68
- }
69
  };
70
  loadData();
71
  }, []);
@@ -85,18 +62,15 @@ export const Dashboard: React.FC<DashboardProps> = ({ onNavigate }) => {
85
  <p className="text-gray-500 mt-1">欢迎回来,今天是 {new Date().toLocaleDateString()}</p>
86
  </div>
87
  <div className="flex space-x-3 mt-4 md:mt-0">
88
- <button className="flex items-center space-x-2 px-4 py-2 bg-white border border-gray-200 rounded-lg text-sm font-medium text-gray-600 hover:bg-gray-50 shadow-sm">
89
- <Calendar size={16}/>
90
- <span>日程安排</span>
91
  </button>
92
- <button className="flex items-center space-x-2 px-4 py-2 bg-blue-600 text-white rounded-lg text-sm font-medium hover:bg-blue-700 shadow-sm shadow-blue-200">
93
- <Activity size={16}/>
94
- <span>系统状态</span>
95
  </button>
96
  </div>
97
  </div>
98
 
99
- {/* Stats Grid */}
100
  <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
101
  {cards.map((card, index) => (
102
  <div key={index} className="bg-white rounded-xl shadow-sm border border-gray-100 p-6 flex items-start justify-between hover:shadow-md transition-shadow relative overflow-hidden group">
@@ -111,31 +85,19 @@ export const Dashboard: React.FC<DashboardProps> = ({ onNavigate }) => {
111
  <div className={`p-3 rounded-xl ${card.color} bg-opacity-10 text-white shadow-sm group-hover:scale-110 transition-transform`}>
112
  <card.icon className={`h-6 w-6 ${card.color.replace('bg-', 'text-')}`} />
113
  </div>
114
- {/* Background Decoration */}
115
  <div className={`absolute -bottom-4 -right-4 w-24 h-24 rounded-full ${card.color} opacity-5`}></div>
116
  </div>
117
  ))}
118
  </div>
119
 
120
  <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
121
- {/* Warning Section */}
122
  <div className="lg:col-span-2 bg-white rounded-xl shadow-sm border border-gray-100 p-6">
123
  <div className="flex items-center justify-between mb-6">
124
- <h3 className="text-lg font-bold text-gray-800 flex items-center">
125
- <AlertTriangle className="text-amber-500 mr-2" size={20}/>
126
- 教学质量预警
127
- </h3>
128
- <span className="text-xs font-semibold bg-amber-100 text-amber-700 px-2 py-1 rounded-full">
129
- {warnings.length} 条待处理
130
- </span>
131
  </div>
132
-
133
  <div className="space-y-4">
134
- {loading ? (
135
- <div className="space-y-3">
136
- {[1,2,3].map(i => <div key={i} className="h-16 bg-gray-50 rounded-lg animate-pulse"></div>)}
137
- </div>
138
- ) : warnings.length > 0 ? (
139
  warnings.map((w, i) => (
140
  <div key={i} className="flex items-start p-4 bg-amber-50/50 border border-amber-100 rounded-lg">
141
  <div className="min-w-[4px] h-full bg-amber-400 rounded-full mr-3 self-stretch"></div>
@@ -145,53 +107,82 @@ export const Dashboard: React.FC<DashboardProps> = ({ onNavigate }) => {
145
  </div>
146
  </div>
147
  ))
148
- ) : (
149
- <div className="text-center py-10 bg-gray-50 rounded-lg">
150
- <div className="bg-green-100 w-12 h-12 rounded-full flex items-center justify-center mx-auto mb-3">
151
- <Activity className="text-green-600" size={24}/>
152
- </div>
153
- <p className="text-gray-500">各项指标正常,暂无预警</p>
154
- </div>
155
- )}
156
  </div>
157
  </div>
158
 
159
- {/* Quick Actions */}
160
  <div className="bg-gradient-to-br from-indigo-600 to-blue-700 rounded-xl shadow-lg shadow-blue-200 p-6 text-white relative overflow-hidden">
161
  <div className="relative z-10">
162
  <h3 className="text-xl font-bold mb-2">快速开始</h3>
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>
187
  </div>
188
  </div>
189
-
190
- {/* Decorative circles */}
191
- <div className="absolute top-[-20%] right-[-10%] w-40 h-40 bg-white/10 rounded-full blur-2xl"></div>
192
- <div className="absolute bottom-[-10%] left-[-10%] w-32 h-32 bg-indigo-400/20 rounded-full blur-xl"></div>
193
  </div>
194
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
195
  </div>
196
  );
197
- };
 
1
+
2
  import React, { useEffect, useState } from 'react';
3
+ import { Users, BookOpen, GraduationCap, TrendingUp, AlertTriangle, ArrowRight, Activity, Calendar, X, CheckCircle } from 'lucide-react';
4
  import { api } from '../services/api';
5
  import { Score, ClassInfo, Subject } from '../types';
6
 
 
12
  const [stats, setStats] = useState({ studentCount: 0, courseCount: 0, avgScore: 0, excellentRate: '0%' });
13
  const [warnings, setWarnings] = useState<string[]>([]);
14
  const [loading, setLoading] = useState(true);
15
+
16
+ // Modal States
17
+ const [showSchedule, setShowSchedule] = useState(false);
18
+ const [showStatus, setShowStatus] = useState(false);
19
 
20
  useEffect(() => {
21
  const loadData = async () => {
 
29
  ]);
30
  setStats(summary);
31
 
 
32
  const newWarnings: string[] = [];
 
 
33
  subjects.forEach((sub: Subject) => {
34
  const subScores = scores.filter((s: Score) => s.courseName === sub.name && s.status === 'Normal');
35
  if (subScores.length > 0) {
36
  const avg = subScores.reduce((a: number, b: Score) => a + b.score, 0) / subScores.length;
37
+ if (avg < 60) newWarnings.push(`全校 ${sub.name} 学科平均分过低 (${avg.toFixed(1)}分),请关注。`);
 
 
38
  }
39
  });
 
 
 
 
 
 
 
 
 
 
 
 
40
  const failedCount = scores.filter((s: Score) => s.score < 60 && s.status === 'Normal').length;
41
+ if (failedCount > 10) newWarnings.push(`近期共有 ${failedCount} 人次考试不及格,请关注后进生辅导。`);
 
 
 
 
 
 
 
 
42
  setWarnings(newWarnings);
43
 
44
+ } catch (error) { console.error(error); }
45
+ finally { setLoading(false); }
 
 
 
46
  };
47
  loadData();
48
  }, []);
 
62
  <p className="text-gray-500 mt-1">欢迎回来,今天是 {new Date().toLocaleDateString()}</p>
63
  </div>
64
  <div className="flex space-x-3 mt-4 md:mt-0">
65
+ <button onClick={() => setShowSchedule(true)} className="flex items-center space-x-2 px-4 py-2 bg-white border border-gray-200 rounded-lg text-sm font-medium text-gray-600 hover:bg-gray-50 shadow-sm transition-colors">
66
+ <Calendar size={16}/><span>日程安排</span>
 
67
  </button>
68
+ <button onClick={() => setShowStatus(true)} className="flex items-center space-x-2 px-4 py-2 bg-blue-600 text-white rounded-lg text-sm font-medium hover:bg-blue-700 shadow-sm shadow-blue-200 transition-colors">
69
+ <Activity size={16}/><span>系统状态</span>
 
70
  </button>
71
  </div>
72
  </div>
73
 
 
74
  <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
75
  {cards.map((card, index) => (
76
  <div key={index} className="bg-white rounded-xl shadow-sm border border-gray-100 p-6 flex items-start justify-between hover:shadow-md transition-shadow relative overflow-hidden group">
 
85
  <div className={`p-3 rounded-xl ${card.color} bg-opacity-10 text-white shadow-sm group-hover:scale-110 transition-transform`}>
86
  <card.icon className={`h-6 w-6 ${card.color.replace('bg-', 'text-')}`} />
87
  </div>
 
88
  <div className={`absolute -bottom-4 -right-4 w-24 h-24 rounded-full ${card.color} opacity-5`}></div>
89
  </div>
90
  ))}
91
  </div>
92
 
93
  <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
 
94
  <div className="lg:col-span-2 bg-white rounded-xl shadow-sm border border-gray-100 p-6">
95
  <div className="flex items-center justify-between mb-6">
96
+ <h3 className="text-lg font-bold text-gray-800 flex items-center"><AlertTriangle className="text-amber-500 mr-2" size={20}/>教学质量预警</h3>
97
+ <span className="text-xs font-semibold bg-amber-100 text-amber-700 px-2 py-1 rounded-full">{warnings.length} 条待处理</span>
 
 
 
 
 
98
  </div>
 
99
  <div className="space-y-4">
100
+ {loading ? <div className="space-y-3 animate-pulse">{[1,2].map(i => <div key={i} className="h-12 bg-gray-50 rounded"></div>)}</div> : warnings.length > 0 ? (
 
 
 
 
101
  warnings.map((w, i) => (
102
  <div key={i} className="flex items-start p-4 bg-amber-50/50 border border-amber-100 rounded-lg">
103
  <div className="min-w-[4px] h-full bg-amber-400 rounded-full mr-3 self-stretch"></div>
 
107
  </div>
108
  </div>
109
  ))
110
+ ) : <div className="text-center py-10 text-gray-400">各项指标正常</div>}
 
 
 
 
 
 
 
111
  </div>
112
  </div>
113
 
 
114
  <div className="bg-gradient-to-br from-indigo-600 to-blue-700 rounded-xl shadow-lg shadow-blue-200 p-6 text-white relative overflow-hidden">
115
  <div className="relative z-10">
116
  <h3 className="text-xl font-bold mb-2">快速开始</h3>
117
  <p className="text-blue-100 text-sm mb-6">管理您的日常教务工作</p>
 
118
  <div className="space-y-3">
119
+ <button onClick={() => onNavigate('grades')} 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">
120
+ <span>录入新成绩</span><ArrowRight size={16} className="opacity-0 group-hover:opacity-100 transition-opacity"/>
 
 
 
 
121
  </button>
122
+ <button onClick={() => onNavigate('students')} 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">
123
+ <span>添加学生档案</span><ArrowRight size={16} className="opacity-0 group-hover:opacity-100 transition-opacity"/>
 
 
 
 
124
  </button>
125
+ <button onClick={() => onNavigate('reports')} 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">
126
+ <span>下载本月报表</span><ArrowRight size={16} className="opacity-0 group-hover:opacity-100 transition-opacity"/>
 
 
 
 
127
  </button>
128
  </div>
129
  </div>
 
 
 
 
130
  </div>
131
  </div>
132
+
133
+ {/* Schedule Modal */}
134
+ {showSchedule && (
135
+ <div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
136
+ <div className="bg-white rounded-xl w-full max-w-md p-6 relative animate-in fade-in zoom-in-95">
137
+ <button onClick={()=>setShowSchedule(false)} className="absolute top-4 right-4 text-gray-400 hover:text-gray-600"><X size={20}/></button>
138
+ <h3 className="text-xl font-bold mb-4 flex items-center"><Calendar className="mr-2 text-blue-600"/>本周日程</h3>
139
+ <div className="space-y-3">
140
+ <div className="flex gap-4 p-3 bg-blue-50 rounded-lg border-l-4 border-blue-500">
141
+ <div className="text-center w-12"><div className="text-xs text-gray-500">周一</div><div className="font-bold">09:00</div></div>
142
+ <div><div className="font-bold text-gray-800">全校升旗仪式</div><div className="text-xs text-gray-500">大操场</div></div>
143
+ </div>
144
+ <div className="flex gap-4 p-3 bg-gray-50 rounded-lg border-l-4 border-gray-300">
145
+ <div className="text-center w-12"><div className="text-xs text-gray-500">周三</div><div className="font-bold">14:30</div></div>
146
+ <div><div className="font-bold text-gray-800">教职工会议</div><div className="text-xs text-gray-500">第一会议室</div></div>
147
+ </div>
148
+ <div className="flex gap-4 p-3 bg-emerald-50 rounded-lg border-l-4 border-emerald-500">
149
+ <div className="text-center w-12"><div className="text-xs text-gray-500">周五</div><div className="font-bold">16:00</div></div>
150
+ <div><div className="font-bold text-gray-800">期末考试考务会</div><div className="text-xs text-gray-500">报告厅</div></div>
151
+ </div>
152
+ </div>
153
+ <button onClick={()=>setShowSchedule(false)} className="w-full mt-6 bg-gray-100 text-gray-600 py-2 rounded-lg hover:bg-gray-200">关闭</button>
154
+ </div>
155
+ </div>
156
+ )}
157
+
158
+ {/* System Status Modal */}
159
+ {showStatus && (
160
+ <div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
161
+ <div className="bg-white rounded-xl w-full max-w-sm p-6 relative animate-in fade-in zoom-in-95">
162
+ <button onClick={()=>setShowStatus(false)} className="absolute top-4 right-4 text-gray-400 hover:text-gray-600"><X size={20}/></button>
163
+ <h3 className="text-xl font-bold mb-6 flex items-center"><Activity className="mr-2 text-green-600"/>系统运行状态</h3>
164
+ <div className="space-y-4">
165
+ <div className="flex justify-between items-center">
166
+ <span className="text-gray-600">数据库连接</span>
167
+ <span className="text-green-600 font-bold flex items-center"><CheckCircle size={14} className="mr-1"/> 正常</span>
168
+ </div>
169
+ <div className="flex justify-between items-center">
170
+ <span className="text-gray-600">API 响应延迟</span>
171
+ <span className="text-green-600 font-bold">24ms</span>
172
+ </div>
173
+ <div className="flex justify-between items-center">
174
+ <span className="text-gray-600">内存占用</span>
175
+ <span className="text-blue-600 font-bold">128MB / 512MB</span>
176
+ </div>
177
+ <div className="flex justify-between items-center">
178
+ <span className="text-gray-600">版本号</span>
179
+ <span className="text-gray-400 font-mono text-xs">v1.2.4</span>
180
+ </div>
181
+ </div>
182
+ <button onClick={()=>setShowStatus(false)} className="w-full mt-6 bg-blue-600 text-white py-2 rounded-lg hover:bg-blue-700">确认</button>
183
+ </div>
184
+ </div>
185
+ )}
186
  </div>
187
  );
188
+ };
pages/Settings.tsx CHANGED
@@ -1,35 +1,33 @@
1
 
2
  import React, { useState, useEffect } from 'react';
3
- import { Save, Bell, Lock, Database, Loader2, Plus, X } from 'lucide-react';
4
  import { api } from '../services/api';
 
5
 
6
  export const Settings: React.FC = () => {
7
  const [loading, setLoading] = useState(false);
8
  const [fetching, setFetching] = useState(true);
9
 
10
- const [config, setConfig] = useState({
11
  systemName: '智慧校园管理系统',
12
  allowRegister: true,
13
  allowAdminRegister: false,
14
  maintenanceMode: false,
15
  emailNotify: true,
16
- semester: '2023-2024学年 第一学期'
 
17
  });
18
 
19
- const [semesters, setSemesters] = useState([
20
- '2023-2024学年 第一学期',
21
- '2023-2024学年 第二学期',
22
- '2024-2025学年 第一学期'
23
- ]);
24
- const [isAddingSemester, setIsAddingSemester] = useState(false);
25
  const [newSemesterVal, setNewSemesterVal] = useState('');
 
 
26
 
27
  useEffect(() => {
28
  const loadConfig = async () => {
29
  try {
30
  const data = await api.config.get();
31
  if (data && data.systemName) {
32
- // Merge with defaults to ensure all fields exist
33
  setConfig(prev => ({ ...prev, ...data }));
34
  }
35
  } catch (e) {
@@ -45,7 +43,7 @@ export const Settings: React.FC = () => {
45
  setLoading(true);
46
  try {
47
  await api.config.save(config);
48
- alert('系统配置已保存!');
49
  } catch (e) {
50
  alert('保存失败');
51
  } finally {
@@ -53,81 +51,116 @@ export const Settings: React.FC = () => {
53
  }
54
  };
55
 
56
- const handleAddSemester = () => {
 
57
  if (newSemesterVal.trim()) {
58
  const newVal = newSemesterVal.trim();
59
- if (!semesters.includes(newVal)) {
60
- setSemesters(prev => [...prev, newVal]);
61
  }
62
- setConfig(prev => ({ ...prev, semester: newVal }));
63
  setNewSemesterVal('');
64
- setIsAddingSemester(false);
65
  }
66
  };
67
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  if (fetching) return <div className="p-10 text-center text-gray-500">加载配置中...</div>;
69
 
70
  return (
71
  <div className="space-y-6">
 
 
 
 
 
 
72
  {/* General Settings */}
73
  <div className="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
74
  <div className="p-6 border-b border-gray-100 flex items-center space-x-3">
75
  <div className="p-2 bg-blue-50 rounded-lg text-blue-600">
76
  <Database size={20} />
77
  </div>
78
- <h3 className="text-lg font-bold text-gray-800">基础设置</h3>
79
  </div>
80
 
81
- <div className="p-6 space-y-4">
82
- <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
83
- <div className="space-y-2">
84
- <label className="text-sm font-medium text-gray-700">系统名称</label>
85
- <input
86
- type="text"
87
- value={config.systemName}
88
- onChange={(e) => setConfig({...config, systemName: e.target.value})}
89
- className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:outline-none"
90
- />
91
- </div>
92
- <div className="space-y-2">
93
- <label className="text-sm font-medium text-gray-700">当前学期</label>
94
- <div className="flex gap-2">
95
- <select
96
- value={config.semester}
97
- onChange={(e) => setConfig({...config, semester: e.target.value})}
98
- className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:outline-none"
99
- >
100
- {semesters.map(s => <option key={s} value={s}>{s}</option>)}
101
- </select>
102
- <button
103
- onClick={() => setIsAddingSemester(!isAddingSemester)}
104
- className={`p-2 rounded-lg border transition-colors ${isAddingSemester ? 'bg-red-50 text-red-500 border-red-200' : 'bg-gray-50 text-gray-600 border-gray-200 hover:bg-gray-100'}`}
105
- title={isAddingSemester ? "取消" : "添加新学期"}
106
- >
107
- {isAddingSemester ? <X size={20} /> : <Plus size={20} />}
108
- </button>
109
- </div>
110
-
111
- {isAddingSemester && (
112
- <div className="flex gap-2 mt-2 p-3 bg-gray-50 rounded-lg border border-gray-100 animate-in fade-in slide-in-from-top-2">
113
- <input
114
- type="text"
115
- value={newSemesterVal}
116
- onChange={(e) => setNewSemesterVal(e.target.value)}
117
- placeholder="输入新学期名称 (如: 2025学年 第一学期)"
118
- className="flex-1 px-3 py-2 text-sm border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:outline-none"
119
- autoFocus
120
- />
121
- <button
122
- onClick={handleAddSemester}
123
- disabled={!newSemesterVal.trim()}
124
- className="px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
125
- >
126
- 添加
127
- </button>
128
- </div>
129
- )}
130
- </div>
 
 
 
131
  </div>
132
  </div>
133
  </div>
@@ -207,7 +240,7 @@ export const Settings: React.FC = () => {
207
  className="flex items-center space-x-2 px-6 py-3 bg-blue-600 text-white rounded-xl font-medium hover:bg-blue-700 transition-colors shadow-lg shadow-blue-200"
208
  >
209
  {loading ? <Loader2 className="animate-spin" size={20} /> : <Save size={20} />}
210
- <span>保存配置</span>
211
  </button>
212
  </div>
213
  </div>
 
1
 
2
  import React, { useState, useEffect } from 'react';
3
+ import { Save, Bell, Lock, Database, Loader2, Plus, X, Trash2, Globe, Edit } from 'lucide-react';
4
  import { api } from '../services/api';
5
+ import { SystemConfig } from '../types';
6
 
7
  export const Settings: React.FC = () => {
8
  const [loading, setLoading] = useState(false);
9
  const [fetching, setFetching] = useState(true);
10
 
11
+ const [config, setConfig] = useState<SystemConfig>({
12
  systemName: '智慧校园管理系统',
13
  allowRegister: true,
14
  allowAdminRegister: false,
15
  maintenanceMode: false,
16
  emailNotify: true,
17
+ semester: '2023-2024学年 第一学期',
18
+ semesters: ['2023-2024学年 第一学期', '2023-2024学年 第二学期']
19
  });
20
 
21
+ // Semester Management
 
 
 
 
 
22
  const [newSemesterVal, setNewSemesterVal] = useState('');
23
+ const [editingIndex, setEditingIndex] = useState<number | null>(null);
24
+ const [editVal, setEditVal] = useState('');
25
 
26
  useEffect(() => {
27
  const loadConfig = async () => {
28
  try {
29
  const data = await api.config.get();
30
  if (data && data.systemName) {
 
31
  setConfig(prev => ({ ...prev, ...data }));
32
  }
33
  } catch (e) {
 
43
  setLoading(true);
44
  try {
45
  await api.config.save(config);
46
+ alert('全局系统配置已保存!');
47
  } catch (e) {
48
  alert('保存失败');
49
  } finally {
 
51
  }
52
  };
53
 
54
+ // --- Semester Logic ---
55
+ const addSemester = () => {
56
  if (newSemesterVal.trim()) {
57
  const newVal = newSemesterVal.trim();
58
+ if (!config.semesters?.includes(newVal)) {
59
+ setConfig(prev => ({ ...prev, semesters: [...(prev.semesters || []), newVal] }));
60
  }
 
61
  setNewSemesterVal('');
 
62
  }
63
  };
64
 
65
+ const removeSemester = (idx: number) => {
66
+ const list = [...(config.semesters || [])];
67
+ const valToRemove = list[idx];
68
+ if (config.semester === valToRemove) {
69
+ return alert('无法删除当前正在使用的学期,请先切换学期。');
70
+ }
71
+ list.splice(idx, 1);
72
+ setConfig(prev => ({ ...prev, semesters: list }));
73
+ };
74
+
75
+ const startEdit = (idx: number, val: string) => {
76
+ setEditingIndex(idx);
77
+ setEditVal(val);
78
+ };
79
+
80
+ const saveEdit = (idx: number) => {
81
+ if(!editVal.trim()) return;
82
+ const list = [...(config.semesters || [])];
83
+ // If we are editing the currently active semester, update selection too
84
+ if (config.semester === list[idx]) {
85
+ setConfig(prev => ({ ...prev, semester: editVal.trim() }));
86
+ }
87
+ list[idx] = editVal.trim();
88
+ setConfig(prev => ({ ...prev, semesters: list }));
89
+ setEditingIndex(null);
90
+ };
91
+
92
  if (fetching) return <div className="p-10 text-center text-gray-500">加载配置中...</div>;
93
 
94
  return (
95
  <div className="space-y-6">
96
+ <div className="flex items-center space-x-2 bg-blue-50 text-blue-800 p-4 rounded-xl border border-blue-100">
97
+ <Globe size={20}/>
98
+ <span className="font-bold">全局系统配置中心</span>
99
+ <span className="text-xs bg-blue-200 text-blue-800 px-2 py-0.5 rounded-full ml-2">仅管理员可见</span>
100
+ </div>
101
+
102
  {/* General Settings */}
103
  <div className="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
104
  <div className="p-6 border-b border-gray-100 flex items-center space-x-3">
105
  <div className="p-2 bg-blue-50 rounded-lg text-blue-600">
106
  <Database size={20} />
107
  </div>
108
+ <h3 className="text-lg font-bold text-gray-800">基础信息</h3>
109
  </div>
110
 
111
+ <div className="p-6 space-y-6">
112
+ <div className="space-y-2">
113
+ <label className="text-sm font-medium text-gray-700">系统名称</label>
114
+ <input
115
+ type="text"
116
+ value={config.systemName}
117
+ onChange={(e) => setConfig({...config, systemName: e.target.value})}
118
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:outline-none"
119
+ />
120
+ </div>
121
+
122
+ <div className="space-y-2">
123
+ <label className="text-sm font-medium text-gray-700">当前生效学期</label>
124
+ <select
125
+ value={config.semester}
126
+ onChange={(e) => setConfig({...config, semester: e.target.value})}
127
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:outline-none bg-white"
128
+ >
129
+ {(config.semesters || []).map(s => <option key={s} value={s}>{s}</option>)}
130
+ </select>
131
+ </div>
132
+
133
+ {/* Semester Management List */}
134
+ <div className="bg-gray-50 p-4 rounded-lg border border-gray-200">
135
+ <label className="text-xs font-bold text-gray-500 uppercase mb-2 block">学期列表管理</label>
136
+ <div className="space-y-2 mb-3">
137
+ {(config.semesters || []).map((s, idx) => (
138
+ <div key={idx} className="flex items-center gap-2 bg-white p-2 rounded border border-gray-200">
139
+ {editingIndex === idx ? (
140
+ <>
141
+ <input className="flex-1 border p-1 rounded text-sm" value={editVal} onChange={e=>setEditVal(e.target.value)} autoFocus/>
142
+ <button onClick={()=>saveEdit(idx)} className="text-green-600 hover:bg-green-50 p-1 rounded"><Save size={16}/></button>
143
+ <button onClick={()=>setEditingIndex(null)} className="text-gray-400 hover:bg-gray-50 p-1 rounded"><X size={16}/></button>
144
+ </>
145
+ ) : (
146
+ <>
147
+ <span className="flex-1 text-sm text-gray-700">{s} {config.semester===s && <span className="text-xs text-blue-600 bg-blue-50 px-1 rounded ml-2">当前</span>}</span>
148
+ <button onClick={()=>startEdit(idx, s)} className="text-gray-400 hover:text-blue-600 p-1"><Edit size={14}/></button>
149
+ <button onClick={()=>removeSemester(idx)} className="text-gray-400 hover:text-red-600 p-1"><Trash2 size={14}/></button>
150
+ </>
151
+ )}
152
+ </div>
153
+ ))}
154
+ </div>
155
+ <div className="flex gap-2">
156
+ <input
157
+ className="flex-1 border border-gray-300 rounded px-3 py-1 text-sm"
158
+ placeholder="输入新学期名称"
159
+ value={newSemesterVal}
160
+ onChange={e=>setNewSemesterVal(e.target.value)}
161
+ />
162
+ <button onClick={addSemester} disabled={!newSemesterVal.trim()} className="px-3 py-1 bg-blue-600 text-white rounded text-sm disabled:opacity-50">添加</button>
163
+ </div>
164
  </div>
165
  </div>
166
  </div>
 
240
  className="flex items-center space-x-2 px-6 py-3 bg-blue-600 text-white rounded-xl font-medium hover:bg-blue-700 transition-colors shadow-lg shadow-blue-200"
241
  >
242
  {loading ? <Loader2 className="animate-spin" size={20} /> : <Save size={20} />}
243
+ <span>保存全局配置</span>
244
  </button>
245
  </div>
246
  </div>
pages/StudentList.tsx CHANGED
@@ -1,3 +1,4 @@
 
1
  import React, { useState, useEffect } from 'react';
2
  import { Search, Plus, Upload, Edit, Trash2, X, Loader2, User, FileSpreadsheet } from 'lucide-react';
3
  import { api } from '../services/api';
@@ -21,14 +22,17 @@ export const StudentList: React.FC = () => {
21
  const [importFile, setImportFile] = useState<File | null>(null);
22
  const [importTargetClass, setImportTargetClass] = useState('');
23
 
24
- // Form State
25
  const [formData, setFormData] = useState({
26
  name: '',
27
  studentNo: '',
28
  gender: 'Male',
29
  className: '',
30
  phone: '',
31
- idCard: ''
 
 
 
32
  });
33
 
34
  const loadData = async () => {
@@ -140,7 +144,11 @@ export const StudentList: React.FC = () => {
140
  className: targetClassName,
141
  phone: cleanRow['电话'] || String(cleanRow['联系方式'] || ''),
142
  birthday: '2015-01-01',
143
- status: 'Enrolled'
 
 
 
 
144
  }).then(() => successCount++).catch(err => console.error(err));
145
  });
146
 
@@ -167,8 +175,6 @@ export const StudentList: React.FC = () => {
167
  });
168
 
169
  const uniqueGrades = Array.from(new Set(classList.map(c => c.grade))).sort();
170
-
171
- // FIX: Cascading Filter - Only show classes belonging to selected grade
172
  const filteredClasses = classList.filter(c => selectedGrade === 'All' || c.grade === selectedGrade);
173
  const uniqueClasses = Array.from(new Set(filteredClasses.map(c => c.className))).sort();
174
 
@@ -217,6 +223,7 @@ export const StudentList: React.FC = () => {
217
  <th className="px-6 py-3">基本信息</th>
218
  <th className="px-6 py-3">学号</th>
219
  <th className="px-6 py-3">班级</th>
 
220
  <th className="px-6 py-3 text-right">操作</th>
221
  </tr>
222
  </thead>
@@ -228,19 +235,30 @@ export const StudentList: React.FC = () => {
228
  </td>
229
  <td className="px-6 py-4 flex items-center space-x-3">
230
  <div className={`w-8 h-8 rounded-full flex items-center justify-center text-xs text-white ${s.gender==='Male'?'bg-blue-400':'bg-pink-400'}`}>{s.name[0]}</div>
231
- <span className="font-bold text-gray-800">{s.name}</span>
 
 
 
232
  </td>
233
  <td className="px-6 py-4 text-sm font-mono text-gray-600">{s.studentNo}</td>
234
  <td className="px-6 py-4 text-sm">
235
  <span className="bg-gray-100 px-2 py-1 rounded text-gray-600 text-xs">{s.className}</span>
236
  </td>
 
 
 
 
 
 
 
 
237
  <td className="px-6 py-4 text-right">
238
  <button onClick={() => handleDelete(s._id || String(s.id))} className="text-red-400 hover:text-red-600"><Trash2 size={16}/></button>
239
  </td>
240
  </tr>
241
  )) : (
242
  <tr>
243
- <td colSpan={5} className="text-center py-10 text-gray-400">没有找到匹配的学生</td>
244
  </tr>
245
  )}
246
  </tbody>
@@ -256,20 +274,35 @@ export const StudentList: React.FC = () => {
256
 
257
  {isModalOpen && (
258
  <div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
259
- <div className="bg-white rounded-xl p-6 w-full max-w-md">
260
- <h3 className="font-bold text-lg mb-4">新增学生</h3>
261
  <form onSubmit={handleAddSubmit} className="space-y-4">
262
- <input className="w-full border p-2 rounded" placeholder="姓名" value={formData.name} onChange={e=>setFormData({...formData, name:e.target.value})} required/>
263
- <input className="w-full border p-2 rounded" placeholder="学号" value={formData.studentNo} onChange={e=>setFormData({...formData, studentNo:e.target.value})} required/>
264
- <select className="w-full border p-2 rounded" value={formData.className} onChange={e=>setFormData({...formData, className:e.target.value})} required>
265
- <option value="">选择班级</option>
266
- {classList.map(c=><option key={c._id} value={c.grade+c.className}>{c.grade}{c.className}</option>)}
267
- </select>
268
- <select className="w-full border p-2 rounded" value={formData.gender} onChange={e=>setFormData({...formData, gender:e.target.value})} required>
269
- <option value="Male">男</option>
270
- <option value="Female">女</option>
271
- </select>
272
- <button type="submit" disabled={submitting} className="w-full bg-blue-600 text-white py-2 rounded">{submitting?'提交中':'保存'}</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
273
  <button type="button" onClick={()=>setIsModalOpen(false)} className="w-full border py-2 rounded mt-2">取消</button>
274
  </form>
275
  </div>
@@ -296,7 +329,7 @@ export const StudentList: React.FC = () => {
296
  <div className="relative border-2 border-dashed border-gray-300 rounded-lg p-6 text-center hover:bg-gray-50 transition-colors">
297
  <Upload className="mx-auto h-10 w-10 text-gray-400" />
298
  <p className="mt-2 text-sm text-gray-600">点击上传 .xlsx 文件</p>
299
- <p className="text-xs text-gray-400 mt-1">支持列名:姓名、学号</p>
300
  <input
301
  type="file"
302
  accept=".xlsx, .xls"
@@ -326,4 +359,4 @@ export const StudentList: React.FC = () => {
326
  )}
327
  </div>
328
  );
329
- };
 
1
+
2
  import React, { useState, useEffect } from 'react';
3
  import { Search, Plus, Upload, Edit, Trash2, X, Loader2, User, FileSpreadsheet } from 'lucide-react';
4
  import { api } from '../services/api';
 
22
  const [importFile, setImportFile] = useState<File | null>(null);
23
  const [importTargetClass, setImportTargetClass] = useState('');
24
 
25
+ // Form State (Extended)
26
  const [formData, setFormData] = useState({
27
  name: '',
28
  studentNo: '',
29
  gender: 'Male',
30
  className: '',
31
  phone: '',
32
+ idCard: '',
33
+ parentName: '',
34
+ parentPhone: '',
35
+ address: ''
36
  });
37
 
38
  const loadData = async () => {
 
144
  className: targetClassName,
145
  phone: cleanRow['电话'] || String(cleanRow['联系方式'] || ''),
146
  birthday: '2015-01-01',
147
+ status: 'Enrolled',
148
+ // Extended
149
+ parentName: cleanRow['家长姓名'] || cleanRow['家长'] || '',
150
+ parentPhone: cleanRow['家长电话'] || cleanRow['联系电话'] || '',
151
+ address: cleanRow['家庭住址'] || cleanRow['住址'] || cleanRow['地址'] || ''
152
  }).then(() => successCount++).catch(err => console.error(err));
153
  });
154
 
 
175
  });
176
 
177
  const uniqueGrades = Array.from(new Set(classList.map(c => c.grade))).sort();
 
 
178
  const filteredClasses = classList.filter(c => selectedGrade === 'All' || c.grade === selectedGrade);
179
  const uniqueClasses = Array.from(new Set(filteredClasses.map(c => c.className))).sort();
180
 
 
223
  <th className="px-6 py-3">基本信息</th>
224
  <th className="px-6 py-3">学号</th>
225
  <th className="px-6 py-3">班级</th>
226
+ <th className="px-6 py-3">家庭信息</th>
227
  <th className="px-6 py-3 text-right">操作</th>
228
  </tr>
229
  </thead>
 
235
  </td>
236
  <td className="px-6 py-4 flex items-center space-x-3">
237
  <div className={`w-8 h-8 rounded-full flex items-center justify-center text-xs text-white ${s.gender==='Male'?'bg-blue-400':'bg-pink-400'}`}>{s.name[0]}</div>
238
+ <div>
239
+ <span className="font-bold text-gray-800">{s.name}</span>
240
+ <div className="text-xs text-gray-400">{s.idCard || '身份证未录入'}</div>
241
+ </div>
242
  </td>
243
  <td className="px-6 py-4 text-sm font-mono text-gray-600">{s.studentNo}</td>
244
  <td className="px-6 py-4 text-sm">
245
  <span className="bg-gray-100 px-2 py-1 rounded text-gray-600 text-xs">{s.className}</span>
246
  </td>
247
+ <td className="px-6 py-4 text-xs text-gray-500 max-w-xs truncate">
248
+ {s.parentName ? (
249
+ <div className="flex flex-col">
250
+ <span>{s.parentName} {s.parentPhone}</span>
251
+ <span className="text-gray-400 truncate" title={s.address}>{s.address}</span>
252
+ </div>
253
+ ) : '-'}
254
+ </td>
255
  <td className="px-6 py-4 text-right">
256
  <button onClick={() => handleDelete(s._id || String(s.id))} className="text-red-400 hover:text-red-600"><Trash2 size={16}/></button>
257
  </td>
258
  </tr>
259
  )) : (
260
  <tr>
261
+ <td colSpan={6} className="text-center py-10 text-gray-400">没有找到匹配的学生</td>
262
  </tr>
263
  )}
264
  </tbody>
 
274
 
275
  {isModalOpen && (
276
  <div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
277
+ <div className="bg-white rounded-xl p-6 w-full max-w-lg max-h-[90vh] overflow-y-auto">
278
+ <h3 className="font-bold text-lg mb-4">新增学生档案</h3>
279
  <form onSubmit={handleAddSubmit} className="space-y-4">
280
+ <div className="grid grid-cols-2 gap-4">
281
+ <input className="w-full border p-2 rounded" placeholder="姓名 *" value={formData.name} onChange={e=>setFormData({...formData, name:e.target.value})} required/>
282
+ <input className="w-full border p-2 rounded" placeholder="学号 *" value={formData.studentNo} onChange={e=>setFormData({...formData, studentNo:e.target.value})} required/>
283
+ </div>
284
+ <div className="grid grid-cols-2 gap-4">
285
+ <select className="w-full border p-2 rounded" value={formData.className} onChange={e=>setFormData({...formData, className:e.target.value})} required>
286
+ <option value="">选择班级 *</option>
287
+ {classList.map(c=><option key={c._id} value={c.grade+c.className}>{c.grade}{c.className}</option>)}
288
+ </select>
289
+ <select className="w-full border p-2 rounded" value={formData.gender} onChange={e=>setFormData({...formData, gender:e.target.value})} required>
290
+ <option value="Male">男</option>
291
+ <option value="Female">女</option>
292
+ </select>
293
+ </div>
294
+ <input className="w-full border p-2 rounded" placeholder="身份证号 (选填)" value={formData.idCard} onChange={e=>setFormData({...formData, idCard:e.target.value})}/>
295
+
296
+ <div className="border-t border-gray-100 pt-4 mt-2">
297
+ <p className="text-xs font-bold text-gray-500 uppercase mb-2">家庭信息 (选填)</p>
298
+ <div className="grid grid-cols-2 gap-4 mb-2">
299
+ <input className="w-full border p-2 rounded" placeholder="家长姓名" value={formData.parentName} onChange={e=>setFormData({...formData, parentName:e.target.value})}/>
300
+ <input className="w-full border p-2 rounded" placeholder="家长电话" value={formData.parentPhone} onChange={e=>setFormData({...formData, parentPhone:e.target.value})}/>
301
+ </div>
302
+ <input className="w-full border p-2 rounded" placeholder="家庭住址" value={formData.address} onChange={e=>setFormData({...formData, address:e.target.value})}/>
303
+ </div>
304
+
305
+ <button type="submit" disabled={submitting} className="w-full bg-blue-600 text-white py-2 rounded mt-4">{submitting?'提交中':'保存'}</button>
306
  <button type="button" onClick={()=>setIsModalOpen(false)} className="w-full border py-2 rounded mt-2">取消</button>
307
  </form>
308
  </div>
 
329
  <div className="relative border-2 border-dashed border-gray-300 rounded-lg p-6 text-center hover:bg-gray-50 transition-colors">
330
  <Upload className="mx-auto h-10 w-10 text-gray-400" />
331
  <p className="mt-2 text-sm text-gray-600">点击上传 .xlsx 文件</p>
332
+ <p className="text-xs text-gray-400 mt-1">支持列名:姓名, 学号, 家长姓名, 家长电话, 地址</p>
333
  <input
334
  type="file"
335
  accept=".xlsx, .xls"
 
359
  )}
360
  </div>
361
  );
362
+ };
server.js CHANGED
@@ -14,7 +14,7 @@ const MONGO_URI = 'mongodb+srv://dv890a:db8822723@chatpro.gw3v0v7.mongodb.net/ch
14
  const app = express();
15
 
16
  app.use(cors());
17
- app.use(bodyParser.json());
18
  app.use(express.static(path.join(__dirname, 'dist')));
19
 
20
  // ==========================================
@@ -72,8 +72,8 @@ const UserSchema = new mongoose.Schema({
72
  status: { type: String, enum: ['active', 'pending', 'banned'], default: 'pending' },
73
  avatar: String,
74
  createTime: { type: Date, default: Date.now },
75
- teachingSubject: String, // e.g. "语文"
76
- homeroomClass: String // e.g. "六年级(1)班"
77
  });
78
  const User = mongoose.model('User', UserSchema);
79
 
@@ -86,7 +86,11 @@ const StudentSchema = new mongoose.Schema({
86
  idCard: String,
87
  phone: String,
88
  className: String,
89
- status: { type: String, default: 'Enrolled' }
 
 
 
 
90
  });
91
  // Composite index for uniqueness within school
92
  StudentSchema.index({ schoolId: 1, studentNo: 1 }, { unique: true });
@@ -142,12 +146,13 @@ const ExamSchema = new mongoose.Schema({
142
  const ExamModel = mongoose.model('Exam', ExamSchema);
143
 
144
  const ConfigSchema = new mongoose.Schema({
145
- schoolId: String,
146
- key: { type: String, default: 'main' },
147
  systemName: String,
148
  semester: String,
 
149
  allowRegister: Boolean,
150
- allowAdminRegister: { type: Boolean, default: false }, // New
151
  maintenanceMode: Boolean,
152
  emailNotify: Boolean
153
  });
@@ -159,24 +164,19 @@ const initData = async () => {
159
  try {
160
  console.log('🔄 Checking database initialization...');
161
 
162
- // --- FIX: Drop Legacy Indexes ---
163
- // In multi-tenancy, 'name' on subjects should NOT be unique globally.
164
- // We try to drop the index 'name_1' if it exists.
165
  try {
166
  await mongoose.connection.collection('subjects').dropIndex('name_1');
167
- console.log('⚠️ Dropped legacy unique index on subjects collection to support multi-school subjects.');
168
- } catch (e) {
169
- // Index might not exist or error code 27 (index not found), ignore
170
- }
171
 
172
- // 1. Create Default School (Upsert)
173
  let defaultSchool = await School.findOne({ code: 'EXP01' });
174
  if (!defaultSchool) {
175
  defaultSchool = await School.create({ name: '第一实验小学', code: 'EXP01' });
176
  console.log('✅ Initialized Default School');
177
  }
178
 
179
- // 2. Create Admin (Upsert)
180
  const adminExists = await User.findOne({ username: 'admin' });
181
  if (!adminExists) {
182
  await User.create({
@@ -191,23 +191,19 @@ const initData = async () => {
191
  console.log('✅ Initialized Admin User');
192
  }
193
 
194
- // 3. Create Default Subjects (Upsert)
195
- // Use updateOne with upsert to prevent E11000 errors if run multiple times
196
- const defaultSubjects = [
197
- { schoolId: defaultSchool._id.toString(), name: '语文', code: 'CHI', color: '#ef4444', excellenceThreshold: 90 },
198
- { schoolId: defaultSchool._id.toString(), name: '数学', code: 'MAT', color: '#3b82f6', excellenceThreshold: 90 },
199
- { schoolId: defaultSchool._id.toString(), name: '英语', code: 'ENG', color: '#f59e0b', excellenceThreshold: 90 },
200
- { schoolId: defaultSchool._id.toString(), name: '科学', code: 'SCI', color: '#10b981', excellenceThreshold: 85 }
201
- ];
202
-
203
- for (const sub of defaultSubjects) {
204
- await SubjectModel.updateOne(
205
- { schoolId: sub.schoolId, name: sub.name }, // Check if subject exists for this school
206
- { $set: sub },
207
- { upsert: true }
208
- );
209
  }
210
- console.log('✅ Initialized/Updated Default Subjects');
211
 
212
  } catch (err) {
213
  console.error('❌ Init Data Error', err);
@@ -241,10 +237,9 @@ app.get('/api/public/schools', async (req, res) => {
241
  });
242
 
243
  app.get('/api/public/config', async (req, res) => {
244
- // Try to find any config or default
245
  if (InMemoryDB.isFallback) return res.json({ allowRegister: true, allowAdminRegister: true });
246
- // In a real multi-tenant system, config might depend on domain, but here we pick the first available or default
247
- const config = await ConfigModel.findOne() || { allowRegister: true, allowAdminRegister: false };
248
  res.json(config);
249
  });
250
 
@@ -284,7 +279,6 @@ app.post('/api/auth/login', async (req, res) => {
284
  });
285
 
286
  app.post('/api/auth/register', async (req, res) => {
287
- // Accepted fields
288
  const {
289
  username, password, role, schoolId, trueName, phone, email, avatar,
290
  teachingSubject, homeroomClass
@@ -331,20 +325,20 @@ app.put('/api/schools/:id', async (req, res) => {
331
 
332
  // --- Users ---
333
  app.get('/api/users', async (req, res) => {
334
- // If global=true is passed, ignore school filter (for User Management page)
335
- const { global } = req.query;
336
- const filter = global === 'true' ? {} : getQueryFilter(req);
337
 
 
 
338
  if (InMemoryDB.isFallback) {
339
  if (global === 'true') return res.json(InMemoryDB.users);
340
- return res.json(InMemoryDB.users.filter(u => !filter.schoolId || u.schoolId === filter.schoolId));
341
  }
342
  const users = await User.find(filter).sort({ createTime: -1 });
343
  res.json(users);
344
  });
345
  app.put('/api/users/:id', async (req, res) => {
346
  try {
347
- // Check if activating user
348
  const updateData = req.body;
349
 
350
  if (InMemoryDB.isFallback) {
@@ -355,18 +349,13 @@ app.put('/api/users/:id', async (req, res) => {
355
 
356
  const user = await User.findByIdAndUpdate(req.params.id, updateData, { new: true });
357
 
358
- // Linkage Logic: If approving a teacher, and they are a homeroom teacher, update the Class record.
359
  if (updateData.status === 'active' && user.role === 'TEACHER' && user.homeroomClass) {
360
- // Find Class by name (e.g. "六年级(1)班") and SchoolId
361
  const classes = await ClassModel.find({ schoolId: user.schoolId });
362
  const targetClass = classes.find(c => (c.grade + c.className) === user.homeroomClass);
363
-
364
  if (targetClass) {
365
  await ClassModel.findByIdAndUpdate(targetClass._id, { teacherName: user.trueName || user.username });
366
- console.log(`Linked teacher ${user.trueName} to class ${targetClass.grade}${targetClass.className}`);
367
  }
368
  }
369
-
370
  res.json({ success: true });
371
  } catch (e) { res.status(500).json({ error: e.message }); }
372
  });
@@ -503,8 +492,6 @@ app.delete('/api/scores/:id', async (req, res) => {
503
  // --- Stats & Config ---
504
  app.get('/api/stats', async (req, res) => {
505
  const filter = getQueryFilter(req);
506
- // ... (Stats Logic adjusted for filter) ...
507
- // Keeping simple for now
508
  if (InMemoryDB.isFallback) return res.json({ studentCount: 0, courseCount: 0, avgScore: 0, excellentRate: '0%' });
509
  const studentCount = await Student.countDocuments(filter);
510
  const courseCount = await Course.countDocuments(filter);
@@ -517,20 +504,19 @@ app.get('/api/stats', async (req, res) => {
517
  res.json({ studentCount, courseCount, avgScore, excellentRate });
518
  });
519
 
 
520
  app.get('/api/config', async (req, res) => {
521
- const filter = getQueryFilter(req);
522
  if (InMemoryDB.isFallback) return res.json(InMemoryDB.config);
523
- res.json(await ConfigModel.findOne({ schoolId: filter.schoolId }) || {});
524
  });
525
  app.post('/api/config', async (req, res) => {
526
- const data = injectSchoolId(req, req.body);
527
- if (InMemoryDB.isFallback) { InMemoryDB.config = data; return res.json({}); }
528
- res.json(await ConfigModel.findOneAndUpdate({ schoolId: data.schoolId }, data, { upsert: true, new: true }));
529
  });
530
 
531
  app.post('/api/batch-delete', async (req, res) => {
532
  const { type, ids } = req.body;
533
- // Note: For batch delete, strict school checking should ideally be here too.
534
  if (type === 'student') await Student.deleteMany({ _id: { $in: ids } });
535
  if (type === 'score') await Score.deleteMany({ _id: { $in: ids } });
536
  if (type === 'user') await User.deleteMany({ _id: { $in: ids } });
 
14
  const app = express();
15
 
16
  app.use(cors());
17
+ app.use(bodyParser.json({ limit: '10mb' })); // Increased limit for avatar base64
18
  app.use(express.static(path.join(__dirname, 'dist')));
19
 
20
  // ==========================================
 
72
  status: { type: String, enum: ['active', 'pending', 'banned'], default: 'pending' },
73
  avatar: String,
74
  createTime: { type: Date, default: Date.now },
75
+ teachingSubject: String,
76
+ homeroomClass: String
77
  });
78
  const User = mongoose.model('User', UserSchema);
79
 
 
86
  idCard: String,
87
  phone: String,
88
  className: String,
89
+ status: { type: String, default: 'Enrolled' },
90
+ // Extended Fields
91
+ parentName: String,
92
+ parentPhone: String,
93
+ address: String
94
  });
95
  // Composite index for uniqueness within school
96
  StudentSchema.index({ schoolId: 1, studentNo: 1 }, { unique: true });
 
146
  const ExamModel = mongoose.model('Exam', ExamSchema);
147
 
148
  const ConfigSchema = new mongoose.Schema({
149
+ // Global config, no schoolId
150
+ key: { type: String, default: 'main', unique: true },
151
  systemName: String,
152
  semester: String,
153
+ semesters: [String], // Array of semester strings
154
  allowRegister: Boolean,
155
+ allowAdminRegister: { type: Boolean, default: false },
156
  maintenanceMode: Boolean,
157
  emailNotify: Boolean
158
  });
 
164
  try {
165
  console.log('🔄 Checking database initialization...');
166
 
167
+ // Drop legacy indexes
 
 
168
  try {
169
  await mongoose.connection.collection('subjects').dropIndex('name_1');
170
+ } catch (e) {}
 
 
 
171
 
172
+ // 1. Default School
173
  let defaultSchool = await School.findOne({ code: 'EXP01' });
174
  if (!defaultSchool) {
175
  defaultSchool = await School.create({ name: '第一实验小学', code: 'EXP01' });
176
  console.log('✅ Initialized Default School');
177
  }
178
 
179
+ // 2. Admin
180
  const adminExists = await User.findOne({ username: 'admin' });
181
  if (!adminExists) {
182
  await User.create({
 
191
  console.log('✅ Initialized Admin User');
192
  }
193
 
194
+ // 3. Global Config
195
+ const configExists = await ConfigModel.findOne({ key: 'main' });
196
+ if (!configExists) {
197
+ await ConfigModel.create({
198
+ key: 'main',
199
+ systemName: '智慧校园管理系统',
200
+ semester: '2023-2024学年 第一学期',
201
+ semesters: ['2023-2024学年 第一学期', '2023-2024学年 第二学期'],
202
+ allowRegister: true,
203
+ allowAdminRegister: false
204
+ });
205
+ console.log('✅ Initialized Global Config');
 
 
 
206
  }
 
207
 
208
  } catch (err) {
209
  console.error('❌ Init Data Error', err);
 
237
  });
238
 
239
  app.get('/api/public/config', async (req, res) => {
 
240
  if (InMemoryDB.isFallback) return res.json({ allowRegister: true, allowAdminRegister: true });
241
+ // Global config
242
+ const config = await ConfigModel.findOne({ key: 'main' }) || { allowRegister: true, allowAdminRegister: false };
243
  res.json(config);
244
  });
245
 
 
279
  });
280
 
281
  app.post('/api/auth/register', async (req, res) => {
 
282
  const {
283
  username, password, role, schoolId, trueName, phone, email, avatar,
284
  teachingSubject, homeroomClass
 
325
 
326
  // --- Users ---
327
  app.get('/api/users', async (req, res) => {
328
+ const { global, role } = req.query; // Add role filter support
329
+ let filter = global === 'true' ? {} : getQueryFilter(req);
 
330
 
331
+ if (role) filter.role = role;
332
+
333
  if (InMemoryDB.isFallback) {
334
  if (global === 'true') return res.json(InMemoryDB.users);
335
+ return res.json(InMemoryDB.users.filter(u => (!filter.schoolId || u.schoolId === filter.schoolId) && (!role || u.role === role)));
336
  }
337
  const users = await User.find(filter).sort({ createTime: -1 });
338
  res.json(users);
339
  });
340
  app.put('/api/users/:id', async (req, res) => {
341
  try {
 
342
  const updateData = req.body;
343
 
344
  if (InMemoryDB.isFallback) {
 
349
 
350
  const user = await User.findByIdAndUpdate(req.params.id, updateData, { new: true });
351
 
 
352
  if (updateData.status === 'active' && user.role === 'TEACHER' && user.homeroomClass) {
 
353
  const classes = await ClassModel.find({ schoolId: user.schoolId });
354
  const targetClass = classes.find(c => (c.grade + c.className) === user.homeroomClass);
 
355
  if (targetClass) {
356
  await ClassModel.findByIdAndUpdate(targetClass._id, { teacherName: user.trueName || user.username });
 
357
  }
358
  }
 
359
  res.json({ success: true });
360
  } catch (e) { res.status(500).json({ error: e.message }); }
361
  });
 
492
  // --- Stats & Config ---
493
  app.get('/api/stats', async (req, res) => {
494
  const filter = getQueryFilter(req);
 
 
495
  if (InMemoryDB.isFallback) return res.json({ studentCount: 0, courseCount: 0, avgScore: 0, excellentRate: '0%' });
496
  const studentCount = await Student.countDocuments(filter);
497
  const courseCount = await Course.countDocuments(filter);
 
504
  res.json({ studentCount, courseCount, avgScore, excellentRate });
505
  });
506
 
507
+ // Global Config (No school filter)
508
  app.get('/api/config', async (req, res) => {
 
509
  if (InMemoryDB.isFallback) return res.json(InMemoryDB.config);
510
+ res.json(await ConfigModel.findOne({ key: 'main' }) || {});
511
  });
512
  app.post('/api/config', async (req, res) => {
513
+ // Global config update
514
+ if (InMemoryDB.isFallback) { InMemoryDB.config = req.body; return res.json({}); }
515
+ res.json(await ConfigModel.findOneAndUpdate({ key: 'main' }, req.body, { upsert: true, new: true }));
516
  });
517
 
518
  app.post('/api/batch-delete', async (req, res) => {
519
  const { type, ids } = req.body;
 
520
  if (type === 'student') await Student.deleteMany({ _id: { $in: ids } });
521
  if (type === 'score') await Score.deleteMany({ _id: { $in: ids } });
522
  if (type === 'user') await User.deleteMany({ _id: { $in: ids } });
services/api.ts CHANGED
@@ -93,7 +93,13 @@ export const api = {
93
 
94
  users: {
95
  // If global is true, ask backend to ignore schoolId filter
96
- getAll: (options?: { global?: boolean }) => request(`/users${options?.global ? '?global=true' : ''}`),
 
 
 
 
 
 
97
  update: (id: string, data: Partial<User>) => request(`/users/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
98
  delete: (id: string) => request(`/users/${id}`, { method: 'DELETE' })
99
  },
@@ -141,7 +147,7 @@ export const api = {
141
  },
142
 
143
  config: {
144
- get: () => request('/config'),
145
  getPublic: () => request('/public/config'),
146
  save: (data: SystemConfig) => request('/config', { method: 'POST', body: JSON.stringify(data) })
147
  },
 
93
 
94
  users: {
95
  // If global is true, ask backend to ignore schoolId filter
96
+ // Added role parameter to filter teachers
97
+ getAll: (options?: { global?: boolean; role?: string }) => {
98
+ const params = new URLSearchParams();
99
+ if (options?.global) params.append('global', 'true');
100
+ if (options?.role) params.append('role', options.role);
101
+ return request(`/users?${params.toString()}`);
102
+ },
103
  update: (id: string, data: Partial<User>) => request(`/users/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
104
  delete: (id: string) => request(`/users/${id}`, { method: 'DELETE' })
105
  },
 
147
  },
148
 
149
  config: {
150
+ get: () => request('/config'), // Global
151
  getPublic: () => request('/public/config'),
152
  save: (data: SystemConfig) => request('/config', { method: 'POST', body: JSON.stringify(data) })
153
  },
types.ts CHANGED
@@ -57,11 +57,12 @@ export interface Subject {
57
  }
58
 
59
  export interface SystemConfig {
60
- schoolId?: string;
61
  systemName: string;
62
- semester: string;
 
63
  allowRegister: boolean;
64
- allowAdminRegister: boolean; // New: Admin registration toggle
65
  maintenanceMode: boolean;
66
  emailNotify: boolean;
67
  }
@@ -78,6 +79,10 @@ export interface Student {
78
  phone: string;
79
  className: string;
80
  status: 'Enrolled' | 'Graduated' | 'Suspended';
 
 
 
 
81
  }
82
 
83
  export interface Course {
 
57
  }
58
 
59
  export interface SystemConfig {
60
+ // Global config, no schoolId
61
  systemName: string;
62
+ semester: string; // Current active semester
63
+ semesters?: string[]; // List of available semesters
64
  allowRegister: boolean;
65
+ allowAdminRegister: boolean;
66
  maintenanceMode: boolean;
67
  emailNotify: boolean;
68
  }
 
79
  phone: string;
80
  className: string;
81
  status: 'Enrolled' | 'Graduated' | 'Suspended';
82
+ // Extended info
83
+ parentName?: string;
84
+ parentPhone?: string;
85
+ address?: string;
86
  }
87
 
88
  export interface Course {