Spaces:
Sleeping
Sleeping
File size: 11,904 Bytes
33ebc65 b166dac 33ebc65 58b7bd4 33ebc65 700b192 b166dac 58b7bd4 33ebc65 b166dac 5ba43ca 58b7bd4 700b192 5ba43ca 58b7bd4 700b192 5ba43ca 58b7bd4 1e5e242 58b7bd4 1e5e242 58b7bd4 700b192 1e5e242 62ec62d 700b192 5ba43ca 33ebc65 700b192 5ba43ca 700b192 58b7bd4 700b192 5ba43ca 700b192 58b7bd4 700b192 33ebc65 b166dac 58b7bd4 62ec62d 33ebc65 b166dac 58b7bd4 b166dac 58b7bd4 b166dac 700b192 33ebc65 b166dac 58b7bd4 c9a1ebd 58b7bd4 c9a1ebd 58b7bd4 b166dac 58b7bd4 b166dac 700b192 33ebc65 700b192 b166dac 58b7bd4 700b192 58b7bd4 62ec62d 58b7bd4 700b192 b166dac 58b7bd4 700b192 1e5e242 58b7bd4 1e5e242 b166dac 700b192 33ebc65 700b192 33ebc65 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 |
import React, { useEffect, useState } from 'react';
import { Plus, Clock, Loader2, Edit, Trash2, X, Filter } from 'lucide-react';
import { api } from '../services/api';
import { Course, Subject, User, ClassInfo } from '../types';
import { sortClasses } from './Dashboard';
export const CourseList: React.FC = () => {
const [courses, setCourses] = useState<Course[]>([]);
const [subjects, setSubjects] = useState<Subject[]>([]);
const [teachers, setTeachers] = useState<User[]>([]);
const [classes, setClasses] = useState<ClassInfo[]>([]);
const [loading, setLoading] = useState(true);
// Filter state
const [filterSubject, setFilterSubject] = useState('All');
const [isModalOpen, setIsModalOpen] = useState(false);
const [formData, setFormData] = useState<{
courseCode: string,
courseName: string,
teacherName: string,
teacherId: string,
className: string,
credits: number,
capacity: number
}>({ courseCode: '', courseName: '', teacherName: '', teacherId: '', className: '', credits: 2, capacity: 45 });
const [editId, setEditId] = useState<string | null>(null);
const currentUser = api.auth.getCurrentUser();
const isAdmin = currentUser?.role === 'ADMIN';
const isTeacher = currentUser?.role === 'TEACHER';
const loadData = async () => {
setLoading(true);
try {
const [c, s, t, cls] = await Promise.all([
api.courses.getAll(),
api.subjects.getAll(),
api.users.getAll({ role: 'TEACHER' }),
api.classes.getAll()
]);
let filteredCourses = c;
if (isTeacher) {
// Teachers only see their own courses
filteredCourses = c.filter((course: Course) => course.teacherId === currentUser._id || course.teacherName === (currentUser.trueName || currentUser.username));
}
setCourses(filteredCourses);
setSubjects(s);
setTeachers(t);
// Safety check: Ensure cls is array and sort safely
const safeClasses = Array.isArray(cls) ? cls : [];
try {
safeClasses.sort(sortClasses);
} catch (e) {
console.warn("Class sort failed", e);
}
setClasses(safeClasses);
} catch (e) { console.error(e); } finally { setLoading(false); }
};
useEffect(() => { loadData(); }, []);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (editId) await api.courses.update(editId, formData);
else {
try {
await api.courses.add(formData);
} catch (e: any) {
if (e.message.includes('DUPLICATE')) {
alert('该班级的该科目已经有任课老师了,请勿重复添加。');
return;
}
alert('添加失败');
return;
}
}
setIsModalOpen(false);
loadData();
};
const handleDelete = async (id: string) => {
if(confirm('删除课程关联?')) { await api.courses.delete(id); loadData(); }
};
const filteredCourses = courses.filter(c => filterSubject === 'All' || c.courseName === filterSubject);
const handleTeacherSelect = (e: React.ChangeEvent<HTMLSelectElement>) => {
const tId = e.target.value;
const t = teachers.find(u => u._id === tId);
if(t) {
setFormData({ ...formData, teacherId: tId, teacherName: t.trueName || t.username });
}
};
// If teacher is logged in, default the teacher selection to themselves
const handleOpenAdd = () => {
setEditId(null);
setFormData({
courseCode: '',
courseName: '',
teacherName: isTeacher ? (currentUser?.trueName || currentUser?.username || '') : '',
teacherId: isTeacher ? (currentUser?._id || '') : '',
className: '',
credits: 2,
capacity: 45
});
setIsModalOpen(true);
};
// Helper to ensure class name is consistent
const getFullClassName = (c: ClassInfo) => {
if (c.className && c.grade && c.className.startsWith(c.grade)) return c.className;
return (c.grade || '') + (c.className || '');
};
return (
<div className="space-y-6">
<div className="flex flex-col md:flex-row justify-between items-center gap-4">
<div>
<h2 className="text-xl font-bold text-gray-800">任课安排</h2>
<p className="text-xs text-gray-500">在此设置每个班级各科目的任课老师</p>
</div>
<div className="flex items-center gap-3 w-full md:w-auto">
<div className="flex items-center bg-white border rounded-lg px-2 py-1.5 flex-1 md:flex-none">
<Filter size={16} className="text-gray-400 mr-2"/>
<select
className="bg-transparent text-sm outline-none text-gray-700 min-w-[100px]"
value={filterSubject}
onChange={e => setFilterSubject(e.target.value)}
>
<option value="All">所有科目</option>
{subjects.map(s => <option key={s._id} value={s.name}>{s.name}</option>)}
</select>
</div>
<button onClick={handleOpenAdd} className="px-4 py-2 bg-indigo-600 text-white rounded-lg flex items-center text-sm hover:bg-indigo-700 transition-colors whitespace-nowrap">
<Plus size={16} className="mr-1"/> 绑定新班级
</button>
</div>
</div>
{loading ? <div className="flex justify-center py-10"><Loader2 className="animate-spin text-indigo-600"/></div> : (
filteredCourses.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{filteredCourses.map(c => (
<div key={c._id || c.id} className="bg-white p-6 rounded-xl border shadow-sm hover:shadow-md transition-shadow relative overflow-hidden group">
<div className="absolute top-0 right-0 p-4 opacity-10 font-black text-6xl text-indigo-900 pointer-events-none -mr-4 -mt-4">
{c.courseName[0]}
</div>
<div className="relative z-10">
<h3 className="font-bold text-lg text-gray-800 mb-1">{c.className}</h3>
<div className="text-indigo-600 font-bold text-md mb-4 flex items-center">
{c.courseName}
</div>
<div className="text-sm text-gray-500 mb-4 space-y-1">
<p className="flex items-center"><span className="w-16 text-gray-400">教师:</span> {c.teacherName}</p>
{c.courseCode && <p className="flex items-center"><span className="w-16 text-gray-400">代码:</span> {c.courseCode}</p>}
</div>
<div className="flex gap-2 pt-2 border-t border-gray-50">
<button onClick={() => {
setFormData({
courseCode: c.courseCode || '',
courseName: c.courseName,
teacherName: c.teacherName,
teacherId: c.teacherId || '',
className: c.className,
credits: c.credits || 2,
capacity: c.capacity || 45
});
setEditId(c._id || String(c.id));
setIsModalOpen(true);
}} className="flex-1 bg-gray-50 text-blue-600 py-1.5 rounded text-sm hover:bg-blue-50 transition-colors font-medium">编辑</button>
<button onClick={() => handleDelete(c._id || String(c.id))} className="flex-1 bg-gray-50 text-red-600 py-1.5 rounded text-sm hover:bg-red-50 transition-colors font-medium">解绑</button>
</div>
</div>
</div>
))}
</div>
) : (
<div className="text-center py-12 text-gray-400 bg-white rounded-xl border border-dashed border-gray-200">
{isTeacher ? '您尚未绑定任何教学班级,请点击右上角绑定' : '没有找到课程安排'}
</div>
)
)}
{isModalOpen && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-50 backdrop-blur-sm">
<div className="bg-white p-6 rounded-xl w-full max-w-md shadow-xl animate-in zoom-in-95">
<h3 className="font-bold mb-4 text-lg text-gray-800">{editId ? '编辑课程关联' : '新增课程关联'}</h3>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="text-sm font-bold text-gray-500 mb-1 block">教学班级</label>
<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>
<option value="">-- 选择班级 --</option>
{classes.map(cls => {
const fullName = getFullClassName(cls);
return <option key={cls._id} value={fullName}>{fullName}</option>;
})}
</select>
</div>
<div>
<label className="text-sm font-bold text-gray-500 mb-1 block">科目</label>
<select className="w-full border p-2 rounded-lg focus:ring-2 focus:ring-indigo-500 outline-none" value={formData.courseName} onChange={e=>setFormData({...formData, courseName:e.target.value})} required>
<option value="">-- 选择科目 --</option>
{subjects.map(s=><option key={s._id} value={s.name}>{s.name}</option>)}
</select>
</div>
{!isTeacher && (
<div>
<label className="text-sm font-bold text-gray-500 mb-1 block">任课教师</label>
<select
className="w-full border p-2 rounded-lg focus:ring-2 focus:ring-indigo-500 outline-none"
value={formData.teacherId}
onChange={handleTeacherSelect}
required
>
<option value="">-- 选择教师 --</option>
{teachers.map(t => <option key={t._id} value={t._id}>{t.trueName || t.username}</option>)}
</select>
</div>
)}
{isTeacher && (
<div className="bg-blue-50 p-2 rounded text-sm text-blue-700">
您正在将自己 ({formData.teacherName}) 绑定为 {formData.className || '某班'} 的 {formData.courseName || '某科'} 老师。
</div>
)}
<div className="flex gap-3 pt-2">
<button type="button" onClick={()=>setIsModalOpen(false)} className="flex-1 border py-2 rounded-lg hover:bg-gray-50">取消</button>
<button className="flex-1 bg-indigo-600 text-white py-2 rounded-lg hover:bg-indigo-700 shadow-md">保存</button>
</div>
</form>
</div>
</div>
)}
</div>
);
};
|