Spaces:
Sleeping
Sleeping
| import React, { useEffect, useState } from 'react'; | |
| import { api } from '../services/api'; | |
| import { Schedule, Student, Attendance } from '../types'; | |
| import { Calendar, CheckCircle, Clock, Coffee, FileText, MapPin, X } from 'lucide-react'; | |
| export const StudentDashboard: React.FC = () => { | |
| const [student, setStudent] = useState<Student | null>(null); | |
| const [schedules, setSchedules] = useState<Schedule[]>([]); | |
| const [todayAttendance, setTodayAttendance] = useState<Attendance | null>(null); | |
| const [currentTime, setCurrentTime] = useState(new Date()); | |
| // Leave Modal | |
| const [isLeaveOpen, setIsLeaveOpen] = useState(false); | |
| const [leaveReason, setLeaveReason] = useState(''); | |
| const [leaveDates, setLeaveDates] = useState({ start: '', end: '' }); | |
| const currentUser = api.auth.getCurrentUser(); | |
| useEffect(() => { | |
| const timer = setInterval(() => setCurrentTime(new Date()), 60000); | |
| loadData(); | |
| return () => clearInterval(timer); | |
| }, []); | |
| const loadData = async () => { | |
| try { | |
| const students = await api.students.getAll(); | |
| const me = students.find((s: Student) => s.name === (currentUser?.trueName || currentUser?.username)); | |
| if (me) { | |
| setStudent(me); | |
| const sched = await api.schedules.get({ className: me.className }); | |
| setSchedules(sched); | |
| // Check today's attendance | |
| const todayStr = new Date().toISOString().split('T')[0]; | |
| const att = await api.attendance.get({ studentId: me._id || String(me.id), date: todayStr }); | |
| if (att && att.length > 0) setTodayAttendance(att[0]); | |
| } | |
| } catch (e) { console.error(e); } | |
| }; | |
| const handleCheckIn = async () => { | |
| if(!student) return; | |
| try { | |
| const todayStr = new Date().toISOString().split('T')[0]; | |
| await api.attendance.checkIn({ | |
| studentId: student._id || String(student.id), | |
| date: todayStr | |
| }); | |
| alert('打卡成功!已记录考勤。'); | |
| loadData(); | |
| } catch(e: any) { | |
| alert('打卡失败: ' + (e.message || '未知错误')); | |
| } | |
| }; | |
| const handleSubmitLeave = async () => { | |
| if(!student || !leaveReason || !leaveDates.start) return alert('请填写完整'); | |
| try { | |
| await api.attendance.applyLeave({ | |
| studentId: student._id || String(student.id), | |
| studentName: student.name, | |
| className: student.className, | |
| reason: leaveReason, | |
| startDate: leaveDates.start, | |
| endDate: leaveDates.end || leaveDates.start | |
| }); | |
| alert('请假申请已提交'); | |
| setIsLeaveOpen(false); | |
| loadData(); // Update status if leave is for today | |
| } catch(e) { alert('提交失败'); } | |
| }; | |
| const today = new Date().getDay(); // 0-6 | |
| const weekDays = ['周日','周一','周二','周三','周四','周五','周六']; | |
| const todaySchedules = schedules.filter(s => s.dayOfWeek === today).sort((a,b) => a.period - b.period); | |
| return ( | |
| <div className="space-y-6"> | |
| <div className="bg-gradient-to-r from-blue-600 to-indigo-600 rounded-2xl p-8 text-white shadow-lg"> | |
| <h1 className="text-3xl font-bold mb-2">你好, {currentUser?.trueName || currentUser?.username} 👋</h1> | |
| <p className="opacity-90">今天是 {currentTime.toLocaleDateString()} {weekDays[today]} | {student?.className || '加载中...'}</p> | |
| </div> | |
| <div className="grid grid-cols-1 md:grid-cols-3 gap-6"> | |
| {/* Attendance Card */} | |
| <div className="bg-white p-6 rounded-xl shadow-sm border border-gray-100 flex flex-col items-center justify-center text-center relative overflow-hidden"> | |
| <div className="mb-4 bg-blue-50 p-4 rounded-full z-10"> | |
| {todayAttendance?.status === 'Leave' ? <FileText size={32} className="text-orange-500"/> : <MapPin size={32} className="text-blue-600" />} | |
| </div> | |
| <h3 className="text-lg font-bold text-gray-800 mb-2 z-10">每日考勤</h3> | |
| {todayAttendance ? ( | |
| <div className="mb-6 z-10"> | |
| <p className={`text-lg font-bold ${todayAttendance.status==='Present'?'text-green-600':todayAttendance.status==='Leave'?'text-orange-500':'text-red-500'}`}> | |
| {todayAttendance.status==='Present'?'已签到':todayAttendance.status==='Leave'?'请假中':'缺勤'} | |
| </p> | |
| <p className="text-xs text-gray-400">{new Date(todayAttendance.checkInTime || new Date()).toLocaleTimeString()}</p> | |
| </div> | |
| ) : ( | |
| <p className="text-sm text-gray-500 mb-6 z-10">记录你的在校状态</p> | |
| )} | |
| <button | |
| onClick={handleCheckIn} | |
| disabled={!!todayAttendance} | |
| className={`w-full py-3 rounded-xl font-bold transition-all z-10 ${todayAttendance ? 'bg-gray-100 text-gray-400 cursor-not-allowed' : 'bg-blue-600 text-white hover:bg-blue-700 shadow-md'}`} | |
| > | |
| {todayAttendance ? '今日已记录' : '立即打卡'} | |
| </button> | |
| </div> | |
| {/* Today's Schedule */} | |
| <div className="md:col-span-2 bg-white p-6 rounded-xl shadow-sm border border-gray-100"> | |
| <div className="flex justify-between items-center mb-6"> | |
| <h3 className="font-bold text-gray-800 flex items-center"><Calendar className="mr-2 text-indigo-600"/> 今日课表</h3> | |
| <span className="text-xs bg-indigo-50 text-indigo-600 px-2 py-1 rounded-full">{todaySchedules.length} 节课</span> | |
| </div> | |
| <div className="space-y-3 max-h-64 overflow-y-auto custom-scrollbar"> | |
| {todaySchedules.length > 0 ? todaySchedules.map(s => ( | |
| <div key={s._id} className="flex items-center p-3 bg-gray-50 rounded-lg border border-gray-100"> | |
| <div className="w-12 text-center font-bold text-gray-400 text-sm">第{s.period}节</div> | |
| <div className="w-px h-8 bg-gray-200 mx-4"></div> | |
| <div className="flex-1"> | |
| <div className="font-bold text-gray-800">{s.subject}</div> | |
| <div className="text-xs text-gray-500">{s.teacherName}</div> | |
| </div> | |
| <Clock size={16} className="text-gray-300"/> | |
| </div> | |
| )) : ( | |
| <div className="text-center py-10 text-gray-400"> | |
| <Coffee size={32} className="mx-auto mb-2 opacity-50"/> | |
| <p>今天没有课程安排,好好休息!</p> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| {/* Quick Actions */} | |
| <div className="bg-white p-6 rounded-xl shadow-sm border border-gray-100"> | |
| <h3 className="font-bold text-gray-800 mb-4">常用功能</h3> | |
| <div className="grid grid-cols-2 md:grid-cols-4 gap-4"> | |
| <button onClick={() => setIsLeaveOpen(true)} className="p-4 bg-orange-50 rounded-xl text-orange-700 flex flex-col items-center hover:bg-orange-100 transition-colors"> | |
| <FileText className="mb-2"/> | |
| <span className="text-sm font-medium">请假申请</span> | |
| </button> | |
| </div> | |
| </div> | |
| {/* Leave Modal */} | |
| {isLeaveOpen && ( | |
| <div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4"> | |
| <div className="bg-white rounded-xl p-6 w-full max-w-sm animate-in zoom-in-95"> | |
| <div className="flex justify-between items-center mb-4"> | |
| <h3 className="font-bold text-lg">请假申请</h3> | |
| <button onClick={()=>setIsLeaveOpen(false)}><X size={20} className="text-gray-400"/></button> | |
| </div> | |
| <div className="space-y-3"> | |
| <div> | |
| <label className="text-xs text-gray-500 block mb-1">开始日期</label> | |
| <input type="date" className="w-full border rounded p-2 text-sm" value={leaveDates.start} onChange={e=>setLeaveDates({...leaveDates, start:e.target.value})}/> | |
| </div> | |
| <div> | |
| <label className="text-xs text-gray-500 block mb-1">结束日期 (可选)</label> | |
| <input type="date" className="w-full border rounded p-2 text-sm" value={leaveDates.end} onChange={e=>setLeaveDates({...leaveDates, end:e.target.value})}/> | |
| </div> | |
| <div> | |
| <label className="text-xs text-gray-500 block mb-1">请假事由</label> | |
| <textarea className="w-full border rounded p-2 text-sm h-24" placeholder="请输入请假原因..." value={leaveReason} onChange={e=>setLeaveReason(e.target.value)}/> | |
| </div> | |
| <button onClick={handleSubmitLeave} className="w-full bg-orange-500 text-white py-2 rounded-lg font-bold hover:bg-orange-600 mt-2">提交申请</button> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| }; |