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
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import type { JSX } from "react"
|
||||
import Link from "next/link"
|
||||
import { GraduationCap, TrendingUp, Award, BookOpen } from "lucide-react"
|
||||
import { useTranslations } from "next-intl"
|
||||
import { GraduationCap, TrendingUp, Award, BookOpen, ChevronRight } from "lucide-react"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
@@ -32,14 +33,19 @@ interface ChildExamDetailProps {
|
||||
}
|
||||
|
||||
/**
|
||||
* V3-11: 家长端子女考试详情视图
|
||||
* V3-11 / P3-4: 家长端子女考试详情视图
|
||||
*
|
||||
* 对标智学网家长端,展示:
|
||||
* - 考试成绩汇总卡片(已参加考试数、平均分、最高分)
|
||||
* - 考试成绩列表(考试标题、分数、得分率、提交时间)
|
||||
* - 成绩趋势可视化
|
||||
*
|
||||
* P3-4 改进:
|
||||
* - 所有用户可见文本使用 i18n 翻译键(next-intl)
|
||||
* - 触控目标 ≥ 44px(min-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
|
||||
@@ -60,33 +66,39 @@ export function ChildExamDetail({ examResults, childId, childName }: ChildExamDe
|
||||
<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" />
|
||||
<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">Exams Taken</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" />
|
||||
<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">Average Score</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" />
|
||||
<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">Best Score</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("homework.parentExam.bestScore")}
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -97,54 +109,60 @@ export function ChildExamDetail({ examResults, childId, childName }: ChildExamDe
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-base">
|
||||
<BookOpen className="h-4 w-4 text-muted-foreground" aria-hidden />
|
||||
{childName}'s Exam Results
|
||||
{t("homework.parentExam.examResults", { name: childName })}
|
||||
</CardTitle>
|
||||
<CardDescription>Recent exam scores and performance trends</CardDescription>
|
||||
<CardDescription>
|
||||
{t("homework.parentExam.examResultsDescription")}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{!hasResults ? (
|
||||
<EmptyState
|
||||
icon={GraduationCap}
|
||||
title="No exam results"
|
||||
description="Exam results will appear here once available."
|
||||
title={t("homework.parentExam.noResults")}
|
||||
description={t("homework.parentExam.noResultsHint")}
|
||||
className="border-none h-48"
|
||||
/>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
<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 (
|
||||
<Link
|
||||
key={r.submissionId}
|
||||
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 ? "Pass" : "Below 60%"}
|
||||
</Badge>
|
||||
<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 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}
|
||||
<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>
|
||||
<Progress value={scoreRate} className="h-1.5 mt-1" />
|
||||
</div>
|
||||
<div className="text-sm font-semibold tabular-nums shrink-0 ml-2">
|
||||
{scoreRate.toFixed(0)}%
|
||||
</div>
|
||||
</Link>
|
||||
</Link>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</ul>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
Reference in New Issue
Block a user