dvc890 commited on
Commit
08cac2e
·
verified ·
1 Parent(s): ecc1164

Upload 37 files

Browse files
components/Header.tsx CHANGED
@@ -1,3 +1,4 @@
 
1
  import React, { useState, useEffect } from 'react';
2
  import { Bell, Search, Menu, Building, Info, Check, AlertTriangle } from 'lucide-react';
3
  import { User, School, Notification } from '../types';
@@ -19,36 +20,46 @@ export const Header: React.FC<HeaderProps> = ({ user, title, onMenuClick }) => {
19
  const [showNotif, setShowNotif] = useState(false);
20
  const [hasUnread, setHasUnread] = useState(false);
21
 
22
- useEffect(() => {
23
  if (user.role === 'ADMIN') {
24
- api.schools.getAll().then(data => {
25
- setSchools(data);
26
- if (data.length > 0) {
27
- if (!selectedSchool || !data.find((s: School) => s._id === selectedSchool)) {
28
- const defaultId = data[0]._id!;
29
- setSelectedSchool(defaultId);
30
- localStorage.setItem('admin_view_school_id', defaultId);
31
- if (!localStorage.getItem('admin_view_school_id_init')) {
32
- localStorage.setItem('admin_view_school_id_init', 'true');
33
- window.location.reload();
34
- }
35
- }
36
- }
37
- }).catch(console.error);
 
38
  } else {
39
- api.schools.getPublic().then(data => {
40
- const mySchool = data.find((s: School) => s._id === user.schoolId);
41
- if (mySchool) setCurrentSchoolName(mySchool.name);
42
- });
 
43
  }
 
 
 
 
 
 
 
 
 
 
44
 
45
  // Fetch notifications
46
  const fetchNotifs = async () => {
47
  try {
48
  const list = await api.notifications.getAll(user._id || '', user.role);
49
  setNotifications(list);
50
- // Simple logic: if recent notifications exist, show red dot.
51
- // In real app, track 'read' IDs in local storage or DB.
52
  const lastReadTime = localStorage.getItem('last_read_notif_time');
53
  if (list.length > 0) {
54
  if (!lastReadTime || new Date(list[0].createTime) > new Date(lastReadTime)) {
@@ -58,6 +69,10 @@ export const Header: React.FC<HeaderProps> = ({ user, title, onMenuClick }) => {
58
  } catch (e) { console.error(e); }
59
  };
60
  fetchNotifs();
 
 
 
 
61
  }, [user]);
62
 
63
  const handleSchoolChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
 
1
+
2
  import React, { useState, useEffect } from 'react';
3
  import { Bell, Search, Menu, Building, Info, Check, AlertTriangle } from 'lucide-react';
4
  import { User, School, Notification } from '../types';
 
20
  const [showNotif, setShowNotif] = useState(false);
21
  const [hasUnread, setHasUnread] = useState(false);
22
 
23
+ const fetchSchools = async () => {
24
  if (user.role === 'ADMIN') {
25
+ try {
26
+ const data = await api.schools.getAll();
27
+ setSchools(data);
28
+ if (data.length > 0) {
29
+ if (!selectedSchool || !data.find((s: School) => s._id === selectedSchool)) {
30
+ const defaultId = data[0]._id!;
31
+ setSelectedSchool(defaultId);
32
+ localStorage.setItem('admin_view_school_id', defaultId);
33
+ if (!localStorage.getItem('admin_view_school_id_init')) {
34
+ localStorage.setItem('admin_view_school_id_init', 'true');
35
+ window.location.reload();
36
+ }
37
+ }
38
+ }
39
+ } catch (e) { console.error(e); }
40
  } else {
41
+ try {
42
+ const data = await api.schools.getPublic();
43
+ const mySchool = data.find((s: School) => s._id === user.schoolId);
44
+ if (mySchool) setCurrentSchoolName(mySchool.name);
45
+ } catch(e) { console.error(e); }
46
  }
47
+ };
48
+
49
+ useEffect(() => {
50
+ fetchSchools();
51
+
52
+ // Listen for custom event to refresh schools
53
+ const handleSchoolUpdate = () => {
54
+ fetchSchools();
55
+ };
56
+ window.addEventListener('school-updated', handleSchoolUpdate);
57
 
58
  // Fetch notifications
59
  const fetchNotifs = async () => {
60
  try {
61
  const list = await api.notifications.getAll(user._id || '', user.role);
62
  setNotifications(list);
 
 
63
  const lastReadTime = localStorage.getItem('last_read_notif_time');
64
  if (list.length > 0) {
65
  if (!lastReadTime || new Date(list[0].createTime) > new Date(lastReadTime)) {
 
69
  } catch (e) { console.error(e); }
70
  };
71
  fetchNotifs();
72
+
73
+ return () => {
74
+ window.removeEventListener('school-updated', handleSchoolUpdate);
75
+ };
76
  }, [user]);
77
 
78
  const handleSchoolChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
pages/ClassList.tsx CHANGED
@@ -31,10 +31,10 @@ export const ClassList: React.FC = () => {
31
  try {
32
  const [clsData, userData] = await Promise.all([
33
  api.classes.getAll(),
34
- api.users.getAll({ role: 'TEACHER', global: true }) // Get all users to check apps
35
  ]);
36
  setClasses(clsData);
37
- setTeachers(userData);
38
 
39
  // Filter users with pending class applications
40
  const apps = userData.filter((u: User) => u.classApplication && u.classApplication.status === 'PENDING');
@@ -264,4 +264,4 @@ export const ClassList: React.FC = () => {
264
  )}
265
  </div>
266
  );
267
- };
 
31
  try {
32
  const [clsData, userData] = await Promise.all([
33
  api.classes.getAll(),
34
+ api.users.getAll({ role: 'TEACHER', global: true }) // Get filtered users
35
  ]);
36
  setClasses(clsData);
37
+ setTeachers(userData); // Now only contains teachers
38
 
39
  // Filter users with pending class applications
40
  const apps = userData.filter((u: User) => u.classApplication && u.classApplication.status === 'PENDING');
 
264
  )}
265
  </div>
266
  );
267
+ };
pages/Login.tsx CHANGED
@@ -1,6 +1,6 @@
1
 
2
  import React, { useState, useEffect } from 'react';
3
- import { GraduationCap, Lock, User as UserIcon, AlertCircle, ArrowRight, Loader2, ArrowLeft, School as SchoolIcon, Mail, Phone, Smile, BookOpen, Users } from 'lucide-react';
4
  import { User, UserRole, School, ClassInfo, Subject } from '../types';
5
  import { api } from '../services/api';
6
 
@@ -28,7 +28,7 @@ export const Login: React.FC<LoginProps> = ({ onLogin }) => {
28
  schoolId: '', trueName: '', phone: '', email: '', avatar: '',
29
  teachingSubject: '', homeroomClass: '',
30
  // Student specific
31
- studentNo: '', parentName: '', parentPhone: '', address: '', gender: 'Male'
32
  });
33
 
34
  const [schools, setSchools] = useState<School[]>([]);
@@ -40,6 +40,7 @@ export const Login: React.FC<LoginProps> = ({ onLogin }) => {
40
  const [error, setError] = useState('');
41
  const [loading, setLoading] = useState(false);
42
  const [successMsg, setSuccessMsg] = useState('');
 
43
 
44
  useEffect(() => {
45
  if (view === 'register') {
@@ -80,20 +81,23 @@ export const Login: React.FC<LoginProps> = ({ onLogin }) => {
80
  setLoading(true);
81
  try {
82
  // Default avatar if none selected
83
- const finalAvatar = regForm.avatar || `https://api.dicebear.com/7.x/avataaars/svg?seed=${regForm.username}`;
84
- // Map username to studentNo if student (simplification)
85
  const payload = { ...regForm, avatar: finalAvatar };
86
- if (payload.role === UserRole.STUDENT) {
87
- payload.username = payload.studentNo; // Use Student No as username
88
- payload.trueName = payload.trueName;
89
- }
90
 
91
- await api.auth.register(payload);
92
- setSuccessMsg('注册成功!请等待审核。');
93
- setView('login');
94
- setStep(1);
 
 
 
 
 
 
 
95
  } catch (err: any) {
96
- setError(err.message || '注册失败: 用户名可能已存在');
97
  } finally { setLoading(false); }
98
  };
99
 
@@ -145,38 +149,26 @@ export const Login: React.FC<LoginProps> = ({ onLogin }) => {
145
  {/* Dynamic Fields based on Role */}
146
  {regForm.role === UserRole.STUDENT ? (
147
  <>
 
 
 
148
  <div className="grid grid-cols-2 gap-2">
149
  <input
150
  className="w-full p-2 border rounded-lg"
151
- placeholder="学号 (作为登录账号)"
152
- value={regForm.studentNo} onChange={e => setRegForm({...regForm, studentNo: e.target.value})}
153
- />
154
- <input
155
- className="w-full p-2 border rounded-lg"
156
- placeholder="真实姓名"
157
  value={regForm.trueName} onChange={e => setRegForm({...regForm, trueName: e.target.value})}
158
  />
159
- </div>
160
- <div className="grid grid-cols-2 gap-2">
161
  <input
162
  className="w-full p-2 border rounded-lg"
163
- type="password" placeholder="密码"
164
  value={regForm.password} onChange={e => setRegForm({...regForm, password: e.target.value})}
165
  />
166
- <select
167
- className="w-full p-2 border rounded-lg bg-white"
168
- value={regForm.gender}
169
- onChange={e => setRegForm({...regForm, gender: e.target.value})}
170
- >
171
- <option value="Male">男</option>
172
- <option value="Female">女</option>
173
- </select>
174
  </div>
175
 
176
  <div className="bg-blue-50 p-3 rounded-lg space-y-2 border border-blue-100">
177
- <p className="text-xs font-bold text-blue-600 uppercase">班级与家庭 (必填)</p>
178
  <select
179
- className="w-full p-1.5 border rounded text-sm"
180
  value={regForm.homeroomClass} // Reusing field for class selection
181
  onChange={e => setRegForm({...regForm, homeroomClass: e.target.value})}
182
  >
@@ -184,10 +176,24 @@ export const Login: React.FC<LoginProps> = ({ onLogin }) => {
184
  {schoolClasses.map(c => <option key={c._id} value={c.grade+c.className}>{c.grade}{c.className}</option>)}
185
  </select>
186
  <div className="grid grid-cols-2 gap-2">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
187
  <input className="border p-1.5 rounded text-sm w-full" placeholder="家长姓名" value={regForm.parentName} onChange={e => setRegForm({...regForm, parentName: e.target.value})}/>
188
  <input className="border p-1.5 rounded text-sm w-full" placeholder="家长电话" value={regForm.parentPhone} onChange={e => setRegForm({...regForm, parentPhone: e.target.value})}/>
189
  </div>
190
- <input className="border p-1.5 rounded text-sm w-full" placeholder="家庭住址" value={regForm.address} onChange={e => setRegForm({...regForm, address: e.target.value})}/>
191
  </div>
192
  </>
193
  ) : (
@@ -251,7 +257,7 @@ export const Login: React.FC<LoginProps> = ({ onLogin }) => {
251
  <button onClick={() => setStep(1)} className="flex-1 border py-2 rounded-lg">上一步</button>
252
  <button
253
  onClick={() => setStep(3)}
254
- disabled={regForm.role === UserRole.STUDENT ? (!regForm.studentNo || !regForm.trueName) : (!regForm.username || !regForm.password)}
255
  className="flex-1 bg-blue-600 text-white py-2 rounded-lg disabled:opacity-50"
256
  >
257
  下一步
@@ -287,6 +293,25 @@ export const Login: React.FC<LoginProps> = ({ onLogin }) => {
287
  </div>
288
  );
289
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
290
  return (
291
  <div className="min-h-screen bg-gray-100 flex items-center justify-center p-4">
292
  <div className="max-w-md w-full bg-white rounded-2xl shadow-xl overflow-hidden transition-all duration-300">
@@ -302,45 +327,49 @@ export const Login: React.FC<LoginProps> = ({ onLogin }) => {
302
  </div>
303
 
304
  <div className="p-8">
305
- <div className="mb-6 flex justify-center">
306
- <div className="bg-gray-100 p-1 rounded-lg flex text-sm font-medium">
307
- <button onClick={() => { setView('login'); setError(''); }} className={`px-6 py-2 rounded-md transition-all ${view==='login' ? 'bg-white text-blue-600 shadow-sm' : 'text-gray-500'}`}>登录</button>
308
- <button onClick={() => { setView('register'); setError(''); }} className={`px-6 py-2 rounded-md transition-all ${view==='register' ? 'bg-white text-blue-600 shadow-sm' : 'text-gray-500'}`}>注册账号</button>
309
- </div>
310
- </div>
 
 
311
 
312
- {error && <div className="bg-red-50 text-red-600 p-3 rounded-lg text-sm mb-4 flex items-center"><AlertCircle size={16} className="mr-2"/>{error}</div>}
313
- {successMsg && <div className="bg-green-50 text-green-600 p-3 rounded-lg text-sm mb-4">{successMsg}</div>}
314
 
315
- {view === 'login' ? (
316
- <form onSubmit={handleLogin} className="space-y-5">
317
- <div className="space-y-2">
318
- <label className="text-sm font-medium text-gray-700">用户名 / 学号</label>
319
- <div className="relative">
320
- <UserIcon className="absolute left-3 top-2.5 h-5 w-5 text-gray-400" />
321
- <input type="text" required value={loginForm.username} onChange={e => setLoginForm({...loginForm, username:e.target.value})} className="block w-full pl-10 pr-3 py-2.5 border border-gray-300 rounded-lg focus:ring-blue-500" placeholder="请输入用户名" />
322
- </div>
323
- </div>
324
- <div className="space-y-2">
325
- <label className="text-sm font-medium text-gray-700">密码</label>
326
- <div className="relative">
327
- <Lock className="absolute left-3 top-2.5 h-5 w-5 text-gray-400" />
328
- <input type="password" required value={loginForm.password} onChange={e => setLoginForm({...loginForm, password:e.target.value})} className="block w-full pl-10 pr-3 py-2.5 border border-gray-300 rounded-lg focus:ring-blue-500" placeholder="请输入密码" />
329
- </div>
330
- </div>
331
- <button type="submit" disabled={loading} className="w-full py-3 bg-blue-600 text-white rounded-lg font-bold hover:bg-blue-700 transition-colors">
332
- {loading ? <Loader2 className="animate-spin inline"/> : '登 录'}
333
- </button>
334
- </form>
335
- ) : (
336
- <div className="min-h-[300px]">
337
- {step === 1 && renderStep1()}
338
- {step === 2 && renderStep2()}
339
- {step === 3 && renderStep3()}
340
- </div>
 
 
341
  )}
342
  </div>
343
  </div>
344
  </div>
345
  );
346
- };
 
1
 
2
  import React, { useState, useEffect } from 'react';
3
+ import { GraduationCap, Lock, User as UserIcon, AlertCircle, ArrowRight, Loader2, ArrowLeft, School as SchoolIcon, Mail, Phone, Smile, BookOpen, Users, Clipboard } from 'lucide-react';
4
  import { User, UserRole, School, ClassInfo, Subject } from '../types';
5
  import { api } from '../services/api';
6
 
 
28
  schoolId: '', trueName: '', phone: '', email: '', avatar: '',
29
  teachingSubject: '', homeroomClass: '',
30
  // Student specific
31
+ seatNo: '', parentName: '', parentPhone: '', address: '', gender: 'Male'
32
  });
33
 
34
  const [schools, setSchools] = useState<School[]>([]);
 
40
  const [error, setError] = useState('');
41
  const [loading, setLoading] = useState(false);
42
  const [successMsg, setSuccessMsg] = useState('');
43
+ const [generatedId, setGeneratedId] = useState('');
44
 
45
  useEffect(() => {
46
  if (view === 'register') {
 
81
  setLoading(true);
82
  try {
83
  // Default avatar if none selected
84
+ const finalAvatar = regForm.avatar || `https://api.dicebear.com/7.x/avataaars/svg?seed=${regForm.username || regForm.trueName}`;
85
+
86
  const payload = { ...regForm, avatar: finalAvatar };
 
 
 
 
87
 
88
+ const res = await api.auth.register(payload);
89
+
90
+ if (regForm.role === UserRole.STUDENT && res.username) {
91
+ setGeneratedId(res.username);
92
+ setSuccessMsg('注册成功!');
93
+ // Don't switch view immediately, let them see the ID
94
+ } else {
95
+ setSuccessMsg('注册成功!请等待审核。');
96
+ setView('login');
97
+ setStep(1);
98
+ }
99
  } catch (err: any) {
100
+ setError(err.message || '注册失败: 可能用户名冲突');
101
  } finally { setLoading(false); }
102
  };
103
 
 
149
  {/* Dynamic Fields based on Role */}
150
  {regForm.role === UserRole.STUDENT ? (
151
  <>
152
+ <div className="bg-yellow-50 p-2 rounded text-xs text-yellow-800 border border-yellow-200 mb-2">
153
+ 注意:注册成功后系统将自动生成您的 <b>登录账号(系统学号)</b>,请务必牢记。
154
+ </div>
155
  <div className="grid grid-cols-2 gap-2">
156
  <input
157
  className="w-full p-2 border rounded-lg"
158
+ placeholder="真实姓名 (必填)"
 
 
 
 
 
159
  value={regForm.trueName} onChange={e => setRegForm({...regForm, trueName: e.target.value})}
160
  />
 
 
161
  <input
162
  className="w-full p-2 border rounded-lg"
163
+ type="password" placeholder="设置密码 (必填)"
164
  value={regForm.password} onChange={e => setRegForm({...regForm, password: e.target.value})}
165
  />
 
 
 
 
 
 
 
 
166
  </div>
167
 
168
  <div className="bg-blue-50 p-3 rounded-lg space-y-2 border border-blue-100">
169
+ <p className="text-xs font-bold text-blue-600 uppercase">班级信息 (必填)</p>
170
  <select
171
+ className="w-full p-1.5 border rounded text-sm bg-white"
172
  value={regForm.homeroomClass} // Reusing field for class selection
173
  onChange={e => setRegForm({...regForm, homeroomClass: e.target.value})}
174
  >
 
176
  {schoolClasses.map(c => <option key={c._id} value={c.grade+c.className}>{c.grade}{c.className}</option>)}
177
  </select>
178
  <div className="grid grid-cols-2 gap-2">
179
+ <select
180
+ className="w-full p-1.5 border rounded text-sm bg-white"
181
+ value={regForm.gender}
182
+ onChange={e => setRegForm({...regForm, gender: e.target.value})}
183
+ >
184
+ <option value="Male">男</option>
185
+ <option value="Female">女</option>
186
+ </select>
187
+ <input className="border p-1.5 rounded text-sm w-full" placeholder="座号 (选填, 如: 05)" value={regForm.seatNo} onChange={e => setRegForm({...regForm, seatNo: e.target.value})}/>
188
+ </div>
189
+ </div>
190
+
191
+ <div className="border-t pt-2">
192
+ <p className="text-xs font-bold text-gray-400 uppercase mb-1">家庭信息 (选填)</p>
193
+ <div className="grid grid-cols-2 gap-2 mb-2">
194
  <input className="border p-1.5 rounded text-sm w-full" placeholder="家长姓名" value={regForm.parentName} onChange={e => setRegForm({...regForm, parentName: e.target.value})}/>
195
  <input className="border p-1.5 rounded text-sm w-full" placeholder="家长电话" value={regForm.parentPhone} onChange={e => setRegForm({...regForm, parentPhone: e.target.value})}/>
196
  </div>
 
197
  </div>
198
  </>
199
  ) : (
 
257
  <button onClick={() => setStep(1)} className="flex-1 border py-2 rounded-lg">上一步</button>
258
  <button
259
  onClick={() => setStep(3)}
260
+ disabled={regForm.role === UserRole.STUDENT ? (!regForm.trueName || !regForm.homeroomClass) : (!regForm.username || !regForm.password)}
261
  className="flex-1 bg-blue-600 text-white py-2 rounded-lg disabled:opacity-50"
262
  >
263
  下一步
 
293
  </div>
294
  );
295
 
296
+ const renderSuccess = () => (
297
+ <div className="text-center animate-in zoom-in space-y-4">
298
+ <div className="w-16 h-16 bg-green-100 text-green-600 rounded-full flex items-center justify-center mx-auto mb-4">
299
+ <Smile size={32}/>
300
+ </div>
301
+ <h3 className="text-2xl font-bold text-gray-800">注册成功!</h3>
302
+ <div className="bg-blue-50 border border-blue-200 rounded-xl p-6">
303
+ <p className="text-sm text-gray-600 mb-2">您的登录账号 (系统学号)</p>
304
+ <div className="text-3xl font-black text-blue-700 tracking-wider font-mono select-all">
305
+ {generatedId}
306
+ </div>
307
+ <p className="text-xs text-red-500 mt-2 font-bold">请务必截图或记下此号码,用于以后登录!</p>
308
+ </div>
309
+ <button onClick={() => window.location.reload()} className="w-full bg-blue-600 text-white py-3 rounded-lg font-bold hover:bg-blue-700">
310
+ 去登录
311
+ </button>
312
+ </div>
313
+ );
314
+
315
  return (
316
  <div className="min-h-screen bg-gray-100 flex items-center justify-center p-4">
317
  <div className="max-w-md w-full bg-white rounded-2xl shadow-xl overflow-hidden transition-all duration-300">
 
327
  </div>
328
 
329
  <div className="p-8">
330
+ {generatedId ? renderSuccess() : (
331
+ <>
332
+ <div className="mb-6 flex justify-center">
333
+ <div className="bg-gray-100 p-1 rounded-lg flex text-sm font-medium">
334
+ <button onClick={() => { setView('login'); setError(''); }} className={`px-6 py-2 rounded-md transition-all ${view==='login' ? 'bg-white text-blue-600 shadow-sm' : 'text-gray-500'}`}>登录</button>
335
+ <button onClick={() => { setView('register'); setError(''); }} className={`px-6 py-2 rounded-md transition-all ${view==='register' ? 'bg-white text-blue-600 shadow-sm' : 'text-gray-500'}`}>注册账号</button>
336
+ </div>
337
+ </div>
338
 
339
+ {error && <div className="bg-red-50 text-red-600 p-3 rounded-lg text-sm mb-4 flex items-center"><AlertCircle size={16} className="mr-2"/>{error}</div>}
340
+ {successMsg && !generatedId && <div className="bg-green-50 text-green-600 p-3 rounded-lg text-sm mb-4">{successMsg}</div>}
341
 
342
+ {view === 'login' ? (
343
+ <form onSubmit={handleLogin} className="space-y-5">
344
+ <div className="space-y-2">
345
+ <label className="text-sm font-medium text-gray-700">用户名 / 学号</label>
346
+ <div className="relative">
347
+ <UserIcon className="absolute left-3 top-2.5 h-5 w-5 text-gray-400" />
348
+ <input type="text" required value={loginForm.username} onChange={e => setLoginForm({...loginForm, username:e.target.value})} className="block w-full pl-10 pr-3 py-2.5 border border-gray-300 rounded-lg focus:ring-blue-500" placeholder="请输入用户名" />
349
+ </div>
350
+ </div>
351
+ <div className="space-y-2">
352
+ <label className="text-sm font-medium text-gray-700">密码</label>
353
+ <div className="relative">
354
+ <Lock className="absolute left-3 top-2.5 h-5 w-5 text-gray-400" />
355
+ <input type="password" required value={loginForm.password} onChange={e => setLoginForm({...loginForm, password:e.target.value})} className="block w-full pl-10 pr-3 py-2.5 border border-gray-300 rounded-lg focus:ring-blue-500" placeholder="请输入密码" />
356
+ </div>
357
+ </div>
358
+ <button type="submit" disabled={loading} className="w-full py-3 bg-blue-600 text-white rounded-lg font-bold hover:bg-blue-700 transition-colors">
359
+ {loading ? <Loader2 className="animate-spin inline"/> : '登 录'}
360
+ </button>
361
+ </form>
362
+ ) : (
363
+ <div className="min-h-[300px]">
364
+ {step === 1 && renderStep1()}
365
+ {step === 2 && renderStep2()}
366
+ {step === 3 && renderStep3()}
367
+ </div>
368
+ )}
369
+ </>
370
  )}
371
  </div>
372
  </div>
373
  </div>
374
  );
375
+ };
pages/SchoolList.tsx CHANGED
@@ -31,6 +31,8 @@ export const SchoolList: React.FC = () => {
31
  setIsAdding(false);
32
  setForm({ name: '', code: '' });
33
  loadSchools();
 
 
34
  } catch (e) { alert('保存失败,代码可能重复'); }
35
  };
36
 
@@ -97,4 +99,4 @@ export const SchoolList: React.FC = () => {
97
  </div>
98
  </div>
99
  );
100
- };
 
31
  setIsAdding(false);
32
  setForm({ name: '', code: '' });
33
  loadSchools();
34
+ // Notify Header to update
35
+ window.dispatchEvent(new Event('school-updated'));
36
  } catch (e) { alert('保存失败,代码可能重复'); }
37
  };
38
 
 
99
  </div>
100
  </div>
101
  );
102
+ };
pages/StudentList.tsx CHANGED
@@ -32,7 +32,8 @@ export const StudentList: React.FC = () => {
32
 
33
  const initialForm = {
34
  name: '',
35
- studentNo: '',
 
36
  gender: 'Male',
37
  className: '',
38
  phone: '',
@@ -52,8 +53,14 @@ export const StudentList: React.FC = () => {
52
  api.students.getAll(),
53
  api.classes.getAll()
54
  ]);
55
- // Sort students by studentNo by default
56
- setStudents(studentData.sort((a: Student, b: Student) => a.studentNo.localeCompare(b.studentNo, undefined, {numeric: true})));
 
 
 
 
 
 
57
  setClassList(classData);
58
  } catch (error) {
59
  console.error(error);
@@ -117,6 +124,7 @@ export const StudentList: React.FC = () => {
117
  setFormData({
118
  name: student.name,
119
  studentNo: student.studentNo,
 
120
  gender: (student.gender === 'Female' ? 'Female' : 'Male'),
121
  className: student.className,
122
  phone: student.phone || '',
@@ -225,8 +233,8 @@ export const StudentList: React.FC = () => {
225
  };
226
 
227
  const name = getVal(['姓名', 'Name'], 1);
228
- const studentNo = getVal(['学号', 'No', 'ID'], 0);
229
- if (!name || !studentNo) continue;
230
 
231
  const genderVal = getVal(['性别', 'Gender'], 2);
232
  const gender = (genderVal === '女' || genderVal === 'Female') ? 'Female' : 'Male';
@@ -241,7 +249,8 @@ export const StudentList: React.FC = () => {
241
  promises.push(
242
  api.students.add({
243
  name: String(name).trim(),
244
- studentNo: String(studentNo).trim(),
 
245
  gender,
246
  className: targetClassName,
247
  phone: String(getVal(['电话', '联系方式', 'Mobile'], 3)).trim(),
@@ -324,7 +333,7 @@ export const StudentList: React.FC = () => {
324
  />
325
  )}
326
  </th>
327
- <th className="px-6 py-3">学号</th>
328
  <th className="px-6 py-3">姓名/性别</th>
329
  <th className="px-6 py-3">班级</th>
330
  <th className="px-6 py-3">家长/住址</th>
@@ -339,7 +348,12 @@ export const StudentList: React.FC = () => {
339
  <td className="px-6 py-4">
340
  {canEdit && <input type="checkbox" checked={selectedIds.has(s._id || String(s.id))} onChange={() => toggleSelect(s._id || String(s.id))} />}
341
  </td>
342
- <td className="px-6 py-4 text-sm font-mono text-gray-600">{s.studentNo}</td>
 
 
 
 
 
343
  <td className="px-6 py-4 flex items-center space-x-3">
344
  <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>
345
  <div>
@@ -390,9 +404,20 @@ export const StudentList: React.FC = () => {
390
  <h3 className="font-bold text-lg mb-4">{editStudentId ? '编辑学生档案' : '新增学生档案'}</h3>
391
  <form onSubmit={handleSubmit} className="space-y-4">
392
  <div className="grid grid-cols-2 gap-4">
393
- <input className="w-full border p-2 rounded" placeholder="姓名 *" value={formData.name} onChange={e=>setFormData({...formData, name:e.target.value})} required/>
394
- <input className="w-full border p-2 rounded" placeholder="学号 *" value={formData.studentNo} onChange={e=>setFormData({...formData, studentNo:e.target.value})} required/>
 
 
 
 
 
 
395
  </div>
 
 
 
 
 
396
  <div className="grid grid-cols-2 gap-4">
397
  <select className="w-full border p-2 rounded" value={formData.className} onChange={e=>setFormData({...formData, className:e.target.value})} required>
398
  <option value="">选择班级 *</option>
@@ -460,8 +485,8 @@ export const StudentList: React.FC = () => {
460
  <div className="relative border-2 border-dashed border-gray-300 rounded-lg p-6 text-center hover:bg-gray-50 transition-colors">
461
  <Upload className="mx-auto h-10 w-10 text-gray-400" />
462
  <p className="mt-2 text-sm text-gray-600">点击上传 .xlsx 文件</p>
463
- <p className="text-xs text-gray-400 mt-1">支持列名:学号, 姓名, 家长姓名, 家长电话, 地址</p>
464
- <p className="text-xs text-gray-400">或:第一列学号,第二列姓名</p>
465
  <input type="file" accept=".xlsx, .xls" className="opacity-0 absolute inset-0 cursor-pointer h-full w-full z-10"
466
  // @ts-ignore
467
  onChange={e => setImportFile(e.target.files?.[0])}
@@ -477,4 +502,4 @@ export const StudentList: React.FC = () => {
477
  )}
478
  </div>
479
  );
480
- };
 
32
 
33
  const initialForm = {
34
  name: '',
35
+ studentNo: '', // System ID
36
+ seatNo: '', // Seat No
37
  gender: 'Male',
38
  className: '',
39
  phone: '',
 
53
  api.students.getAll(),
54
  api.classes.getAll()
55
  ]);
56
+ // Sort students: Primary by SeatNo (if numeric), Secondary by System ID
57
+ const sorted = studentData.sort((a: Student, b: Student) => {
58
+ const seatA = parseInt(a.seatNo || '9999');
59
+ const seatB = parseInt(b.seatNo || '9999');
60
+ if (seatA !== seatB) return seatA - seatB;
61
+ return a.studentNo.localeCompare(b.studentNo, undefined, {numeric: true});
62
+ });
63
+ setStudents(sorted);
64
  setClassList(classData);
65
  } catch (error) {
66
  console.error(error);
 
124
  setFormData({
125
  name: student.name,
126
  studentNo: student.studentNo,
127
+ seatNo: student.seatNo || '',
128
  gender: (student.gender === 'Female' ? 'Female' : 'Male'),
129
  className: student.className,
130
  phone: student.phone || '',
 
233
  };
234
 
235
  const name = getVal(['姓名', 'Name'], 1);
236
+ const seatNo = getVal(['学号', '座号', 'No', 'ID'], 0); // Re-mapped: treat import "No" as Seat No
237
+ if (!name) continue;
238
 
239
  const genderVal = getVal(['性别', 'Gender'], 2);
240
  const gender = (genderVal === '女' || genderVal === 'Female') ? 'Female' : 'Male';
 
249
  promises.push(
250
  api.students.add({
251
  name: String(name).trim(),
252
+ seatNo: seatNo ? String(seatNo).trim() : '',
253
+ studentNo: '', // Auto Generate
254
  gender,
255
  className: targetClassName,
256
  phone: String(getVal(['电话', '联系方式', 'Mobile'], 3)).trim(),
 
333
  />
334
  )}
335
  </th>
336
+ <th className="px-6 py-3">座号 / 系统ID</th>
337
  <th className="px-6 py-3">姓名/性别</th>
338
  <th className="px-6 py-3">班级</th>
339
  <th className="px-6 py-3">家长/住址</th>
 
348
  <td className="px-6 py-4">
349
  {canEdit && <input type="checkbox" checked={selectedIds.has(s._id || String(s.id))} onChange={() => toggleSelect(s._id || String(s.id))} />}
350
  </td>
351
+ <td className="px-6 py-4">
352
+ <div className="flex flex-col">
353
+ <span className="font-bold text-gray-800 text-lg">{s.seatNo || '-'}</span>
354
+ <span className="text-xs text-gray-400 font-mono" title="系统登录账号">{s.studentNo}</span>
355
+ </div>
356
+ </td>
357
  <td className="px-6 py-4 flex items-center space-x-3">
358
  <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>
359
  <div>
 
404
  <h3 className="font-bold text-lg mb-4">{editStudentId ? '编辑学生档案' : '新增学生档案'}</h3>
405
  <form onSubmit={handleSubmit} className="space-y-4">
406
  <div className="grid grid-cols-2 gap-4">
407
+ <div>
408
+ <label className="text-xs font-bold text-gray-500 block mb-1">姓名 *</label>
409
+ <input className="w-full border p-2 rounded" value={formData.name} onChange={e=>setFormData({...formData, name:e.target.value})} required/>
410
+ </div>
411
+ <div>
412
+ <label className="text-xs font-bold text-gray-500 block mb-1">班级座号 (选填)</label>
413
+ <input className="w-full border p-2 rounded" value={formData.seatNo} onChange={e=>setFormData({...formData, seatNo:e.target.value})} placeholder="如: 05"/>
414
+ </div>
415
  </div>
416
+ {editStudentId && (
417
+ <div className="bg-gray-50 p-2 rounded text-xs text-gray-600 mb-2">
418
+ <span className="font-bold">系统ID (登录账号): </span> {formData.studentNo}
419
+ </div>
420
+ )}
421
  <div className="grid grid-cols-2 gap-4">
422
  <select className="w-full border p-2 rounded" value={formData.className} onChange={e=>setFormData({...formData, className:e.target.value})} required>
423
  <option value="">选择班级 *</option>
 
485
  <div className="relative border-2 border-dashed border-gray-300 rounded-lg p-6 text-center hover:bg-gray-50 transition-colors">
486
  <Upload className="mx-auto h-10 w-10 text-gray-400" />
487
  <p className="mt-2 text-sm text-gray-600">点击上传 .xlsx 文件</p>
488
+ <p className="text-xs text-gray-400 mt-1">支持列名:座号(学号), 姓名, 家长姓名, 家长电话, 地址</p>
489
+ <p className="text-xs text-gray-400">注意: 导入的"学号"列将作为座号</p>
490
  <input type="file" accept=".xlsx, .xls" className="opacity-0 absolute inset-0 cursor-pointer h-full w-full z-10"
491
  // @ts-ignore
492
  onChange={e => setImportFile(e.target.files?.[0])}
 
502
  )}
503
  </div>
504
  );
505
+ };
server.js CHANGED
@@ -80,6 +80,7 @@ const UserSchema = new mongoose.Schema({
80
  parentPhone: String,
81
  address: String,
82
  gender: String,
 
83
  classApplication: {
84
  type: { type: String }, // Explicitly define type to avoid Mongoose casting error
85
  targetClass: String,
@@ -88,7 +89,25 @@ const UserSchema = new mongoose.Schema({
88
  });
89
  const User = mongoose.model('User', UserSchema);
90
 
91
- const StudentSchema = new mongoose.Schema({ schoolId: String, studentNo: String, name: String, gender: String, birthday: String, idCard: String, phone: String, className: String, status: String, parentName: String, parentPhone: String, address: String, teamId: String, drawAttempts: { type: Number, default: 0 }, dailyDrawLog: { date: String, count: { type: Number, default: 0 } }, flowerBalance: { type: Number, default: 0 } });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
  const Student = mongoose.model('Student', StudentSchema);
93
  const CourseSchema = new mongoose.Schema({ schoolId: String, courseCode: String, courseName: String, teacherName: String, credits: Number, capacity: Number, enrolled: Number });
94
  const Course = mongoose.model('Course', CourseSchema);
@@ -149,8 +168,119 @@ const getAutoSemester = () => {
149
  }
150
  };
151
 
 
 
 
 
 
 
152
  // --- ROUTES ---
153
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
  // --- TEACHER CLASS APPLICATION ROUTES ---
155
  app.post('/api/users/class-application', async (req, res) => {
156
  const { userId, type, targetClass, action } = req.body;
@@ -472,15 +602,18 @@ app.post('/api/auth/login', async (req, res) => {
472
  if (user.status !== 'active') return res.status(403).json({ error: 'PENDING_APPROVAL' });
473
  res.json(user);
474
  });
475
- app.post('/api/auth/register', async (req, res) => { try { await User.create({...req.body, status: 'pending'}); res.json({}); } catch(e) { res.status(500).json({}); } });
476
  app.get('/api/schools', async (req, res) => { res.json(await School.find()); });
477
  app.post('/api/schools', async (req, res) => { res.json(await School.create(req.body)); });
478
  app.put('/api/schools/:id', async (req, res) => { await School.findByIdAndUpdate(req.params.id, req.body); res.json({}); });
479
- app.get('/api/users', async (req, res) => { res.json(await User.find(getQueryFilter(req))); });
480
  app.put('/api/users/:id', async (req, res) => { await User.findByIdAndUpdate(req.params.id, req.body); res.json({}); });
481
  app.delete('/api/users/:id', async (req, res) => { await User.findByIdAndDelete(req.params.id); res.json({}); });
482
  app.get('/api/students', async (req, res) => { res.json(await Student.find(getQueryFilter(req))); });
483
- app.post('/api/students', async (req, res) => { await Student.findOneAndUpdate({ studentNo: req.body.studentNo }, injectSchoolId(req, req.body), {upsert:true}); res.json({}); });
 
 
 
 
 
484
  app.put('/api/students/:id', async (req, res) => { await Student.findByIdAndUpdate(req.params.id, req.body); res.json({}); });
485
  app.delete('/api/students/:id', async (req, res) => { await Student.findByIdAndDelete(req.params.id); res.json({}); });
486
  app.get('/api/classes', async (req, res) => {
@@ -698,4 +831,4 @@ app.post('/api/batch-delete', async (req, res) => {
698
  });
699
 
700
  app.get('*', (req, res) => { res.sendFile(path.join(__dirname, 'dist', 'index.html')); });
701
- app.listen(PORT, () => console.log(`🚀 Server running on port ${PORT}`));
 
80
  parentPhone: String,
81
  address: String,
82
  gender: String,
83
+ seatNo: String, // Added seatNo
84
  classApplication: {
85
  type: { type: String }, // Explicitly define type to avoid Mongoose casting error
86
  targetClass: String,
 
89
  });
90
  const User = mongoose.model('User', UserSchema);
91
 
92
+ const StudentSchema = new mongoose.Schema({
93
+ schoolId: String,
94
+ studentNo: String,
95
+ seatNo: String, // Added seatNo
96
+ name: String,
97
+ gender: String,
98
+ birthday: String,
99
+ idCard: String,
100
+ phone: String,
101
+ className: String,
102
+ status: String,
103
+ parentName: String,
104
+ parentPhone: String,
105
+ address: String,
106
+ teamId: String,
107
+ drawAttempts: { type: Number, default: 0 },
108
+ dailyDrawLog: { date: String, count: { type: Number, default: 0 } },
109
+ flowerBalance: { type: Number, default: 0 }
110
+ });
111
  const Student = mongoose.model('Student', StudentSchema);
112
  const CourseSchema = new mongoose.Schema({ schoolId: String, courseCode: String, courseName: String, teacherName: String, credits: Number, capacity: Number, enrolled: Number });
113
  const Course = mongoose.model('Course', CourseSchema);
 
168
  }
169
  };
170
 
171
+ const generateStudentNo = async () => {
172
+ const year = new Date().getFullYear();
173
+ const random = Math.floor(100000 + Math.random() * 900000); // 6 digits
174
+ return `${year}${random}`;
175
+ };
176
+
177
  // --- ROUTES ---
178
 
179
+ // --- AUTH / REGISTER ---
180
+
181
+ app.post('/api/auth/register', async (req, res) => {
182
+ const { role, username, password, schoolId, trueName, className, seatNo } = req.body;
183
+
184
+ try {
185
+ if (role === 'STUDENT') {
186
+ // Student Registration Logic
187
+ // 1. Check if a Student Profile already exists for this Name + Class + School
188
+ let student = await Student.findOne({ name: trueName, className, schoolId });
189
+ let finalUsername = '';
190
+
191
+ if (student) {
192
+ // Profile exists.
193
+ // 1a. Check if it already has a User account
194
+ const existingUser = await User.findOne({
195
+ role: 'STUDENT',
196
+ schoolId,
197
+ $or: [{ username: student.studentNo }, { trueName: trueName, homeroomClass: className }] // Defensive check
198
+ });
199
+
200
+ if (existingUser) {
201
+ return res.status(409).json({ error: 'ACCOUNT_EXISTS', message: '该学生账号已存在,请直接登录或联系老师重置密码。' });
202
+ }
203
+
204
+ // 1b. Migration Logic: If current studentNo looks like a seat number (short), move it to seatNo
205
+ // and generate a new System ID for login.
206
+ if (student.studentNo && student.studentNo.length < 6) {
207
+ if (!student.seatNo) student.seatNo = student.studentNo;
208
+ student.studentNo = await generateStudentNo();
209
+ await student.save();
210
+ } else if (!student.studentNo) {
211
+ student.studentNo = await generateStudentNo();
212
+ await student.save();
213
+ }
214
+
215
+ // If user provided a seatNo during reg, update it
216
+ if (seatNo) {
217
+ student.seatNo = seatNo;
218
+ await student.save();
219
+ }
220
+
221
+ finalUsername = student.studentNo;
222
+
223
+ } else {
224
+ // Profile does NOT exist. Create new.
225
+ finalUsername = await generateStudentNo();
226
+ student = await Student.create({
227
+ schoolId,
228
+ studentNo: finalUsername,
229
+ seatNo: seatNo || '',
230
+ name: trueName,
231
+ className: className,
232
+ gender: req.body.gender || 'Male',
233
+ parentName: req.body.parentName,
234
+ parentPhone: req.body.parentPhone,
235
+ address: req.body.address,
236
+ status: 'Enrolled',
237
+ birthday: '2015-01-01'
238
+ });
239
+ }
240
+
241
+ // Create User Account
242
+ await User.create({
243
+ username: finalUsername,
244
+ password,
245
+ role: 'STUDENT',
246
+ trueName,
247
+ schoolId,
248
+ status: 'pending', // Requires approval? Or auto active? User said "Pending approval"
249
+ homeroomClass: className, // Store class in user for easy access
250
+ studentNo: finalUsername,
251
+ seatNo: student.seatNo,
252
+ parentName: req.body.parentName,
253
+ parentPhone: req.body.parentPhone,
254
+ address: req.body.address,
255
+ gender: req.body.gender
256
+ });
257
+
258
+ return res.json({ username: finalUsername });
259
+ }
260
+
261
+ // Teacher/Admin Registration
262
+ const existing = await User.findOne({ username });
263
+ if (existing) return res.status(409).json({ error: 'USERNAME_EXISTS', message: '用户名已存在' });
264
+
265
+ await User.create({...req.body, status: 'pending'});
266
+ res.json({ username });
267
+
268
+ } catch(e) {
269
+ console.error(e);
270
+ res.status(500).json({ error: e.message });
271
+ }
272
+ });
273
+
274
+
275
+ // --- USERS ---
276
+ app.get('/api/users', async (req, res) => {
277
+ const filter = getQueryFilter(req);
278
+ // Explicitly handle role filtering
279
+ if (req.query.role) filter.role = req.query.role;
280
+ res.json(await User.find(filter));
281
+ });
282
+
283
+
284
  // --- TEACHER CLASS APPLICATION ROUTES ---
285
  app.post('/api/users/class-application', async (req, res) => {
286
  const { userId, type, targetClass, action } = req.body;
 
602
  if (user.status !== 'active') return res.status(403).json({ error: 'PENDING_APPROVAL' });
603
  res.json(user);
604
  });
 
605
  app.get('/api/schools', async (req, res) => { res.json(await School.find()); });
606
  app.post('/api/schools', async (req, res) => { res.json(await School.create(req.body)); });
607
  app.put('/api/schools/:id', async (req, res) => { await School.findByIdAndUpdate(req.params.id, req.body); res.json({}); });
 
608
  app.put('/api/users/:id', async (req, res) => { await User.findByIdAndUpdate(req.params.id, req.body); res.json({}); });
609
  app.delete('/api/users/:id', async (req, res) => { await User.findByIdAndDelete(req.params.id); res.json({}); });
610
  app.get('/api/students', async (req, res) => { res.json(await Student.find(getQueryFilter(req))); });
611
+ app.post('/api/students', async (req, res) => {
612
+ const data = injectSchoolId(req, req.body);
613
+ if (!data.studentNo) data.studentNo = await generateStudentNo();
614
+ await Student.findOneAndUpdate({ studentNo: data.studentNo }, data, {upsert:true});
615
+ res.json({});
616
+ });
617
  app.put('/api/students/:id', async (req, res) => { await Student.findByIdAndUpdate(req.params.id, req.body); res.json({}); });
618
  app.delete('/api/students/:id', async (req, res) => { await Student.findByIdAndDelete(req.params.id); res.json({}); });
619
  app.get('/api/classes', async (req, res) => {
 
831
  });
832
 
833
  app.get('*', (req, res) => { res.sendFile(path.join(__dirname, 'dist', 'index.html')); });
834
+ app.listen(PORT, () => console.log(`🚀 Server running on port ${PORT}`));
types.ts CHANGED
@@ -45,6 +45,7 @@ export interface User {
45
  parentPhone?: string;
46
  address?: string;
47
  gender?: 'Male' | 'Female';
 
48
  }
49
 
50
  export interface ClassInfo {
@@ -82,7 +83,8 @@ export interface Student {
82
  id?: number;
83
  _id?: string;
84
  schoolId?: string;
85
- studentNo: string;
 
86
  name: string;
87
  gender: 'Male' | 'Female' | 'Other';
88
  birthday: string;
@@ -301,4 +303,4 @@ export interface LeaveRequest {
301
  endDate: string;
302
  status: 'Pending' | 'Approved' | 'Rejected';
303
  createTime: string;
304
- }
 
45
  parentPhone?: string;
46
  address?: string;
47
  gender?: 'Male' | 'Female';
48
+ seatNo?: string; // NEW
49
  }
50
 
51
  export interface ClassInfo {
 
83
  id?: number;
84
  _id?: string;
85
  schoolId?: string;
86
+ studentNo: string; // System ID (Login ID)
87
+ seatNo?: string; // Class Seat Number (Optional, for sorting/display)
88
  name: string;
89
  gender: 'Male' | 'Female' | 'Other';
90
  birthday: string;
 
303
  endDate: string;
304
  status: 'Pending' | 'Approved' | 'Rejected';
305
  createTime: string;
306
+ }