dvc890 commited on
Commit
8661c24
·
verified ·
1 Parent(s): ece8a3e

Upload 25 files

Browse files
Files changed (8) hide show
  1. App.tsx +3 -62
  2. README.md +23 -3
  3. index.html +5 -4
  4. pages/ScoreList.tsx +27 -57
  5. server.js +1 -2
  6. services/api.ts +4 -8
  7. types.ts +0 -1
  8. vite.config.ts +1 -11
App.tsx CHANGED
@@ -1,3 +1,4 @@
 
1
  import React, { useState, useEffect } from 'react';
2
  import { Sidebar } from './components/Sidebar';
3
  import { Header } from './components/Header';
@@ -13,60 +14,8 @@ import { UserList } from './pages/UserList';
13
  import { Login } from './pages/Login';
14
  import { User, UserRole } from './types';
15
  import { api } from './services/api';
16
- import { AlertTriangle } from 'lucide-react';
17
-
18
- // Error Boundary Component
19
- class ErrorBoundary extends React.Component<{children: React.ReactNode}, {hasError: boolean, error: Error | null}> {
20
- constructor(props: any) {
21
- super(props);
22
- this.state = { hasError: false, error: null };
23
- }
24
-
25
- static getDerivedStateFromError(error: Error) {
26
- return { hasError: true, error };
27
- }
28
-
29
- componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
30
- console.error("Uncaught error:", error, errorInfo);
31
- }
32
-
33
- render() {
34
- if (this.state.hasError) {
35
- return (
36
- <div className="min-h-screen flex items-center justify-center bg-gray-50 p-4">
37
- <div className="bg-white p-8 rounded-xl shadow-lg max-w-lg w-full text-center">
38
- <div className="mx-auto flex h-16 w-16 items-center justify-center rounded-full bg-red-100 mb-4">
39
- <AlertTriangle className="h-8 w-8 text-red-600" />
40
- </div>
41
- <h2 className="text-2xl font-bold text-gray-900 mb-2">哎呀,页面出错了</h2>
42
- <p className="text-gray-500 mb-6">系统遇到了一些非预期的问题。通常这可能是网络波动或缓存问题导致的。</p>
43
-
44
- <div className="bg-gray-50 p-4 rounded text-left text-xs text-red-500 overflow-auto max-h-40 mb-6 border border-red-100 font-mono">
45
- {this.state.error?.message || 'Unknown Error'}
46
- </div>
47
-
48
- <button
49
- onClick={() => window.location.reload()}
50
- className="w-full px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors font-medium"
51
- >
52
- 刷新页面重试
53
- </button>
54
- <button
55
- onClick={() => { localStorage.clear(); window.location.reload(); }}
56
- className="mt-3 w-full text-sm text-gray-400 hover:text-gray-600 underline"
57
- >
58
- 清除缓存并重置 (解决白屏)
59
- </button>
60
- </div>
61
- </div>
62
- );
63
- }
64
 
65
- return this.props.children;
66
- }
67
- }
68
-
69
- const AppContent: React.FC = () => {
70
  const [isAuthenticated, setIsAuthenticated] = useState(false);
71
  const [currentUser, setCurrentUser] = useState<User | null>(null);
72
  const [currentView, setCurrentView] = useState('dashboard');
@@ -145,12 +94,4 @@ const AppContent: React.FC = () => {
145
  );
146
  };
147
 
148
- const App: React.FC = () => {
149
- return (
150
- <ErrorBoundary>
151
- <AppContent />
152
- </ErrorBoundary>
153
- );
154
- };
155
-
156
- export default App;
 
1
+
2
  import React, { useState, useEffect } from 'react';
3
  import { Sidebar } from './components/Sidebar';
4
  import { Header } from './components/Header';
 
14
  import { Login } from './pages/Login';
15
  import { User, UserRole } from './types';
16
  import { api } from './services/api';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
 
18
+ const App: React.FC = () => {
 
 
 
 
19
  const [isAuthenticated, setIsAuthenticated] = useState(false);
20
  const [currentUser, setCurrentUser] = useState<User | null>(null);
21
  const [currentView, setCurrentView] = useState('dashboard');
 
94
  );
