dvc890 commited on
Commit
cdd802a
·
verified ·
1 Parent(s): 5ee975f

Upload 27 files

Browse files
App.tsx CHANGED
@@ -1,21 +1,23 @@
1
 
2
- import React, { useState, useEffect } from 'react';
3
  import { Sidebar } from './components/Sidebar';
4
  import { Header } from './components/Header';
5
- import { Dashboard } from './pages/Dashboard';
6
- import { StudentList } from './pages/StudentList';
7
- import { CourseList } from './pages/CourseList';
8
- import { ScoreList } from './pages/ScoreList';
9
- import { ClassList } from './pages/ClassList';
10
- import { Settings } from './pages/Settings';
11
- import { Reports } from './pages/Reports';
12
- import { SubjectList } from './pages/SubjectList';
13
- import { UserList } from './pages/UserList';
14
- import { SchoolList } from './pages/SchoolList';
 
 
15
  import { Login } from './pages/Login';
16
  import { User, UserRole } from './types';
17
  import { api } from './services/api';
18
- import { AlertTriangle } from 'lucide-react';
19
 
20
  // Error Boundary Component
21
  class ErrorBoundary extends React.Component<{children: React.ReactNode}, {hasError: boolean, error: Error | null}> {
@@ -68,6 +70,15 @@ class ErrorBoundary extends React.Component<{children: React.ReactNode}, {hasErr
68
  }
69
  }
70
 
 
 
 
 
 
 
 
 
 
71
  const AppContent: React.FC = () => {
72
  const [isAuthenticated, setIsAuthenticated] = useState(false);
73
  const [currentUser, setCurrentUser] = useState<User | null>(null);
@@ -108,7 +119,7 @@ const AppContent: React.FC = () => {
108
  case 'reports': return <Reports />;
109
  case 'subjects': return <SubjectList />;
110
  case 'users': return <UserList />;
111
- case 'schools': return <SchoolList />; // New Route
112
  default: return <Dashboard onNavigate={(view) => setCurrentView(view)} />;
113
  }
114
  };
@@ -149,7 +160,9 @@ const AppContent: React.FC = () => {
149
  />
150
  <main className="flex-1 overflow-x-hidden overflow-y-auto bg-gray-50 p-4 md:p-6 w-full">
151
  <div className="max-w-7xl mx-auto w-full">
152
- {renderContent()}
 
 
153
  </div>
154
  </main>
155
  </div>
 
1
 
2
+ import React, { useState, useEffect, Suspense } from 'react';
3
  import { Sidebar } from './components/Sidebar';
4
  import { Header } from './components/Header';
5
+ // Lazy load pages for performance optimization
6
+ const Dashboard = React.lazy(() => import('./pages/Dashboard').then(module => ({ default: module.Dashboard })));
7
+ const StudentList = React.lazy(() => import('./pages/StudentList').then(module => ({ default: module.StudentList })));
8
+ const CourseList = React.lazy(() => import('./pages/CourseList').then(module => ({ default: module.CourseList })));
9
+ const ScoreList = React.lazy(() => import('./pages/ScoreList').then(module => ({ default: module.ScoreList })));
10
+ const ClassList = React.lazy(() => import('./pages/ClassList').then(module => ({ default: module.ClassList })));
11
+ const Settings = React.lazy(() => import('./pages/Settings').then(module => ({ default: module.Settings })));
12
+ const Reports = React.lazy(() => import('./pages/Reports').then(module => ({ default: module.Reports })));
13
+ const SubjectList = React.lazy(() => import('./pages/SubjectList').then(module => ({ default: module.SubjectList })));
14
+ const UserList = React.lazy(() => import('./pages/UserList').then(module => ({ default: module.UserList })));
15
+ const SchoolList = React.lazy(() => import('./pages/SchoolList').then(module => ({ default: module.SchoolList })));
16
+
17
  import { Login } from './pages/Login';
18
  import { User, UserRole } from './types';
19
  import { api } from './services/api';
20
+ import { AlertTriangle, Loader2 } from 'lucide-react';
21
 
22
  // Error Boundary Component
23
  class ErrorBoundary extends React.Component<{children: React.ReactNode}, {hasError: boolean, error: Error | null}> {
 
70
  }
71
  }
72
 
73
+ const PageLoading = () => (
74
+ <div className="flex h-full w-full items-center justify-center min-h-[400px]">
75
+ <div className="flex flex-col items-center space-y-3">
76
+ <Loader2 className="h-8 w-8 animate-spin text-blue-600" />
77
+ <p className="text-sm text-gray-400">资源加载中...</p>
78
+ </div>
79
+ </div>
80
+ );
81
+
82
  const AppContent: React.FC = () => {
83
  const [isAuthenticated, setIsAuthenticated] = useState(false);
84
  const [currentUser, setCurrentUser] = useState<User | null>(null);
 
119
  case 'reports': return <Reports />;
120
  case 'subjects': return <SubjectList />;
121
  case 'users': return <UserList />;
122
+ case 'schools': return <SchoolList />;
123
  default: return <Dashboard onNavigate={(view) => setCurrentView(view)} />;
124
  }
125
  };
 
160
  />
161
  <main className="flex-1 overflow-x-hidden overflow-y-auto bg-gray-50 p-4 md:p-6 w-full">
162
  <div className="max-w-7xl mx-auto w-full">
163
+ <Suspense fallback={<PageLoading />}>
164
+ {renderContent()}
165
+ </Suspense>
166
  </div>
167
  </main>
168
  </div>
components/Sidebar.tsx CHANGED
@@ -1,3 +1,4 @@
 
1
  import React from 'react';
2
  import { LayoutDashboard, Users, BookOpen, GraduationCap, Settings, LogOut, FileText, School, UserCog, Palette, X, Building } from 'lucide-react';
3
  import { UserRole } from '../types';
@@ -21,7 +22,8 @@ export const Sidebar: React.FC<SidebarProps> = ({ currentView, onChangeView, use
21
  { id: 'grades', label: '成绩管理', icon: GraduationCap, roles: [UserRole.ADMIN, UserRole.TEACHER, UserRole.STUDENT] },
22
  // Update: Allow TEACHER to access reports
23
  { id: 'reports', label: '报表统计', icon: FileText, roles: [UserRole.ADMIN, UserRole.TEACHER] },
24
- { id: 'subjects', label: '学科设置', icon: Palette, roles: [UserRole.ADMIN] },
 
25
  { id: 'users', label: '用户管理', icon: UserCog, roles: [UserRole.ADMIN] },
26
  { id: 'settings', label: '系统设置', icon: Settings, roles: [UserRole.ADMIN] },
27
  ];
@@ -80,4 +82,4 @@ export const Sidebar: React.FC<SidebarProps> = ({ currentView, onChangeView, use
80
  </div>
81
  </>
82
  );
83
- };
 
1
+
2
  import React from 'react';
3
  import { LayoutDashboard, Users, BookOpen, GraduationCap, Settings, LogOut, FileText, School, UserCog, Palette, X, Building } from 'lucide-react';
4
  import { UserRole } from '../types';
 
22
  { id: 'grades', label: '成绩管理', icon: GraduationCap, roles: [UserRole.ADMIN, UserRole.TEACHER, UserRole.STUDENT] },
23
  // Update: Allow TEACHER to access reports
24
  { id: 'reports', label: '报表统计', icon: FileText, roles: [UserRole.ADMIN, UserRole.TEACHER] },
25
+ // Update: Allow TEACHER to access subjects (limited view)
26
+ { id: 'subjects', label: '学科设置', icon: Palette, roles: [UserRole.ADMIN, UserRole.TEACHER] },
27
  { id: 'users', label: '用户管理', icon: UserCog, roles: [UserRole.ADMIN] },
28
  { id: 'settings', label: '系统设置', icon: Settings, roles: [UserRole.ADMIN] },
29
  ];
 
82
  </div>
83
  </>
84
  );
85
+ };
metadata.json CHANGED
@@ -1,5 +1,5 @@
1
  {
2
- "name": "智慧校园管理系统",
3
  "description": "一个综合性的学生管理系统仪表板,具有基于角色的访问控制、学生档案、课程管理和绩效分析功能。",
4
  "requestFramePermissions": []
5
  }
 
