Files
NextEdu/src/app/(dashboard)/teacher/exams/[id]/proctoring/page.tsx
SpecialX b86255f0ea feat(P2): 实现选课管理、考试监考、学情诊断三大功能模块
## 新增功能模块

### 1. 选课管理(elective)
- 新增表:electiveCourses、courseSelections
- 新增权限:ELECTIVE_MANAGE/ELECTIVE_READ/ELECTIVE_SELECT
- 支持先到先得 + 抽签两种选课模式
- admin/teacher/student 三端页面

### 2. 考试监考(proctoring)
- exams 表扩展:examMode/durationMinutes/antiCheatEnabled 等字段
- 新增表:examProctoringEvents
- 新增权限:EXAM_PROCTOR/EXAM_PROCTOR_READ
- 教师监考面板 + 学生端防作弊监控
- API:/api/proctoring/event 接收事件上报

### 3. 学情诊断报告(diagnostic)
- 新增表:knowledgePointMastery、learningDiagnosticReports
- 新增权限:DIAGNOSTIC_MANAGE/DIAGNOSTIC_READ
- 基于提交答案自动计算知识点掌握度
- 生成个人/班级诊断报告(强项/弱项/建议)
- 雷达图可视化

## 其他改动
- 项目规则:单文件行数限制从 300 行调整为企业级规范(组件≤500/Actions≤800/硬上限1000)
- scripts/seed.ts:消除全部 any 类型,定义内部类型,0 lint 错误
- 架构文档 004/005 同步更新三个新模块
- 迁移文件 0001_heavy_sage.sql 生成

## 验证
- npx tsc --noEmit:0 错误
- npm run lint:0 错误 0 警告
2026-06-17 19:12:51 +08:00

56 lines
1.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { notFound } from "next/navigation"
import { requirePermission, PermissionDeniedError } from "@/shared/lib/auth-guard"
import { Permissions } from "@/shared/types/permissions"
import { ProctoringDashboard } from "@/modules/proctoring/components/proctoring-dashboard"
import {
getExamForProctoring,
getExamProctoringSummary,
getStudentProctoringStatuses,
getRecentProctoringEvents,
} from "@/modules/proctoring/data-access"
import type { ProctoringDashboardData } from "@/modules/proctoring/types"
export const dynamic = "force-dynamic"
export default async function ExamProctoringPage({
params,
}: {
params: Promise<{ id: string }>
}) {
try {
await requirePermission(Permissions.EXAM_PROCTOR)
} catch (error) {
if (error instanceof PermissionDeniedError) {
return (
<div className="p-10 text-center text-muted-foreground">
exam:proctor
</div>
)
}
throw error
}
const { id } = await params
const exam = await getExamForProctoring(id)
if (!exam) return notFound()
// 并行拉取面板初始数据
const [summary, students, recentEvents] = await Promise.all([
getExamProctoringSummary(id),
getStudentProctoringStatuses(id),
getRecentProctoringEvents(id, 20),
])
const initialData: ProctoringDashboardData = {
summary,
students,
recentEvents,
}
return (
<div className="flex h-full flex-col space-y-4 p-4">
<ProctoringDashboard examId={id} initialData={initialData} />
</div>
)
}