95
  };
96
 
97
+ export default App;
 
 
 
 
 
 
 
 
README.md CHANGED
@@ -1,8 +1,28 @@
1
  ---
2
- title: Stud Manager
3
- emoji: 🔥
4
  colorFrom: blue
5
- colorTo: blue
6
  sdk: docker
 
7
  pinned: false
8
  ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Smart School System
3
+ emoji: 🏫
4
  colorFrom: blue
5
+ colorTo: indigo
6
  sdk: docker
7
+ app_port: 7860
8
  pinned: false
9
  ---
10
+
11
+ # 智慧校园管理系统 (Smart School System)
12
+
13
+ 这是一个基于 React (前端) 和 Node.js + MongoDB (后端) 的现代化校园教务管理系统。
14
+
15
+ ## 功能特性
16
+ - **RBAC 权限控制**: 管理员、教师、学生三级权限。
17
+ - **全栈架构**:
18
+ - 前端: Vite + React + Tailwind CSS + Recharts
19
+ - 后端: Express + Mongoose
20
+ - **核心模块**:
21
+ - 学生档案管理 (CRUD + 筛选)
22
+ - 课程管理
23
+ - 成绩录入与分析
24
+ - 多维度报表统计 (班级/学科/教师)
25
+
26
+ ## 部署说明
27
+ 本项目已配置为 Docker 部署,适用于 Hugging Face Spaces。
28
+ 端口配置为 7860。
index.html CHANGED
@@ -37,14 +37,15 @@
37
  "react/": "https://aistudiocdn.com/react@^19.2.0/",
38
  "lucide-react": "https://aistudiocdn.com/lucide-react@^0.555.0",
39
  "recharts": "https://aistudiocdn.com/recharts@^3.5.1",
40
- "vite": "https://aistudiocdn.com/vite@^7.2.4",
41
- "@vitejs/plugin-react": "https://aistudiocdn.com/@vitejs/plugin-react@^5.1.1"
42
  }
43
  }
44
  </script>
 
45
  </head>
46
  <body>
47
  <div id="root"></div>
48
- <script type="module" src="/index.tsx"></script>
49
- </body>
50
  </html>
 
37
  "react/": "https://aistudiocdn.com/react@^19.2.0/",
38
  "lucide-react": "https://aistudiocdn.com/lucide-react@^0.555.0",
39
  "recharts": "https://aistudiocdn.com/recharts@^3.5.1",
40
+ "@vitejs/plugin-react": "https://aistudiocdn.com/@vitejs/plugin-react@^5.1.1",
41
+ "vite": "https://aistudiocdn.com/vite@^7.2.4"
42
  }
43
  }
44
  </script>
45
+ <link rel="stylesheet" href="/index.css">
46
  </head>
47
  <body>
48
  <div id="root"></div>
49
+ <script type="module" src="/index.tsx"></script>
50
+ </body>
51
  </html>
pages/ScoreList.tsx CHANGED
@@ -1,7 +1,7 @@
1
 
2
  import React, { useState, useEffect } from 'react';
3
  import { api } from '../services/api';
4
- import { Score, Student, Subject, ClassInfo } from '../types';
5
  import { Loader2, Plus, Trash2, Award, FileSpreadsheet, X, Upload } from 'lucide-react';
6
 
