stud-manager / pages /Dashboard.tsx
dvc890's picture
Update pages/Dashboard.tsx
85a6eba verified
import React, { useEffect, useState } from 'react';
import { Users, BookOpen, GraduationCap, TrendingUp, AlertTriangle, Activity, Calendar, X, CheckCircle, Plus } from 'lucide-react';
import { api } from '../services/api';
import { Score, ClassInfo, Subject, Schedule, User, Exam } from '../types';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
import { StudentDashboard } from './StudentDashboard';
import { TeacherDashboard } from './TeacherDashboard';
import { TodoList } from '../components/TodoList';
interface DashboardProps {
onNavigate: (view: string) => void;
}
export const gradeOrder: Record<string, number> = {
'一年级': 1, '二年级': 2, '三年级': 3, '四年级': 4, '五年级': 5, '六年级': 6,
'初一': 7, '七年级': 7, '初二': 8, '八年级': 8, '初三': 9, '九年级': 9,
'高一': 10, '高二': 11, '高三': 12
};
export const sortGrades = (a: string, b: string) => (gradeOrder[a] || 99) - (gradeOrder[b] || 99);
const extractGrade = (s: string) => {
if (!s) return '';
const keys = Object.keys(gradeOrder);
return keys.find(g => s.startsWith(g)) || '';
};
export const sortClasses = (a: ClassInfo | string, b: ClassInfo | string) => {
const nameA = typeof a === 'string' ? a : (a.grade + a.className);
const nameB = typeof b === 'string' ? b : (b.grade + b.className);
if (!nameA || !nameB) return 0;
const gradeA = extractGrade(nameA);
const gradeB = extractGrade(nameB);
if (gradeA && gradeB && gradeA !== gradeB) { return sortGrades(gradeA, gradeB); }
const getNum = (str: string) => { const match = str.match(/(\d+)/); return match ? parseInt(match[1]) : 0; };
return getNum(nameA) - getNum(nameB);
};
export const Dashboard: React.FC<DashboardProps> = ({ onNavigate }) => {
const [stats, setStats] = useState({ studentCount: 0, courseCount: 0, avgScore: 0, excellentRate: '0%' });
const [warnings, setWarnings] = useState<string[]>([]);
const [loading, setLoading] = useState(true);
const [trendData, setTrendData] = useState<any[]>([]);
const currentUser = api.auth.getCurrentUser();
const isAdmin = currentUser?.role === 'ADMIN' || currentUser?.role === 'PRINCIPAL';
const isTeacher = currentUser?.role === 'TEACHER';
const isStudent = currentUser?.role === 'STUDENT';
useEffect(() => {
if (isStudent || isTeacher) {
setLoading(false);
return;
}
const loadStats = async () => {
try {
const [summary, scoresData, examsData] = await Promise.all([
api.stats.getSummary(),
api.scores.getAll(),
api.exams.getAll()
]);
setStats(summary);
// Calculate Real Trend Data
const examMap = new Map<string, { total: number, count: number, date: string }>();
// 1. Initialize map with known exams to get dates
(examsData as Exam[]).forEach((e) => {
if (e.name) {
examMap.set(e.name, { total: 0, count: 0, date: e.date || '9999-99-99' });
}
});
// 2. Aggregate scores
(scoresData as Score[]).forEach((s) => {
if (s.status === 'Normal') {
const name = s.examName || s.type;
if (!examMap.has(name)) {
// Fallback for exams not in Exam definitions
examMap.set(name, { total: 0, count: 0, date: '9999-99-99' });
}
const entry = examMap.get(name)!;
entry.total += s.score;
entry.count += 1;
}
});
// 3. Convert to array and sort
const trend = Array.from(examMap.entries())
.filter(([_, data]) => data.count > 0) // Filter out exams with no scores
.map(([name, data]) => ({
name: name,
score: Math.round(data.total / data.count),
date: data.date
}))
.sort((a, b) => a.date.localeCompare(b.date));
// If no real data, keep empty to show empty state or fallback?
// We will just set whatever we found.
setTrendData(trend);
} catch (e) {
console.error(e);
} finally {
setLoading(false);
}
};
loadStats();
}, [isStudent, isTeacher]);
if (isStudent) return <StudentDashboard />;
if (isTeacher) return <TeacherDashboard />;
// Admin Dashboard View
return (
<div className="space-y-6">
<h1 className="text-2xl font-bold text-gray-800">全校概览</h1>
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
<div className="bg-white p-6 rounded-xl shadow-sm border border-gray-100 flex items-center">
<div className="p-3 bg-blue-100 rounded-full text-blue-600 mr-4">
<Users size={24} />
</div>
<div>
<p className="text-gray-500 text-sm">在校学生</p>
<p className="text-2xl font-bold text-gray-800">{stats.studentCount}</p>
</div>
</div>
<div className="bg-white p-6 rounded-xl shadow-sm border border-gray-100 flex items-center">
<div className="p-3 bg-green-100 rounded-full text-green-600 mr-4">
<BookOpen size={24} />
</div>
<div>
<p className="text-gray-500 text-sm">开设课程</p>
<p className="text-2xl font-bold text-gray-800">{stats.courseCount}</p>
</div>
</div>
<div className="bg-white p-6 rounded-xl shadow-sm border border-gray-100 flex items-center">
<div className="p-3 bg-yellow-100 rounded-full text-yellow-600 mr-4">
<GraduationCap size={24} />
</div>
<div>
<p className="text-gray-500 text-sm">平均分</p>
<p className="text-2xl font-bold text-gray-800">{stats.avgScore}</p>
</div>
</div>
<div className="bg-white p-6 rounded-xl shadow-sm border border-gray-100 flex items-center">
<div className="p-3 bg-purple-100 rounded-full text-purple-600 mr-4">
<Activity size={24} />
</div>
<div>
<p className="text-gray-500 text-sm">优秀率</p>
<p className="text-2xl font-bold text-gray-800">{stats.excellentRate}</p>
</div>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="bg-white p-6 rounded-xl shadow-sm border border-gray-100">
<h3 className="font-bold text-gray-800 mb-4 flex items-center"><TrendingUp size={20} className="mr-2 text-blue-600"/> 成绩走势</h3>
<div className="h-64">
{trendData.length > 0 ? (
<ResponsiveContainer width="100%" height="100%">
<LineChart data={trendData}>
<CartesianGrid strokeDasharray="3 3" vertical={false}/>
<XAxis dataKey="name" tick={{fontSize: 12}} />
<YAxis domain={[0, 100]} />
<Tooltip />
<Line type="monotone" dataKey="score" stroke="#3b82f6" strokeWidth={3} dot={{r: 4}} activeDot={{r: 6}} />
</LineChart>
</ResponsiveContainer>
) : (
<div className="flex flex-col items-center justify-center h-full text-gray-400">
<Activity size={48} className="mb-2 opacity-20"/>
<p>暂无考试数据</p>
<p className="text-xs mt-1">请在“成绩管理”中录入成绩</p>
</div>
)}
</div>
</div>
<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 gap-4">
<button onClick={() => onNavigate('students')} className="p-4 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors flex flex-col items-center justify-center text-gray-700">
<Users className="mb-2 text-blue-500" />
<span>学生管理</span>
</button>
<button onClick={() => onNavigate('classes')} className="p-4 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors flex flex-col items-center justify-center text-gray-700">
<BookOpen className="mb-2 text-green-500" />
<span>班级管理</span>
</button>
<button onClick={() => onNavigate('reports')} className="p-4 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors flex flex-col items-center justify-center text-gray-700">
<Activity className="mb-2 text-purple-500" />
<span>报表统计</span>
</button>
<button onClick={() => onNavigate('settings')} className="p-4 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors flex flex-col items-center justify-center text-gray-700">
<Activity className="mb-2 text-gray-500" />
<span>系统设置</span>
</button>
</div>
</div>
</div>
</div>
);
};