feat(exams,homework,parent): V3 审计深度修复 — 批量批改/考试分析/提交反馈/家长视图/移动端优化
V3-5: exam-actions.tsx 集成 useExamHomeworkFeatures hook,按角色控制菜单项可见性 V3-7: 批量批改 — 新增 batchAutoGradeSubmissions data-access + Server Action + HomeworkBatchGradingView 组件 V3-8: 考试分析仪表盘 — 新增 getExamAnalytics stats-service + ExamAnalyticsDashboard 组件 + /teacher/exams/[id]/analytics 路由 V3-9: 提交后即时反馈页 — 新增 HomeworkSubmissionResult 组件 + /student/learning/assignments/[id]/result 路由 V3-11: 家长考试详情 — 新增 ChildExamDetail 组件 + getStudentExamResults data-access + child-detail-panel exams Tab V3-12: 移动端触控优化 — 题目导航与考试操作按钮 44px 最小触控目标 修复: instrumentation.ts 适配器补全 questionCount/averageScore/overdueCount 字段 修复: exam-homework-port.ts 类型导入对齐 ExamWithQuestionsForHomework 修复: trend-line-chart.tsx 数据类型允许 undefined(classAverage 可选场景) 同步更新 004/005 架构文档
This commit is contained in:
57
src/app/(dashboard)/teacher/exams/[id]/analytics/page.tsx
Normal file
57
src/app/(dashboard)/teacher/exams/[id]/analytics/page.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import type { JSX } from "react"
|
||||
import { notFound } from "next/navigation"
|
||||
import { getTranslations } from "next-intl/server"
|
||||
import Link from "next/link"
|
||||
import { Button } from "@/shared/components/ui/button"
|
||||
import { EmptyState } from "@/shared/components/ui/empty-state"
|
||||
import { BarChart3, ArrowLeft } from "lucide-react"
|
||||
import { getExamById } from "@/modules/exams/data-access"
|
||||
import { getExamAnalytics } from "@/modules/exams/stats-service"
|
||||
import { ExamAnalyticsDashboard } from "@/modules/exams/components/exam-analytics-dashboard"
|
||||
|
||||
export const dynamic = "force-dynamic"
|
||||
|
||||
export default async function ExamAnalyticsPage({ params }: { params: Promise<{ id: string }> }): Promise<JSX.Element> {
|
||||
const { id } = await params
|
||||
const t = await getTranslations("examHomework")
|
||||
|
||||
const [exam, analytics] = await Promise.all([
|
||||
getExamById(id),
|
||||
getExamAnalytics(id),
|
||||
])
|
||||
|
||||
if (!exam) return notFound()
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col space-y-6 p-8">
|
||||
<div className="flex flex-col justify-between gap-4 md:flex-row md:items-center">
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<BarChart3 className="h-6 w-6 text-muted-foreground" />
|
||||
<h1 className="text-2xl font-bold tracking-tight">{t("exam.analytics.title")}</h1>
|
||||
</div>
|
||||
<p className="text-muted-foreground truncate">{exam.title}</p>
|
||||
<p className="mt-1 text-sm text-muted-foreground">{t("exam.analytics.description")}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button asChild variant="outline">
|
||||
<Link href="/teacher/exams/all">
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
{t("homework.grade.back")}
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{analytics && analytics.gradedCount > 0 ? (
|
||||
<ExamAnalyticsDashboard analytics={analytics} />
|
||||
) : (
|
||||
<EmptyState
|
||||
title={t("exam.analytics.title")}
|
||||
description={t("exam.analytics.noData")}
|
||||
icon={BarChart3}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -2,18 +2,9 @@ import type { JSX } from "react"
|
||||
import Link from "next/link"
|
||||
import { notFound } from "next/navigation"
|
||||
import { getTranslations } from "next-intl/server"
|
||||
import { Badge } from "@/shared/components/ui/badge"
|
||||
import { Button } from "@/shared/components/ui/button"
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/shared/components/ui/table"
|
||||
import { formatDate } from "@/shared/lib/utils"
|
||||
import { getHomeworkAssignmentById, getHomeworkSubmissions } from "@/modules/homework/data-access"
|
||||
import { HomeworkBatchGradingView } from "@/modules/homework/components/homework-batch-grading-view"
|
||||
|
||||
export const dynamic = "force-dynamic"
|
||||
|
||||
@@ -52,39 +43,7 @@ export default async function HomeworkAssignmentSubmissionsPage({ params }: { pa
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-md border bg-card">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>{t("homework.grade.student")}</TableHead>
|
||||
<TableHead>{t("homework.grade.status")}</TableHead>
|
||||
<TableHead>{t("homework.grade.submitted")}</TableHead>
|
||||
<TableHead>{t("homework.grade.score")}</TableHead>
|
||||
<TableHead>{t("homework.grade.action")}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{submissions.map((s) => (
|
||||
<TableRow key={s.id}>
|
||||
<TableCell className="font-medium truncate max-w-[160px]">{s.studentName}</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline" className="capitalize">
|
||||
{s.status}
|
||||
</Badge>
|
||||
{s.isLate ? <span className="ml-2 text-xs text-destructive">{t("homework.grade.late")}</span> : null}
|
||||
</TableCell>
|
||||
<TableCell className="text-muted-foreground tabular-nums">{s.submittedAt ? formatDate(s.submittedAt) : "-"}</TableCell>
|
||||
<TableCell className="tabular-nums">{typeof s.score === "number" ? s.score : "-"}</TableCell>
|
||||
<TableCell>
|
||||
<Link href={`/teacher/homework/submissions/${s.id}`} className="text-sm underline-offset-4 hover:underline">
|
||||
{t("homework.grade.title")}
|
||||
</Link>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<HomeworkBatchGradingView submissions={submissions} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user