7
  export const ScoreList: React.FC = () => {
@@ -9,40 +9,33 @@ export const ScoreList: React.FC = () => {
9
  const [activeSubject, setActiveSubject] = useState('');
10
  const [scores, setScores] = useState<Score[]>([]);
11
  const [students, setStudents] = useState<Student[]>([]);
12
- const [classList, setClassList] = useState<ClassInfo[]>([]);
13
  const [loading, setLoading] = useState(true);
14
 
15
- const [selectedGrade, setSelectedGrade] = useState('All');
16
- const [selectedClass, setSelectedClass] = useState('All');
17
  const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
18
 
19
  const [isAddOpen, setIsAddOpen] = useState(false);
20
  const [isImportOpen, setIsImportOpen] = useState(false);
21
  const [importFile, setImportFile] = useState<File | null>(null);
22
  const [submitting, setSubmitting] = useState(false);
23
-
24
- // Import Config
25
  const [importSemester, setImportSemester] = useState('2023-2024学年 第一学期');
26
  const [importType, setImportType] = useState('Final');
27
- const [importExamName, setImportExamName] = useState(''); // New: Exam Name for import
28
 
29
- // Manual Add Form
30
- const [formData, setFormData] = useState({ studentId: '', score: '', type: 'Final', examName: '' });
31
 
32
  const loadData = async () => {
33
  setLoading(true);
34
  try {
35
- const [subs, scs, stus, cls] = await Promise.all([
36
  api.subjects.getAll(),
37
  api.scores.getAll(),
38
- api.students.getAll(),
39
- api.classes.getAll()
40
  ]);
41
  setSubjects(subs);
42
  if (subs.length > 0 && !activeSubject) setActiveSubject(subs[0].name);
43
  setScores(scs);
44
  setStudents(stus);
45
- setClassList(cls);
46
  } catch (e) { console.error(e); }
47
  finally { setLoading(false); }
48
  };
@@ -52,12 +45,7 @@ export const ScoreList: React.FC = () => {
52
  const filteredScores = scores.filter(s => {
53
  if (s.courseName !== activeSubject) return false;
54
  const stu = students.find(st => st.studentNo === s.studentNo);
55
- if (!stu) return false;
56
-
57
- // Filter by grade and class
58
- const matchesGrade = selectedGrade === 'All' || stu.className.includes(selectedGrade);
59
- const matchesClass = selectedClass === 'All' || stu.className.includes(selectedClass);
60
- return matchesGrade && matchesClass;
61
  });
62
 
63
  const handleBatchDelete = async () => {
@@ -77,12 +65,10 @@ export const ScoreList: React.FC = () => {
77
  studentNo: stu.studentNo,
78
  courseName: activeSubject,
79
  score: Number(formData.score),
80
- semester: '2023-2024学年 第一学期',
81
- type: formData.type,
82
- examName: formData.examName
83
  });
84
  setIsAddOpen(false);
85
- setFormData({ studentId: '', score: '', type: 'Final', examName: '' });
86
  loadData();
87
  };
88
 
@@ -122,12 +108,14 @@ export const ScoreList: React.FC = () => {
122
  const promises: Promise<any>[] = [];
123
 
124
  jsonData.forEach((row: any) => {
 
125
  const cleanRow: any = {};
126
  Object.keys(row).forEach(k => cleanRow[k.trim()] = row[k]);
127
 
128
  const studentName = cleanRow['姓名'] || cleanRow['Name'];
129
  if (!studentName) return;
130
 
 
131
  const student = students.find(s => s.name === studentName);
132
 
133
  if (student) {
@@ -141,8 +129,7 @@ export const ScoreList: React.FC = () => {
141
  courseName: sub.name,
142
  score: Number(scoreVal),
143
  semester: importSemester,
144
- type: importType,
145
- examName: importExamName || '批量导入'
146
  }).then(() => successCount++)
147
  );
148
  }
@@ -154,7 +141,6 @@ export const ScoreList: React.FC = () => {
154
  alert(`成功导入 ${successCount} 条成绩记录`);
155
  setIsImportOpen(false);
156
  setImportFile(null);
157
- setImportExamName('');
158
  loadData();
159
  } catch (err) {
160
  console.error(err);
@@ -173,9 +159,6 @@ export const ScoreList: React.FC = () => {
173
  setSelectedIds(newSet);
174
  };
175
 
176
- const uniqueGrades = Array.from(new Set(classList.map(c => c.grade))).sort();
177
- const uniqueClasses = Array.from(new Set(classList.map(c => c.className))).sort();
178
-
179
  return (
180
  <div className="space-y-6">
181
  <div className="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden min-h-[500px]">
@@ -195,13 +178,11 @@ export const ScoreList: React.FC = () => {
195
  </div>
196
 
197
  <div className="flex gap-4 items-center bg-gray-50 p-3 rounded-lg">
198
- <select className="border border-gray-300 p-2 rounded text-sm text-gray-900 focus:ring-2 focus:ring-blue-500 min-w-[120px]" value={selectedGrade} onChange={e=>setSelectedGrade(e.target.value)}>
199
- <option value="All">所有年级</option>
200
- {uniqueGrades.map(g=><option key={g} value={g}>{g}</option>)}
201
  </select>
202
- <select className="border border-gray-300 p-2 rounded text-sm text-gray-900 focus:ring-2 focus:ring-blue-500 min-w-[120px]" value={selectedClass} onChange={e=>setSelectedClass(e.target.value)}>
203
- <option value="All">所有班级</option>
204
- {uniqueClasses.map(c=><option key={c} value={c}>{c}</option>)}
205
  </select>
206
  </div>
207
  </div>
@@ -228,7 +209,6 @@ export const ScoreList: React.FC = () => {
228
  }} checked={filteredScores.length > 0 && selectedIds.size === filteredScores.length}/>
229
  </th>
230
  <th className="px-6 py-3">姓名</th>
231
- <th className="px-6 py-3">考试名称</th>
232
  <th className="px-6 py-3">分数</th>
233
  <th className="px-6 py-3 text-right">操作</th>
234
  </tr>
@@ -238,12 +218,11 @@ export const ScoreList: React.FC = () => {
238
  <tr key={s._id || s.id} className="hover:bg-gray-50">
239
  <td className="px-6 py-3"><input type="checkbox" checked={selectedIds.has(s._id||String(s.id))} onChange={()=>toggleSelect(s._id||String(s.id))}/></td>
240
  <td className="px-6 py-3 font-medium text-gray-800">{s.studentName}</td>
241
- <td className="px-6 py-3 text-sm text-gray-500">{s.examName || s.type}</td>
242
  <td className="px-6 py-3 font-bold text-blue-600">{s.score}</td>
243
  <td className="px-6 py-3 text-right"><button onClick={async()=>{await api.scores.delete(s._id||String(s.id));loadData();}} className="text-red-400 hover:text-red-600"><Trash2 size={16}/></button></td>
244
  </tr>
245
  )) : (
246
- <tr><td colSpan={5} className="text-center py-10 text-gray-400">该班级暂无 {activeSubject} 成绩</td></tr>
247
  )}
248
  </tbody>
249
  </table>
@@ -264,14 +243,8 @@ export const ScoreList: React.FC = () => {
264
  <form onSubmit={handleSubmit} className="space-y-4">
265
  <select className="w-full border p-2 rounded" value={formData.studentId} onChange={e=>setFormData({...formData, studentId:e.target.value})} required>
266
  <option value="">选择学生</option>
267
- {/* Filter students in dropdown based on selected grade/class if specific ones selected, else show all */}
268
- {students.filter(s => {
269
- if (selectedGrade !== 'All' && !s.className.includes(selectedGrade)) return false;
270
- if (selectedClass !== 'All' && !s.className.includes(selectedClass)) return false;
271
- return true;
272
- }).map(s=><option key={s._id||s.id} value={s._id||s.id}>{s.name} - {s.className}</option>)}
273
  </select>
274
- <input className="w-full border p-2 rounded" placeholder="考试名称 (如: 期中测验)" value={formData.examName} onChange={e=>setFormData({...formData, examName:e.target.value})} required/>
275
  <input type="number" className="w-full border p-2 rounded" placeholder="分数" value={formData.score} onChange={e=>setFormData({...formData, score:e.target.value})} required/>
276
  <div className="flex gap-2">
277
  <button type="submit" className="flex-1 bg-blue-600 text-white py-2 rounded">保存</button>
@@ -293,24 +266,21 @@ export const ScoreList: React.FC = () => {
293
 
294
  <div className="space-y-4">
295
  <div>
296
- <label className="text-sm font-bold text-gray-600">考试信息</label>
297
- <div className="space-y-2 mt-1">
298
- <input className="w-full border border-gray-300 p-2 rounded text-sm text-gray-900" value={importExamName} onChange={e=>setImportExamName(e.target.value)} placeholder="自定义考试名称 (如: 第三次月考)" />
299
- <div className="grid grid-cols-2 gap-2">
300
- <select className="border border-gray-300 p-2 rounded text-sm text-gray-900" value={importType} onChange={e=>setImportType(e.target.value)}>
301
- <option value="Final">期末考试</option>
302
- <option value="Midterm">期中考试</option>
303
- <option value="Quiz">平时测验</option>
304
- </select>
305
- <input className="border border-gray-300 p-2 rounded text-sm text-gray-900" value={importSemester} onChange={e=>setImportSemester(e.target.value)} placeholder="学期" />
306
- </div>
307
  </div>
308
  </div>
309
 
310
  <div className="relative border-2 border-dashed border-gray-300 rounded-lg p-6 text-center hover:bg-gray-50 transition-colors">
311
  <Upload className="mx-auto h-10 w-10 text-gray-400" />
312
  <p className="mt-2 text-sm text-gray-600">点击上传 .xlsx 文件</p>
313
- <p className="text-xs text-gray-400 mt-1">系统将自动识别表头中的学科列</p>
314
  <input
315
  type="file"
316
  accept=".xlsx, .xls"
 
1
 
2
  import React, { useState, useEffect } from 'react';
3
  import { api } from '../services/api';
4
+ import { Score, Student, Subject } from '../types';
5
  import { Loader2, Plus, Trash2, Award, FileSpreadsheet, X, Upload } from 'lucide-react';
6
 
7
  export const ScoreList: React.FC = () => {
 
9
  const [activeSubject, setActiveSubject] = useState('');
10
  const [scores, setScores] = useState<Score[]>([]);
11
  const [students, setStudents] = useState<Student[]>([]);
 
12
  const [loading, setLoading] = useState(true);
13
 
14
+ const [selectedGrade, setSelectedGrade] = useState('六年级');
15
+ const [selectedClass, setSelectedClass] = useState('(1)班');
16
  const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
17
 
18
  const [isAddOpen, setIsAddOpen] = useState(false);
19
  const [isImportOpen, setIsImportOpen] = useState(false);
20
  const [importFile, setImportFile] = useState<File | null>(null);
21
  const [submitting, setSubmitting] = useState(false);
 
 
22
  const [importSemester, setImportSemester] = useState('2023-2024学年 第一学期');
23
  const [importType, setImportType] = useState('Final');
 
24
 
25
+ const [formData, setFormData] = useState({ studentId: '', score: '', type: 'Final' });
 
26
 
27
  const loadData = async () => {
28
  setLoading(true);
29
  try {
30
+ const [subs, scs, stus] = await Promise.all([
31
  api.subjects.getAll(),
32
  api.scores.getAll(),
33
+ api.students.getAll()
 
34
  ]);
35
  setSubjects(subs);
36
  if (subs.length > 0 && !activeSubject) setActiveSubject(subs[0].name);
37
  setScores(scs);
38
  setStudents(stus);
 
39
  } catch (e) { console.error(e); }
40
  finally { setLoading(false); }
41
  };
 
45
  const filteredScores = scores.filter(s => {
46
  if (s.courseName !== activeSubject) return false;
47
  const stu = students.find(st => st.studentNo === s.studentNo);
48
+ return stu && stu.className === (selectedGrade + selectedClass);
 
 
 
 
 
49
  });
50
 
51
  const handleBatchDelete = async () => {
 
65
  studentNo: stu.studentNo,
66
  courseName: activeSubject,
67
  score: Number(formData.score),
68
+ semester: '2023-Fall',
69
+ type: formData.type
 
70
  });
71
  setIsAddOpen(false);
 
72
  loadData();
73
  };
74
 
 
108
  const promises: Promise<any>[] = [];
109
 
110
  jsonData.forEach((row: any) => {
111
+ // Create a clean row object with trimmed keys
112
  const cleanRow: any = {};
113
  Object.keys(row).forEach(k => cleanRow[k.trim()] = row[k]);
114
 
115
  const studentName = cleanRow['姓名'] || cleanRow['Name'];
116
  if (!studentName) return;
117
 
118
+ // Try to find student
119
  const student = students.find(s => s.name === studentName);
120
 
121
  if (student) {
 
129
  courseName: sub.name,
130
  score: Number(scoreVal),
131
  semester: importSemester,
132
+ type: importType
 
133
  }).then(() => successCount++)
134
  );
135
  }
 
141
  alert(`成功导入 ${successCount} 条成绩记录`);
142
  setIsImportOpen(false);
143
  setImportFile(null);
 
144
  loadData();
145
  } catch (err) {
146
  console.error(err);
 
159
  setSelectedIds(newSet);
160
  };
161
 
 
 
 
162
  return (
163
  <div className="space-y-6">
164
  <div className="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden min-h-[500px]">
 
178
  </div>
179
 
180
  <div className="flex gap-4 items-center bg-gray-50 p-3 rounded-lg">
181
+ <select className="border border-gray-300 p-2 rounded text-sm text-gray-900 focus:ring-2 focus:ring-blue-500" value={selectedGrade} onChange={e=>setSelectedGrade(e.target.value)}>
182
+ {['一年级','二年级','三年级','四年级','五年级','六年级'].map(g=><option key={g} value={g}>{g}</option>)}
 
183
  </select>
184
+ <select className="border border-gray-300 p-2 rounded text-sm text-gray-900 focus:ring-2 focus:ring-blue-500" value={selectedClass} onChange={e=>setSelectedClass(e.target.value)}>
185
+ {['(1)班','(2)班','(3)班'].map(c=><option key={c} value={c}>{c}</option>)}
 
186
  </select>
187
  </div>
188
  </div>
 
209
  }} checked={filteredScores.length > 0 && selectedIds.size === filteredScores.length}/>
210
  </th>
211
  <th className="px-6 py-3">姓名</th>
 
212
  <th className="px-6 py-3">分数</th>
213
  <th className="px-6 py-3 text-right">操作</th>
214
  </tr>
 
218
  <tr key={s._id || s.id} className="hover:bg-gray-50">
219
  <td className="px-6 py-3"><input type="checkbox" checked={selectedIds.has(s._id||String(s.id))} onChange={()=>toggleSelect(s._id||String(s.id))}/></td>
220
  <td className="px-6 py-3 font-medium text-gray-800">{s.studentName}</td>
 
221
  <td className="px-6 py-3 font-bold text-blue-600">{s.score}</td>
222
  <td className="px-6 py-3 text-right"><button onClick={async()=>{await api.scores.delete(s._id||String(s.id));loadData();}} className="text-red-400 hover:text-red-600"><Trash2 size={16}/></button></td>
223
  </tr>
224
  )) : (
225
+ <tr><td colSpan={4} className="text-center py-10 text-gray-400">该班级暂无 {activeSubject} 成绩</td></tr>
226
  )}
