dvc890 commited on
Commit
6e8372b
·
verified ·
1 Parent(s): 079d21f

Upload 26 files

Browse files
Files changed (6) hide show
  1. pages/Login.tsx +22 -7
  2. pages/Settings.tsx +14 -2
  3. pages/UserList.tsx +30 -12
  4. server.js +18 -7
  5. services/api.ts +3 -1
  6. types.ts +2 -1
pages/Login.tsx CHANGED
@@ -33,6 +33,7 @@ export const Login: React.FC<LoginProps> = ({ onLogin }) => {
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);
@@ -41,6 +42,7 @@ export const Login: React.FC<LoginProps> = ({ onLogin }) => {
41
  useEffect(() => {
42
  if (view === 'register') {
43
  api.schools.getPublic().then(setSchools).catch(console.error);
 
44
  }
45
  }, [view]);
46
 
@@ -89,7 +91,7 @@ export const Login: React.FC<LoginProps> = ({ onLogin }) => {
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
@@ -100,7 +102,20 @@ export const Login: React.FC<LoginProps> = ({ onLogin }) => {
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,7 +130,7 @@ export const Login: React.FC<LoginProps> = ({ onLogin }) => {
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">
@@ -145,9 +160,9 @@ export const Login: React.FC<LoginProps> = ({ onLogin }) => {
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
@@ -160,7 +175,7 @@ export const Login: React.FC<LoginProps> = ({ onLogin }) => {
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}
@@ -269,4 +284,4 @@ export const Login: React.FC<LoginProps> = ({ onLogin }) => {
269
  </div>
270
  </div>
271
  );
272
- };
 
33
  // School Specific Metadata for linkage
34
  const [schoolClasses, setSchoolClasses] = useState<ClassInfo[]>([]);
35
  const [schoolSubjects, setSchoolSubjects] = useState<Subject[]>([]);
36
+ const [publicConfig, setPublicConfig] = useState({ allowRegister: true, allowAdminRegister: false });
37
 
38
  const [error, setError] = useState('');
39
  const [loading, setLoading] = useState(false);
 
42
  useEffect(() => {
43
  if (view === 'register') {
44
  api.schools.getPublic().then(setSchools).catch(console.error);
45
+ api.config.getPublic().then(setPublicConfig).catch(console.error);
46
  }
47
  }, [view]);
48
 
 
91
  // Render Steps
92
  const renderStep1 = () => (
93
  <div className="space-y-4 animate-in fade-in">
94
+ <h3 className="text-center font-bold text-gray-700 mb-4">第一步: 选择所属学校 & 角色</h3>
95
  <div>
96
  <label className="text-sm font-medium text-gray-700">学校列表</label>
97
  <select
 
102
  <option value="">-- 请选择 --</option>
103
  {schools.map(s => <option key={s._id} value={s._id}>{s.name}</option>)}
104
  </select>
105
+ </div>
106
+ <div>
107
+ <label className="text-sm font-medium text-gray-700">申请身份</label>
108
+ <select
109
+ className="w-full mt-1 p-2 border rounded-lg focus:ring-2 focus:ring-blue-500 h-10 bg-white"
110
+ value={regForm.role}
111
+ onChange={e => setRegForm({...regForm, role: e.target.value as any})}
112
+ >
113
+ <option value={UserRole.TEACHER}>教师</option>
114
+ {publicConfig.allowAdminRegister && <option value={UserRole.ADMIN}>学校管理员</option>}
115
+ </select>
116
+ {regForm.role === UserRole.ADMIN && (
117
+ <p className="text-xs text-amber-600 mt-1">注意:管理员权限需要经过严格审核</p>
118
+ )}
119
  </div>
120
  <button
121
  type="button"
 
130
 
131
  const renderStep2 = () => (
132
  <div className="space-y-3 animate-in fade-in">
133
+ <h3 className="text-center font-bold text-gray-700 mb-2">第二步: 完善信息</h3>
134
 
135
  {/* Account Info */}
136
  <div className="grid grid-cols-2 gap-2">
 
160
  />
161
  </div>
162
 
163
+ {/* Teacher Linkage - Show for both Teachers AND Admins who might teach */}
164
  <div className="bg-blue-50 p-3 rounded-lg space-y-2 border border-blue-100">
165
+ <p className="text-xs font-bold text-blue-600 uppercase">教学信息绑定 (可选)</p>
166
  <div>
167
  <label className="text-xs text-gray-600 block mb-1">主要任教科目</label>
168
  <select
 
175
  </select>
176
  </div>
177
  <div>
178
+ <label className="text-xs text-gray-600 block mb-1">我是班主任</label>
179
  <select
180
  className="w-full p-1.5 border rounded text-sm"
181
  value={regForm.homeroomClass}
 
284
  </div>
285
  </div>
286
  );
287
+ };
pages/Settings.tsx CHANGED
@@ -10,6 +10,7 @@ export const Settings: React.FC = () => {
10
  const [config, setConfig] = useState({
11
  systemName: '智慧校园管理系统',
12
  allowRegister: true,
 
13
  maintenanceMode: false,
14
  emailNotify: true,
15
  semester: '2023-2024学年 第一学期'
@@ -143,8 +144,8 @@ export const Settings: React.FC = () => {
143
  <div className="p-6 space-y-4">
144
  <div className="flex items-center justify-between p-4 bg-gray-50 rounded-lg">
145
  <div>
146
- <p className="font-medium text-gray-900">开放新用户注册</p>
147
- <p className="text-sm text-gray-500">允许教师或学生自行注册账号</p>
148
  </div>
149
  <label className="relative inline-flex items-center cursor-pointer">
150
  <input type="checkbox" checked={config.allowRegister} onChange={(e) => setConfig({...config, allowRegister: e.target.checked})} className="sr-only peer" />
@@ -152,6 +153,17 @@ export const Settings: React.FC = () => {
152
  </label>
153
  </div>
154
 
 
 
 
 
 
 
 
 
 
 
 
155
  <div className="flex items-center justify-between p-4 bg-gray-50 rounded-lg">
156
  <div>
157
  <p className="font-medium text-gray-900">系统维护模式</p>
 
10
  const [config, setConfig] = useState({
11
  systemName: '智慧校园管理系统',
12
  allowRegister: true,
13
+ allowAdminRegister: false,
14
  maintenanceMode: false,
15
  emailNotify: true,
16
  semester: '2023-2024学年 第一学期'
 
144
  <div className="p-6 space-y-4">
145
  <div className="flex items-center justify-between p-4 bg-gray-50 rounded-lg">
146
  <div>
147
+ <p className="font-medium text-gray-900">开放教师注册</p>
148
+ <p className="text-sm text-gray-500">允许教师自行注册账号</p>
149
  </div>
150
  <label className="relative inline-flex items-center cursor-pointer">
151
  <input type="checkbox" checked={config.allowRegister} onChange={(e) => setConfig({...config, allowRegister: e.target.checked})} className="sr-only peer" />
 
153
  </label>
154
  </div>
155
 
156
+ <div className="flex items-center justify-between p-4 bg-gray-50 rounded-lg">
157
+ <div>
158
+ <p className="font-medium text-gray-900">开放管理员注册申请</p>
159
+ <p className="text-sm text-gray-500">允许用户在注册时申请成为管理员 (需审核)</p>
160
+ </div>
161
+ <label className="relative inline-flex items-center cursor-pointer">
162
+ <input type="checkbox" checked={config.allowAdminRegister} onChange={(e) => setConfig({...config, allowAdminRegister: e.target.checked})} className="sr-only peer" />
163
+ <div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div>
164
+ </label>
165
+ </div>
166
+
167
  <div className="flex items-center justify-between p-4 bg-gray-50 rounded-lg">
168
  <div>
169
  <p className="font-medium text-gray-900">系统维护模式</p>
pages/UserList.tsx CHANGED
@@ -9,11 +9,17 @@ export const UserList: React.FC = () => {
9
  const [schools, setSchools] = useState<School[]>([]);
10
  const [loading, setLoading] = useState(true);
11
  const [editingUser, setEditingUser] = useState<User | null>(null);
 
 
12
 
13
  const loadData = async () => {
14
  setLoading(true);
15
  try {
16
- const [u, s] = await Promise.all([api.users.getAll(), api.schools.getAll()]);
 
 
 
 
17
  setUsers(u);
18
  setSchools(s);
19
  } catch (e) { console.error(e); }
@@ -55,7 +61,10 @@ export const UserList: React.FC = () => {
55
 
56
  return (
57
  <div className="bg-white rounded-xl shadow-sm border border-gray-100 p-6">
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]">
@@ -71,13 +80,15 @@ export const UserList: React.FC = () => {
71
  </tr>
72
  </thead>
73
  <tbody className="divide-y divide-gray-100">
74
- {users.map(user => (
 
 
75
  <tr key={user._id || user.id} className="hover:bg-gray-50">
76
  <td className="px-4 py-3">
77
  <div className="flex items-center space-x-3">
78
  <img src={user.avatar || `https://api.dicebear.com/7.x/avataaars/svg?seed=${user.username}`} className="w-8 h-8 rounded-full" />
79
  <div>
80
- <p className="font-bold text-gray-800 text-sm">{user.trueName || user.username}</p>
81
  <p className="text-xs text-gray-500">@{user.username}</p>
82
  </div>
83
  </div>
@@ -87,7 +98,7 @@ export const UserList: React.FC = () => {
87
  <div className="text-xs text-gray-400">{user.email}</div>
88
  </td>
89
  <td className="px-4 py-3 text-sm">
90
- {editingUser?._id === user._id ? (
91
  <select
92
  className="border rounded p-1 text-xs w-32"
93
  value={user.schoolId}
@@ -98,9 +109,9 @@ export const UserList: React.FC = () => {
98
  {schools.map(s => <option key={s._id} value={s._id}>{s.name}</option>)}
99
  </select>
100
  ) : (
101
- <div className="flex items-center space-x-1 group cursor-pointer" onClick={() => setEditingUser(user)}>
102
  <span className="text-gray-700">{getSchoolName(user.schoolId)}</span>
103
- <Edit size={12} className="text-gray-300 opacity-0 group-hover:opacity-100"/>
104
  </div>
105
  )}
106
  </td>
@@ -120,8 +131,8 @@ export const UserList: React.FC = () => {
120
  <select
121
  value={user.role}
122
  onChange={(e) => handleRoleChange(user, e.target.value as UserRole)}
123
- disabled={user.username === 'admin'}
124
- className="bg-white border border-gray-200 rounded px-2 py-1 text-xs focus:ring-2 focus:ring-blue-500"
125
  >
126
  <option value={UserRole.ADMIN}>管理员</option>
127
  <option value={UserRole.TEACHER}>教师</option>
@@ -137,13 +148,20 @@ export const UserList: React.FC = () => {
137
  {user.status === UserStatus.PENDING && (
138
  <button onClick={() => handleApprove(user)} className="text-green-600 hover:bg-green-50 p-1 rounded" title="通过审核"><Check size={18} /></button>
139
  )}
140
- <button onClick={() => handleDelete(user)} className="text-red-600 hover:bg-red-50 p-1 rounded" title="删除" disabled={user.username === 'admin'}><Trash2 size={18} /></button>
 
 
 
 
 
 
 
141
  </td>
142
  </tr>
143
- ))}
144
  </tbody>
145
  </table>
146
  </div>
147
  </div>
148
  );
149
- };
 
9
  const [schools, setSchools] = useState<School[]>([]);
10
  const [loading, setLoading] = useState(true);
11
  const [editingUser, setEditingUser] = useState<User | null>(null);
12
+
13
+ const currentUser = api.auth.getCurrentUser();
14
 
15
  const loadData = async () => {
16
  setLoading(true);
17
  try {
18
+ // Load ALL users globally, bypassing school filter
19
+ const [u, s] = await Promise.all([
20
+ api.users.getAll({ global: true }),
21
+ api.schools.getAll()
22
+ ]);
23
  setUsers(u);
24
  setSchools(s);
25
  } catch (e) { console.error(e); }
 
61
 
62
  return (
63
  <div className="bg-white rounded-xl shadow-sm border border-gray-100 p-6">
64
+ <div className="flex justify-between items-center mb-4">
65
+ <h2 className="text-xl font-bold text-gray-800">用户权限与审核</h2>
66
+ <span className="text-xs text-gray-500 bg-gray-100 px-2 py-1 rounded">全局视图 (显示所有学校用户)</span>
67
+ </div>
68
 
69
  <div className="overflow-x-auto">
70
  <table className="w-full text-left border-collapse min-w-[1000px]">
 
80
  </tr>
81
  </thead>
82
  <tbody className="divide-y divide-gray-100">
83
+ {users.map(user => {
84
+ const isSelf = currentUser && (user._id === currentUser._id || user.username === currentUser.username);
85
+ return (
86
  <tr key={user._id || user.id} className="hover:bg-gray-50">
87
  <td className="px-4 py-3">
88
  <div className="flex items-center space-x-3">
89
  <img src={user.avatar || `https://api.dicebear.com/7.x/avataaars/svg?seed=${user.username}`} className="w-8 h-8 rounded-full" />
90
  <div>
91
+ <p className="font-bold text-gray-800 text-sm">{user.trueName || user.username} {isSelf && '(我)'}</p>
92
  <p className="text-xs text-gray-500">@{user.username}</p>
93
  </div>
94
  </div>
 
98
  <div className="text-xs text-gray-400">{user.email}</div>
99
  </td>
100
  <td className="px-4 py-3 text-sm">
101
+ {editingUser?._id === user._id && !isSelf ? (
102
  <select
103
  className="border rounded p-1 text-xs w-32"
104
  value={user.schoolId}
 
109
  {schools.map(s => <option key={s._id} value={s._id}>{s.name}</option>)}
110
  </select>
111
  ) : (
112
+ <div className={`flex items-center space-x-1 group ${!isSelf ? 'cursor-pointer' : ''}`} onClick={() => !isSelf && setEditingUser(user)}>
113
  <span className="text-gray-700">{getSchoolName(user.schoolId)}</span>
114
+ {!isSelf && <Edit size={12} className="text-gray-300 opacity-0 group-hover:opacity-100"/>}
115
  </div>
116
  )}
117
  </td>
 
131
  <select
132
  value={user.role}
133
  onChange={(e) => handleRoleChange(user, e.target.value as UserRole)}
134
+ disabled={user.username === 'admin' || isSelf} // Cannot change self or super admin
135
+ className="bg-white border border-gray-200 rounded px-2 py-1 text-xs focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100 disabled:text-gray-400"
136
  >
137
  <option value={UserRole.ADMIN}>管理员</option>
138
  <option value={UserRole.TEACHER}>教师</option>
 
148
  {user.status === UserStatus.PENDING && (
149
  <button onClick={() => handleApprove(user)} className="text-green-600 hover:bg-green-50 p-1 rounded" title="通过审核"><Check size={18} /></button>
150
  )}
151
+ <button
152
+ onClick={() => handleDelete(user)}
153
+ className={`p-1 rounded ${user.username === 'admin' || isSelf ? 'text-gray-300 cursor-not-allowed' : 'text-red-600 hover:bg-red-50'}`}
154
+ title="删除"
155
+ disabled={user.username === 'admin' || isSelf}
156
+ >
157
+ <Trash2 size={18} />
158
+ </button>
159
  </td>
160
  </tr>
161
+ );})}
162
  </tbody>
163
  </table>
164
  </div>
165
  </div>
166
  );
167
+ };
server.js CHANGED
@@ -147,6 +147,7 @@ const ConfigSchema = new mongoose.Schema({
147
  systemName: String,
148
  semester: String,
149
  allowRegister: Boolean,
 
150
  maintenanceMode: Boolean,
151
  emailNotify: Boolean
152
  });
@@ -223,6 +224,14 @@ app.get('/api/public/schools', async (req, res) => {
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: [] });
@@ -306,8 +315,14 @@ app.put('/api/schools/:id', async (req, res) => {
306
 
307
  // --- Users ---
308
  app.get('/api/users', async (req, res) => {
309
- const filter = getQueryFilter(req);
310
- if (InMemoryDB.isFallback) return res.json(InMemoryDB.users.filter(u => !filter.schoolId || u.schoolId === filter.schoolId));
 
 
 
 
 
 
311
  const users = await User.find(filter).sort({ createTime: -1 });
312
  res.json(users);
313
  });
@@ -327,10 +342,6 @@ app.put('/api/users/:id', async (req, res) => {
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
 
@@ -515,4 +526,4 @@ app.get('*', (req, res) => {
515
  res.sendFile(path.join(__dirname, 'dist', 'index.html'));
516
  });
517
 
518
- app.listen(PORT, () => console.log(`🚀 Service running on port ${PORT}`));
 
147
  systemName: String,
148
  semester: String,
149
  allowRegister: Boolean,
150
+ allowAdminRegister: { type: Boolean, default: false }, // New
151
  maintenanceMode: Boolean,
152
  emailNotify: Boolean
153
  });
 
224
  res.json(schools);
225
  });
226
 
227
+ app.get('/api/public/config', async (req, res) => {
228
+ // Try to find any config or default
229
+ if (InMemoryDB.isFallback) return res.json({ allowRegister: true, allowAdminRegister: true });
230
+ // In a real multi-tenant system, config might depend on domain, but here we pick the first available or default
231
+ const config = await ConfigModel.findOne() || { allowRegister: true, allowAdminRegister: false };
232
+ res.json(config);
233
+ });
234
+
235
  app.get('/api/public/meta', async (req, res) => {
236
  const { schoolId } = req.query;
237
  if (!schoolId) return res.json({ classes: [], subjects: [] });
 
315
 
316
  // --- Users ---
317
  app.get('/api/users', async (req, res) => {
318
+ // If global=true is passed, ignore school filter (for User Management page)
319
+ const { global } = req.query;
320
+ const filter = global === 'true' ? {} : getQueryFilter(req);
321
+
322
+ if (InMemoryDB.isFallback) {
323
+ if (global === 'true') return res.json(InMemoryDB.users);
324
+ return res.json(InMemoryDB.users.filter(u => !filter.schoolId || u.schoolId === filter.schoolId));
325
+ }
326
  const users = await User.find(filter).sort({ createTime: -1 });
327
  res.json(users);
328
  });
 
342
  // Linkage Logic: If approving a teacher, and they are a homeroom teacher, update the Class record.
343
  if (updateData.status === 'active' && user.role === 'TEACHER' && user.homeroomClass) {
344
  // Find Class by name (e.g. "六年级(1)班") and SchoolId
 
 
 
 
345
  const classes = await ClassModel.find({ schoolId: user.schoolId });
346
  const targetClass = classes.find(c => (c.grade + c.className) === user.homeroomClass);
347
 
 
526
  res.sendFile(path.join(__dirname, 'dist', 'index.html'));
527
  });
528
 
529
+ app.listen(PORT, () => console.log(`🚀 Service running on port ${PORT}`));
services/api.ts CHANGED
@@ -92,7 +92,8 @@ export const api = {
92
  },
93
 
94
  users: {
95
- getAll: () => request('/users'),
 
96
  update: (id: string, data: Partial<User>) => request(`/users/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
97
  delete: (id: string) => request(`/users/${id}`, { method: 'DELETE' })
98
  },
@@ -141,6 +142,7 @@ export const api = {
141
 
142
  config: {
143
  get: () => request('/config'),
 
144
  save: (data: SystemConfig) => request('/config', { method: 'POST', body: JSON.stringify(data) })
145
  },
146
 
 
92
  },
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
  },
 
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
  },
148
 
types.ts CHANGED
@@ -61,6 +61,7 @@ export interface SystemConfig {
61
  systemName: string;
62
  semester: string;
63
  allowRegister: boolean;
 
64
  maintenanceMode: boolean;
65
  emailNotify: boolean;
66
  }
@@ -121,4 +122,4 @@ export interface ApiResponse<T> {
121
  message: string;
122
  data: T;
123
  timestamp: number;
124
- }
 
61
  systemName: string;
62
  semester: string;
63
  allowRegister: boolean;
64
+ allowAdminRegister: boolean; // New: Admin registration toggle
65
  maintenanceMode: boolean;
66
  emailNotify: boolean;
67
  }
 
122
  message: string;
123
  data: T;
124
  timestamp: number;
125
+ }