import React, { useEffect, useMemo, useState } from 'react'; import { motion } from 'motion/react'; import { Award, BookOpen, Calendar, 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 { cacheLatestSchoolData, fetchLatestSchoolData } from '@/src/lib/school'; import { buildScheduleTable } from '@/src/lib/schedule-table'; import type { CourseResponse, GradeResponse, LatestSchoolDataResponse } 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} 节`; } function getCourseTheme(courseName?: string) { const themes = [ { panel: 'bg-gradient-to-br from-[#336EFF]/26 via-[#4D7FFF]/18 to-[#7AA2FF]/12', border: 'border-[#5E88FF]/45', accent: 'bg-[#5D8BFF]', title: 'text-blue-50', meta: 'text-blue-100/80', badge: 'bg-[#336EFF]/22 text-blue-100', shadow: 'shadow-[0_10px_30px_rgba(51,110,255,0.18)]', }, { panel: 'bg-gradient-to-br from-cyan-500/24 via-sky-500/18 to-blue-500/10', border: 'border-cyan-400/40', accent: 'bg-cyan-400', title: 'text-cyan-50', meta: 'text-cyan-100/80', badge: 'bg-cyan-500/18 text-cyan-100', shadow: 'shadow-[0_10px_30px_rgba(34,211,238,0.16)]', }, { panel: 'bg-gradient-to-br from-indigo-500/24 via-blue-500/18 to-slate-500/8', border: 'border-indigo-400/40', accent: 'bg-indigo-400', title: 'text-indigo-50', meta: 'text-indigo-100/80', badge: 'bg-indigo-500/18 text-indigo-100', shadow: 'shadow-[0_10px_30px_rgba(99,102,241,0.16)]', }, { panel: 'bg-gradient-to-br from-sky-500/24 via-blue-500/16 to-violet-500/10', border: 'border-sky-400/40', accent: 'bg-sky-400', title: 'text-sky-50', meta: 'text-sky-100/80', badge: 'bg-sky-500/18 text-sky-100', shadow: 'shadow-[0_10px_30px_rgba(14,165,233,0.16)]', }, { panel: 'bg-gradient-to-br from-violet-500/22 via-indigo-500/16 to-blue-500/10', border: 'border-violet-400/38', accent: 'bg-violet-400', title: 'text-violet-50', meta: 'text-violet-100/80', badge: 'bg-violet-500/18 text-violet-100', shadow: 'shadow-[0_10px_30px_rgba(139,92,246,0.14)]', }, { panel: 'bg-gradient-to-br from-teal-500/22 via-cyan-500/16 to-sky-500/10', border: 'border-teal-400/38', accent: 'bg-teal-400', title: 'text-teal-50', meta: 'text-teal-100/80', badge: 'bg-teal-500/18 text-teal-100', shadow: 'shadow-[0_10px_30px_rgba(45,212,191,0.14)]', }, ]; if (!courseName) { return themes[0]; } let hash = 0; for (let index = 0; index < courseName.length; index += 1) { hash = courseName.charCodeAt(index) + ((hash << 5) - hash); } return themes[Math.abs(hash) % themes.length]; } 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 applySchoolResults = (results: LatestSchoolDataResponse) => { setStudentId(results.studentId); setSemester(results.semester); setQueried(true); setSchedule(results.schedule); setGrades(results.grades); cacheLatestSchoolData(results); }; 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; refresh?: 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, refresh: options.refresh ? 'true' : 'false', }).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(() => { let cancelled = false; async function loadInitialSchoolData() { if (storedQuery) { await loadSchoolData(storedQuery.studentId, storedQuery.semester, { background: true, }); return; } const latest = await fetchLatestSchoolData(); if (!latest || cancelled) { return; } applySchoolResults(latest); } loadInitialSchoolData().catch(() => undefined); return () => { cancelled = true; }; }, []); const handleQuery = async (event: React.FormEvent) => { event.preventDefault(); await loadSchoolData(studentId, semester, { refresh: true }); }; 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' | 'noon' | 'afternoon' | 'evening', string> = { morning: '上午', noon: '中午', afternoon: '下午', evening: '晚上', }; const periodOrder = ['morning', 'noon', 'afternoon', 'evening'] as const; const rows = buildScheduleTable(schedule); return (
本周课表 周一到周日完整展示,空白节次保持固定格子,跨节课程会按节数占满网格。
上午 1-4 节 中午 5 节 下午 6-9 节 晚上 10-12 节
时段
节次
{days.map((day) => (
{day}
))} {periodOrder.map((period, index) => (
{periodLabels[period]}
))} {rows.map((row) => (
Section {row.section}
))} {rows.flatMap((row) => row.slots.map((slot, columnIndex) => { if (slot.type !== 'empty') { return null; } return (
); }), )} {rows.flatMap((row) => row.slots.map((slot, columnIndex) => { if (slot.type !== 'course') { return null; } const theme = getCourseTheme(slot.course?.courseName); const rowSpan = slot.rowSpan ?? 1; return (

{slot.course?.courseName}

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

{slot.course?.classroom ?? '教室待定'}

{slot.course?.teacher ?? '教师待定'}

); }), )}
); } function GradesView({ queried, grades }: { queried: boolean; grades: GradeResponse[] }) { if (!queried) { return (

请先查询成绩

); } const terms = grades.reduce>((accumulator, grade) => { const semester = grade.semester ?? '未分类'; if (!accumulator[semester]) { accumulator[semester] = []; } accumulator[semester].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 ?
暂无成绩数据
: null}
); }