feat(web): migrate student academic insights
Add React exam and invigilation schedules, student transcript metrics and score breakdowns, and academic warning acknowledgement with support follow-up timelines. Connect role-aware navigation and dashboard links.
This commit is contained in:
1 parent
0a64424035
commit
2c14001677
9 files changed
+217
-5
No files matched your search
@@ -11,6 +11,9 @@ const AccountPage = lazy(() => import('../pages/AccountPage'))
|
||||
const UsersPage = lazy(() => import('../pages/UsersPage'))
|
||||
const NotificationsPage = lazy(() => import('../pages/NotificationsPage'))
|
||||
const MyTimetablePage = lazy(() => import('../pages/MyTimetablePage'))
|
||||
const ExamsPage = lazy(() => import('../pages/ExamsPage'))
|
||||
const StudentGradesPage = lazy(() => import('../pages/StudentGradesPage'))
|
||||
const StudentWarningsPage = lazy(() => import('../pages/StudentWarningsPage'))
|
||||
const SsoCallbackPage = lazy(() => import('../pages/SsoCallbackPage'))
|
||||
const SsoBindPage = lazy(() => import('../pages/SsoBindPage'))
|
||||
const AppShell = lazy(() => import('../layout/AppShell'))
|
||||
@@ -56,6 +59,9 @@ function AppRoutes() {
|
||||
<Route path="/users" element={<UsersPage />} />
|
||||
<Route path="/notifications" element={<NotificationsPage />} />
|
||||
<Route path="/my-timetable" element={<MyTimetablePage />} />
|
||||
<Route path="/exams" element={<ExamsPage />} />
|
||||
<Route path="/grades" element={<StudentGradesPage />} />
|
||||
<Route path="/warnings" element={<StudentWarningsPage />} />
|
||||
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
||||
</Route>
|
||||
<Route path="*" element={<Navigate to="/dashboard" replace />} />
|
||||
|
||||
@@ -8,6 +8,9 @@ import {
|
||||
MenuUnfoldOutlined,
|
||||
SafetyCertificateOutlined,
|
||||
CalendarOutlined,
|
||||
FileDoneOutlined,
|
||||
ReadOutlined,
|
||||
WarningOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import { Avatar, Badge, Button, Layout, Menu, Tag } from 'antd'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
@@ -44,6 +47,13 @@ export default function AppShell() {
|
||||
...((roles.includes('Student') || roles.includes('Teacher'))
|
||||
? [{ key: '/my-timetable', icon: <CalendarOutlined />, label: '我的课表' }]
|
||||
: []),
|
||||
...((roles.includes('Student') || roles.includes('Teacher'))
|
||||
? [{ key: '/exams', icon: <FileDoneOutlined />, label: roles.includes('Teacher') ? '监考安排' : '考试安排' }]
|
||||
: []),
|
||||
...(isStudentHome ? [
|
||||
{ key: '/grades', icon: <ReadOutlined />, label: '我的成绩' },
|
||||
{ key: '/warnings', icon: <WarningOutlined />, label: '学业预警' },
|
||||
] : []),
|
||||
...(user?.roles.includes('SuperAdmin')
|
||||
? [{ key: '/users', icon: <SafetyCertificateOutlined />, label: '用户与权限' }]
|
||||
: []),
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { ClockCircleOutlined, EnvironmentOutlined, ReloadOutlined, TeamOutlined } 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 { useAuthStore } from '../stores/auth'
|
||||
import './student-learning.css'
|
||||
|
||||
interface Exam { id: string; planName: string; examDate: string; startsAt: string; endsAt: string; taskNumber: string; courseCode: string; courseName: string; classroomName?: string; buildingName?: string; invigilatorNames: string[]; seatNumber?: string; studentCount?: number; isExam: boolean }
|
||||
function date(value: string) { return new Date(`${value.slice(0, 10)}T00:00:00`) }
|
||||
function formatDate(value: string) { return new Intl.DateTimeFormat('zh-CN', { month: 'long', day: 'numeric', weekday: 'long' }).format(date(value)) }
|
||||
function formatTime(value: string) { return new Intl.DateTimeFormat('zh-CN', { hour: '2-digit', minute: '2-digit', hour12: false }).format(new Date(value)) }
|
||||
|
||||
export default function ExamsPage() {
|
||||
const roles = useAuthStore((state) => state.user?.roles ?? [])
|
||||
const teacher = roles.includes('Teacher') && !roles.includes('Student')
|
||||
const query = useQuery({ queryKey: ['my-exams'], queryFn: async () => (await http.get<Exam[]>('/exams/my-schedule')).data })
|
||||
if (query.isLoading) return <main className="page-container"><Skeleton active paragraph={{ rows: 12 }} /></main>
|
||||
if (query.isError) return <main className="page-container"><Alert type="error" showIcon message="考试安排暂时无法加载" description={apiErrorMessage(query.error)} action={<Button icon={<ReloadOutlined />} onClick={() => void query.refetch()}>重新加载</Button>} /></main>
|
||||
const now = Date.now()
|
||||
const items = query.data ?? []
|
||||
const upcoming = items.filter((item) => new Date(item.endsAt).getTime() >= now)
|
||||
const history = items.filter((item) => new Date(item.endsAt).getTime() < now).reverse()
|
||||
return <main className="page-container learning-page">
|
||||
<PageHeader kicker={teacher ? 'INVIGILATION' : 'EXAM SCHEDULE'} title={teacher ? '我的监考安排' : '我的考试安排'} description={teacher ? '查看已发布考试中的监考时间、考场和考生规模。' : '查看已发布考试的时间、考场、座位和监考信息。'} aside={<div className="learning-ledger"><span>UPCOMING</span><strong>{upcoming.length}</strong><small>场待进行</small></div>} />
|
||||
<section className="exam-timeline">{upcoming.map((item, index) => <article key={item.id}><div className="exam-date-block"><span>{formatDate(item.examDate)}</span><strong>{Math.max(0, Math.ceil((date(item.examDate).getTime() - new Date().setHours(0,0,0,0)) / 86400000))}</strong><small>{index === 0 ? '距离最近安排 / 天' : '天后'}</small></div><div className="exam-card"><header><div><Tag color={item.isExam ? 'blue' : 'default'}>{item.planName}</Tag><span>{item.taskNumber}</span></div><time><ClockCircleOutlined /> {formatTime(item.startsAt)}—{formatTime(item.endsAt)}</time></header><h2>{item.courseName}</h2><p>{item.courseCode}</p><footer><span><EnvironmentOutlined /> {[item.buildingName, item.classroomName].filter(Boolean).join(' · ') || '考场待定'}</span>{teacher ? <span><TeamOutlined /> {item.studentCount ?? 0} 名考生</span> : <span>座位号 <b>{item.seatNumber ?? '待分配'}</b></span>}<span>监考:{item.invigilatorNames.join('、') || '待安排'}</span></footer></div></article>)}</section>
|
||||
{!upcoming.length && <section className="learning-empty"><Empty description="当前没有待进行的考试安排" /></section>}
|
||||
{!!history.length && <section className="history-panel"><header><span>HISTORY</span><h2>已结束安排</h2></header><div>{history.map((item) => <article key={item.id}><time>{formatDate(item.examDate)} · {formatTime(item.startsAt)}</time><strong>{item.courseName}</strong><span>{[item.buildingName, item.classroomName].filter(Boolean).join(' · ') || '未记录考场'}</span></article>)}</div></section>}
|
||||
</main>
|
||||
}
|
||||
Loaded 3 of 9 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user