import React, { useEffect, useMemo, useState } from 'react'; import { motion } from 'motion/react'; import { Award, BookOpen, Calendar, ChevronRight, GraduationCap, Lock, 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 ScheduleView({ queried, schedule }: { queried: boolean; schedule: CourseResponse[] }) { if (!queried) { return (

请先查询课表

); } const days = ['周一', '周二', '周三', '周四', '周五']; const periodLabels: Record<'morning' | 'afternoon' | 'evening', string> = { morning: '上午', afternoon: '下午', evening: '晚上', }; const rows = buildScheduleTable(schedule); return ( 本周课表 上午 4 节、下午 4 节、晚上 4 节。没有课的格子保持为空。
{days.map((day) => ( ))} {rows.map((row) => ( {row.slots.map((slot, index) => { if (slot.type === 'covered') { return null; } if (slot.type === 'empty') { return ( ); })} ))}
节次 {day}

{periodLabels[row.period]}

第 {row.section} 节

); } return (

{formatSections(slot.course?.startTime, slot.course?.endTime)}

{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 &&
暂无成绩数据
}
); }