1
  {
2
+ "name": "Copy of 智慧校园管理系统",
3
  "description": "一个综合性的学生管理系统仪表板,具有基于角色的访问控制、学生档案、课程管理和绩效分析功能。",
4
  "requestFramePermissions": []
5
  }
pages/Dashboard.tsx CHANGED
@@ -1,3 +1,4 @@
 
1
  import React, { useEffect, useState } from 'react';
2
  import { Users, BookOpen, GraduationCap, TrendingUp, AlertTriangle, ArrowRight, Activity, Calendar, X, CheckCircle, Plus, Trash2 } from 'lucide-react';
3
  import { api } from '../services/api';
@@ -8,10 +9,23 @@ interface DashboardProps {
8
  onNavigate: (view: string) => void;
9
  }
10
 
11
- // 排序辅助函数
12
- const gradeOrder: Record<string, number> = { '一年级': 1, '二年级': 2, '三年级': 3, '四年级': 4, '五年级': 5, '六年级': 6, '初一': 7, '七年级': 7, '初二': 8, '八年级': 8, '初三': 9, '九年级': 9, '高一': 10, '高二': 11, '高三': 12 };
13
- const sortGrades = (a: string, b: string) => (gradeOrder[a] || 99) - (gradeOrder[b] || 99);
14
- const sortClasses = (a: string, b: string) => a.localeCompare(b, 'zh-CN', { numeric: true });
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
  export const Dashboard: React.FC<DashboardProps> = ({ onNavigate }) => {
17
  const [stats, setStats] = useState({ studentCount: 0, courseCount: 0, avgScore: 0, excellentRate: '0%' });
@@ -28,10 +42,8 @@ export const Dashboard: React.FC<DashboardProps> = ({ onNavigate }) => {
28
 
29
  // 课表视图状态
30
  const [viewGrade, setViewGrade] = useState(''); // 管理员年级视角
31
- // 注意:viewClass 状态移除,改为在排课弹窗中临时选择
32
 
33
  const [editingCell, setEditingCell] = useState<{day: number, period: number} | null>(null);
34
- // 排课表单增加 className 字段
35
  const [editForm, setEditForm] = useState({ className: '', subject: '', teacherName: '' });
36
 
37
  // Chart Data
@@ -39,6 +51,7 @@ export const Dashboard: React.FC<DashboardProps> = ({ onNavigate }) => {
39
 
40
  const currentUser = api.auth.getCurrentUser();
41
  const isAdmin = currentUser?.role === 'ADMIN';
 
42
 
43
  useEffect(() => {
44
  const loadData = async () => {
@@ -58,7 +71,6 @@ export const Dashboard: React.FC<DashboardProps> = ({ onNavigate }) => {
58
 
59
  // 如果是管理员,且没有选年级,默认选中第一个有数据的年级
60
  if (isAdmin && classes.length > 0) {
61
- // Explicitly cast mapped array to string[] to fix TS error
62
  const grades = Array.from(new Set((classes as ClassInfo[]).map((c: ClassInfo) => c.grade))).sort(sortGrades);
63
  if (grades.length > 0) {
64
  setViewGrade(grades[0] as string);
@@ -68,7 +80,6 @@ export const Dashboard: React.FC<DashboardProps> = ({ onNavigate }) => {
68
  // Generate Warnings (Simplified)
69
  const newWarnings: string[] = [];
70
  subs.forEach((sub: Subject) => {
71
- // Explicitly cast scores
72
  const subScores = (scores as Score[]).filter((s: Score) => s.courseName === sub.name && s.status === 'Normal');
73
  if (subScores.length > 0) {
74
  const avg = subScores.reduce((a: number, b: Score) => a + b.score, 0) / subScores.length;
@@ -119,19 +130,24 @@ export const Dashboard: React.FC<DashboardProps> = ({ onNavigate }) => {
119
  } catch(e) { console.error(e); }
120
  };
121
 
 
 
 
 
 
 
 
 
 
122
  const handleSaveSchedule = async () => {
123
  if (!editingCell) return;
124
 
125
  // 校验
126
- if (isAdmin && !editForm.className) return alert('请选择班级');
127
  if (!editForm.subject) return alert('请选择科目');
128
  if (!editForm.teacherName) return alert('请选择任课教师');
129
 
130
- // 如果是老师排课(虽然通常不开放),班级可能是空的,这里假设老师排课时不需要选班级或者逻辑不同
131
- // 但为了兼容管理员排班逻辑,这里主要处理管理员
132
-
133
- // 构造班级全名:如果是管理员模式,editForm.className 只是 "(1)班",需要拼上年级 viewGrade
134
- // 但为了方便,下拉框可以直接存全名 "六年级(1)班"
135
  const targetClassName = editForm.className;
136
 
137
  await api.schedules.save({
@@ -156,14 +172,15 @@ export const Dashboard: React.FC<DashboardProps> = ({ onNavigate }) => {
156
  fetchSchedules();
157
  };
158
 
159
- // 排序后的年级选项
160
  const uniqueGrades = Array.from(new Set(classList.map(c => c.grade))).sort(sortGrades);
161
 
162
- // 当前选中年级的所有班级(用于排课下拉框)
163
- const currentGradeClasses = classList
164
- .filter(c => c.grade === viewGrade)
165
- .map(c => c.grade + c.className)
166
- .sort(sortClasses);
 
167
 
168
  const cards = [
169
  { label: '在校学生', value: stats.studentCount, icon: Users, color: 'bg-blue-500', trend: '+12%' },
@@ -280,7 +297,7 @@ export const Dashboard: React.FC<DashboardProps> = ({ onNavigate }) => {
280
  </div>
281
  )}
282
 
283
- {!isAdmin && currentUser?.role === 'TEACHER' && (
284
  <div className="text-sm text-gray-500">
285
  查看: <span className="font-bold text-gray-800">{currentUser.trueName || currentUser.username}</span> 的课表
286
  </div>
@@ -303,8 +320,9 @@ export const Dashboard: React.FC<DashboardProps> = ({ onNavigate }) => {
303
  <td className="p-4 border-b border-r font-bold text-gray-400 bg-gray-50/50">第{period}节</td>
304
  {[1,2,3,4,5].map(day => {
305
  const slotItems = schedules.filter(s => s.dayOfWeek === day && s.period === period);
306
- // 是否为管理员(始终可以编辑)或老师(看情况,暂定老师只能看)
307
- const canAdd = isAdmin;
 
308
 
309
  return (
310
  <td
@@ -312,43 +330,39 @@ export const Dashboard: React.FC<DashboardProps> = ({ onNavigate }) => {
312
  className="p-2 border-b h-28 align-top transition-colors relative group"
313
  >
314
  <div className="flex flex-wrap gap-2 content-start h-full overflow-y-auto custom-scrollbar pb-6">
315
- {slotItems.map(item => (
316
- <div key={item._id} className="text-xs text-left bg-white border border-blue-200 rounded-md p-2 w-full shadow-sm relative group/item hover:border-blue-400 hover:shadow-md transition-all">
317
- <div className="font-bold text-blue-700 flex justify-between items-center mb-1">
318
- <span className="truncate mr-1">{item.subject}</span>
319
- {/* 删除按钮 */}
320
- {isAdmin && (
321
- <button
322
- onClick={(e)=>{ e.stopPropagation(); handleDeleteSchedule(item); }}
323
- className="text-gray-300 hover:text-red-500 transition-colors p-0.5"
324
- title="删除课程"
325
- >
326
- <X size={14}/>
327
- </button>
328
- )}
 
 
 
 
 
 
 
 
 
 
329
  </div>
330
- <div className="flex justify-between items-center text-gray-600">
331
- {/* 显示班级名(去除年级前缀,保持简洁)和教师 */}
332
- <span className="bg-blue-50 text-blue-600 px-1.5 rounded font-mono text-[10px] font-bold">
333
- {item.className.replace(viewGrade, '')}
334
- </span>
335
- <span className="text-[10px] truncate max-w-[50%]">{item.teacherName}</span>
336
- </div>
337
- </div>
338
- ))}
339
 
340
- {/* Add Button (Only for Admin) */}
341
  {canAdd && (
342
  <div className="absolute bottom-1 right-1 left-1 flex justify-center opacity-0 group-hover:opacity-100 transition-opacity">
343
  <button
344
- onClick={() => {
345
- setEditingCell({ day, period });
346
- setEditForm({
347
- className: '', // 需要在弹窗选
348
- subject: '',
349
- teacherName: ''
350
- });
351
- }}
352
  className="w-full bg-blue-50 border border-blue-200 text-blue-500 rounded py-1 hover:bg-blue-100 flex items-center justify-center shadow-sm"
353
  >
354
  <Plus size={14}/>
@@ -371,28 +385,31 @@ export const Dashboard: React.FC<DashboardProps> = ({ onNavigate }) => {
371
  <div className="absolute inset-0 bg-black/20 flex items-center justify-center backdrop-blur-[1px] z-50">
372
  <div className="bg-white p-6 rounded-xl shadow-2xl w-80 animate-in zoom-in-95 border border-gray-100">
373
  <h4 className="font-bold mb-4 text-gray-800 border-b pb-2">
374
- 排课信息 <br/>
375
- <span className="text-sm font-normal text-gray-500">{viewGrade} 周{['一','二','三','四','五'][editingCell.day-1]} 第{editingCell.period}节</span>
376
  </h4>
377
  <div className="space-y-4">
378
- {/* 班级选择 (仅管理员) */}
379
- {isAdmin && (
380
- <div>
381
- <label className="text-xs font-bold text-gray-500 uppercase mb-1 block">班级</label>
382
- <select
383
- className="w-full border border-gray-300 rounded-lg p-2.5 text-sm focus:ring-2 focus:ring-blue-500 outline-none"
384
- value={editForm.className}
385
- onChange={e=>setEditForm({...editForm, className: e.target.value})}
386
- >
387
- <option value="">-- 请选择班级 --</option>
388
- {currentGradeClasses.map(c => <option key={c} value={c}>{c}</option>)}
389
- </select>
390
- </div>
391
- )}
392
 
393
  <div>
394
  <label className="text-xs font-bold text-gray-500 uppercase mb-1 block">科目</label>
395
- <select className="w-full border border-gray-300 rounded-lg p-2.5 text-sm focus:ring-2 focus:ring-blue-500 outline-none" value={editForm.subject} onChange={e=>setEditForm({...editForm, subject: e.target.value})}>
 
 
 
 
 
396
  <option value="">-- 请选择科目 --</option>
397
  {subjects.map(s=><option key={s._id} value={s.name}>{s.name}</option>)}
398
  </select>
@@ -400,7 +417,12 @@ export const Dashboard: React.FC<DashboardProps> = ({ onNavigate }) => {
400
 
401
  <div>
402
  <label className="text-xs font-bold text-gray-500 uppercase mb-1 block">任课教师</label>
403
- <select className="w-full border border-gray-300 rounded-lg p-2.5 text-sm focus:ring-2 focus:ring-blue-500 outline-none" value={editForm.teacherName} onChange={e=>setEditForm({...editForm, teacherName: e.target.value})}>
 
 
 
 
 
404
  <option value="">-- 请选择教师 --</option>
405
  {teachers.map(t=><option key={t._id} value={t.trueName || t.username}>{t.trueName} ({t.teachingSubject || '无科目'})</option>)}
406
  </select>
@@ -433,4 +455,4 @@ export const Dashboard: React.FC<DashboardProps> = ({ onNavigate }) => {
433
  )}
434
  </div>
435
  );
436
- };
 
1
+
2
  import React, { useEffect, useState } from 'react';
3
  import { Users, BookOpen, GraduationCap, TrendingUp, AlertTriangle, ArrowRight, Activity, Calendar, X, CheckCircle, Plus, Trash2 } from 'lucide-react';
4
  import { api } from '../services/api';
 
9
  onNavigate: (view: string) => void;
10
  }
11
 
12
+ // 优化后的年级排序:支持 K12 全学段中文习惯排序
13
+ export const gradeOrder: Record<string, number> = {
14
+ '一年级': 1, '二年级': 2, '三年级': 3, '四年级': 4, '五年级': 5, '六年级': 6,
15
+ '初一': 7, '七年级': 7, '初二': 8, '八年级': 8, '初三': 9, '九年级': 9,
16
+ '高一': 10, '高二': 11, '高三': 12
17
+ };
18
+ export const sortGrades = (a: string, b: string) => (gradeOrder[a] || 99) - (gradeOrder[b] || 99);
19
+ export const sortClasses = (a: string, b: string) => {
20
+ // 提取年级部分
21
+ const getGrade = (s: string) => Object.keys(gradeOrder).find(g => s.startsWith(g)) || '';
22
+ const gradeA = getGrade(a);
23
+ const gradeB = getGrade(b);
24
+ if (gradeA !== gradeB) return sortGrades(gradeA, gradeB);
25
+ // 年级相同,比较班级号 (假设格式如 (1)班, 1班)
26
+ const getNum = (s: string) => parseInt(s.replace(/[^0-9]/g, '')) || 0;
27
+ return getNum(a) - getNum(b);
28
+ };
29
 
30
  export const Dashboard: React.FC<DashboardProps> = ({ onNavigate }) => {
31
  const [stats, setStats] = useState({ studentCount: 0, courseCount: 0, avgScore: 0, excellentRate: '0%' });
 
42
 
43
  // 课表视图状态
44
  const [viewGrade, setViewGrade] = useState(''); // 管理员年级视角
 
45
 
46
  const [editingCell, setEditingCell] = useState<{day: number, period: number} | null>(null);
 
47
  const [editForm, setEditForm] = useState({ className: '', subject: '', teacherName: '' });
48
 
49
  // Chart Data
 
51
 
52
  const currentUser = api.auth.getCurrentUser();
53
  const isAdmin = currentUser?.role === 'ADMIN';
54
+ const isTeacher = currentUser?.role === 'TEACHER';
55
 
56
  useEffect(() => {
57
  const loadData = async () => {
 
71
 
72
  // 如果是管理员,且没有选年级,默认选中第一个有数据的年级
73
  if (isAdmin && classes.length > 0) {
 
74
  const grades = Array.from(new Set((classes as ClassInfo[]).map((c: ClassInfo) => c.grade))).sort(sortGrades);
75
  if (grades.length > 0) {
76
  setViewGrade(grades[0] as string);
 
80
  // Generate Warnings (Simplified)
81
  const newWarnings: string[] = [];
82
  subs.forEach((sub: Subject) => {
 
83
  const subScores = (scores as Score[]).filter((s: Score) => s.courseName === sub.name && s.status === 'Normal');
84
  if (subScores.length > 0) {
85
  const avg = subScores.reduce((a: number, b: Score) => a + b.score, 0) / subScores.length;
 
130
  } catch(e) { console.error(e); }
131
  };
132
 
133
+ const handleOpenAddModal = (day: number, period: number) => {
134
+ setEditingCell({ day, period });
135
+ setEditForm({
136
+ className: '', // 需选择
137
+ subject: isTeacher ? (currentUser?.teachingSubject || '') : '', // 老师自动填科目
138
+ teacherName: isTeacher ? (currentUser?.trueName || currentUser?.username || '') : '' // 老师自动填名字
139
+ });
140
+ };
141
+
142
  const handleSaveSchedule = async () => {
143
  if (!editingCell) return;
144
 
145
  // 校验
146
+ if (!editForm.className) return alert('请选择班级');
147
  if (!editForm.subject) return alert('请选择科目');
148
  if (!editForm.teacherName) return alert('请选择任课教师');
149
 
150
+ // 构造班级全名
 
 
 
 
151
  const targetClassName = editForm.className;
152
 
153
  await api.schedules.save({
 
172
  fetchSchedules();
173
  };
174
 
175
+ // 排序后的年级选项 (Admin Only)
176
  const uniqueGrades = Array.from(new Set(classList.map(c => c.grade))).sort(sortGrades);
177
 
178
+ // 弹窗中可选的班级列表:
179
+ // 1. 如果是管理员,显示当前选中年级 viewGrade 的班级 (保持上下文一致)
180
+ // 2. 如果是老师,显示全校所有班级 (因为老师可能跨年级授课)
181
+ const modalClassOptions = isAdmin
182
+ ? classList.filter(c => c.grade === viewGrade).map(c => c.grade + c.className).sort(sortClasses)
183
+ : classList.map(c => c.grade + c.className).sort(sortClasses);
184
 
185
  const cards = [
186
  { label: '在校学生', value: stats.studentCount, icon: Users, color: 'bg-blue-500', trend: '+12%' },
 
297
  </div>
298
  )}
299
 
300
+ {!isAdmin && isTeacher && (
301
  <div className="text-sm text-gray-500">
302
  查看: <span className="font-bold text-gray-800">{currentUser.trueName || currentUser.username}</span> 的课表
303
  </div>
 
320
  <td className="p-4 border-b border-r font-bold text-gray-400 bg-gray-50/50">第{period}节</td>
321
  {[1,2,3,4,5].map(day => {
322
  const slotItems = schedules.filter(s => s.dayOfWeek === day && s.period === period);
323
+ // Update: Admins OR Teachers can add/edit
324
+ // Teacher can add slots for themselves
325
+ const canAdd = isAdmin || isTeacher;
326
 
327
  return (
328
  <td
 
330
  className="p-2 border-b h-28 align-top transition-colors relative group"
331
  >
332
  <div className="flex flex-wrap gap-2 content-start h-full overflow-y-auto custom-scrollbar pb-6">
333
+ {slotItems.map(item => {
334
+ // Teacher can only delete their own lessons
335
+ const canDelete = isAdmin || (isTeacher && (item.teacherName === currentUser.trueName || item.teacherName === currentUser.username));
336
+ return (
337
+ <div key={item._id} className="text-xs text-left bg-white border border-blue-200 rounded-md p-2 w-full shadow-sm relative group/item hover:border-blue-400 hover:shadow-md transition-all">
338
+ <div className="font-bold text-blue-700 flex justify-between items-center mb-1">
339
+ <span className="truncate mr-1">{item.subject}</span>
340
+ {canDelete && (
341
+ <button
342
+ onClick={(e)=>{ e.stopPropagation(); handleDeleteSchedule(item); }}
343
+ className="text-gray-300 hover:text-red-500 transition-colors p-0.5"
344
+ title="删除课程"
345
+ >
346
+ <X size={14}/>
347
+ </button>
348
+ )}
349
+ </div>
350
+ <div className="flex justify-between items-center text-gray-600">
351
+ {/* 显示班级名 */}
352
+ <span className="bg-blue-50 text-blue-600 px-1.5 rounded font-mono text-[10px] font-bold">
353
+ {item.className.replace(viewGrade, '')}
354
+ </span>
355
+ <span className="text-[10px] truncate max-w-[50%]">{item.teacherName}</span>
356
+ </div>
357
  </div>
358
+ );
359
+ })}
 
 
 
 
 
 
 
360
 
361
+ {/* Add Button */}
362
  {canAdd && (
363
  <div className="absolute bottom-1 right-1 left-1 flex justify-center opacity-0 group-hover:opacity-100 transition-opacity">
364
  <button
365
+ onClick={() => handleOpenAddModal(day, period)}
 
 
 
 
 
 
 
366
  className="w-full bg-blue-50 border border-blue-200 text-blue-500 rounded py-1 hover:bg-blue-100 flex items-center justify-center shadow-sm"
367
  >
368
  <Plus size={14}/>
 
385
  <div className="absolute inset-0 bg-black/20 flex items-center justify-center backdrop-blur-[1px] z-50">
386
  <div className="bg-white p-6 rounded-xl shadow-2xl w-80 animate-in zoom-in-95 border border-gray-100">
387
  <h4 className="font-bold mb-4 text-gray-800 border-b pb-2">
388
+ {isTeacher ? '我的排课' : '排课管理'} <br/>
389
+ <span className="text-sm font-normal text-gray-500">周{['一','二','三','四','五'][editingCell.day-1]} 第{editingCell.period}节</span>
390
  </h4>
391
  <div className="space-y-4">
392
+ {/* 班级选择: Admin 选择 viewGrade 下的班级,Teacher 可以选全校班级 */}
393
+ <div>
394
+ <label className="text-xs font-bold text-gray-500 uppercase mb-1 block">班级</label>
395
+ <select
396
+ className="w-full border border-gray-300 rounded-lg p-2.5 text-sm focus:ring-2 focus:ring-blue-500 outline-none"
397
+ value={editForm.className}
398
+ onChange={e=>setEditForm({...editForm, className: e.target.value})}
399
+ >
400
+ <option value="">-- 请选择班级 --</option>
401
+ {modalClassOptions.map(c => <option key={c} value={c}>{c}</option>)}
402
+ </select>
403
+ </div>
 
 
404
 
405
  <div>
406
  <label className="text-xs font-bold text-gray-500 uppercase mb-1 block">科目</label>
407
+ <select
408
+ className="w-full border border-gray-300 rounded-lg p-2.5 text-sm focus:ring-2 focus:ring-blue-500 outline-none disabled:bg-gray-100 disabled:text-gray-500"
409
+ value={editForm.subject}
410
+ onChange={e=>setEditForm({...editForm, subject: e.target.value})}
411
+ disabled={isTeacher && !!currentUser?.teachingSubject} // 老师如果有任教科目则锁定
412
+ >
413
  <option value="">-- 请选择科目 --</option>
414
  {subjects.map(s=><option key={s._id} value={s.name}>{s.name}</option>)}
415
  </select>
 
417
 
418
  <div>
419
  <label className="text-xs font-bold text-gray-500 uppercase mb-1 block">任课教师</label>
420
+ <select
421
+ className="w-full border border-gray-300 rounded-lg p-2.5 text-sm focus:ring-2 focus:ring-blue-500 outline-none disabled:bg-gray-100 disabled:text-gray-500"
422
+ value={editForm.teacherName}
423
+ onChange={e=>setEditForm({...editForm, teacherName: e.target.value})}
424
+ disabled={isTeacher} // 老师只能给自己排课
425
+ >
426
  <option value="">-- 请选择教师 --</option>
427
  {teachers.map(t=><option key={t._id} value={t.trueName || t.username}>{t.trueName} ({t.teachingSubject || '无科目'})</option>)}
428
  </select>
 
455
  )}
456
  </div>
457
  );
458
+ };
pages/Reports.tsx CHANGED
@@ -1,3 +1,4 @@
 
1
  import React, { useState, useEffect } from 'react';
2
  import {
3
  BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer,
@@ -7,6 +8,12 @@ import { api } from '../services/api';
7
  import { Loader2, Download, Filter, TrendingUp, Grid, BarChart2, User, PieChart as PieChartIcon } from 'lucide-react';
8
  import { Score, Student, ClassInfo, Subject, Exam } from '../types';
9
 
 
 
 
 
 
 
10
  export const Reports: React.FC = () => {
11
  const [loading, setLoading] = useState(true);
12
  const [activeTab, setActiveTab] = useState<'overview' | 'grade' | 'trend' | 'matrix' | 'student'>('overview');
@@ -80,7 +87,7 @@ export const Reports: React.FC = () => {
80
  const passRate = normalScores.length ? (totalPass / normalScores.length)*100 : 0;
81
 
82
  // Grade Ladder (Avg score by grade)
83
- const uniqueGradesList = Array.from(new Set(classes.map(c => c.grade))).sort();
84
  const ladderData = uniqueGradesList.map(g => {
85
  const gradeClasses = classes.filter(c => c.grade === g).map(c => c.grade+c.className);
86
  const gradeStus = students.filter(s => gradeClasses.includes(s.className)).map(s => s.studentNo);
@@ -258,7 +265,7 @@ export const Reports: React.FC = () => {
258
  };
259
  };
260
 
261
- const uniqueGrades = Array.from(new Set(classes.map(c => c.grade))).sort();
262
  const allClasses = classes.map(c => c.grade + c.className);
263
 
264
  // Student List for Focus Tab
@@ -604,4 +611,4 @@ export const Reports: React.FC = () => {
604
  )}
605
  </div>
606
  );
607
- };
 
1
+
2
  import React, { useState, useEffect } from 'react';
3
  import {
4
  BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer,
 
8
  import { Loader2, Download, Filter, TrendingUp, Grid, BarChart2, User, PieChart as PieChartIcon } from 'lucide-react';
9
  import { Score, Student, ClassInfo, Subject, Exam } from '../types';
10
 
11
+ // Reuse sort logic
12
+ const localSortGrades = (a: string, b: string) => {
13
+ const order: Record<string, number> = { '一年级': 1, '二年级': 2, '三年级': 3, '四年级': 4, '五年级': 5, '六年级': 6, '初一': 7, '七年级': 7, '初二': 8, '八年级': 8, '初三': 9, '九年级': 9, '高一': 10, '高二': 11, '高三': 12 };
14
+ return (order[a] || 99) - (order[b] || 99);
15
+ };
16
+
17
  export const Reports: React.FC = () => {
18
  const [loading, setLoading] = useState(true);
19
  const [activeTab, setActiveTab] = useState<'overview' | 'grade' | 'trend' | 'matrix' | 'student'>('overview');
 
87
  const passRate = normalScores.length ? (totalPass / normalScores.length)*100 : 0;
88
 
89
  // Grade Ladder (Avg score by grade)
90
+ const uniqueGradesList = Array.from(new Set(classes.map(c => c.grade))).sort(localSortGrades);
91
  const ladderData = uniqueGradesList.map(g => {
92
  const gradeClasses = classes.filter(c => c.grade === g).map(c => c.grade+c.className);
93
  const gradeStus = students.filter(s => gradeClasses.includes(s.className)).map(s => s.studentNo);
 
265
  };
266
  };
267
 
268
+ const uniqueGrades = Array.from(new Set(classes.map(c => c.grade))).sort(localSortGrades);
269
  const allClasses = classes.map(c => c.grade + c.className);
270
 
271
  // Student List for Focus Tab
 
611
  )}
612
  </div>
613
  );
614
+ };
pages/ScoreList.tsx CHANGED
@@ -1,8 +1,15 @@
 
1
  import React, { useState, useEffect } from 'react';
2
  import { api } from '../services/api';
3
  import { Score, Student, Subject, ClassInfo, ExamStatus, Exam, SystemConfig } from '../types';
4
  import { Loader2, Plus, Trash2, Award, FileSpreadsheet, X, Upload, Edit, Save, Calendar } from 'lucide-react';
5
 
 
 
 
 
 
 
6
  export const ScoreList: React.FC = () => {
7
  // ... state ...
8
  const [subjects, setSubjects] = useState<Subject[]>([]);
@@ -178,7 +185,10 @@ export const ScoreList: React.FC = () => {
178
 
179
  const handleUpdateExamDate = async (name: string, date: string) => { await api.exams.save({ name, date }); loadData(); };
180
  const toggleSelect = (id: string) => { const newSet = new Set(selectedIds); if (newSet.has(id)) newSet.delete(id); else newSet.add(id); setSelectedIds(newSet); };
181
- const uniqueGrades = Array.from(new Set(classList.map(c => c.grade))).sort();
 
 
 
182
  const filteredClasses = classList.filter(c => selectedGrade === 'All' || c.grade === selectedGrade);
183
  const uniqueClasses = Array.from(new Set(filteredClasses.map(c => c.className))).sort();
184
  const uniqueExamNames = Array.from(new Set(scores.map(s => s.examName || s.type)));
@@ -405,4 +415,4 @@ export const ScoreList: React.FC = () => {
405
  )}
406
  </div>
407
  );
408
- };
 
1
+
2
  import React, { useState, useEffect } from 'react';
3
  import { api } from '../services/api';
4
  import { Score, Student, Subject, ClassInfo, ExamStatus, Exam, SystemConfig } from '../types';
5
  import { Loader2, Plus, Trash2, Award, FileSpreadsheet, X, Upload, Edit, Save, Calendar } from 'lucide-react';
6
 
7
+ // Reuse sort helper
8
+ const localSortGrades = (a: string, b: string) => {
9
+ const order: Record<string, number> = { '一年级': 1, '二年级': 2, '三年级': 3, '四年级': 4, '五年级': 5, '六年级': 6, '初一': 7, '七年级': 7, '初二': 8, '八年级': 8, '初三': 9, '九年级': 9, '高一': 10, '高二': 11, '高三': 12 };
10
+ return (order[a] || 99) - (order[b] || 99);
11
+ };
12
+
13
  export const ScoreList: React.FC = () => {
14
  // ... state ...
15
  const [subjects, setSubjects] = useState<Subject[]>([]);
 
185
 
186
  const handleUpdateExamDate = async (name: string, date: string) => { await api.exams.save({ name, date }); loadData(); };
187
  const toggleSelect = (id: string) => { const newSet = new Set(selectedIds); if (newSet.has(id)) newSet.delete(id); else newSet.add(id); setSelectedIds(newSet); };
188
+
189
+ // SORTED Grades
190
+ const uniqueGrades = Array.from(new Set(classList.map(c => c.grade))).sort(localSortGrades);
191
+
192
  const filteredClasses = classList.filter(c => selectedGrade === 'All' || c.grade === selectedGrade);
193
  const uniqueClasses = Array.from(new Set(filteredClasses.map(c => c.className))).sort();
194
  const uniqueExamNames = Array.from(new Set(scores.map(s => s.examName || s.type)));
 
415
  )}
416
  </div>
417
  );
418
+ };
pages/StudentList.tsx CHANGED
@@ -1,10 +1,17 @@
 
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';
4
  import { Student, ClassInfo } from '../types';
 
 
 
 
 
 
 
5
 
6
  export const StudentList: React.FC = () => {
7
- // ... (你的 state 定义保持不变) ...
8
  const [searchTerm, setSearchTerm] = useState('');
9
  const [students, setStudents] = useState<Student[]>([]);
10
  const [classList, setClassList] = useState<ClassInfo[]>([]);
@@ -15,6 +22,9 @@ export const StudentList: React.FC = () => {
15
  const [submitting, setSubmitting] = useState(false);
16
  const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
17
 
 
 
 
18
  // Filters
19
  const [selectedGrade, setSelectedGrade] = useState('All');
20
  const [selectedClass, setSelectedClass] = useState('All');
@@ -24,7 +34,7 @@ export const StudentList: React.FC = () => {
24
  const [importTargetClass, setImportTargetClass] = useState('');
25
 
26
  // Form State
27
- const [formData, setFormData] = useState({
28
  name: '',
29
  studentNo: '',
30
  gender: 'Male',
@@ -34,7 +44,8 @@ export const StudentList: React.FC = () => {
34
  parentName: '',
35
  parentPhone: '',
36
  address: ''
37
- });
 
38
 
39
  const currentUser = api.auth.getCurrentUser();
40
 
@@ -56,19 +67,14 @@ export const StudentList: React.FC = () => {
56
 
57
  useEffect(() => { loadData(); }, []);
58
 
59
- // --------------- 修复点开始 ---------------
60
- // 这里删除了原来的 return false; };
61
- // 并添加了 hasPermission 函数定义
62
  const hasPermission = (className: string) => {
63
  if (!currentUser) return false;
64
  if (currentUser.role === 'ADMIN') return true;
65
- // 如果是老师,检查是否是班主任
66
  if (currentUser.role === 'TEACHER') {
67
  return currentUser.homeroomClass === className;
68
  }
69
  return false;
70
  };
71
- // --------------- 修复点结束 ---------------
72
 
73
  const handleDelete = async (id: string, className: string) => {
74
  if (!hasPermission(className)) return alert('您没有权限操作此班级学生');
@@ -102,19 +108,48 @@ export const StudentList: React.FC = () => {
102
  }
103
  };
104
 
105
- const handleAddSubmit = async (e: React.FormEvent) => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
  e.preventDefault();
107
  setSubmitting(true);
108
  try {
109
- await api.students.add({
110
  ...formData,
111
- birthday: '2015-01-01',
112
  status: 'Enrolled',
113
  gender: formData.gender as any
114
- });
 
 
 
 
 
 
115
  setIsModalOpen(false);
116
  loadData();
117
- } catch (error) { alert('添加失败'); }
118
  finally { setSubmitting(false); }
119
  };
120
 
@@ -181,7 +216,6 @@ export const StudentList: React.FC = () => {
181
 
182
  const targetClassName = importTargetClass || getVal(['班级', 'Class'], 99) || '未分配';
183
 
184
- // 使用我们修复好的 hasPermission 函数
185
  if (!hasPermission(targetClassName)) {
186
  console.warn('Skipping import for class due to permission:', targetClassName);
187
  continue;
@@ -221,9 +255,9 @@ export const StudentList: React.FC = () => {
221
  return matchesSearch && matchesGrade && matchesClass;
222
  });
223
 
224
- const uniqueGrades = Array.from(new Set(classList.map(c => c.grade))).sort();
225
  const filteredClasses = classList.filter(c => selectedGrade === 'All' || c.grade === selectedGrade);
226
- const uniqueClasses = Array.from(new Set(filteredClasses.map(c => c.className))).sort();
227
 
228
  const canGlobalAdd = currentUser?.role === 'ADMIN' || (currentUser?.role === 'TEACHER' && currentUser.homeroomClass);
229
 
@@ -242,7 +276,7 @@ export const StudentList: React.FC = () => {
242
  <button onClick={() => { setIsImportOpen(true); setImportFile(null); }} className="flex-1 md:flex-none btn-secondary flex items-center justify-center space-x-2 px-3 py-2 border rounded-lg text-sm bg-emerald-50 text-emerald-600 hover:bg-emerald-100 border-emerald-200">
243
  <FileSpreadsheet size={16}/><span>Excel导入</span>
244
  </button>
245
- <button onClick={() => setIsModalOpen(true)} className="flex-1 md:flex-none btn-primary flex items-center justify-center space-x-2 px-3 py-2 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700">
246
  <Plus size={16}/><span>新增</span>
247
  </button>
248
  </div>
@@ -276,13 +310,17 @@ export const StudentList: React.FC = () => {
276
  <th className="px-6 py-3">基本信息</th>
277
  <th className="px-6 py-3">学号</th>
278
  <th className="px-6 py-3">班级</th>
 
 
279
  </tr>
280
  </thead>
281
  <tbody className="divide-y divide-gray-100">
282
- {filteredStudents.length > 0 ? filteredStudents.map(s => (
 
 
283
  <tr key={s._id || s.id} className="hover:bg-blue-50/30 transition-colors">
284
  <td className="px-6 py-4">
285
- {hasPermission(s.className) && <input type="checkbox" checked={selectedIds.has(s._id || String(s.id))} onChange={() => toggleSelect(s._id || String(s.id))} />}
286
  </td>
287
  <td className="px-6 py-4 flex items-center space-x-3">
288
  <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>
@@ -303,16 +341,16 @@ export const StudentList: React.FC = () => {
303
  </div>
304
  ) : '-'}
305
  </td>
306
- <td className="px-6 py-4 text-right">
307
- {hasPermission(s.className) ? (
308
- <button onClick={() => handleDelete(s._id || String(s.id), s.className)} className="text-red-400 hover:text-red-600"><Trash2 size={16}/></button>
309
-
310
-
311
-
312
  ) : <span className="text-gray-300 text-xs">只读</span>}
313
  </td>
314
  </tr>
315
- )) : (
316
  <tr>
317
  <td colSpan={6} className="text-center py-10 text-gray-400">没有找到匹配的学生</td>
318
  </tr>
@@ -328,12 +366,12 @@ export const StudentList: React.FC = () => {
328
  </div>
329
  )}
330
 
331
- {/* Modal and Import Modal logic remains same, just ensure class selection in Add Modal respects permission */}
332
  {isModalOpen && (
333
  <div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
334
  <div className="bg-white rounded-xl p-6 w-full max-w-lg max-h-[90vh] overflow-y-auto">
335
- <h3 className="font-bold text-lg mb-4">新增学生档案</h3>
336
- <form onSubmit={handleAddSubmit} className="space-y-4">
337
  <div className="grid grid-cols-2 gap-4">
338
  <input className="w-full border p-2 rounded" placeholder="姓名 *" value={formData.name} onChange={e=>setFormData({...formData, name:e.target.value})} required/>
339
  <input className="w-full border p-2 rounded" placeholder="学号 *" value={formData.studentNo} onChange={e=>setFormData({...formData, studentNo:e.target.value})} required/>
@@ -364,8 +402,6 @@ export const StudentList: React.FC = () => {
364
  </div>
365
  <button type="submit" disabled={submitting} className="w-full bg-blue-600 text-white py-2 rounded mt-4">{submitting?'提交中':'保存'}</button>
366
  <button type="button" onClick={()=>setIsModalOpen(false)} className="w-full border py-2 rounded mt-2">取消</button>
367
-
368
-
369
  </form>
370
  </div>
371
  </div>
@@ -411,4 +447,4 @@ export const StudentList: React.FC = () => {
411
  )}
412
  </div>
413
  );
414
- };
 
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';
5
  import { Student, ClassInfo } from '../types';
6
+ import { sortGrades, sortClasses } from './Dashboard'; // Reuse Sort Logic if possible, otherwise define locally
7
+
8
+ // Re-define sort logic locally to be safe if imports fail or circular dependency risks
9
+ const localSortGrades = (a: string, b: string) => {
10
+ const order: Record<string, number> = { '一年级': 1, '二年级': 2, '三年级': 3, '四年级': 4, '五年级': 5, '六年级': 6, '初一': 7, '七年级': 7, '初二': 8, '八年级': 8, '初三': 9, '九年级': 9, '高一': 10, '高二': 11, '高三': 12 };
11
+ return (order[a] || 99) - (order[b] || 99);
12
+ };
13
 
14
  export const StudentList: React.FC = () => {
 
15
  const [searchTerm, setSearchTerm] = useState('');
16
  const [students, setStudents] = useState<Student[]>([]);
17
  const [classList, setClassList] = useState<ClassInfo[]>([]);
 
22
  const [submitting, setSubmitting] = useState(false);
23
  const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
24
 
25
+ // Edit Mode State
26
+ const [editStudentId, setEditStudentId] = useState<string | null>(null);
27
+
28
  // Filters
29
  const [selectedGrade, setSelectedGrade] = useState('All');
30
  const [selectedClass, setSelectedClass] = useState('All');
 
34
  const [importTargetClass, setImportTargetClass] = useState('');
35
 
36
  // Form State
37
+ const initialForm = {
38
  name: '',
39
  studentNo: '',
40
  gender: 'Male',
 
44
  parentName: '',
45
  parentPhone: '',
46
  address: ''
47
+ };
48
+ const [formData, setFormData] = useState(initialForm);
49
 
50
  const currentUser = api.auth.getCurrentUser();
51
 
 
67
 
68
  useEffect(() => { loadData(); }, []);
69
 
 
 
 
70
  const hasPermission = (className: string) => {
71
  if (!currentUser) return false;
72
  if (currentUser.role === 'ADMIN') return true;
 
73
  if (currentUser.role === 'TEACHER') {
74
  return currentUser.homeroomClass === className;
75
  }
76
  return false;
77
  };
 
78
 
79
  const handleDelete = async (id: string, className: string) => {
80
  if (!hasPermission(className)) return alert('您没有权限操作此班级学生');
 
108
  }
109
  };
110
 
111
+ const handleOpenAdd = () => {
112
+ setEditStudentId(null);
113
+ setFormData(initialForm);
114
+ setIsModalOpen(true);
115
+ };
116
+
117
+ const handleOpenEdit = (student: Student) => {
118
+ if (!hasPermission(student.className)) return alert('无权编辑此学生');
119
+ setEditStudentId(student._id || String(student.id));
120
+ setFormData({
121
+ name: student.name,
122
+ studentNo: student.studentNo,
123
+ gender: (student.gender === 'Female' ? 'Female' : 'Male'), // Ensure type match
124
+ className: student.className,
125
+ phone: student.phone || '',
126
+ idCard: student.idCard || '',
127
+ parentName: student.parentName || '',
128
+ parentPhone: student.parentPhone || '',
129
+ address: student.address || ''
130
+ });
131
+ setIsModalOpen(true);
132
+ };
133
+
134
+ const handleSubmit = async (e: React.FormEvent) => {
135
  e.preventDefault();
136
  setSubmitting(true);
137
  try {
138
+ const payload = {
139
  ...formData,
140
+ birthday: '2015-01-01', // Default for now, could add to form
141
  status: 'Enrolled',
142
  gender: formData.gender as any
143
+ };
144
+
145
+ if (editStudentId) {
146
+ await api.students.update(editStudentId, payload);
147
+ } else {
148
+ await api.students.add(payload);
149
+ }
150
  setIsModalOpen(false);
151
  loadData();
152
+ } catch (error) { alert(editStudentId ? '更新失败' : '添加失败'); }
153
  finally { setSubmitting(false); }
154
  };
155
 
 
216
 
217
  const targetClassName = importTargetClass || getVal(['班级', 'Class'], 99) || '未分配';
218
 
 
219
  if (!hasPermission(targetClassName)) {
220
  console.warn('Skipping import for class due to permission:', targetClassName);
221
  continue;
 
255
  return matchesSearch && matchesGrade && matchesClass;
256
  });
257
 
258
+ const uniqueGrades = Array.from(new Set(classList.map(c => c.grade))).sort(localSortGrades);
259
  const filteredClasses = classList.filter(c => selectedGrade === 'All' || c.grade === selectedGrade);
260
+ const uniqueClasses = Array.from(new Set(filteredClasses.map(c => c.className))).sort(); // Class names usually 1班 2班 sort naturally
261
 
262
  const canGlobalAdd = currentUser?.role === 'ADMIN' || (currentUser?.role === 'TEACHER' && currentUser.homeroomClass);
263
 
 
276
  <button onClick={() => { setIsImportOpen(true); setImportFile(null); }} className="flex-1 md:flex-none btn-secondary flex items-center justify-center space-x-2 px-3 py-2 border rounded-lg text-sm bg-emerald-50 text-emerald-600 hover:bg-emerald-100 border-emerald-200">
277
  <FileSpreadsheet size={16}/><span>Excel导入</span>
278
  </button>
279
+ <button onClick={handleOpenAdd} className="flex-1 md:flex-none btn-primary flex items-center justify-center space-x-2 px-3 py-2 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700">
280
  <Plus size={16}/><span>新增</span>
281
  </button>
282
  </div>
 
310
  <th className="px-6 py-3">基本信息</th>
311
  <th className="px-6 py-3">学号</th>
312
  <th className="px-6 py-3">班级</th>
313
+ <th className="px-6 py-3">家长/住址</th>
314
+ <th className="px-6 py-3 text-right">操作</th>
315
  </tr>
316
  </thead>
317
  <tbody className="divide-y divide-gray-100">
318
+ {filteredStudents.length > 0 ? filteredStudents.map(s => {
319
+ const canEdit = hasPermission(s.className);
320
+ return (
321
  <tr key={s._id || s.id} className="hover:bg-blue-50/30 transition-colors">
322
  <td className="px-6 py-4">
323
+ {canEdit && <input type="checkbox" checked={selectedIds.has(s._id || String(s.id))} onChange={() => toggleSelect(s._id || String(s.id))} />}
324
  </td>
325
  <td className="px-6 py-4 flex items-center space-x-3">
326
  <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>
 
341
  </div>
342
  ) : '-'}
343
  </td>
344
+ <td className="px-6 py-4 text-right flex justify-end space-x-2">
345
+ {canEdit ? (
346
+ <>
347
+ <button onClick={() => handleOpenEdit(s)} className="text-blue-400 hover:text-blue-600" title="编辑"><Edit size={16}/></button>
348
+ <button onClick={() => handleDelete(s._id || String(s.id), s.className)} className="text-red-400 hover:text-red-600" title="删除"><Trash2 size={16}/></button>
349
+ </>
350
  ) : <span className="text-gray-300 text-xs">只读</span>}
351
  </td>
352
  </tr>
353
+ )}) : (
354
  <tr>
355
  <td colSpan={6} className="text-center py-10 text-gray-400">没有找到匹配的学生</td>
356
  </tr>
 
366
  </div>
367
  )}
368
 
369
+ {/* Modal */}
370
  {isModalOpen && (
371
  <div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
372
  <div className="bg-white rounded-xl p-6 w-full max-w-lg max-h-[90vh] overflow-y-auto">
373
+ <h3 className="font-bold text-lg mb-4">{editStudentId ? '编辑学生档案' : '新增学生档案'}</h3>
374
+ <form onSubmit={handleSubmit} className="space-y-4">
375
  <div className="grid grid-cols-2 gap-4">
376
  <input className="w-full border p-2 rounded" placeholder="姓名 *" value={formData.name} onChange={e=>setFormData({...formData, name:e.target.value})} required/>
377
  <input className="w-full border p-2 rounded" placeholder="学号 *" value={formData.studentNo} onChange={e=>setFormData({...formData, studentNo:e.target.value})} required/>
 
402
  </div>
403
  <button type="submit" disabled={submitting} className="w-full bg-blue-600 text-white py-2 rounded mt-4">{submitting?'提交中':'保存'}</button>
404
  <button type="button" onClick={()=>setIsModalOpen(false)} className="w-full border py-2 rounded mt-2">取消</button>
 
 
405
  </form>
406
  </div>
407
  </div>
 
447
  )}
448
  </div>
449
  );
450
+ };
pages/SubjectList.tsx CHANGED
@@ -2,11 +2,13 @@
2
  import React, { useState, useEffect } from 'react';
3
  import { Subject } from '../types';
4
  import { api } from '../services/api';
5
- import { Loader2, Plus, Trash2, Palette, Edit, Save, X } from 'lucide-react';
6
 
7
  export const SubjectList: React.FC = () => {
8
  const [subjects, setSubjects] = useState<Subject[]>([]);
9
  const [loading, setLoading] = useState(true);
 
 
10
 
11
  // State for adding new subject
12
  const [newSub, setNewSub] = useState({ name: '', code: '', color: '#3b82f6', excellenceThreshold: 90 });
@@ -55,6 +57,14 @@ export const SubjectList: React.FC = () => {
55
  }
56
  };
57
 
 
 
 
 
 
 
 
 
58
  if(loading) return <div className="flex justify-center p-10"><Loader2 className="animate-spin text-blue-600"/></div>;
59
 
60
  return (
@@ -65,7 +75,15 @@ export const SubjectList: React.FC = () => {
65
  学科设置
66
  </h2>
67
 
68
- {/* Add Form */}
 
 
 
 
 
 
 
 
69
  <div className="flex flex-col md:flex-row gap-4 mb-8 bg-gray-50 p-4 rounded-lg md:items-end">
70
  <div className="flex-1">
71
  <label className="text-xs font-bold text-gray-500 uppercase">学科名称</label>
@@ -95,38 +113,75 @@ export const SubjectList: React.FC = () => {
95
  <Plus size={16} className="mr-1" /> 添加
96
  </button>
97
  </div>
 
98
 
99
  {/* List */}
100
  <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
101
  {subjects.map(sub => {
102
  const isEditing = editingId === (sub._id || String(sub.id));
 
 
103
  return (
104
  <div key={sub._id || sub.id} className="border border-gray-200 rounded-lg p-4 bg-white shadow-sm hover:shadow-md transition-shadow">
105
  {isEditing ? (
106
  <div className="space-y-3">
107
- <input className="w-full border p-1 rounded text-sm" value={editForm.name} onChange={e => setEditForm({...editForm, name: e.target.value})} />
108
- <div className="flex gap-2">
109
- <input className="w-1/2 border p-1 rounded text-sm" placeholder="代码" value={editForm.code} onChange={e => setEditForm({...editForm, code: e.target.value})} />
110
- <input className="w-1/2 border p-1 rounded text-sm" type="number" placeholder="优秀线" value={editForm.excellenceThreshold} onChange={e => setEditForm({...editForm, excellenceThreshold: Number(e.target.value)})} />
 
 
 
 
 
111
  </div>
 
112
  <div className="flex gap-2">
113
- <input type="color" className="h-8 flex-1 cursor-pointer" value={editForm.color} onChange={e => setEditForm({...editForm, color: e.target.value})} />
114
- <button onClick={handleUpdate} className="text-green-600 p-1 border rounded hover:bg-green-50"><Save size={16}/></button>
115
- <button onClick={() => setEditingId(null)} className="text-gray-500 p-1 border rounded hover:bg-gray-50"><X size={16}/></button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
  </div>
117
  </div>
118
  ) : (
119
- <div className="flex justify-between items-center">
120
  <div className="flex items-center space-x-3">
121
- <div className="w-4 h-4 rounded-full" style={{ backgroundColor: sub.color }}></div>
122
  <div>
123
  <p className="font-bold text-gray-800">{sub.name}</p>
124
- <p className="text-xs text-gray-500">{sub.code || '-'} | 优秀线: {sub.excellenceThreshold || 90}</p>
125
  </div>
126
  </div>
127
  <div className="flex space-x-1">
128
- <button onClick={() => handleEdit(sub)} className="text-gray-400 hover:text-blue-500 p-1"><Edit size={16} /></button>
129
- <button onClick={() => handleDelete(sub._id || String(sub.id))} className="text-gray-400 hover:text-red-500 p-1"><Trash2 size={16} /></button>
 
130
  </div>
131
  </div>
132
  )}
 
2
  import React, { useState, useEffect } from 'react';
3
  import { Subject } from '../types';
4
  import { api } from '../services/api';
5
+ import { Loader2, Plus, Trash2, Palette, Edit, Save, X, Lock } from 'lucide-react';
6
 
7
  export const SubjectList: React.FC = () => {
8
  const [subjects, setSubjects] = useState<Subject[]>([]);
9
  const [loading, setLoading] = useState(true);
10
+ const currentUser = api.auth.getCurrentUser();
11
+ const isAdmin = currentUser?.role === 'ADMIN';
12
 
13
  // State for adding new subject
14
  const [newSub, setNewSub] = useState({ name: '', code: '', color: '#3b82f6', excellenceThreshold: 90 });
 
57
  }
58
  };
59
 
60
+ // Permission Check for Teachers
61
+ const canEditSubject = (sub: Subject) => {
62
+ if (isAdmin) return true;
63
+ // Teacher can only edit if it matches their teaching subject
64
+ if (currentUser?.role === 'TEACHER' && currentUser.teachingSubject === sub.name) return true;
65
+ return false;
66
+ };
67
+
68
  if(loading) return <div className="flex justify-center p-10"><Loader2 className="animate-spin text-blue-600"/></div>;
69
 
70
  return (
 
75
  学科设置
76
  </h2>
77
 
78
+ {!isAdmin && (
79
+ <div className="bg-blue-50 text-blue-700 px-4 py-2 rounded-lg text-sm mb-4 border border-blue-100 flex items-center">
80
+ <Lock size={14} className="mr-2"/>
81
+ 教师权限提示:您仅可调整自己任教科目 ({currentUser?.teachingSubject || '无'}) 的“优秀线”标准,其他信息只读。
82
+ </div>
83
+ )}
84
+
85
+ {/* Add Form (Only Admin) */}
86
+ {isAdmin && (
87
  <div className="flex flex-col md:flex-row gap-4 mb-8 bg-gray-50 p-4 rounded-lg md:items-end">
88
  <div className="flex-1">
89
  <label className="text-xs font-bold text-gray-500 uppercase">学科名称</label>
 
113
  <Plus size={16} className="mr-1" /> 添加
114
  </button>
115
  </div>
116
+ )}
117
 
118
  {/* List */}
119
  <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
120
  {subjects.map(sub => {
121
  const isEditing = editingId === (sub._id || String(sub.id));
122
+ const canEdit = canEditSubject(sub);
123
+
124
  return (
125
  <div key={sub._id || sub.id} className="border border-gray-200 rounded-lg p-4 bg-white shadow-sm hover:shadow-md transition-shadow">
126
  {isEditing ? (
127
  <div className="space-y-3">
128
+ {/* Admin gets full edit, Teacher gets limited edit */}
129
+ <div className="space-y-1">
130
+ <label className="text-xs text-gray-400">学科名称</label>
131
+ <input
132
+ className={`w-full border p-1 rounded text-sm ${!isAdmin ? 'bg-gray-100 text-gray-500' : ''}`}
133
+ value={editForm.name}
134
+ onChange={e => setEditForm({...editForm, name: e.target.value})}
135
+ disabled={!isAdmin}
136
+ />
137
  </div>
138
+
139
  <div className="flex gap-2">
140
+ <div className="w-1/2 space-y-1">
141
+ <label className="text-xs text-gray-400">代码</label>
142
+ <input
143
+ className={`w-full border p-1 rounded text-sm ${!isAdmin ? 'bg-gray-100 text-gray-500' : ''}`}
144
+ placeholder="代码"
145
+ value={editForm.code}
146
+ onChange={e => setEditForm({...editForm, code: e.target.value})}
147
+ disabled={!isAdmin}
148
+ />
149
+ </div>
150
+ <div className="w-1/2 space-y-1">
151
+ <label className="text-xs text-blue-600 font-bold">优秀线</label>
152
+ <input
153
+ className="w-full border p-1 rounded text-sm border-blue-300 focus:ring-1 focus:ring-blue-500"
154
+ type="number"
155
+ placeholder="优秀线"
156
+ value={editForm.excellenceThreshold}
157
+ onChange={e => setEditForm({...editForm, excellenceThreshold: Number(e.target.value)})}
158
+ autoFocus={!isAdmin}
159
+ />
160
+ </div>
161
+ </div>
162
+
163
+ <div className="flex gap-2 items-end">
164
+ <div className="flex-1">
165
+ <label className="text-xs text-gray-400 block mb-1">颜色</label>
166
+ <input type="color" className="h-8 w-full cursor-pointer" value={editForm.color} onChange={e => setEditForm({...editForm, color: e.target.value})} disabled={!isAdmin} />
167
+ </div>
168
+ <button onClick={handleUpdate} className="text-green-600 p-1.5 border rounded hover:bg-green-50"><Save size={16}/></button>
169
+ <button onClick={() => setEditingId(null)} className="text-gray-500 p-1.5 border rounded hover:bg-gray-50"><X size={16}/></button>
170
  </div>
171
  </div>
172
  ) : (
173
+ <div className="flex justify-between items-center h-full">
174
  <div className="flex items-center space-x-3">
175
+ <div className="w-4 h-4 rounded-full shadow-sm" style={{ backgroundColor: sub.color }}></div>
176
  <div>
177
  <p className="font-bold text-gray-800">{sub.name}</p>
178
+ <p className="text-xs text-gray-500">{sub.code || '-'} | 优秀线: <span className="font-bold text-blue-600">{sub.excellenceThreshold || 90}</span></p>
179
  </div>
180
  </div>
181
  <div className="flex space-x-1">
182
+ {canEdit && <button onClick={() => handleEdit(sub)} className="text-gray-400 hover:text-blue-500 p-1" title="编辑"><Edit size={16} /></button>}
183
+ {isAdmin && <button onClick={() => handleDelete(sub._id || String(sub.id))} className="text-gray-400 hover:text-red-500 p-1" title="删除"><Trash2 size={16} /></button>}
184
+ {!canEdit && !isAdmin && <span className="text-xs text-gray-300">只读</span>}
185
  </div>
186
  </div>
187
  )}