227
  </tbody>
228
  </table>
 
243
  <form onSubmit={handleSubmit} className="space-y-4">
244
  <select className="w-full border p-2 rounded" value={formData.studentId} onChange={e=>setFormData({...formData, studentId:e.target.value})} required>
245
  <option value="">选择学生</option>
246
+ {students.filter(s=>s.className===selectedGrade+selectedClass).map(s=><option key={s._id||s.id} value={s._id||s.id}>{s.name}</option>)}
 
 
 
 
 
247
  </select>
 
248
  <input type="number" className="w-full border p-2 rounded" placeholder="分数" value={formData.score} onChange={e=>setFormData({...formData, score:e.target.value})} required/>
249
  <div className="flex gap-2">
250
  <button type="submit" className="flex-1 bg-blue-600 text-white py-2 rounded">保存</button>
 
266
 
267
  <div className="space-y-4">
268
  <div>
269
+ <label className="text-sm font-bold text-gray-600">选择考试类型</label>
270
+ <div className="grid grid-cols-2 gap-2 mt-1">
271
+ <select className="border border-gray-300 p-2 rounded text-sm text-gray-900 focus:ring-2 focus:ring-blue-500" value={importType} onChange={e=>setImportType(e.target.value)}>
272
+ <option value="Final">期末考试</option>
273
+ <option value="Midterm">期中考试</option>
274
+ <option value="Quiz">平时测验</option>
275
+ </select>
276
+ <input className="border border-gray-300 p-2 rounded text-sm text-gray-900" value={importSemester} onChange={e=>setImportSemester(e.target.value)} placeholder="学期" />
 
 
 
