Files
NextEdu/src/app/(dashboard)/student/grades/page.tsx
SpecialX 21142f9b99 feat(app): add error/loading boundaries across all dashboard routes and new routes
- Add error.tsx and loading.tsx boundaries for admin, parent, student, teacher routes

- Add admin announcements edit, audit-logs overview, curriculum-map, invitation-codes, permissions, questions, roles routes

- Add admin elective detail and components, files, course-plans, users, scheduling boundaries

- Add messages group-compose route

- Add parent course-plans, elective, grades report-card, practice routes

- Add student course-plans, elective detail, error-book dialogs, grades report-card, learning study-path, leave, schedule boundaries

- Add teacher attendance report, classes boundaries, course-plans boundaries, elective, exams analytics/edit-rich/all/create/new, grades report-card, homework boundaries, leave, lesson-plans calendar

- Add auth loading, onboarding loading, api cron
2026-07-03 10:26:25 +08:00

106 lines
4.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { getTranslations } from "next-intl/server"
import { requirePermission } from "@/shared/lib/auth-guard"
import { Permissions } from "@/shared/types/permissions"
import { getStudentGradeSummary } from "@/modules/grades/data-access"
import { getRankingTrend, getClassAverageTrend } from "@/modules/grades/data-access-ranking"
import { getStudentPositionInClassDistribution, getStudentGrowthArchive } from "@/modules/grades/data-access-analytics"
import { getSubjectOptions } from "@/modules/school/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 { RankingTrendCard } from "@/modules/grades/components/ranking-trend-card"
import { GradeDistributionChart } from "@/modules/grades/components/grade-distribution-chart"
import { GrowthArchiveChart } from "@/modules/grades/components/growth-archive-chart"
import { EmptyState } from "@/shared/components/ui/empty-state"
import { UserX } from "lucide-react"
import { getParam, type SearchParams } from "@/shared/lib/search-params"
export const dynamic = "force-dynamic"
export default async function StudentGradesPage({
searchParams,
}: {
searchParams: Promise<SearchParams>
}) {
const ctx = await requirePermission(Permissions.GRADE_RECORD_READ)
const t = await getTranslations("grades")
const [sp, summary, rankingTrend, classAverageTrend, subjectOptions, classDistribution, growthArchive] = await Promise.all([
searchParams,
getStudentGradeSummary(ctx.userId, ctx.dataScope),
// v3-P1-3接入排名趋势图
getRankingTrend(ctx.userId, undefined, undefined, ctx.dataScope),
// v3-P2-2接入班级平均趋势对比线
getClassAverageTrend(ctx.userId, undefined, undefined, ctx.dataScope),
// v3-P2-1获取科目列表用于过滤器
getSubjectOptions(),
// P3-9获取学生所在班级的分布 + 本人位置标注
getStudentPositionInClassDistribution(ctx.userId, ctx.dataScope),
// P3-4获取学生纵向成长档案跨学年/学期聚合)
getStudentGrowthArchive(ctx.userId, ctx.dataScope),
])
if (!summary) {
return (
<div className="space-y-8">
<div>
<h2 className="text-2xl font-bold tracking-tight">{t("title.myGrades")}</h2>
<p className="text-muted-foreground">{t("summary.noDataDescription")}</p>
</div>
<EmptyState
title={t("summary.noDataTitle")}
description={t("summary.noDataDescription")}
icon={UserX}
className="border-none shadow-none"
/>
</div>
)
}
// 应用筛选
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
// v3-P2-1 修复:按 subjectId 而非 subjectName 过滤
if (subjectFilter !== "all" && r.subjectId !== 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="space-y-8">
<div>
<h2 className="text-2xl font-bold tracking-tight">{summary.studentName}</h2>
<p className="text-muted-foreground">{t("summary.recordCount", { count: summary.records.length })}</p>
</div>
<GradeFilters subjects={subjectOptions.map((s) => ({ id: s.id, name: s.name }))} />
{filteredSummary.records.length > 0 && (
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
<GradeTrendCard summary={filteredSummary} classAverageData={classAverageTrend} />
<RankingTrendCard trend={rankingTrend} />
</div>
)}
{/* P3-9班级分布 + 学生位置标注(隐私保护视图) */}
{classDistribution && (
<GradeDistributionChart
data={classDistribution.distribution}
highlightBucketIndex={classDistribution.studentBucketIndex}
studentScore={classDistribution.studentNormalizedScore}
/>
)}
{/* P3-4学生纵向成长档案跨学年/学期聚合) */}
<GrowthArchiveChart data={growthArchive} />
<StudentGradeSummary summary={filteredSummary} />
</div>
)
}