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

Upload 25 files

Browse files
Files changed (7) hide show
  1. App.tsx +62 -3
  2. index.html +4 -4
  3. pages/ScoreList.tsx +57 -27
  4. server.js +2 -1
  5. services/api.ts +8 -4
  6. types.ts +1 -0
  7. vite.config.ts +11 -1
App.tsx CHANGED
@@ -1,4 +1,3 @@
1
-
2
  import React, { useState, useEffect } from 'react';
3
  import { Sidebar } from './components/Sidebar';
4
  import { Header } from './components/Header';
@@ -14,8 +13,60 @@ import { UserList } from './pages/UserList';
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,4 +145,12 @@ const App: React.FC = () => {
94
  );
95
  };
96
 
97
- export default App;
 
 
 
 
 
 
 
 
 
 
1
  import React, { useState, useEffect } from 'react';
2
  import { Sidebar } from './components/Sidebar';
3
  import { Header } from './components/Header';
 
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
  );
146
  };
147
 
148
+ const App: React.FC = () => {
149
+ return (
150
+ <ErrorBoundary>
151
+ <AppContent />
152
+ </ErrorBoundary>
153
+ );
154
+ };
155
+
156
+ export default App;
index.html CHANGED
@@ -37,8 +37,8 @@
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>
@@ -46,6 +46,6 @@
46
  </head>
47
  <body>
48
  <div id="root"></div>
49
- <script type="module" src="/index.tsx"></script>
50
- </body>
51
  </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
+ "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>
 
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 } from '../types';
5
  import { Loader2, Plus, Trash2, Award, FileSpreadsheet, X, Upload } from 'lucide-react';
6
 
7
  export const ScoreList: React.FC = () => {
@@ -9,33 +9,40 @@ 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,7 +52,12 @@ export const ScoreList: React.FC = () => {
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,10 +77,12 @@ export const ScoreList: React.FC = () => {
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,14 +122,12 @@ export const ScoreList: React.FC = () => {
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,7 +141,8 @@ export const ScoreList: React.FC = () => {
129
  courseName: sub.name,
130
  score: Number(scoreVal),
131
  semester: importSemester,
132
- type: importType
 
133
  }).then(() => successCount++)
134
  );
135
  }
@@ -141,6 +154,7 @@ export const ScoreList: React.FC = () => {
141
  alert(`成功导入 ${successCount} 条成绩记录`);
142
  setIsImportOpen(false);
143
  setImportFile(null);
 
144
  loadData();
145
  } catch (err) {
146
  console.error(err);
@@ -159,6 +173,9 @@ export const ScoreList: React.FC = () => {
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,11 +195,13 @@ export const ScoreList: React.FC = () => {
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,6 +228,7 @@ export const ScoreList: React.FC = () => {
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,11 +238,12 @@ export const ScoreList: React.FC = () => {
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,8 +264,14 @@ export const ScoreList: React.FC = () => {
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,21 +293,24 @@ export const ScoreList: React.FC = () => {
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"
 
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
  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
  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
  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
  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
  courseName: sub.name,
142
  score: Number(scoreVal),
143
  semester: importSemester,
144
+ type: importType,
145
+ examName: importExamName || '批量导入'
146
  }).then(() => successCount++)
147
  );
148
  }
 
154
  alert(`成功导入 ${successCount} 条成绩记录`);
155
  setIsImportOpen(false);
156
  setImportFile(null);
157
+ setImportExamName('');
158
  loadData();
159
  } catch (err) {
160
  console.error(err);
 
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
  </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
  }} 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
  <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
  <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
 
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"
server.js CHANGED
@@ -94,7 +94,8 @@ const ScoreSchema = new mongoose.Schema({
94
  courseName: String,
95
  score: Number,
96
  semester: String,
97
- type: String
 
98
  });
99
  const Score = mongoose.model('Score', ScoreSchema);
100
 
 
94
  courseName: String,
95
  score: Number,
96
  semester: String,
97
+ type: String,
98
+ examName: String
99
  });
100
  const Score = mongoose.model('Score', ScoreSchema);
101
 
services/api.ts CHANGED
@@ -1,4 +1,3 @@
1
-
2
  /// <reference types="vite/client" />
3
  import { User, ClassInfo, SystemConfig, Subject } from '../types';
4
 
@@ -54,8 +53,13 @@ export const api = {
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,4 +114,4 @@ export const api = {
110
  batchDelete: (type: 'student' | 'score' | 'user', ids: string[]) => {
111
  return request('/batch-delete', { method: 'POST', body: JSON.stringify({ type, ids }) });
112
  }
113
- };
 
 
1
  /// <reference types="vite/client" />
2
  import { User, ClassInfo, SystemConfig, Subject } from '../types';
3
 
 
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
  batchDelete: (type: 'student' | 'score' | 'user', ids: string[]) => {
115
  return request('/batch-delete', { method: 'POST', body: JSON.stringify({ type, ids }) });
116
  }
117
+ };
types.ts CHANGED
@@ -81,6 +81,7 @@ export interface Score {
81
  score: number;
82
  semester: string;
83
  type: 'Midterm' | 'Final' | 'Quiz';
 
84
  }
85
 
86
  export interface ApiResponse<T> {
 
81
  score: number;
82
  semester: string;
83
  type: 'Midterm' | 'Final' | 'Quiz';
84
+ examName?: string; // e.g. "期中限时练习"
85
  }
86
 
87
  export interface ApiResponse<T> {
vite.config.ts CHANGED
@@ -6,6 +6,16 @@ export default defineConfig({
6
  plugins: [react()],
7
  build: {
8
  outDir: 'dist', // 确保编译输出到 dist 目录
9
- emptyOutDir: true
 
 
 
 
 
 
 
 
 
 
10
  }
11
  });
 
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
  });