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:
SpecialX
2026-06-23 01:06:27 +08:00
parent 21c5eba96c
commit a60105455e
23 changed files with 2407 additions and 263 deletions

View File

@@ -125,18 +125,23 @@ export function HomeworkTakeView({ assignmentId, initialData }: HomeworkTakeView
const handleStart = async () => {
setIsBusy(true)
const fd = new FormData()
fd.set("assignmentId", assignmentId)
const res = await startHomeworkSubmissionAction(null, fd)
if (res.success && res.data) {
setSubmissionId(res.data)
setSubmissionStatus("started")
toast.success(t("homework.take.startSuccess"))
router.refresh()
} else {
toast.error(res.message || t("homework.take.startFailed"))
try {
const fd = new FormData()
fd.set("assignmentId", assignmentId)
const res = await startHomeworkSubmissionAction(null, fd)
if (res.success && res.data) {
setSubmissionId(res.data)
setSubmissionStatus("started")
toast.success(t("homework.take.startSuccess"))
router.refresh()
} else {
toast.error(res.message || t("homework.take.startFailed"))
}
} catch {
toast.error(t("homework.take.startFailed"))
} finally {
setIsBusy(false)
}
setIsBusy(false)
}
const handleSaveQuestion = async (questionId: string) => {
@@ -156,22 +161,29 @@ export function HomeworkTakeView({ assignmentId, initialData }: HomeworkTakeView
const handleSubmit = async () => {
if (!submissionId) return
setIsBusy(true)
// P2-9: 提交前 flush 自动保存队列,确保所有答案已落库
await autoSave.flush()
try {
// P2-9: 提交前 flush 自动保存队列,确保所有答案已落库
// flush 失败应中止提交,避免丢失未保存的答案
await autoSave.flush()
const submitFd = new FormData()
submitFd.set("submissionId", submissionId)
const submitRes = await submitHomeworkAction(null, submitFd)
if (submitRes.success) {
// 提交成功后清除离线缓存
clearOfflineCache(offlineStorageKey)
toast.success(t("homework.take.submitSuccess"))
setSubmissionStatus("submitted")
router.push("/student/learning/assignments")
} else {
toast.error(submitRes.message || t("homework.take.submitFailed"))
const submitFd = new FormData()
submitFd.set("submissionId", submissionId)
const submitRes = await submitHomeworkAction(null, submitFd)
if (submitRes.success) {
// 提交成功后清除离线缓存
clearOfflineCache(offlineStorageKey)
toast.success(t("homework.take.submitSuccess"))
setSubmissionStatus("submitted")
// V3-9: 提交后跳转到结果页,展示即时反馈
router.push(`/student/learning/assignments/${assignmentId}/result`)
} else {
toast.error(submitRes.message || t("homework.take.submitFailed"))
}
} catch {
toast.error(t("homework.take.submitFailed"))
} finally {
setIsBusy(false)
}
setIsBusy(false)
}
// 统计未作答题目数
@@ -378,7 +390,7 @@ export function HomeworkTakeView({ assignmentId, initialData }: HomeworkTakeView
if (el) el.scrollIntoView({ behavior: "smooth", block: "start" })
}}
className={cn(
"h-8 w-8 rounded flex items-center justify-center text-xs font-medium border transition-colors hover:opacity-80 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1",
"h-11 w-11 sm:h-8 sm:w-8 rounded flex items-center justify-center text-xs font-medium border transition-colors hover:opacity-80 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1",
hasAnswer ? "bg-primary text-primary-foreground border-primary" : "bg-background text-muted-foreground border-input"
)}
aria-label={t("homework.take.jumpToQuestion", { index: i + 1 })}