277
  </div>
278
  </div>
279
 
280
  <div className="relative border-2 border-dashed border-gray-300 rounded-lg p-6 text-center hover:bg-gray-50 transition-colors">
281
  <Upload className="mx-auto h-10 w-10 text-gray-400" />
282
  <p className="mt-2 text-sm text-gray-600">点击上传 .xlsx 文件</p>
283
+ <p className="text-xs text-gray-400 mt-1">系统将自动识别表头中的学科列(如: 语文, 数学)</p>
284
  <input
285
  type="file"
286
  accept=".xlsx, .xls"
server.js CHANGED
@@ -94,8 +94,7 @@ const ScoreSchema = new mongoose.Schema({
94
  courseName: String,
95
  score: Number,
96
  semester: String,
97
- type: String,
98
- examName: String
99
  });
100
  const Score = mongoose.model('Score', ScoreSchema);
101
 
 
94
  courseName: String,
95
  score: Number,
96
  semester: String,
97
+ type: String
 
98
  });
99
  const Score = mongoose.model('Score', ScoreSchema);
100
 
services/api.ts CHANGED
@@ -1,3 +1,4 @@
 
1
  /// <reference types="vite/client" />
2
  import { User, ClassInfo, SystemConfig, Subject } from '../types';
3
 
@@ -53,13 +54,8 @@ export const api = {
53
  },
