From 0d9dfaf8ffc48e892c650d5596935feffc45f33f Mon Sep 17 00:00:00 2001 From: biss Date: Fri, 2 Oct 2026 08:33:52 +0800 Subject: [PATCH] feat(web): migrate student progress workflows Add React experiences for academic planning simulations, published physical fitness results, degree award decisions, and graduation clearance tracking. Wire student navigation and document the migrated API contracts. --- web-react/MIGRATION_MATRIX.md | 4 ++ web-react/README.md | 1 + web-react/src/app/App.tsx | 8 ++++ web-react/src/layout/AppShell.tsx | 8 ++++ web-react/src/pages/AcademicPlanningPage.tsx | 37 +++++++++++++++++++ web-react/src/pages/DegreeAwardsPage.tsx | 15 ++++++++ .../src/pages/GraduationClearancePage.tsx | 19 ++++++++++ web-react/src/pages/PhysicalFitnessPage.tsx | 21 +++++++++++ web-react/src/pages/student-progress.css | 4 ++ 9 files changed, 117 insertions(+) create mode 100644 web-react/src/pages/AcademicPlanningPage.tsx create mode 100644 web-react/src/pages/DegreeAwardsPage.tsx create mode 100644 web-react/src/pages/GraduationClearancePage.tsx create mode 100644 web-react/src/pages/PhysicalFitnessPage.tsx create mode 100644 web-react/src/pages/student-progress.css diff --git a/web-react/MIGRATION_MATRIX.md b/web-react/MIGRATION_MATRIX.md index 2e54349..a5d9e0c 100644 --- a/web-react/MIGRATION_MATRIX.md +++ b/web-react/MIGRATION_MATRIX.md @@ -27,5 +27,9 @@ | `/other-exams` | 其他考试 | Student | 最佳成绩、历次考试、通过/等级/分数制结果 | verification | | `/makeup-exams` | 补考安排 | Student | 已发布场次、补考原因、考场、座位和成绩 | verification | | `/evaluations` | 教学评价 | Student | 开放任务、动态评分维度、一次性提交 | verification | +| `/academic-planning` | 学业规划与毕业模拟 | Student | 未来学期排课、下学期建议、先修冲突、负荷和毕业时间模拟 | verification | +| `/physical-fitness` | 我的体质健康 | Student | 已发布学年、单项成绩、标准分、附加分和综合等级 | verification | +| `/degree-awards` | 我的学位授予 | Student | 正式发布结论、绩点门槛、规则或人工复核说明 | verification | +| `/graduation-clearance` | 我的毕业离校 | Student | 跨部门事项、必办标记、办结/豁免状态和整体进度 | verification | 后续从 `web/src/router/index.ts` 逐条补全。只有关键合同和各状态验收完整后,状态才能更新为 `complete`。 diff --git a/web-react/README.md b/web-react/README.md index 8f41605..c693b8c 100644 --- a/web-react/README.md +++ b/web-react/README.md @@ -37,6 +37,7 @@ npm.cmd run build - 迁移学生 `/my-curriculum` 培养方案进度、`/academic-overview` 学业排名趋势与 `/free-classrooms` 空闲教室查询。 - 批量迁移学生校历、学籍异动、电子凭证、毕业审核和学费分项缴纳流程。 - 迁移学生其他考试成绩、补考安排和教学评价提交闭环。 +- 迁移学生学业规划与毕业模拟、体质健康、学位授予和毕业离校进度。 ## 迁移纪律 diff --git a/web-react/src/app/App.tsx b/web-react/src/app/App.tsx index 2f8b89a..92e3c25 100644 --- a/web-react/src/app/App.tsx +++ b/web-react/src/app/App.tsx @@ -28,6 +28,10 @@ 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 AcademicPlanningPage = lazy(() => import('../pages/AcademicPlanningPage')) +const PhysicalFitnessPage = lazy(() => import('../pages/PhysicalFitnessPage')) +const DegreeAwardsPage = lazy(() => import('../pages/DegreeAwardsPage')) +const GraduationClearancePage = lazy(() => import('../pages/GraduationClearancePage')) const SsoCallbackPage = lazy(() => import('../pages/SsoCallbackPage')) const SsoBindPage = lazy(() => import('../pages/SsoBindPage')) const AppShell = lazy(() => import('../layout/AppShell')) @@ -90,6 +94,10 @@ function AppRoutes() { } /> } /> } /> + } /> + } /> + } /> + } /> } /> } /> diff --git a/web-react/src/layout/AppShell.tsx b/web-react/src/layout/AppShell.tsx index bc584af..6143cec 100644 --- a/web-react/src/layout/AppShell.tsx +++ b/web-react/src/layout/AppShell.tsx @@ -25,6 +25,10 @@ import { TrophyOutlined, SolutionOutlined, CommentOutlined, + RocketOutlined, + HeartOutlined, + SafetyCertificateOutlined as DegreeOutlined, + CheckCircleOutlined, } from '@ant-design/icons' import { Avatar, Badge, Button, Layout, Menu, Tag } from 'antd' import { useQuery } from '@tanstack/react-query' @@ -80,6 +84,10 @@ export default function AppShell() { { key: '/other-exams', icon: , label: '其他考试' }, { key: '/makeup-exams', icon: , label: '补考安排' }, { key: '/evaluations', icon: , label: '教学评价' }, + { key: '/academic-planning', icon: , label: '学业规划' }, + { key: '/physical-fitness', icon: , label: '体质健康' }, + { key: '/degree-awards', icon: , label: '学位授予' }, + { key: '/graduation-clearance', icon: , label: '毕业离校' }, { key: '/warnings', icon: , label: '学业预警' }, ] : []), ...(user?.roles.includes('SuperAdmin') diff --git a/web-react/src/pages/AcademicPlanningPage.tsx b/web-react/src/pages/AcademicPlanningPage.tsx new file mode 100644 index 0000000..f977df1 --- /dev/null +++ b/web-react/src/pages/AcademicPlanningPage.tsx @@ -0,0 +1,37 @@ +import { DeleteOutlined, ExperimentOutlined, ReloadOutlined, ThunderboltOutlined } from '@ant-design/icons' +import { Alert, App, Button, Empty, Progress, Select, Skeleton, Tag } from 'antd' +import { useMutation, useQuery } from '@tanstack/react-query' +import { useEffect, useMemo, useState } from 'react' +import http, { apiErrorMessage } from '../api/http' +import PageHeader from '../components/PageHeader' +import './student-progress.css' + +interface PlanningCourse { courseId: string; courseCode: string; courseName: string; credits: number; recommendedSemester: number; type: string; moduleCode: string; moduleName: string; status: string; isSuggested: boolean; prerequisites: { courseName: string; isCompleted: boolean; isInProgress: boolean }[] } +interface PlanningTerm { semester: number; label: string; isBeyondStandard: boolean } +interface PlanningPayload { student: { studentNumber: string; name: string; enrollmentYear: number; majorName: string; collegeName: string }; plan: { name: string; version: string; totalCredits: number; nextSemester: number }; baseline: Projection; terms: PlanningTerm[]; courses: PlanningCourse[]; nextSemesterSuggestion: PlanningCourse[]; assumptions: string[] } +interface Projection { earnedCredits: number; planCompletedCredits: number; creditGap: number; completionRate: number; requirementCount: number; passedRequirementCount: number; missingRequirements: string[]; graduationConclusion?: string; estimatedGraduationTerm?: string; isBeyondStandard?: boolean } +interface Simulation { projected: Projection; conflicts: { courseId: string; message: string }[]; warnings: { message: string }[]; termSummaries: { semester: number; courseCount: number; credits: number }[] } + +export default function AcademicPlanningPage() { + const { message } = App.useApp() + const query = useQuery({ queryKey: ['academic-planning'], queryFn: async () => (await http.get('/student/academic-planning')).data }) + const [assignments, setAssignments] = useState>({}) + const simulation = useMutation({ mutationFn: async (value: Record) => (await http.post('/student/academic-planning/simulate', { terms: (query.data?.terms ?? []).map((term) => ({ semester: term.semester, courseIds: Object.entries(value).filter(([, semester]) => semester === term.semester).map(([courseId]) => courseId) })) })).data, onError: (error) => message.error(apiErrorMessage(error)) }) + useEffect(() => { if (!query.data) return; const timer = window.setTimeout(() => simulation.mutate(assignments), 180); return () => window.clearTimeout(timer) }, [assignments, query.data]) + const available = useMemo(() => (query.data?.courses ?? []).filter((course) => !['Completed', 'InProgress', 'Retaking'].includes(course.status)), [query.data]) + const projected = simulation.data?.projected ?? query.data?.baseline + const conflictIds = new Set(simulation.data?.conflicts.map((item) => item.courseId) ?? []) + const assign = (courseId: string, semester?: number) => setAssignments((current) => { const next = { ...current }; if (semester) next[courseId] = semester; else delete next[courseId]; return next }) + const applySuggestion = () => { if (!query.data) return; setAssignments((current) => ({ ...current, ...Object.fromEntries(query.data.nextSemesterSuggestion.map((course) => [course.courseId, query.data!.plan.nextSemester])) })); message.success('已采用下学期建议') } + if (query.isLoading) return
+ if (query.isError || !query.data) return
+ return
+ } /> +
{query.data.student.enrollmentYear} 级 · {query.data.student.collegeName}

