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