54
  getCurrentUser: (): User | null => {
55
  if (typeof window !== 'undefined') {
56
- try {
57
- const stored = localStorage.getItem('user');
58
- if (stored) return JSON.parse(stored);
59
- } catch (e) {
60
- console.error("Error parsing user from localStorage", e);
61
- localStorage.removeItem('user'); // Clean up corrupted data
62
- }
63
  }
64
  return null;
65
  }
@@ -114,4 +110,4 @@ export const api = {
114
  batchDelete: (type: 'student' | 'score' | 'user', ids: string[]) => {
115
  return request('/batch-delete', { method: 'POST', body: JSON.stringify({ type, ids }) });
116
  }
117
- };
 
1
+
2
  /// <reference types="vite/client" />
3
  import { User, ClassInfo, SystemConfig, Subject } from '../types';
4
 
 
54
  },
55
  getCurrentUser: (): User | null => {
56
  if (typeof window !== 'undefined') {
57
+ const stored = localStorage.getItem('user');
58
+ if (stored) return JSON.parse(stored);
 
 
 
 
 
59
  }
60
  return null;
61
  }
 
110
  batchDelete: (type: 'student' | 'score' | 'user', ids: string[]) => {
111
  return request('/batch-delete', { method: 'POST', body: JSON.stringify({ type, ids }) });
112
  }
