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:
SpecialX
2026-07-03 10:26:12 +08:00
parent f3c223d914
commit e9a5264fe7
84 changed files with 6060 additions and 2530 deletions

View File

@@ -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
* - 触控目标 ≥ 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
@@ -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}&apos;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>