Files
NextEdu/src/modules/parent/components/child-exam-detail.tsx
SpecialX e9a5264fe7 feat(parent,auth,onboarding,files,notifications,adaptive-practice,ai): add module updates
parent:

- Add parent-student-attendance-detail component

auth:

- Add actions, data-access, schema, services, types

onboarding:

- Add parent-children-form and hooks directory

files:

- Add actions, schema, hooks directory

notifications:

- Add schema and schema test

adaptive-practice:

- Add answer-input, answer-result, practice-result-view, practice-starter-with-nav

- Add question-card, question-content, lib and services directories

ai:

- Add context/create-ai-client-service, hooks/use-drag-position, hooks/use-position-persistence
2026-07-03 10:26:12 +08:00

172 lines
6.7 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 type { JSX } from "react"
import Link from "next/link"
import { useTranslations } from "next-intl"
import { GraduationCap, TrendingUp, Award, BookOpen, ChevronRight } from "lucide-react"
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/shared/components/ui/card"
import { Badge } from "@/shared/components/ui/badge"
import { Progress } from "@/shared/components/ui/progress"
import { EmptyState } from "@/shared/components/ui/empty-state"
import { formatDate } from "@/shared/lib/utils"
export interface ChildExamResultItem {
submissionId: string
examId: string
examTitle: string
assignmentId: string
assignmentTitle: string
score: number
maxScore: number
submittedAt: string | null
status: string
}
interface ChildExamDetailProps {
examResults: ChildExamResultItem[]
childId: string
childName: string
}
/**
* V3-11 / P3-4: 家长端子女考试详情视图
*
* 对标智学网家长端,展示:
* - 考试成绩汇总卡片(已参加考试数、平均分、最高分)
* - 考试成绩列表(考试标题、分数、得分率、提交时间)
*
* P3-4 改进:
* - 所有用户可见文本使用 i18n 翻译键next-intl
* - 触控目标 ≥ 44pxmin-h-[44px]
* - 语义化标签 + ARIA 属性
*/
export function ChildExamDetail({ examResults, childId, childName }: ChildExamDetailProps): JSX.Element {
const t = useTranslations("examHomework")
const hasResults = examResults.length > 0
const examCount = examResults.length
const averageScore = hasResults
? examResults.reduce((sum, r) => {
const rate = r.maxScore > 0 ? (r.score / r.maxScore) * 100 : 0
return sum + rate
}, 0) / examCount
: 0
const bestScore = hasResults
? Math.max(...examResults.map((r) => (r.maxScore > 0 ? (r.score / r.maxScore) * 100 : 0)))
: 0
return (
<div className="space-y-6">
{/* Summary Cards */}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
<Card>
<CardContent className="flex items-center gap-3 pt-6">
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-primary/10">
<GraduationCap className="h-5 w-5 text-primary" aria-hidden />
</div>
<div>
<p className="text-2xl font-bold tabular-nums">{examCount}</p>
<p className="text-xs text-muted-foreground">
{t("homework.parentExam.examsTaken")}
</p>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="flex items-center gap-3 pt-6">
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-blue-500/10">
<TrendingUp className="h-5 w-5 text-blue-500" aria-hidden />
</div>
<div>
<p className="text-2xl font-bold tabular-nums">{averageScore.toFixed(1)}%</p>
<p className="text-xs text-muted-foreground">
{t("homework.parentExam.averageScore")}
</p>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="flex items-center gap-3 pt-6">
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-green-500/10">
<Award className="h-5 w-5 text-green-500" aria-hidden />
</div>
<div>
<p className="text-2xl font-bold tabular-nums">{bestScore.toFixed(1)}%</p>
<p className="text-xs text-muted-foreground">
{t("homework.parentExam.bestScore")}
</p>
</div>
</CardContent>
</Card>
</div>
{/* Exam Results List */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-base">
<BookOpen className="h-4 w-4 text-muted-foreground" aria-hidden />
{t("homework.parentExam.examResults", { name: childName })}
</CardTitle>
<CardDescription>
{t("homework.parentExam.examResultsDescription")}
</CardDescription>
</CardHeader>
<CardContent>
{!hasResults ? (
<EmptyState
icon={GraduationCap}
title={t("homework.parentExam.noResults")}
description={t("homework.parentExam.noResultsHint")}
className="border-none h-48"
/>
) : (
<ul className="space-y-3" aria-label={t("homework.parentExam.examResults", { name: childName })}>
{examResults.map((r) => {
const scoreRate = r.maxScore > 0 ? (r.score / r.maxScore) * 100 : 0
const isPass = scoreRate >= 60
return (
<li key={r.submissionId}>
<Link
href={`/parent/children/${childId}?tab=grades`}
className="flex min-h-[44px] items-center justify-between rounded-md border bg-card p-3 hover:bg-muted/50 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
>
<div className="min-w-0 flex-1 space-y-1">
<div className="flex items-center gap-2">
<div className="font-medium text-sm truncate">{r.examTitle}</div>
<Badge variant={isPass ? "default" : "destructive"} className="text-[10px] shrink-0">
{isPass ? t("homework.parentExam.pass") : t("homework.parentExam.belowPass")}
</Badge>
</div>
<div className="flex items-center gap-2 text-xs text-muted-foreground">
{r.submittedAt ? (
<span>{formatDate(r.submittedAt)}</span>
) : null}
<span aria-hidden="true"></span>
<span className="tabular-nums">
{r.score} / {r.maxScore}
</span>
</div>
<Progress value={scoreRate} className="h-1.5 mt-1" />
</div>
<div className="flex items-center gap-2 shrink-0 ml-2">
<span className="text-sm font-semibold tabular-nums">
{scoreRate.toFixed(0)}%
</span>
<ChevronRight className="h-4 w-4 text-muted-foreground" aria-hidden />
</div>
</Link>
</li>
)
})}
</ul>
)}
</CardContent>
</Card>
</div>
)
}