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:
168
src/modules/homework/components/homework-batch-grading-view.tsx
Normal file
168
src/modules/homework/components/homework-batch-grading-view.tsx
Normal file
@@ -0,0 +1,168 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useTransition } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { useTranslations } from "next-intl"
|
||||
import { Zap } from "lucide-react"
|
||||
import { toast } from "sonner"
|
||||
|
||||
import { Button } from "@/shared/components/ui/button"
|
||||
import { Badge } from "@/shared/components/ui/badge"
|
||||
import { Checkbox } from "@/shared/components/ui/checkbox"
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/shared/components/ui/table"
|
||||
import { formatDate } from "@/shared/lib/utils"
|
||||
import { batchAutoGradeSubmissionsAction } from "../actions"
|
||||
import type { HomeworkSubmissionListItem } from "../types"
|
||||
|
||||
interface HomeworkBatchGradingViewProps {
|
||||
submissions: HomeworkSubmissionListItem[]
|
||||
}
|
||||
|
||||
/**
|
||||
* V3-7: 批量批改视图
|
||||
*
|
||||
* 教师在提交列表页可勾选多份提交,一键自动批改所有客观题。
|
||||
* 对标智学网的批量批改功能。
|
||||
*/
|
||||
export function HomeworkBatchGradingView({ submissions }: HomeworkBatchGradingViewProps) {
|
||||
const t = useTranslations("examHomework")
|
||||
const router = useRouter()
|
||||
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set())
|
||||
const [isPending, startTransition] = useTransition()
|
||||
|
||||
const selectableSubmissions = submissions.filter(
|
||||
(s) => s.status === "submitted"
|
||||
)
|
||||
|
||||
const allSelectableSelected =
|
||||
selectableSubmissions.length > 0 &&
|
||||
selectableSubmissions.every((s) => selectedIds.has(s.id))
|
||||
|
||||
const toggleSelectAll = () => {
|
||||
if (allSelectableSelected) {
|
||||
setSelectedIds(new Set())
|
||||
} else {
|
||||
setSelectedIds(new Set(selectableSubmissions.map((s) => s.id)))
|
||||
}
|
||||
}
|
||||
|
||||
const toggleSelect = (id: string) => {
|
||||
setSelectedIds((prev) => {
|
||||
const next = new Set(prev)
|
||||
if (next.has(id)) {
|
||||
next.delete(id)
|
||||
} else {
|
||||
next.add(id)
|
||||
}
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
const handleBatchAutoGrade = () => {
|
||||
if (selectedIds.size === 0) {
|
||||
toast.error(t("homework.grade.batchSelectAtLeastOne"))
|
||||
return
|
||||
}
|
||||
|
||||
startTransition(async () => {
|
||||
const formData = new FormData()
|
||||
formData.set("submissionIds", JSON.stringify(Array.from(selectedIds)))
|
||||
const result = await batchAutoGradeSubmissionsAction(null, formData)
|
||||
if (result.success) {
|
||||
toast.success(result.message)
|
||||
setSelectedIds(new Set())
|
||||
router.refresh()
|
||||
} else {
|
||||
toast.error(result.message || t("homework.grade.batchFailed"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{selectedIds.size > 0 && (
|
||||
<div className="flex items-center justify-between rounded-lg border bg-muted/50 px-4 py-3">
|
||||
<span className="text-sm font-medium">
|
||||
{t("homework.grade.batchSelected", { count: selectedIds.size })}
|
||||
</span>
|
||||
<Button
|
||||
onClick={handleBatchAutoGrade}
|
||||
disabled={isPending}
|
||||
size="sm"
|
||||
>
|
||||
<Zap className="mr-2 h-4 w-4" />
|
||||
{t("homework.grade.batchAutoGrade")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="rounded-md border bg-card">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[40px]">
|
||||
<Checkbox
|
||||
checked={allSelectableSelected}
|
||||
onCheckedChange={toggleSelectAll}
|
||||
aria-label={t("homework.grade.selectAll")}
|
||||
/>
|
||||
</TableHead>
|
||||
<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) => {
|
||||
const isSelectable = s.status === "submitted"
|
||||
const isSelected = selectedIds.has(s.id)
|
||||
return (
|
||||
<TableRow key={s.id} data-selected={isSelected}>
|
||||
<TableCell>
|
||||
{isSelectable ? (
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
onCheckedChange={() => toggleSelect(s.id)}
|
||||
aria-label={t("homework.grade.selectRow")}
|
||||
/>
|
||||
) : (
|
||||
<span className="text-muted-foreground" aria-hidden="true">
|
||||
—
|
||||
</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<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>
|
||||
<a
|
||||
href={`/teacher/homework/submissions/${s.id}`}
|
||||
className="text-sm underline-offset-4 hover:underline"
|
||||
>
|
||||
{t("homework.grade.title")}
|
||||
</a>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user