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

@@ -13,6 +13,7 @@ import {
import {
getStudentDashboardGrades,
getStudentHomeworkAssignments,
getStudentExamResults,
} from "@/modules/homework/data-access"
import { getStudentGradeSummary } from "@/modules/grades/data-access"
import { getGradeNameById } from "@/modules/school/data-access"
@@ -22,6 +23,7 @@ import type {
ChildDashboardData,
ChildHomeworkSummaryData,
ChildScheduleItem,
ChildWeeklyScheduleItem,
ParentChildRelation,
ParentDashboardData,
} from "./types"
@@ -174,26 +176,50 @@ const buildTodaySchedule = (
.sort((a, b) => a.startTime.localeCompare(b.startTime))
}
const buildWeeklySchedule = (
schedule: Awaited<ReturnType<typeof getStudentSchedule>>,
): ChildWeeklyScheduleItem[] => {
return schedule
.map((s) => ({
id: s.id,
classId: s.classId,
className: s.className,
course: s.course,
startTime: s.startTime,
endTime: s.endTime,
location: s.location ?? null,
weekday: s.weekday,
}))
.sort((a, b) =>
a.weekday === b.weekday
? a.startTime.localeCompare(b.startTime)
: a.weekday - b.weekday,
)
}
export const getChildDashboardData = cache(
async (studentId: string, relation: string | null = null): Promise<ChildDashboardData | null> => {
const basicInfo = await getChildBasicInfo(studentId, relation)
if (!basicInfo) return null
const [enrolledClasses, schedule, assignments, gradeTrend, gradeSummary] = await Promise.all([
const [enrolledClasses, schedule, assignments, gradeTrend, gradeSummary, examResults] = await Promise.all([
getStudentClasses(studentId),
getStudentSchedule(studentId),
getStudentHomeworkAssignments(studentId),
getStudentDashboardGrades(studentId),
getStudentGradeSummary(studentId),
getStudentExamResults(studentId),
])
return {
basicInfo,
enrolledClasses,
todaySchedule: buildTodaySchedule(schedule),
weeklySchedule: buildWeeklySchedule(schedule),
homeworkSummary: buildHomeworkSummary(assignments),
gradeTrend,
gradeSummary,
examResults,
}
},
)
@@ -224,3 +250,20 @@ export const getParentDashboardData = cache(
}
},
)
/**
* 获取家长所有子女的轻量列表id + name用于详情页头部多子女切换器。
* 一次批量查询,避免 N+1。
*/
export const getChildNameList = cache(
async (parentId: string): Promise<Array<{ id: string; name: string | null }>> => {
const relations = await getChildren(parentId)
if (relations.length === 0) return []
const nameMap = await getUserNamesByIds(relations.map((r) => r.studentId))
return relations.map((r) => ({
id: r.studentId,
name: nameMap.get(r.studentId)?.name ?? null,
}))
},
)