import React, { useEffect, useMemo, useState } from 'react'; import { motion } from 'motion/react'; import { Award, BookOpen, Calendar, ChevronRight, GraduationCap, Lock, MapPin, Search, User } from 'lucide-react'; import { Button } from '@/src/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/src/components/ui/card'; import { Input } from '@/src/components/ui/input'; import { apiRequest } from '@/src/lib/api'; import { readCachedValue, writeCachedValue } from '@/src/lib/cache'; import { getSchoolResultsCacheKey, readStoredSchoolQuery, writeStoredSchoolQuery } from '@/src/lib/page-cache'; import { buildScheduleTable } from '@/src/lib/schedule-table'; import type { CourseResponse, GradeResponse } from '@/src/lib/types'; import { cn } from '@/src/lib/utils'; function formatSections(startTime?: number | null, endTime?: number | null) { if (!startTime || !endTime) { return '节次待定'; } return `第 ${startTime}-${endTime} 节`; } export default function School() { const storedQuery = readStoredSchoolQuery(); const initialStudentId = storedQuery?.studentId ?? '2023123456'; const initialSemester = storedQuery?.semester ?? '2025-spring'; const initialCachedResults = readCachedValue<{ queried: boolean; schedule: CourseResponse[]; grades: GradeResponse[]; }>(getSchoolResultsCacheKey(initialStudentId, initialSemester)); const [activeTab, setActiveTab] = useState<'schedule' | 'grades'>('schedule'); const [studentId, setStudentId] = useState(initialStudentId); const [password, setPassword] = useState('password123'); const [semester, setSemester] = useState(initialSemester); const [loading, setLoading] = useState(false); const [queried, setQueried] = useState(initialCachedResults?.queried ?? false); const [schedule, setSchedule] = useState(initialCachedResults?.schedule ?? []); const [grades, setGrades] = useState(initialCachedResults?.grades ?? []); const averageGrade = useMemo(() => { if (grades.length === 0) { return '0.0'; } const sum = grades.reduce((total, item) => total + (item.grade ?? 0), 0); return (sum / grades.length).toFixed(1); }, [grades]); const loadSchoolData = async ( nextStudentId: string, nextSemester: string, options: { background?: boolean } = {}, ) => { const cacheKey = getSchoolResultsCacheKey(nextStudentId, nextSemester); const cachedResults = readCachedValue<{ queried: boolean; schedule: CourseResponse[]; grades: GradeResponse[]; }>(cacheKey); if (!options.background) { setLoading(true); } writeStoredSchoolQuery({ studentId: nextStudentId, semester: nextSemester, }); try { const queryString = new URLSearchParams({ studentId: nextStudentId, semester: nextSemester, }).toString(); const [scheduleData, gradeData] = await Promise.all([ apiRequest(`/cqu/schedule?${queryString}`), apiRequest(`/cqu/grades?${queryString}`), ]); setQueried(true); setSchedule(scheduleData); setGrades(gradeData); writeCachedValue(cacheKey, { queried: true, studentId: nextStudentId, semester: nextSemester, schedule: scheduleData, grades: gradeData, }); } catch { if (!cachedResults) { setQueried(false); setSchedule([]); setGrades([]); } } finally { if (!options.background) { setLoading(false); } } }; useEffect(() => { if (!storedQuery) { return; } loadSchoolData(storedQuery.studentId, storedQuery.semester, { background: true, }).catch(() => undefined); }, []); const handleQuery = async (event: React.FormEvent) => { event.preventDefault(); await loadSchoolData(studentId, semester); }; return (
教务查询 输入学号、密码和学期后同步课表与成绩。
setStudentId(event.target.value)} className="pl-9 bg-black/20" required />
setPassword(event.target.value)} className="pl-9 bg-black/20" required />
数据摘要 展示当前缓存或最近一次查询结果。 {queried ? (
) : (

暂无缓存数据,请先执行查询。

)}
{activeTab === 'schedule' ? : }
); } function DatabaseIcon(props: React.SVGProps) { return ( ); } function SummaryItem({ label, value, icon: Icon, }: { label: string; value: string; icon: React.ComponentType<{ className?: string }>; }) { return (

{label}

{value}

); } function getCourseTheme(courseName?: string) { if (!courseName) { return { bg: 'bg-slate-500/10', border: 'border-slate-500/20', text: 'text-slate-300', }; } const themes = [ { bg: 'bg-blue-500/20 hover:bg-blue-500/30', border: 'border-blue-400/30', text: 'text-blue-100', }, { bg: 'bg-indigo-500/20 hover:bg-indigo-500/30', border: 'border-indigo-400/30', text: 'text-indigo-100', }, { bg: 'bg-cyan-500/20 hover:bg-cyan-500/30', border: 'border-cyan-400/30', text: 'text-cyan-100', }, { bg: 'bg-sky-500/20 hover:bg-sky-500/30', border: 'border-sky-400/30', text: 'text-sky-100', }, { bg: 'bg-violet-500/20 hover:bg-violet-500/30', border: 'border-violet-400/30', text: 'text-violet-100', }, { bg: 'bg-teal-500/20 hover:bg-teal-500/30', border: 'border-teal-400/30', text: 'text-teal-100', }, ]; let hash = 0; for (let i = 0; i < courseName.length; i += 1) { hash = courseName.charCodeAt(i) + ((hash << 5) - hash); } return themes[Math.abs(hash) % themes.length]; } function ScheduleView({ queried, schedule }: { queried: boolean; schedule: CourseResponse[] }) { if (!queried) { return (

请先查询课表

); } const days = ['周一', '周二', '周三', '周四', '周五']; const rows = buildScheduleTable(schedule); return (
我的课表 2025 春季学期
{' '} 必修 {' '} 选修
))} {rows.map((row, index) => ( {/* Morning/Afternoon label */} {index % 4 === 0 && ( )} {/* Section Number */} {/* Cells */} {row.slots.map((slot, colIdx) => { if (slot.type === 'covered') return null; if (slot.type === 'empty') { return ( ); })} ))}
{days.map((d) => ( {d}
{row.section <= 4 ? '上午' : row.section <= 8 ? '下午' : '晚上'}
{row.section}
); } const theme = getCourseTheme(slot.course?.courseName ?? ''); return (

{slot.course?.courseName}

{' '} {slot.course?.classroom ?? '未知'}

{slot.course?.teacher ?? '老师'}
); } function GradesView({ queried, grades }: { queried: boolean; grades: GradeResponse[] }) { if (!queried) { return (

请先查询成绩

); } const terms = grades.reduce>((accumulator, grade) => { const term = grade.semester ?? '未分类'; if (!accumulator[term]) { accumulator[term] = []; } accumulator[term].push(grade.grade ?? 0); return accumulator; }, {}); const getScoreStyle = (score: number) => { if (score >= 95) return 'bg-[#336EFF]/50 text-white'; if (score >= 90) return 'bg-[#336EFF]/40 text-white/90'; if (score >= 85) return 'bg-[#336EFF]/30 text-white/80'; if (score >= 80) return 'bg-slate-700/60 text-white/70'; if (score >= 75) return 'bg-slate-700/40 text-white/60'; return 'bg-slate-800/60 text-white/50'; }; return ( 成绩热力图
{Object.entries(terms).map(([term, scores]) => (

{term}

{scores.map((score, index) => (
{score}
))}
))} {Object.keys(terms).length === 0 &&
暂无成绩数据
}
); }