Spaces:
Running
Running
Upload 45 files
Browse files- pages/CourseList.tsx +20 -3
- pages/Dashboard.tsx +29 -16
- pages/Games.tsx +8 -1
pages/CourseList.tsx
CHANGED
|
@@ -51,8 +51,16 @@ export const CourseList: React.FC = () => {
|
|
| 51 |
setCourses(filteredCourses);
|
| 52 |
setSubjects(s);
|
| 53 |
setTeachers(t);
|
| 54 |
-
|
| 55 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
} catch (e) { console.error(e); } finally { setLoading(false); }
|
| 57 |
};
|
| 58 |
|
|
@@ -106,6 +114,12 @@ export const CourseList: React.FC = () => {
|
|
| 106 |
setIsModalOpen(true);
|
| 107 |
};
|
| 108 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
return (
|
| 110 |
<div className="space-y-6">
|
| 111 |
<div className="flex flex-col md:flex-row justify-between items-center gap-4">
|
|
@@ -188,7 +202,10 @@ export const CourseList: React.FC = () => {
|
|
| 188 |
<label className="text-sm font-bold text-gray-500 mb-1 block">教学班级</label>
|
| 189 |
<select className="w-full border p-2 rounded-lg focus:ring-2 focus:ring-indigo-500 outline-none" value={formData.className} onChange={e=>setFormData({...formData, className:e.target.value})} required>
|
| 190 |
<option value="">-- 选择班级 --</option>
|
| 191 |
-
{classes.map(cls =>
|
|
|
|
|
|
|
|
|
|
| 192 |
</select>
|
| 193 |
</div>
|
| 194 |
|
|
|
|
| 51 |
setCourses(filteredCourses);
|
| 52 |
setSubjects(s);
|
| 53 |
setTeachers(t);
|
| 54 |
+
|
| 55 |
+
// Safety check: Ensure cls is array and sort safely
|
| 56 |
+
const safeClasses = Array.isArray(cls) ? cls : [];
|
| 57 |
+
try {
|
| 58 |
+
safeClasses.sort(sortClasses);
|
| 59 |
+
} catch (e) {
|
| 60 |
+
console.warn("Class sort failed", e);
|
| 61 |
+
}
|
| 62 |
+
setClasses(safeClasses);
|
| 63 |
+
|
| 64 |
} catch (e) { console.error(e); } finally { setLoading(false); }
|
| 65 |
};
|
| 66 |
|
|
|
|
| 114 |
setIsModalOpen(true);
|
| 115 |
};
|
| 116 |
|
| 117 |
+
// Helper to ensure class name is consistent
|
| 118 |
+
const getFullClassName = (c: ClassInfo) => {
|
| 119 |
+
if (c.className && c.grade && c.className.startsWith(c.grade)) return c.className;
|
| 120 |
+
return (c.grade || '') + (c.className || '');
|
| 121 |
+
};
|
| 122 |
+
|
| 123 |
return (
|
| 124 |
<div className="space-y-6">
|
| 125 |
<div className="flex flex-col md:flex-row justify-between items-center gap-4">
|
|
|
|
| 202 |
<label className="text-sm font-bold text-gray-500 mb-1 block">教学班级</label>
|
| 203 |
<select className="w-full border p-2 rounded-lg focus:ring-2 focus:ring-indigo-500 outline-none" value={formData.className} onChange={e=>setFormData({...formData, className:e.target.value})} required>
|
| 204 |
<option value="">-- 选择班级 --</option>
|
| 205 |
+
{classes.map(cls => {
|
| 206 |
+
const fullName = getFullClassName(cls);
|
| 207 |
+
return <option key={cls._id} value={fullName}>{fullName}</option>;
|
| 208 |
+
})}
|
| 209 |
</select>
|
| 210 |
</div>
|
| 211 |
|
pages/Dashboard.tsx
CHANGED
|
@@ -20,27 +20,33 @@ export const sortGrades = (a: string, b: string) => (gradeOrder[a] || 99) - (gra
|
|
| 20 |
|
| 21 |
// Helper to extract grade from class string (e.g. "一年级(1)班" -> "一年级")
|
| 22 |
const extractGrade = (s: string) => {
|
|
|
|
| 23 |
const keys = Object.keys(gradeOrder);
|
| 24 |
-
// Sort keys by length desc to match "七年级" before "七" if needed, though simple find is usually ok
|
| 25 |
return keys.find(g => s.startsWith(g)) || '';
|
| 26 |
};
|
| 27 |
|
| 28 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
// 1. Sort by Grade First
|
| 30 |
-
const gradeA = extractGrade(
|
| 31 |
-
const gradeB = extractGrade(
|
| 32 |
|
| 33 |
if (gradeA && gradeB && gradeA !== gradeB) {
|
| 34 |
return sortGrades(gradeA, gradeB);
|
| 35 |
}
|
| 36 |
|
| 37 |
-
// 2. Sort by Class Number
|
| 38 |
-
// Matches (1) or 1班
|
| 39 |
const getNum = (str: string) => {
|
| 40 |
const match = str.match(/(\d+)/);
|
| 41 |
return match ? parseInt(match[1]) : 0;
|
| 42 |
};
|
| 43 |
-
return getNum(
|
| 44 |
};
|
| 45 |
|
| 46 |
export const Dashboard: React.FC<DashboardProps> = ({ onNavigate }) => {
|
|
@@ -86,14 +92,17 @@ export const Dashboard: React.FC<DashboardProps> = ({ onNavigate }) => {
|
|
| 86 |
api.users.getAll({ role: 'TEACHER' })
|
| 87 |
]);
|
| 88 |
setStats(summary);
|
| 89 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
setSubjects(subs);
|
| 91 |
setTeachers(userList);
|
| 92 |
|
| 93 |
-
if (isAdmin &&
|
| 94 |
-
const grades = Array.from(new Set(
|
| 95 |
if (grades.length > 0) {
|
| 96 |
-
// IMPORTANT: Default viewGrade to the first available grade so schedule isn't empty
|
| 97 |
setViewGrade(grades[0] as string);
|
| 98 |
}
|
| 99 |
}
|
|
@@ -190,11 +199,15 @@ export const Dashboard: React.FC<DashboardProps> = ({ onNavigate }) => {
|
|
| 190 |
|
| 191 |
const uniqueGrades = Array.from(new Set(classList.map(c => c.grade))).sort(sortGrades);
|
| 192 |
|
| 193 |
-
//
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 198 |
|
| 199 |
const cards = [
|
| 200 |
{ label: '在校学生', value: stats.studentCount, icon: Users, color: 'bg-blue-500', trend: '实时' },
|
|
|
|
| 20 |
|
| 21 |
// Helper to extract grade from class string (e.g. "一年级(1)班" -> "一年级")
|
| 22 |
const extractGrade = (s: string) => {
|
| 23 |
+
if (!s) return '';
|
| 24 |
const keys = Object.keys(gradeOrder);
|
|
|
|
| 25 |
return keys.find(g => s.startsWith(g)) || '';
|
| 26 |
};
|
| 27 |
|
| 28 |
+
// Robust sort function
|
| 29 |
+
export const sortClasses = (a: ClassInfo | string, b: ClassInfo | string) => {
|
| 30 |
+
// Handle both ClassInfo objects and string names
|
| 31 |
+
const nameA = typeof a === 'string' ? a : (a.grade + a.className);
|
| 32 |
+
const nameB = typeof b === 'string' ? b : (b.grade + b.className);
|
| 33 |
+
|
| 34 |
+
if (!nameA || !nameB) return 0;
|
| 35 |
+
|
| 36 |
// 1. Sort by Grade First
|
| 37 |
+
const gradeA = extractGrade(nameA);
|
| 38 |
+
const gradeB = extractGrade(nameB);
|
| 39 |
|
| 40 |
if (gradeA && gradeB && gradeA !== gradeB) {
|
| 41 |
return sortGrades(gradeA, gradeB);
|
| 42 |
}
|
| 43 |
|
| 44 |
+
// 2. Sort by Class Number
|
|
|
|
| 45 |
const getNum = (str: string) => {
|
| 46 |
const match = str.match(/(\d+)/);
|
| 47 |
return match ? parseInt(match[1]) : 0;
|
| 48 |
};
|
| 49 |
+
return getNum(nameA) - getNum(nameB);
|
| 50 |
};
|
| 51 |
|
| 52 |
export const Dashboard: React.FC<DashboardProps> = ({ onNavigate }) => {
|
|
|
|
| 92 |
api.users.getAll({ role: 'TEACHER' })
|
| 93 |
]);
|
| 94 |
setStats(summary);
|
| 95 |
+
|
| 96 |
+
// Safety check for classes
|
| 97 |
+
const safeClasses = Array.isArray(classes) ? classes : [];
|
| 98 |
+
setClassList(safeClasses);
|
| 99 |
+
|
| 100 |
setSubjects(subs);
|
| 101 |
setTeachers(userList);
|
| 102 |
|
| 103 |
+
if (isAdmin && safeClasses.length > 0) {
|
| 104 |
+
const grades = Array.from(new Set(safeClasses.map((c: ClassInfo) => c.grade))).sort(sortGrades);
|
| 105 |
if (grades.length > 0) {
|
|
|
|
| 106 |
setViewGrade(grades[0] as string);
|
| 107 |
}
|
| 108 |
}
|
|
|
|
| 199 |
|
| 200 |
const uniqueGrades = Array.from(new Set(classList.map(c => c.grade))).sort(sortGrades);
|
| 201 |
|
| 202 |
+
// Robust class mapping for modal
|
| 203 |
+
const modalClassOptions = classList
|
| 204 |
+
.filter(c => !isAdmin || !viewGrade || c.grade === viewGrade)
|
| 205 |
+
.map(c => ({
|
| 206 |
+
id: c._id,
|
| 207 |
+
name: (c.className.startsWith(c.grade) ? c.className : c.grade + c.className)
|
| 208 |
+
}))
|
| 209 |
+
.sort((a,b) => sortClasses(a.name, b.name))
|
| 210 |
+
.map(c => c.name);
|
| 211 |
|
| 212 |
const cards = [
|
| 213 |
{ label: '在校学生', value: stats.studentCount, icon: Users, color: 'bg-blue-500', trend: '实时' },
|
pages/Games.tsx
CHANGED
|
@@ -29,6 +29,13 @@ export const Games: React.FC = () => {
|
|
| 29 |
loadContext();
|
| 30 |
}, []);
|
| 31 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
const loadContext = async () => {
|
| 33 |
if (isTeacher) {
|
| 34 |
try {
|
|
@@ -36,7 +43,7 @@ export const Games: React.FC = () => {
|
|
| 36 |
const allClasses = await api.classes.getAll();
|
| 37 |
const homerooms = allClasses
|
| 38 |
.filter((c: ClassInfo) => c.homeroomTeacherIds?.includes(currentUser._id || '') || c.teacherName?.includes(currentUser.trueName || currentUser.username))
|
| 39 |
-
.map(
|
| 40 |
|
| 41 |
// Fetch Courses where I am Subject Teacher
|
| 42 |
const allCourses = await api.courses.getAll();
|
|
|
|
| 29 |
loadContext();
|
| 30 |
}, []);
|
| 31 |
|
| 32 |
+
const getFullClassName = (c: ClassInfo) => {
|
| 33 |
+
// Logic to prevent double grade names like "一年级一年级(1)班"
|
| 34 |
+
// If className already starts with grade, use it as is.
|
| 35 |
+
if (c.className && c.grade && c.className.startsWith(c.grade)) return c.className;
|
| 36 |
+
return (c.grade || '') + (c.className || '');
|
| 37 |
+
};
|
| 38 |
+
|
| 39 |
const loadContext = async () => {
|
| 40 |
if (isTeacher) {
|
| 41 |
try {
|
|
|
|
| 43 |
const allClasses = await api.classes.getAll();
|
| 44 |
const homerooms = allClasses
|
| 45 |
.filter((c: ClassInfo) => c.homeroomTeacherIds?.includes(currentUser._id || '') || c.teacherName?.includes(currentUser.trueName || currentUser.username))
|
| 46 |
+
.map(getFullClassName);
|
| 47 |
|
| 48 |
// Fetch Courses where I am Subject Teacher
|
| 49 |
const allCourses = await api.courses.getAll();
|