dvc890 commited on
Commit
d567faa
·
verified ·
1 Parent(s): 8f856eb

Upload 26 files

Browse files
Files changed (5) hide show
  1. components/Header.tsx +42 -17
  2. pages/Login.tsx +88 -44
  3. pages/UserList.tsx +16 -3
  4. server.js +55 -12
  5. types.ts +5 -2
components/Header.tsx CHANGED
@@ -13,18 +13,42 @@ interface HeaderProps {
13
  export const Header: React.FC<HeaderProps> = ({ user, title, onMenuClick }) => {
14
  const [schools, setSchools] = useState<School[]>([]);
15
  const [selectedSchool, setSelectedSchool] = useState(localStorage.getItem('admin_view_school_id') || '');
 
16
 
17
  useEffect(() => {
18
  if (user.role === 'ADMIN') {
19
- api.schools.getAll().then(setSchools).catch(console.error);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  }
21
  }, [user]);
22
 
23
  const handleSchoolChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
24
  const newVal = e.target.value;
 
25
  setSelectedSchool(newVal);
26
- if (newVal) localStorage.setItem('admin_view_school_id', newVal);
27
- else localStorage.removeItem('admin_view_school_id');
28
  window.location.reload(); // Reload to refresh data context
29
  };
30
 
@@ -41,20 +65,21 @@ export const Header: React.FC<HeaderProps> = ({ user, title, onMenuClick }) => {
41
  </div>
42
 
43
  <div className="flex items-center space-x-2 md:space-x-6">
44
- {/* Admin School Switcher */}
45
- {user.role === 'ADMIN' && (
46
- <div className="hidden md:flex items-center bg-gray-100 rounded-lg px-2 py-1">
47
  <Building size={16} className="text-gray-500 mr-2"/>
48
- <select
49
- className="bg-transparent text-sm border-none focus:ring-0 text-gray-700 font-medium cursor-pointer"
50
- value={selectedSchool}
51
- onChange={handleSchoolChange}
52
- >
53
- <option value="">-- 全平台视图 --</option>
54
- {schools.map(s => <option key={s._id} value={s._id}>{s.name}</option>)}
55
- </select>
56
- </div>
57
- )}
 
 
58
 
59
  <div className="relative hidden md:block">
60
  <input
@@ -86,4 +111,4 @@ export const Header: React.FC<HeaderProps> = ({ user, title, onMenuClick }) => {
86
  </div>
87
  </header>
88
  );
89
- };
 
13
  export const Header: React.FC<HeaderProps> = ({ user, title, onMenuClick }) => {
14
  const [schools, setSchools] = useState<School[]>([]);
15
  const [selectedSchool, setSelectedSchool] = useState(localStorage.getItem('admin_view_school_id') || '');
16
+ const [currentSchoolName, setCurrentSchoolName] = useState('');
17
 
18
  useEffect(() => {
19
  if (user.role === 'ADMIN') {
20
+ api.schools.getAll().then(data => {
21
+ setSchools(data);
22
+ // Enforce default school if none selected or invalid
23
+ if (data.length > 0) {
24
+ if (!selectedSchool || !data.find(s => s._id === selectedSchool)) {
25
+ const defaultId = data[0]._id!;
26
+ setSelectedSchool(defaultId);
27
+ localStorage.setItem('admin_view_school_id', defaultId);
28
+ // Force reload only if it changed from nothing to something, avoiding loops is handled by check above
29
+ // But initially we might need to trigger data fetch.
30
+ // Actually, simply setting state won't trigger re-fetch in other components unless we reload.
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
+ // Fetch own school info to display name
40
+ api.schools.getPublic().then(data => {
41
+ const mySchool = data.find(s => s._id === user.schoolId);
42
+ if (mySchool) setCurrentSchoolName(mySchool.name);
43
+ });
44
  }
45
  }, [user]);
46
 
47
  const handleSchoolChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
48
  const newVal = e.target.value;
49
+ if (!newVal) return; // Prevent empty selection
50
  setSelectedSchool(newVal);
51
+ localStorage.setItem('admin_view_school_id', newVal);
 
52
  window.location.reload(); // Reload to refresh data context
53
  };
54
 
 
65
  </div>
66
 
67
  <div className="flex items-center space-x-2 md:space-x-6">
68
+ {/* School Display / Switcher */}
69
+ <div className="hidden md:flex items-center bg-gray-100 rounded-lg px-3 py-1.5">
 
70
  <Building size={16} className="text-gray-500 mr-2"/>
71
+ {user.role === 'ADMIN' ? (
72
+ <select
73
+ className="bg-transparent text-sm border-none focus:ring-0 text-gray-700 font-medium cursor-pointer min-w-[120px]"
74
+ value={selectedSchool}
75
+ onChange={handleSchoolChange}
76
+ >
77
+ {schools.map(s => <option key={s._id} value={s._id}>{s.name}</option>)}
78
+ </select>
79
+ ) : (
80
+ <span className="text-sm font-medium text-gray-700">{currentSchoolName || '我的学校'}</span>
81
+ )}
82
+ </div>
83
 
84
  <div className="relative hidden md:block">
85
  <input
 
111
  </div>
112
  </header>
113
  );
114
+ };
pages/Login.tsx CHANGED
@@ -1,7 +1,7 @@
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 } from 'lucide-react';
4
- import { User, UserRole, School } from '../types';
5
  import { api } from '../services/api';
6
 
7
  interface LoginProps {
@@ -17,18 +17,23 @@ export const Login: React.FC<LoginProps> = ({ onLogin }) => {
17
  const [view, setView] = useState<'login' | 'register'>('login');
18
 
19
  // Registration Wizard Step
20
- const [step, setStep] = useState(1); // 1: School & Role, 2: Info, 3: Avatar
21
 
22
  // Login Form
23
  const [loginForm, setLoginForm] = useState({ username: '', password: '' });
24
 
25
  // Register Form
26
  const [regForm, setRegForm] = useState({
27
- username: '', password: '', role: UserRole.STUDENT,
28
- schoolId: '', trueName: '', phone: '', email: '', avatar: ''
 
29
  });
30
 
31
  const [schools, setSchools] = useState<School[]>([]);
 
 
 
 
32
  const [error, setError] = useState('');
33
  const [loading, setLoading] = useState(false);
34
  const [successMsg, setSuccessMsg] = useState('');
@@ -39,6 +44,19 @@ export const Login: React.FC<LoginProps> = ({ onLogin }) => {
39
  }
40
  }, [view]);
41
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  const handleLogin = async (e: React.FormEvent) => {
43
  e.preventDefault();
44
  setError('');
@@ -71,28 +89,18 @@ export const Login: React.FC<LoginProps> = ({ onLogin }) => {
71
  // Render Steps
72
  const renderStep1 = () => (
73
  <div className="space-y-4 animate-in fade-in">
74
- <h3 className="text-center font-bold text-gray-700 mb-4">第一步: 选择学校与身份</h3>
75
  <div>
76
- <label className="text-sm font-medium text-gray-700">所属学校</label>
77
  <select
78
- className="w-full mt-1 p-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
79
  value={regForm.schoolId}
80
  onChange={e => setRegForm({...regForm, schoolId: e.target.value})}
81
  >
82
  <option value="">-- 请选择 --</option>
83
  {schools.map(s => <option key={s._id} value={s._id}>{s.name}</option>)}
84
  </select>
85
- </div>
86
- <div>
87
- <label className="text-sm font-medium text-gray-700">申请角色</label>
88
- <select
89
- className="w-full mt-1 p-2 border rounded-lg"
90
- value={regForm.role}
91
- onChange={e => setRegForm({...regForm, role: e.target.value as UserRole})}
92
- >
93
- <option value={UserRole.STUDENT}>学生</option>
94
- <option value={UserRole.TEACHER}>教师</option>
95
- </select>
96
  </div>
97
  <button
98
  type="button"
@@ -107,28 +115,64 @@ export const Login: React.FC<LoginProps> = ({ onLogin }) => {
107
 
108
  const renderStep2 = () => (
109
  <div className="space-y-3 animate-in fade-in">
110
- <h3 className="text-center font-bold text-gray-700 mb-2">第二步: 填写基本信息</h3>
111
- <input
112
- className="w-full p-2 border rounded-lg"
113
- placeholder="用户名 (登录用)"
114
- value={regForm.username} onChange={e => setRegForm({...regForm, username: e.target.value})}
115
- />
116
- <input
117
- className="w-full p-2 border rounded-lg"
118
- type="password" placeholder="设置密码"
119
- value={regForm.password} onChange={e => setRegForm({...regForm, password: e.target.value})}
120
- />
121
- <input
122
- className="w-full p-2 border rounded-lg"
123
- placeholder="真实姓名"
124
- value={regForm.trueName} onChange={e => setRegForm({...regForm, trueName: e.target.value})}
125
- />
126
- <input
127
- className="w-full p-2 border rounded-lg"
128
- placeholder="联系电话"
129
- value={regForm.phone} onChange={e => setRegForm({...regForm, phone: e.target.value})}
130
- />
131
- <div className="flex gap-2">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
  <button onClick={() => setStep(1)} className="flex-1 border py-2 rounded-lg">上一步</button>
133
  <button
134
  onClick={() => setStep(3)}
@@ -162,7 +206,7 @@ export const Login: React.FC<LoginProps> = ({ onLogin }) => {
162
  <div className="flex gap-2">
163
  <button onClick={() => setStep(2)} className="flex-1 border py-2 rounded-lg">上一步</button>
164
  <button onClick={handleRegister} disabled={loading} className="flex-1 bg-green-600 text-white py-2 rounded-lg">
165
- {loading ? '提交中...' : '完成注册'}
166
  </button>
167
  </div>
168
  </div>
@@ -186,7 +230,7 @@ export const Login: React.FC<LoginProps> = ({ onLogin }) => {
186
  <div className="mb-6 flex justify-center">
187
  <div className="bg-gray-100 p-1 rounded-lg flex text-sm font-medium">
188
  <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>
189
- <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>
190
  </div>
191
  </div>
192
 
@@ -225,4 +269,4 @@ export const Login: React.FC<LoginProps> = ({ onLogin }) => {
225
  </div>
226
  </div>
227
  );
228
- };
 
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
 
7
  interface LoginProps {
 
17
  const [view, setView] = useState<'login' | 'register'>('login');
18
 
19
  // Registration Wizard Step
20
+ const [step, setStep] = useState(1); // 1: School, 2: Role & Info & Linkage, 3: Avatar
21
 
22
  // Login Form
23
  const [loginForm, setLoginForm] = useState({ username: '', password: '' });
24
 
25
  // Register Form
26
  const [regForm, setRegForm] = useState({
27
+ username: '', password: '', role: UserRole.TEACHER,
28
+ schoolId: '', trueName: '', phone: '', email: '', avatar: '',
29
+ teachingSubject: '', homeroomClass: ''
30
  });
31
 
32
  const [schools, setSchools] = useState<School[]>([]);
33
+ // School Specific Metadata for linkage
34
+ const [schoolClasses, setSchoolClasses] = useState<ClassInfo[]>([]);
35
+ const [schoolSubjects, setSchoolSubjects] = useState<Subject[]>([]);
36
+
37
  const [error, setError] = useState('');
38
  const [loading, setLoading] = useState(false);
39
  const [successMsg, setSuccessMsg] = useState('');
 
44
  }
45
  }, [view]);
46
 
47
+ // When School ID changes, fetch meta data
48
+ useEffect(() => {
49
+ if (regForm.schoolId) {
50
+ fetch(`${location.origin}/api/public/meta?schoolId=${regForm.schoolId}`)
51
+ .then(res => res.json())
52
+ .then(data => {
53
+ setSchoolClasses(data.classes || []);
54
+ setSchoolSubjects(data.subjects || []);
55
+ })
56
+ .catch(console.error);
57
+ }
58
+ }, [regForm.schoolId]);
59
+
60
  const handleLogin = async (e: React.FormEvent) => {
61
  e.preventDefault();
62
  setError('');
 
89
  // Render Steps
90
  const renderStep1 = () => (
91
  <div className="space-y-4 animate-in fade-in">
92
+ <h3 className="text-center font-bold text-gray-700 mb-4">第一步: 选择所属学校</h3>
93
  <div>
94
+ <label className="text-sm font-medium text-gray-700">学校列表</label>
95
  <select
96
+ className="w-full mt-1 p-2 border rounded-lg focus:ring-2 focus:ring-blue-500 h-10 bg-white"
97
  value={regForm.schoolId}
98
  onChange={e => setRegForm({...regForm, schoolId: e.target.value})}
99
  >
100
  <option value="">-- 请选择 --</option>
101
  {schools.map(s => <option key={s._id} value={s._id}>{s.name}</option>)}
102
  </select>
103
+ <p className="text-xs text-gray-500 mt-2">提示:系统仅面向教职工开放注册</p>
 
 
 
 
 
 
 
 
 
 
104
  </div>
105
  <button
106
  type="button"
 
115
 
116
  const renderStep2 = () => (
117
  <div className="space-y-3 animate-in fade-in">
118
+ <h3 className="text-center font-bold text-gray-700 mb-2">第二步: 完善教职工信息</h3>
119
+
120
+ {/* Account Info */}
121
+ <div className="grid grid-cols-2 gap-2">
122
+ <input
123
+ className="w-full p-2 border rounded-lg"
124
+ placeholder="用户名 (登录)"
125
+ value={regForm.username} onChange={e => setRegForm({...regForm, username: e.target.value})}
126
+ />
127
+ <input
128
+ className="w-full p-2 border rounded-lg"
129
+ type="password" placeholder="密码"
130
+ value={regForm.password} onChange={e => setRegForm({...regForm, password: e.target.value})}
131
+ />
132
+ </div>
133
+
134
+ {/* Personal Info */}
135
+ <div className="grid grid-cols-2 gap-2">
136
+ <input
137
+ className="w-full p-2 border rounded-lg"
138
+ placeholder="真实姓名"
139
+ value={regForm.trueName} onChange={e => setRegForm({...regForm, trueName: e.target.value})}
140
+ />
141
+ <input
142
+ className="w-full p-2 border rounded-lg"
143
+ placeholder="手机号码"
144
+ value={regForm.phone} onChange={e => setRegForm({...regForm, phone: e.target.value})}
145
+ />
146
+ </div>
147
+
148
+ {/* Teacher Linkage */}
149
+ <div className="bg-blue-50 p-3 rounded-lg space-y-2 border border-blue-100">
150
+ <p className="text-xs font-bold text-blue-600 uppercase">教学信息绑定</p>
151
+ <div>
152
+ <label className="text-xs text-gray-600 block mb-1">主要任教科目</label>
153
+ <select
154
+ className="w-full p-1.5 border rounded text-sm"
155
+ value={regForm.teachingSubject}
156
+ onChange={e => setRegForm({...regForm, teachingSubject: e.target.value})}
157
+ >
158
+ <option value="">-- 请选择 --</option>
159
+ {schoolSubjects.map(s => <option key={s._id} value={s.name}>{s.name}</option>)}
160
+ </select>
161
+ </div>
162
+ <div>
163
+ <label className="text-xs text-gray-600 block mb-1">我是班主任 (可选)</label>
164
+ <select
165
+ className="w-full p-1.5 border rounded text-sm"
166
+ value={regForm.homeroomClass}
167
+ onChange={e => setRegForm({...regForm, homeroomClass: e.target.value})}
168
+ >
169
+ <option value="">-- 非班主任 --</option>
170
+ {schoolClasses.map(c => <option key={c._id} value={c.grade+c.className}>{c.grade}{c.className}</option>)}
171
+ </select>
172
+ </div>
173
+ </div>
174
+
175
+ <div className="flex gap-2 pt-2">
176
  <button onClick={() => setStep(1)} className="flex-1 border py-2 rounded-lg">上一步</button>
177
  <button
178
  onClick={() => setStep(3)}
 
206
  <div className="flex gap-2">
207
  <button onClick={() => setStep(2)} className="flex-1 border py-2 rounded-lg">上一步</button>
208
  <button onClick={handleRegister} disabled={loading} className="flex-1 bg-green-600 text-white py-2 rounded-lg">
209
+ {loading ? '提交申请' : '完成注册'}
210
  </button>
211
  </div>
212
  </div>
 
230
  <div className="mb-6 flex justify-center">
231
  <div className="bg-gray-100 p-1 rounded-lg flex text-sm font-medium">
232
  <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>
233
+ <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>
234
  </div>
235
  </div>
236
 
 
269
  </div>
270
  </div>
271
  );
272
+ };
pages/UserList.tsx CHANGED
@@ -23,7 +23,7 @@ export const UserList: React.FC = () => {
23
  useEffect(() => { loadData(); }, []);
24
 
25
  const handleApprove = async (user: User) => {
26
- if (confirm(`确认批准 ${user.username} 的注册申请?`)) {
27
  await api.users.update(user._id || String(user.id), { status: UserStatus.ACTIVE });
28
  loadData();
29
  }
@@ -58,12 +58,13 @@ export const UserList: React.FC = () => {
58
  <h2 className="text-xl font-bold text-gray-800 mb-4">用户权限与审核</h2>
59
 
60
  <div className="overflow-x-auto">
61
- <table className="w-full text-left border-collapse min-w-[800px]">
62
  <thead>
63
  <tr className="bg-gray-50 text-gray-500 text-xs uppercase">
64
  <th className="px-4 py-3">用户</th>
65
  <th className="px-4 py-3">联系方式</th>
66
  <th className="px-4 py-3">所属学校</th>
 
67
  <th className="px-4 py-3">角色</th>
68
  <th className="px-4 py-3">状态</th>
69
  <th className="px-4 py-3 text-right">操作</th>
@@ -103,6 +104,18 @@ export const UserList: React.FC = () => {
103
  </div>
104
  )}
105
  </td>
 
 
 
 
 
 
 
 
 
 
 
 
106
  <td className="px-4 py-3">
107
  <select
108
  value={user.role}
@@ -133,4 +146,4 @@ export const UserList: React.FC = () => {
133
  </div>
134
  </div>
135
  );
136
- };
 
23
  useEffect(() => { loadData(); }, []);
24
 
25
  const handleApprove = async (user: User) => {
26
+ if (confirm(`确认批准 ${user.trueName || user.username} 的注册申请?`)) {
27
  await api.users.update(user._id || String(user.id), { status: UserStatus.ACTIVE });
28
  loadData();
29
  }
 
58
  <h2 className="text-xl font-bold text-gray-800 mb-4">用户权限与审核</h2>
59
 
60
  <div className="overflow-x-auto">
61
+ <table className="w-full text-left border-collapse min-w-[1000px]">
62
  <thead>
63
  <tr className="bg-gray-50 text-gray-500 text-xs uppercase">
64
  <th className="px-4 py-3">用户</th>
65
  <th className="px-4 py-3">联系方式</th>
66
  <th className="px-4 py-3">所属学校</th>
67
+ <th className="px-4 py-3">任教信息 (科目/班主任)</th>
68
  <th className="px-4 py-3">角色</th>
69
  <th className="px-4 py-3">状态</th>
70
  <th className="px-4 py-3 text-right">操作</th>
 
104
  </div>
105
  )}
106
  </td>
107
+ <td className="px-4 py-3 text-sm">
108
+ {user.teachingSubject ? (
109
+ <div className="space-y-1">
110
+ <span className="bg-blue-50 text-blue-700 px-2 py-0.5 rounded text-xs">{user.teachingSubject}</span>
111
+ {user.homeroomClass && (
112
+ <span className="bg-amber-50 text-amber-700 px-2 py-0.5 rounded text-xs block w-fit">
113
+ 班主任: {user.homeroomClass}
114
+ </span>
115
+ )}
116
+ </div>
117
+ ) : <span className="text-gray-400 text-xs">-</span>}
118
+ </td>
119
  <td className="px-4 py-3">
120
  <select
121
  value={user.role}
 
146
  </div>
147
  </div>
148
  );
149
+ };
server.js CHANGED
@@ -68,10 +68,12 @@ const UserSchema = new mongoose.Schema({
68
  phone: String,
69
  email: String,
70
  schoolId: String,
71
- role: { type: String, enum: ['ADMIN', 'TEACHER', 'STUDENT'], default: 'STUDENT' },
72
  status: { type: String, enum: ['active', 'pending', 'banned'], default: 'pending' },
73
  avatar: String,
74
- createTime: { type: Date, default: Date.now }
 
 
75
  });
76
  const User = mongoose.model('User', UserSchema);
77
 
@@ -200,11 +202,8 @@ mongoose.connection.once('open', initData);
200
  // Helper: Get School Context
201
  // ==========================================
202
  const getQueryFilter = (req) => {
203
- // If we had real JWT middleware, we would extract user from token.
204
- // For this setup, we rely on the client sending 'x-school-id'.
205
- // Security Note: In a production app, verify this against the user's token claims.
206
  const schoolId = req.headers['x-school-id'];
207
- if (!schoolId) return {}; // If Admin sees all (or specific logic)
208
  return { schoolId };
209
  };
210
 
@@ -224,6 +223,22 @@ app.get('/api/public/schools', async (req, res) => {
224
  res.json(schools);
225
  });
226
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
227
  // --- Auth ---
228
  app.post('/api/auth/login', async (req, res) => {
229
  const { username, password } = req.body;
@@ -244,13 +259,17 @@ app.post('/api/auth/login', async (req, res) => {
244
  });
245
 
246
  app.post('/api/auth/register', async (req, res) => {
247
- const { username, password, role, schoolId, trueName, phone, email, avatar } = req.body;
 
 
 
 
248
  const status = 'pending';
249
 
250
  try {
251
  if (InMemoryDB.isFallback) {
252
  if (InMemoryDB.users.find(u => u.username === username)) return res.status(400).json({ error: 'Existed' });
253
- const newUser = { id: Date.now(), username, password, role, status, schoolId, trueName, phone, email, avatar };
254
  InMemoryDB.users.push(newUser);
255
  return res.json(newUser);
256
  }
@@ -258,7 +277,10 @@ app.post('/api/auth/register', async (req, res) => {
258
  const existing = await User.findOne({ username });
259
  if (existing) return res.status(400).json({ error: 'Existed' });
260
 
261
- const newUser = await User.create({ username, password, role, status, schoolId, trueName, phone, email, avatar });
 
 
 
262
  res.json(newUser);
263
  } catch (e) { res.status(500).json({ error: e.message }); }
264
  });
@@ -291,12 +313,33 @@ app.get('/api/users', async (req, res) => {
291
  });
292
  app.put('/api/users/:id', async (req, res) => {
293
  try {
 
 
 
294
  if (InMemoryDB.isFallback) {
295
  const idx = InMemoryDB.users.findIndex(u => u._id == req.params.id);
296
- if(idx>=0) InMemoryDB.users[idx] = { ...InMemoryDB.users[idx], ...req.body };
297
  return res.json({ success: true });
298
  }
299
- await User.findByIdAndUpdate(req.params.id, req.body);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
300
  res.json({ success: true });
301
  } catch (e) { res.status(500).json({ error: e.message }); }
302
  });
@@ -472,4 +515,4 @@ app.get('*', (req, res) => {
472
  res.sendFile(path.join(__dirname, 'dist', 'index.html'));
473
  });
474
 
475
- app.listen(PORT, () => console.log(`🚀 Service running on port ${PORT}`));
 
68
  phone: String,
69
  email: String,
70
  schoolId: String,
71
+ role: { type: String, enum: ['ADMIN', 'TEACHER', 'STUDENT'], default: 'TEACHER' },
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
 
 
202
  // Helper: Get School Context
203
  // ==========================================
204
  const getQueryFilter = (req) => {
 
 
 
205
  const schoolId = req.headers['x-school-id'];
206
+ if (!schoolId) return {};
207
  return { schoolId };
208
  };
209
 
 
223
  res.json(schools);
224
  });
225
 
226
+ app.get('/api/public/meta', async (req, res) => {
227
+ const { schoolId } = req.query;
228
+ if (!schoolId) return res.json({ classes: [], subjects: [] });
229
+
230
+ if (InMemoryDB.isFallback) {
231
+ return res.json({
232
+ classes: InMemoryDB.classes.filter(c => c.schoolId === schoolId),
233
+ subjects: InMemoryDB.subjects.filter(s => s.schoolId === schoolId)
234
+ });
235
+ }
236
+
237
+ const classes = await ClassModel.find({ schoolId });
238
+ const subjects = await SubjectModel.find({ schoolId });
239
+ res.json({ classes, subjects });
240
+ });
241
+
242
  // --- Auth ---
243
  app.post('/api/auth/login', async (req, res) => {
244
  const { username, password } = req.body;
 
259
  });
260
 
261
  app.post('/api/auth/register', async (req, res) => {
262
+ // Accepted fields
263
+ const {
264
+ username, password, role, schoolId, trueName, phone, email, avatar,
265
+ teachingSubject, homeroomClass
266
+ } = req.body;
267
  const status = 'pending';
268
 
269
  try {
270
  if (InMemoryDB.isFallback) {
271
  if (InMemoryDB.users.find(u => u.username === username)) return res.status(400).json({ error: 'Existed' });
272
+ const newUser = { id: Date.now(), username, password, role, status, schoolId, trueName, phone, email, avatar, teachingSubject, homeroomClass };
273
  InMemoryDB.users.push(newUser);
274
  return res.json(newUser);
275
  }
 
277
  const existing = await User.findOne({ username });
278
  if (existing) return res.status(400).json({ error: 'Existed' });
279
 
280
+ const newUser = await User.create({
281
+ username, password, role, status, schoolId, trueName, phone, email, avatar,
282
+ teachingSubject, homeroomClass
283
+ });
284
  res.json(newUser);
285
  } catch (e) { res.status(500).json({ error: e.message }); }
286
  });
 
313
  });
314
  app.put('/api/users/:id', async (req, res) => {
315
  try {
316
+ // Check if activating user
317
+ const updateData = req.body;
318
+
319
  if (InMemoryDB.isFallback) {
320
  const idx = InMemoryDB.users.findIndex(u => u._id == req.params.id);
321
+ if(idx>=0) InMemoryDB.users[idx] = { ...InMemoryDB.users[idx], ...updateData };
322
  return res.json({ success: true });
323
  }
324
+
325
+ const user = await User.findByIdAndUpdate(req.params.id, updateData, { new: true });
326
+
327
+ // Linkage Logic: If approving a teacher, and they are a homeroom teacher, update the Class record.
328
+ if (updateData.status === 'active' && user.role === 'TEACHER' && user.homeroomClass) {
329
+ // Find Class by name (e.g. "六年级(1)班") and SchoolId
330
+ // Need to parse grade and className or store exact match.
331
+ // Assuming user.homeroomClass stores "六年级(1)班", we need to match it.
332
+ // Simple regex or strict match if format aligns.
333
+ // For robustness, let's try to match concatenation.
334
+ const classes = await ClassModel.find({ schoolId: user.schoolId });
335
+ const targetClass = classes.find(c => (c.grade + c.className) === user.homeroomClass);
336
+
337
+ if (targetClass) {
338
+ await ClassModel.findByIdAndUpdate(targetClass._id, { teacherName: user.trueName || user.username });
339
+ console.log(`Linked teacher ${user.trueName} to class ${targetClass.grade}${targetClass.className}`);
340
+ }
341
+ }
342
+
343
  res.json({ success: true });
344
  } catch (e) { res.status(500).json({ error: e.message }); }
345
  });
 
515
  res.sendFile(path.join(__dirname, 'dist', 'index.html'));
516
  });
517
 
518
+ app.listen(PORT, () => console.log(`🚀 Service running on port ${PORT}`));
types.ts CHANGED
@@ -31,6 +31,9 @@ export interface User {
31
  status: UserStatus;
32
  avatar?: string;
33
  createTime?: string;
 
 
 
34
  }
35
 
36
  export interface ClassInfo {
@@ -38,7 +41,7 @@ export interface ClassInfo {
38
  _id?: string;
39
  schoolId?: string; // New
40
  grade: string;
41
- className: string; // e.g., "(1)班"
42
  teacherName?: string;
43
  studentCount?: number; // Calculated field
44
  }
@@ -118,4 +121,4 @@ export interface ApiResponse<T> {
118
  message: string;
119
  data: T;
120
  timestamp: number;
121
- }
 
31
  status: UserStatus;
32
  avatar?: string;
33
  createTime?: string;
34
+ // Teacher Specific
35
+ teachingSubject?: string; // e.g. "语文"
36
+ homeroomClass?: string; // e.g. "六年级(1)班" - Class Name string linkage
37
  }
38
 
39
  export interface ClassInfo {
 
41
  _id?: string;
42
  schoolId?: string; // New
43
  grade: string;
44
+ className: string; // e.g. "(1)班"
45
  teacherName?: string;
46
  studentCount?: number; // Calculated field
47
  }
 
121
  message: string;
122
  data: T;
123
  timestamp: number;
124
+ }