feat(web): expand React student services

Migrate curriculum progress, academic analytics, classroom availability, calendar, student status changes, official documents, graduation audits, tuition, other exams, makeup schedules, and course evaluations. Connect role-aware navigation and preserve existing API workflows.
This commit is contained in:
biss committed 2026-10-02 08:24:47 +08:00
1 parent 2a06e37267
commit dcdbb86e2f
18 files changed
+402

No files matched your search

+22
View File
@@ -17,6 +17,17 @@ const StudentWarningsPage = lazy(() => import('../pages/StudentWarningsPage'))
const StudentProfilePage = lazy(() => import('../pages/StudentProfilePage'))
const StudentAttendancePage = lazy(() => import('../pages/StudentAttendancePage'))
const CourseSelectionPage = lazy(() => import('../pages/CourseSelectionPage'))
const StudentCurriculumPage = lazy(() => import('../pages/StudentCurriculumPage'))
const AcademicOverviewPage = lazy(() => import('../pages/AcademicOverviewPage'))
const FreeClassroomsPage = lazy(() => import('../pages/FreeClassroomsPage'))
const StudentStatusChangesPage = lazy(() => import('../pages/StudentStatusChangesPage'))
const OfficialDocumentsPage = lazy(() => import('../pages/OfficialDocumentsPage'))
const GraduationAuditPage = lazy(() => import('../pages/GraduationAuditPage'))
const TuitionPage = lazy(() => import('../pages/TuitionPage'))
const AcademicCalendarPage = lazy(() => import('../pages/AcademicCalendarPage'))
const OtherExamsPage = lazy(() => import('../pages/OtherExamsPage'))
const MakeupExamsPage = lazy(() => import('../pages/MakeupExamsPage'))
const EvaluationsPage = lazy(() => import('../pages/EvaluationsPage'))
const SsoCallbackPage = lazy(() => import('../pages/SsoCallbackPage'))
const SsoBindPage = lazy(() => import('../pages/SsoBindPage'))
const AppShell = lazy(() => import('../layout/AppShell'))
@@ -68,6 +79,17 @@ function AppRoutes() {
<Route path="/my-profile" element={<StudentProfilePage />} />
<Route path="/my-attendance" element={<StudentAttendancePage />} />
<Route path="/course-selections" element={<CourseSelectionPage />} />
<Route path="/my-curriculum" element={<StudentCurriculumPage />} />
<Route path="/academic-overview" element={<AcademicOverviewPage />} />
<Route path="/free-classrooms" element={<FreeClassroomsPage />} />
<Route path="/student-status-changes" element={<StudentStatusChangesPage />} />
<Route path="/official-documents" element={<OfficialDocumentsPage />} />
<Route path="/graduation-audits" element={<GraduationAuditPage />} />
<Route path="/tuition" element={<TuitionPage />} />
<Route path="/academic-calendar" element={<AcademicCalendarPage />} />
<Route path="/other-exams" element={<OtherExamsPage />} />
<Route path="/makeup-exams" element={<MakeupExamsPage />} />
<Route path="/evaluations" element={<EvaluationsPage />} />
<Route path="/" element={<Navigate to="/dashboard" replace />} />
</Route>
<Route path="*" element={<Navigate to="/dashboard" replace />} />
+22
View File
@@ -14,6 +14,17 @@ import {
AuditOutlined,
CheckSquareOutlined,
FormOutlined,
BookOutlined,
LineChartOutlined,
EnvironmentOutlined,
SwapOutlined,
FileProtectOutlined,
SafetyOutlined,
WalletOutlined,
ScheduleOutlined,
TrophyOutlined,
SolutionOutlined,
CommentOutlined,
} from '@ant-design/icons'
import { Avatar, Badge, Button, Layout, Menu, Tag } from 'antd'
import { useQuery } from '@tanstack/react-query'
@@ -58,6 +69,17 @@ export default function AppShell() {
{ key: '/grades', icon: <ReadOutlined />, label: '我的成绩' },
{ key: '/my-attendance', icon: <CheckSquareOutlined />, label: '我的考勤' },
{ key: '/course-selections', icon: <FormOutlined />, label: '学生选课' },
{ key: '/my-curriculum', icon: <BookOutlined />, label: '培养方案' },
{ key: '/academic-overview', icon: <LineChartOutlined />, label: '学业总览' },
{ key: '/free-classrooms', icon: <EnvironmentOutlined />, label: '空闲教室' },
{ key: '/academic-calendar', icon: <ScheduleOutlined />, label: '学术校历' },
{ key: '/student-status-changes', icon: <SwapOutlined />, label: '学籍异动' },
{ key: '/official-documents', icon: <FileProtectOutlined />, label: '电子凭证' },
{ key: '/graduation-audits', icon: <SafetyOutlined />, label: '毕业审核' },
{ key: '/tuition', icon: <WalletOutlined />, label: '我的学费' },
{ key: '/other-exams', icon: <TrophyOutlined />, label: '其他考试' },
{ key: '/makeup-exams', icon: <SolutionOutlined />, label: '补考安排' },
{ key: '/evaluations', icon: <CommentOutlined />, label: '教学评价' },
{ key: '/warnings', icon: <WarningOutlined />, label: '学业预警' },
] : []),
...(user?.roles.includes('SuperAdmin')
@@ -0,0 +1,4 @@
import{useMemo,useState}from'react';import{LeftOutlined,RightOutlined}from'@ant-design/icons';import{Alert,Button,Empty,Skeleton,Tag}from'antd';import{useQuery}from'@tanstack/react-query';import http,{apiErrorMessage}from'../api/http';import PageHeader from'../components/PageHeader';import'./student-records.css'
interface Event{id:string;title:string;kind:string;startsOn:string;endsOn?:string;reminderDays:number;description?:string;isOffDay?:boolean}const kindLabels:Record<string,string>={CourseSelection:'选课',Grade:'成绩',Evaluation:'评教',Exam:'考试',GraduationAudit:'毕业审核',PublicHoliday:'节假日',Other:'其他'};const dateKey=(d:Date)=>`${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`
interface Exam{id:string;examDate:string;courseName:string;startsAt:string;endsAt:string;buildingName?:string;classroomName?:string}
export default function AcademicCalendarPage(){const[cursor,setCursor]=useState(()=>new Date(new Date().getFullYear(),new Date().getMonth(),1));const q=useQuery({queryKey:['academic-calendar'],queryFn:async()=>{const[calendar,exams]=await Promise.all([http.get<Event[]>('/academic-calendar'),http.get<Exam[]>('/exams/my-schedule')]);return[...calendar.data,...exams.data.map(exam=>({id:`exam-${exam.id}`,title:`考试:${exam.courseName}`,kind:'Exam',startsOn:exam.examDate,reminderDays:7,description:`${exam.startsAt.slice(11,16)}—${exam.endsAt.slice(11,16)} · ${[exam.buildingName,exam.classroomName].filter(Boolean).join(' ')}`}))]as Event[]}});const days=useMemo(()=>{const first=new Date(cursor.getFullYear(),cursor.getMonth(),1),start=new Date(first);start.setDate(first.getDate()-((first.getDay()+6)%7));return Array.from({length:42},(_,i)=>{const d=new Date(start);d.setDate(start.getDate()+i);const key=dateKey(d);return{d,key,current:d.getMonth()===cursor.getMonth(),events:(q.data??[]).filter(e=>e.startsOn<=key&&(e.endsOn??e.startsOn)>=key)}})},[cursor,q.data]);if(q.isLoading)return <main className="page-container"><Skeleton active paragraph={{rows:10}}/></main>;if(q.isError)return <main className="page-container"><Alert type="error" showIcon message="校历无法加载" description={apiErrorMessage(q.error)}/></main>;return <main className="page-container record-service-page"><PageHeader kicker="ACADEMIC CALENDAR" title="学术校历" description="集中查看选课、成绩、评教、考试、毕业审核和节假日日程。" aside={<div className="calendar-nav"><Button icon={<LeftOutlined/>} onClick={()=>setCursor(new Date(cursor.getFullYear(),cursor.getMonth()-1,1))}/><strong>{cursor.getFullYear()} 年 {cursor.getMonth()+1} 月</strong><Button icon={<RightOutlined/>} onClick={()=>setCursor(new Date(cursor.getFullYear(),cursor.getMonth()+1,1))}/><Button onClick={()=>setCursor(new Date(new Date().getFullYear(),new Date().getMonth(),1))}>今天</Button></div>}/><section className="calendar-grid"><header>{['一','二','三','四','五','六','日'].map(x=><span key={x}>周{x}</span>)}</header><div>{days.map(day=><article key={day.key} className={`${day.current?'':'muted'} ${day.key===dateKey(new Date())?'today':''}`}><time>{day.d.getDate()}</time>{day.events.slice(0,3).map(e=><div key={e.id} title={e.description}><Tag color={e.kind==='PublicHoliday'?(e.isOffDay?'red':'orange'):e.kind==='Exam'?'purple':'blue'}>{kindLabels[e.kind]??e.kind}</Tag><span>{e.title}</span></div>)}{day.events.length>3&&<small>另有 {day.events.length-3} 项</small>}</article>)}</div></section><section className="calendar-agenda"><header><span>本月日程</span><strong>{q.data?.filter(e=>{const s=`${cursor.getFullYear()}-${String(cursor.getMonth()+1).padStart(2,'0')}`;return e.startsOn.startsWith(s)||(e.endsOn??'').startsWith(s)}).length??0} 项</strong></header>{q.data?.filter(e=>{const month=`${cursor.getFullYear()}-${String(cursor.getMonth()+1).padStart(2,'0')}`;return e.startsOn.startsWith(month)||(e.endsOn??'').startsWith(month)}).map(e=><article key={e.id}><time>{e.startsOn}{e.endsOn&&e.endsOn!==e.startsOn?` — ${e.endsOn}`:''}</time><div><Tag>{kindLabels[e.kind]??e.kind}</Tag><strong>{e.title}</strong><p>{e.description||'暂无补充说明'}</p></div></article>)}{!q.data?.length&&<Empty description="当前学期暂无校历事件"/>}</section></main>}
Loaded 3 of 18 files, more files were not shown because too many files have changed in this diff. Show more