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

80
src/instrumentation.ts Normal file
View File

@@ -0,0 +1,80 @@
/**
* Next.js Instrumentation 钩子
*
* 在应用启动时执行一次性初始化操作。
* 文档https://nextjs.org/docs/app/building-your-application/optimizing/instrumentation
*
* V3-3: 注册 ExamHomeworkServicePort 实现
* 将 modules 层的 data-access 函数注入到 shared 层的 ServicePort
* 使 app 层可以通过 EXAM_HOMEWORK_SERVICE_PROVIDER.get() 调用,
* 而不直接依赖 modules 内部实现。
*/
import { registerExamHomeworkService } from "@/shared/services/exam-homework-port"
import { getExamById, getExams, getExamCreatorId, getExamTitleById } from "@/modules/exams/data-access"
import {
getHomeworkAssignmentById,
getHomeworkAssignments,
getAssignmentMaxScoreById,
} from "@/modules/homework/data-access"
import { getExamWithQuestionsForHomework } from "@/modules/exams/data-access"
import type { DataScope } from "@/shared/types/permissions"
import type { Exam } from "@/modules/exams/types"
import type { HomeworkAssignmentListItem } from "@/modules/homework/types"
/**
* 适配器:将 getExamById 的返回值补全为 Exam 类型
* data-access 返回的对象包含 questions 数组但缺少 questionCount 字段
*/
const adaptExam = (raw: Awaited<ReturnType<typeof getExamById>>): Exam | null => {
if (!raw) return null
return {
...raw,
questionCount: raw.questions?.length ?? 0,
}
}
/**
* 适配器:将 getHomeworkAssignmentById 的返回值补全为 HomeworkAssignmentListItem 类型
* data-access 返回的对象缺少 averageScore 和 overdueCount 字段
*/
const adaptAssignment = (raw: Awaited<ReturnType<typeof getHomeworkAssignmentById>>): HomeworkAssignmentListItem | null => {
if (!raw) return null
return {
id: raw.id,
sourceExamId: raw.sourceExamId,
sourceExamTitle: raw.sourceExamTitle,
title: raw.title,
status: raw.status,
availableAt: raw.availableAt,
dueAt: raw.dueAt,
allowLate: raw.allowLate,
lateDueAt: raw.lateDueAt,
maxAttempts: raw.maxAttempts,
createdAt: raw.createdAt,
updatedAt: raw.updatedAt,
targetCount: raw.targetCount,
submittedCount: raw.submittedCount,
gradedCount: raw.gradedCount,
averageScore: null,
overdueCount: 0,
}
}
export async function register(): Promise<void> {
registerExamHomeworkService({
// 考试
getExamById: async (id: string, scope?: DataScope) => adaptExam(await getExamById(id, scope)),
getExams,
getExamCreatorId,
getExamTitleById,
// 作业
getHomeworkAssignmentById: async (id: string, scope?: DataScope) => adaptAssignment(await getHomeworkAssignmentById(id, scope)),
getHomeworkAssignments,
getAssignmentMaxScoreByIds: getAssignmentMaxScoreById,
// 跨模块
getExamWithQuestionsForHomework,
})
}