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:
1 parent
35b4ad6a30
commit
1be3f7cbbf
18 files changed
+402
No files matched your search
@@ -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 />} />
|
||||
|
||||
@@ -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
Reference in new issue
Block a user