{query.data.student.name}的毕业航线

{query.data.student.studentNumber} · {query.data.student.majorName} · {query.data.plan.name} {query.data.plan.version}

预计毕业{projected?.estimatedGraduationTerm ?? '等待规划'}
+
预计获得学分{projected?.earnedCredits ?? 0} / {query.data.plan.totalCredits}
0 ? 'warning' : ''}>毕业学分缺口{projected?.creditGap ?? 0}
培养要求{projected?.passedRequirementCount ?? 0} / {projected?.requirementCount ?? 0}
已规划课程{Object.keys(assignments).length}
+ {(simulation.data?.conflicts.length || simulation.data?.warnings.length) ? item.message).join(';')} /> : null} +
{query.data.terms.map((term) => { const courses = available.filter((course) => assignments[course.courseId] === term.semester); const summary = simulation.data?.termSummaries.find((item) => item.semester === term.semester); return
SEMESTER {term.semester}

{term.label}

{summary?.credits ?? 0} 学分
{courses.map((course) =>
{course.courseCode}{course.courseName}{course.credits} 学分 · {course.moduleName}
)}{!courses.length && 把下方课程安排到本学期}
})}
+
COURSE POOL

待规划课程

调整学期后会自动重新模拟
{available.map((course) =>
{course.courseCode} · {course.moduleName}

{course.courseName}

{course.credits} 学分 · 建议第 {course.recommendedSemester} 学期{course.prerequisites.length ? ` · 先修 ${course.prerequisites.map((item) => item.courseName).join('、')}` : ''}

{course.isSuggested && 下学期建议}