feat(student): 完成 student 模块 v4 剩余修复

- P1-4.2: 新增班级详情页 courses/[classId],展示教师/学校/教室信息与课表

- P2-2.5: 今日课表卡片高亮当前/下一节课(useMemo 实时计算)

- P2-3.9: 作业作答进度网格支持点击跳转题目(scrollIntoView)

- P2-3.10: 作业复习视图显示正确答案(选择/判断/文本题)

- P2-4.4: 课程列表支持按班级名/教师/学校搜索

- P2-5.2: 成绩页新增趋势折线图组件 GradeTrendCard

- P2-9.2/9.3: 诊断报告新增历史记录卡片与弱点练习入口

- P2-10.2: 选课列表支持搜索与选课模式筛选

- P2-11.3: 修复教材阅读页全屏溢出

- P3-1.5: 面包屑保留首个角色段作为根上下文

- P3-7.3: 课表项支持点击跳转至班级详情页(ScheduleList href)
This commit is contained in:
SpecialX
2026-06-22 14:08:34 +08:00
parent c90748124d
commit 30f4983d49
18 changed files with 912 additions and 137 deletions

View File

@@ -1,19 +1,34 @@
import { getAuthContext } from "@/shared/lib/auth-guard"
import { getStudentGradeSummary } from "@/modules/grades/data-access"
import { StudentGradeSummary } from "@/modules/grades/components/student-grade-summary"
import { GradeFilters } from "@/modules/grades/components/grade-filters"
import { GradeTrendCard } from "@/modules/grades/components/grade-trend-card"
import { EmptyState } from "@/shared/components/ui/empty-state"
import { UserX } from "lucide-react"
export const dynamic = "force-dynamic"
export default async function StudentGradesPage() {
const ctx = await getAuthContext()
type SearchParams = { [key: string]: string | string[] | undefined }
const summary = await getStudentGradeSummary(ctx.userId)
const getParam = (params: SearchParams, key: string) => {
const v = params[key]
return Array.isArray(v) ? v[0] : v
}
export default async function StudentGradesPage({
searchParams,
}: {
searchParams: Promise<SearchParams>
}) {
const ctx = await getAuthContext()
const [sp, summary] = await Promise.all([
searchParams,
getStudentGradeSummary(ctx.userId),
])
if (!summary) {
return (
<div className="h-full flex-1 flex-col space-y-8 p-8 md:flex">
<div className="space-y-8">
<div>
<h2 className="text-2xl font-bold tracking-tight">My Grades</h2>
<p className="text-muted-foreground">View your grade records.</p>
@@ -28,13 +43,34 @@ export default async function StudentGradesPage() {
)
}
// 应用筛选
const q = (getParam(sp, "q") || "").toLowerCase().trim()
const subjectFilter = getParam(sp, "subject") || "all"
const typeFilter = getParam(sp, "type") || "all"
const semesterFilter = getParam(sp, "semester") || "all"
const filteredRecords = summary.records.filter((r) => {
if (q && !r.title.toLowerCase().includes(q)) return false
if (subjectFilter !== "all" && r.subjectName !== subjectFilter) return false
if (typeFilter !== "all" && r.type !== typeFilter) return false
if (semesterFilter !== "all" && r.semester !== semesterFilter) return false
return true
})
const filteredSummary = {
...summary,
records: filteredRecords,
}
return (
<div className="h-full flex-1 flex-col space-y-8 p-8 md:flex">
<div className="space-y-8">
<div>
<h2 className="text-2xl font-bold tracking-tight">My Grades</h2>
<p className="text-muted-foreground">View your grade records.</p>
</div>
<StudentGradeSummary summary={summary} />
<GradeFilters />
{filteredSummary.records.length > 0 && <GradeTrendCard summary={filteredSummary} />}
<StudentGradeSummary summary={filteredSummary} />
</div>
)
}