113
+ };
types.ts CHANGED
@@ -81,7 +81,6 @@ export interface Score {
81
  score: number;
82
  semester: string;
83
  type: 'Midterm' | 'Final' | 'Quiz';
84
- examName?: string; // e.g. "期中限时练习"
85
  }
86
 
87
  export interface ApiResponse<T> {
 
81
  score: number;
82
  semester: string;
83
  type: 'Midterm' | 'Final' | 'Quiz';
 
84
  }
85
 
86
  export interface ApiResponse<T> {
vite.config.ts CHANGED
@@ -6,16 +6,6 @@ export default defineConfig({
6
  plugins: [react()],
7
  build: {
8
  outDir: 'dist', // 确保编译输出到 dist 目录
9
- emptyOutDir: true,
10
- chunkSizeWarningLimit: 1000,
11
- rollupOptions: {
12
- output: {
13
- manualChunks: {
14
- vendor: ['react', 'react-dom'],
15
- charts: ['recharts'],
16
- utils: ['lucide-react']
17
- }
18
- }
19
- }
20
  }
21
  });
 
6
  plugins: [react()],
7
  build: {
8
  outDir: 'dist', // 确保编译输出到 dist 目录
9
+ emptyOutDir: true
 
 
 
 
 
 
 
 
 
 
10
  }
11
  });