stud-manager / pages /CourseList.tsx
dvc890's picture
Upload 51 files
c9a1ebd verified
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>
);
};