Spaces:
Sleeping
Sleeping
| 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> | |
| ); | |
| }; | |