feat(exams,homework): add rich text exam editor and scan-based grading

- Add Tiptap-based rich text editor with custom extensions (dotted-mark,
  blank-node, image-node, group-block, question-block) for exam creation
- Add AI auto-marking action to convert pasted exam text to structured editor doc
- Add resizable split-panel layout for editor + live preview
- Add student scan upload (photo of paper answers) with drag-drop and reorder
- Add scan image viewer with zoom/rotate/fullscreen for teachers
- Add scan grading view with side-by-side questions and scan images
- Add /teacher/exams/new and /teacher/homework/submissions/[id]/scan-grading routes
- Fix getScansAction to support both teacher (HOMEWORK_GRADE) and student
  (HOMEWORK_SUBMIT) permission scopes
- Add i18n keys for rich editor, scan upload, and scan grading (zh-CN/en)
- Sync architecture diagrams (004/005) with new modules, routes, and deps
This commit is contained in:
SpecialX
2026-06-24 13:16:33 +08:00
parent 0c64219cb8
commit 6114607c1e
30 changed files with 3548 additions and 26 deletions

View File

@@ -445,6 +445,7 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
| **UI 组件** | `ConfirmDeleteDialog` | `components/ui/confirm-delete-dialog.tsx` | 通用删除确认对话框AlertDialog 包装,支持自定义 confirmText/cancelText | 5 个P0-1: announcement-detail, message-detail, course-plan-detail, grade-classes-view, students-table |
| **UI 组件** | `Pagination` | `components/ui/pagination.tsx` | 通用分页 UIShowing X-Y of Z + Page X of Y + 上一页/下一页按钮) | 3 个P0-2: audit-log-table, login-log-table, data-change-log-table |
| **UI 组件** | `EmptyTableRow` | `components/ui/empty-table-row.tsx` | 表格空状态行TableRow + TableCell 居中显示空状态文案) | 3 个P0-3: audit-log-table, login-log-table, data-change-log-table |
| **UI 组件** | `ResizablePanel` | `components/ui/resizable-panel.tsx` | 自实现可拖拽分栏容器(左右两栏 + 中间分隔条 pointer 拖拽调整宽度,无新依赖;用于试卷富文本编辑器左编辑右预览、阅卷式批改左题目右扫描图等场景) | 2 个exams/components/exam-rich-form, homework/components/homework-scan-grading-view |
| **UI 组件** | `StatusBadge` | `components/ui/status-badge.tsx` | 通用状态徽章Badge + 状态→variant/label/className 映射表,修复 in_progress 颜色不一致 bug | 9+ 个P1-1: audit 3 文件, grades 2 文件, student/learning/assignments, parent/child-homework-summary, student-upcoming-assignments-card, question-columns |
| **表单字段** | `TextField` | `components/form-fields/text-field.tsx` | 通用文本字段FormField + Input 包装,支持 text/number/password/datetime-local 类型 + value 转换器) | 3 个文件 16 处P1-2: profile-settings-form 6, exam-basic-info-form 4, ai-provider-settings-card 4 |
| **表单字段** | `SelectField` | `components/form-fields/select-field.tsx` | 通用选择字段FormField + Select 包装,支持 toSelectValue/fromSelectValue 处理 number↔string | 4 个文件 8 处P1-2: exam-basic-info-form 3, ai-provider-settings-card 1, create-question-dialog 2, profile-settings-form 1 |
@@ -521,6 +522,7 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
| `components/ui/chip-nav.tsx` | 78 | ChipNav 芯片导航P1-b 新增) |
| `components/ui/page-header.tsx` | 44 | PageHeader 页面头部P2-b 新增,含 icon 属性) |
| `components/ui/filter-bar.tsx` | 124 | FilterBar + FilterSearchInput + FilterResetButtonP3-b 新增) |
| `components/ui/resizable-panel.tsx` | - | ResizablePanel 可拖拽分栏容器(自实现,无新依赖;用于富文本编辑器与阅卷式批改的左右分栏布局) |
| `components/charts/chart-card-shell.tsx` | 90 | ChartCardShell 图表卡片外壳P3-c 新增) |
| `components/charts/trend-line-chart.tsx` | 153 | TrendLineChart 趋势折线图P3-c 新增) |
| `components/charts/simple-bar-chart.tsx` | 162 | SimpleBarChart 柱状图P3-c 新增) |
@@ -540,13 +542,20 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
**职责**:考试全生命周期管理(创建/编辑/预览/发布/删除/复制)+ AI 辅助出题。
**导出函数**
- Actions`createExamAction` / `createAiExamAction` / `previewAiExamAction` / `regenerateAiQuestionAction` / `updateExamAction` / `deleteExamAction` / `duplicateExamAction` / `getExamPreviewAction` / `getSubjectsAction` / `getGradesAction` / `getExamsByGradeIdAction`(✅ v4-P2-7 新增年级仪表盘维度3按 gradeId 查询年级下所有考试 + 提交统计EXAM_READ 权限)(✅ P1-2 已修复actions 层不再直接访问 DB全部下沉到 data-access
- Actions`createExamAction` / `createAiExamAction` / `previewAiExamAction` / `regenerateAiQuestionAction` / `updateExamAction` / `deleteExamAction` / `duplicateExamAction` / `getExamPreviewAction` / `getSubjectsAction` / `getGradesAction` / `getExamsByGradeIdAction`(✅ v4-P2-7 新增年级仪表盘维度3按 gradeId 查询年级下所有考试 + 提交统计EXAM_READ 权限)/ `autoMarkExamAction`(✅ 2026-06-24 新增AI 自动标记试卷文本,将粘贴的试卷文本交给 AI 解析为带题目块/分组/填空/加点字标记的 Tiptap JSONContent 文档EXAM_AI_GENERATE 权限,返回 `{ doc, title }` 供富文本编辑器载入)/ `createExamFromRichEditorAction`(✅ 2026-06-24 新增:从富文本编辑器保存试卷草稿,将 Tiptap JSONContent 通过 `editor/editor-to-structure.editorDocToStructure` 转换为 questions + structure 后复用 `persistAiGeneratedExamDraft` 持久化EXAM_CREATE 权限)(✅ P1-2 已修复actions 层不再直接访问 DB全部下沉到 data-access
- Data-access`getExams` / `getExamById` / `persistExamDraft` / `persistAiGeneratedExamDraft` / `buildExamDescription` / `resolveSubjectGradeNames` / `getExamCreatorId` / `updateExamWithQuestions` / `deleteExamById` / `duplicateExam` / `getExamPreview` / `getExamSubjects` / `getExamGrades` / `getExamsByGradeId`(✅ v4-P2-7 新增年级仪表盘维度3exams 表有直接 gradeId 字段,配合 examSubmissions 聚合提交数/已评分数/平均分,支持 scope 行级过滤)(后 8 个为 P1-2 新增)/ `getExamsForGradeEntry`(✅ 2026-06-24 新增:按 scope 过滤试卷列表,只返回有题目的试卷,供成绩录入页试卷选择器使用,返回 id/title/subjectName/gradeName/questionCount/totalScore/ `getExamForGradeEntry`(✅ 2026-06-24 新增获取单个试卷详情含题目列表innerJoin questions 获取 type含 scope 校验,返回 id/title/subjectId/gradeId/totalScore/questions[{id,order,score,type}],供 grades 模块按试卷录入成绩使用)
- AI Pipeline`generateAiCreateDraftFromSource` / `generateAiPreviewData` / `regenerateAiQuestionByInstruction`
- Utils`normalizeStructure`v3 新增:将持久化的 `exam.structure` unknown JSON 运行时校验并归一化为类型安全的 `ExamNode[]`,类型守卫模式无 `as` 断言,从 `teacher/exams/[id]/build/page.tsx` 提取)
- Stats-serviceV3-8 新增):`getExamAnalytics`cache 包装,聚合考试所有作业的已批改提交,计算平均分/及格率/分数段分布/逐题错误率与难度等级,对标智学网考试分析)+ `ExamAnalyticsSummary` 类型
- Types✅ 2026-06-24 新增成绩录入相关类型):`ExamQuestionItem`(试卷中单个题目的精简结构 { id, order, score, type }/ `ExamForGradeEntry`(成绩录入用的试卷详情,含题目列表)/ `ExamOptionForEntry`(成绩录入页试卷选择器选项 { id, title, subjectName, gradeName, questionCount, totalScore }
- Editor 子模块(✅ 2026-06-24 新增 `editor/` 目录,基于 Tiptap 的试卷富文本编辑器):
- 组件:`ExamRichEditor`富文本编辑器主体forwardRef 暴露 `ExamRichEditorHandle` 以便父组件获取 JSONContent/ `SelectionToolbar`(浮动选择工具栏,对选中文本应用 DottedMark 等标记)
- 自定义 Tiptap 扩展(`editor/extensions/``DottedMark`(加点字标记,拼音注音题)/ `BlankNode`(填空空位节点,原子节点)/ `ImageNode`(图片节点,含 fileId/url/alt 属性)/ `QuestionBlock`(题目块节点,含 type/score 等 attrs/ `GroupBlock`(大题分组节点,含 title
- 类型(`editor/exam-rich-editor-types.ts``RichQuestionType``single_choice | multiple_choice | judgment | text | composite`/ `RichQuestionContent`(题干文本+选项+填空+图片+子题+正确答案)/ `EditorQuestion` / `EditorStructureNode`group|question 递归)/ `EditorDoc`{ title, questions, structure }/ `EditorJSONContent`JSONContent 别名)
- 转换工具:`editorDocToStructure``editor/editor-to-structure.ts`Tiptap JSONContent → EditorDoc`createExamFromRichEditorAction` 持久化使用)/ `structureToEditorDoc``editor/structure-to-editor.ts`EditorDoc → Tiptap JSONContent供编辑器回填已保存试卷使用
- Barrel 导出:`editor/index.ts` 聚合导出上述所有组件、扩展、类型与转换函数
- ComponentsV3-8 新增):`ExamAnalyticsDashboard`(考试分析仪表盘:汇总卡片+分数段分布+逐题分析表)
- Components✅ 2026-06-24 新增):`ExamRichForm`(富文本编辑器试卷创建表单,使用 `ResizablePanel` 左编辑右预览布局,集成 `autoMarkExamAction` 一键 AI 标记 + `createExamFromRichEditorAction` 保存草稿,调用 `getSubjectsAction`/`getGradesAction` 填充科目年级选项)
**依赖关系**
- 依赖:`shared/*``@/auth``questions`(✅ P0-1 已修复:通过 data-access.createQuestionWithRelations`classes`(✅ P0-2 已修复:通过 data-access.getClassGradeIdsByClassIds`school`(✅ P1-1 已修复:通过 school data-access.getSubjectOptions/getGradeOptions`homework`V3-8 新增stats-service 通过 `homework/data-access.getHomeworkAssignmentsByExamId` / `getGradedSubmissionsByExamId` 获取作业与提交数据,合理跨模块调用)
@@ -564,16 +573,29 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
**文件清单**
| 文件 | 行数 | 职责 |
|------|------|------|
| `actions.ts` | 691 | 10 个 Server ActionP1-2 已修复,无直接 DB 操作) |
| `actions.ts` | 691+ | 12 个 Server ActionP1-2 已修复,无直接 DB 操作2026-06-24 新增 `autoMarkExamAction`/`createExamFromRichEditorAction` |
| `ai-pipeline.ts` | 857 | AI 出题管线(超限) |
| `data-access.ts` | 560+ | 考试 CRUD含 P1-2 新增 7 个写/查询函数P0-1/P0-2 已修复:通过 questions/classes data-access 跨模块通信v4-P2-7 新增 getExamsByGradeId2026-06-24 新增 getExamsForGradeEntry/getExamForGradeEntry 供 grades 模块按试卷录入成绩) |
| `stats-service.ts` | - | V3-8 新增:考试分析数据聚合(`getExamAnalytics` + `ExamAnalyticsSummary` 类型) |
| `types.ts` | 50+ | 类型定义2026-06-24 新增ExamQuestionItem/ExamForGradeEntry/ExamOptionForEntry 供成绩录入使用) |
| `types.ts` | 50+ | 类型定义2026-06-24 新增ExamQuestionItem/ExamForGradeEntry/ExamOptionForEntry 供成绩录入使用;新增 `AutoMarkResult`/`AutoMarkSchema`/`RichExamCreateSchema` 供富文本编辑器使用 |
| `editor/index.ts` | 23 | 2026-06-24 新增:富文本编辑器 barrel 导出(聚合组件/扩展/类型/转换函数) |
| `editor/exam-rich-editor.tsx` | - | 2026-06-24 新增ExamRichEditor 富文本编辑器主体forwardRef + ExamRichEditorHandle |
| `editor/selection-toolbar.tsx` | - | 2026-06-24 新增SelectionToolbar 浮动选择工具栏 |
| `editor/exam-rich-editor-types.ts` | 42 | 2026-06-24 新增富文本编辑器类型定义RichQuestionType/RichQuestionContent/EditorQuestion/EditorStructureNode/EditorDoc/EditorJSONContent |
| `editor/editor-to-structure.ts` | - | 2026-06-24 新增editorDocToStructureTiptap JSONContent → EditorDoc |
| `editor/structure-to-editor.ts` | - | 2026-06-24 新增structureToEditorDocEditorDoc → Tiptap JSONContent |
| `editor/extensions/index.ts` | 5 | 2026-06-24 新增Tiptap 扩展 barrel 导出 |
| `editor/extensions/dotted-mark.ts` | - | 2026-06-24 新增DottedMark 加点字标记扩展 |
| `editor/extensions/blank-node.tsx` | - | 2026-06-24 新增BlankNode 填空空位节点扩展 |
| `editor/extensions/image-node.ts` | - | 2026-06-24 新增ImageNode 图片节点扩展 |
| `editor/extensions/group-block.tsx` | - | 2026-06-24 新增GroupBlock 大题分组节点扩展 |
| `editor/extensions/question-block.tsx` | - | 2026-06-24 新增QuestionBlock 题目块节点扩展 |
| `hooks/use-exam-preview.ts` | 295 | 预览 Hook |
| `utils/normalize-structure.ts` | 57 | v3 新增exam.structure 运行时校验与归一化(从 build/page.tsx 提取) |
| `components/exam-analytics-dashboard.tsx` | - | V3-8 新增:考试分析仪表盘组件 |
| `components/exam-actions.tsx` | - | V3-5/V3-8/V3-12 增强:角色化菜单+analytics 链接+移动端触摸优化 |
| `components/*` | 19 文件 | 考试表单/组卷/预览/分析组件 |
| `components/exam-rich-form.tsx` | - | 2026-06-24 新增富文本编辑器试卷创建表单ResizablePanel 左编辑右预览 + AI 自动标记 + 保存草稿) |
| `components/*` | 19+ 文件 | 考试表单/组卷/预览/分析组件 |
---
@@ -582,15 +604,17 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
**职责**:作业全生命周期(创建/发布/作答/批改/分析)。
**导出函数**
- Actions`createHomeworkAssignmentAction` / `startHomeworkSubmissionAction` / `saveHomeworkAnswerAction` / `submitHomeworkAction` / `gradeHomeworkSubmissionAction` / `batchAutoGradeSubmissionsAction`V3-7 新增批量自动批改HOMEWORK_GRADE 权限+非管理员仅可批改自己创建的作业)(✅ P1-2 已修复actions 层不再直接访问 DB全部下沉到 data-access/data-access-write
- Actions`createHomeworkAssignmentAction` / `startHomeworkSubmissionAction` / `saveHomeworkAnswerAction` / `submitHomeworkAction` / `gradeHomeworkSubmissionAction` / `batchAutoGradeSubmissionsAction`V3-7 新增批量自动批改HOMEWORK_GRADE 权限+非管理员仅可批改自己创建的作业)/ `getScansAction`(✅ 2026-06-24 新增:获取某次提交的所有答题扫描图,扫描图存储在 fileAttachments 表中 targetType="homework"/targetId=submissionId支持两类访问者——教师 HOMEWORK_GRADE 仅可访问自己创建的作业的提交,学生 HOMEWORK_SUBMIT 仅可访问自己的提交)/ `deleteScanAction`(✅ 2026-06-24 新增:删除答题扫描图,仅允许提交者本人删除且仅在提交状态为 started 时允许,调用 `files/data-access.deleteFileAttachment` 删除 DB 记录)(✅ P1-2 已修复actions 层不再直接访问 DB全部下沉到 data-access/data-access-write
- Data-access`getHomeworkAssignments` / `getHomeworkAssignmentById` / `getHomeworkSubmissions` / `getStudentHomeworkAssignments` / `getStudentHomeworkTakeData` / `getHomeworkAssignmentReviewList` / `getHomeworkSubmissionDetails` / `getDemoStudentUser`(已迁移至 users 模块 `getCurrentStudentUser`,此处为 re-export 向后兼容)/ `isRecord` / `toQuestionContent` / `getAssignmentMaxScoreById`(后三者供 stats-service 使用)/ `getHomeworkAssignmentsByExamId`V3-8 新增:按考试 ID 查作业+目标/提交/批改计数)/ `getGradedSubmissionsByExamId`V3-8 新增:按考试 ID 查已批改提交,按学生去重)/ `getStudentSubmissionResult`V3-9 新增:查学生指定作业最新提交,用于结果页)/ `getStudentExamResults`V3-11 新增:查学生考试结果列表,供家长端展示)
- Data-access-classes`getAssignmentIdsForStudents` / `getHomeworkAssignmentsWithSubject` / `getHomeworkAssignmentsByIds` / `getAssignmentTargetCounts` / `getHomeworkSubmissionsForStudents` / `getPublishedHomeworkAssignmentsWithSubject` / `getHomeworkSubmissionsForAssignments`P0-7 新增,供 classes 模块跨模块调用;✅ P1-1 已修复:内部不再直查 exams/subjects 表,改为调用 `exams/data-access.getExamSubjectIdMap` + `school/data-access.getSubjectNameMapByIds`
- Data-access-write11 个写操作函数P1-2 新增 10 个从 actions 下沉 + V3-7 新增 `batchAutoGradeSubmissions`
- Stats-service`getTeacherGradeTrends` / `getHomeworkAssignmentAnalytics` / `getStudentDashboardGrades`(从 data-access.ts re-export 以保持向后兼容)
- ComponentsV3-7/V3-9 新增):`HomeworkBatchGradingView`(批量批改视图:勾选+一键批改+toast 反馈)/ `HomeworkSubmissionResult`(提交后即时反馈:分数汇总+对错分布+错题预览)
- Components✅ 2026-06-24 新增扫描图相关):`ScanUploader`(学生扫描图上传组件,调用 /api/upload 上传图片targetType="homework",支持增删/排序,导出 `ScanImage` 类型)/ `ScanImageViewer`(扫描图查看器,支持翻页/缩放/旋转/全屏,用于阅卷式批改时查看学生答题图片)/ `HomeworkScanGradingView`(教师阅卷式批改视图,使用 `ResizablePanel` 左侧题目+批改表单、右侧扫描图查看器,集成 `getScansAction` 拉取扫描图、`gradeHomeworkSubmissionAction` 批改,支持上一份/下一份提交快速切换)
- Types✅ 2026-06-24 新增):`ScanAttachment`(扫描图附件结构 { fileId, url, filename, originalName, page },由 `getScansAction` 返回)/ `ScanImage`(扫描图 UI 结构,由 `ScanUploader` 导出供 `ScanImageViewer` 复用)
**依赖关系**
- 依赖:`shared/*``@/auth``exams`(✅ P1-1 已修复:通过 exams data-access.getExamIdsByGradeIds/getExamSubjectIdMap/getExamWithQuestionsForHomework/getExamForProctoringCrossModule`classes`(✅ P1-1 已修复:通过 classes data-access.getStudentIdsByClassId 等 7 个函数)、`school`(✅ P1-1 已修复:通过 school data-access.getSubjectOptions/getSubjectNameMapByIds`users`(✅ P1-1 已修复:通过 users data-access.getUserWithRole/getUserNamesByIds
- 依赖:`shared/*``@/auth``exams`(✅ P1-1 已修复:通过 exams data-access.getExamIdsByGradeIds/getExamSubjectIdMap/getExamWithQuestionsForHomework/getExamForProctoringCrossModule`classes`(✅ P1-1 已修复:通过 classes data-access.getStudentIdsByClassId 等 7 个函数)、`school`(✅ P1-1 已修复:通过 school data-access.getSubjectOptions/getSubjectNameMapByIds`users`(✅ P1-1 已修复:通过 users data-access.getUserWithRole/getUserNamesByIds`files`(✅ 2026-06-24 新增:`deleteScanAction` 调用 `files/data-access.deleteFileAttachment` 删除扫描图 DB 记录,合理跨模块调用)
- 被依赖:`dashboard`(通过 data-access合理`parent`(通过 data-access合理V3-11 新增 `getStudentExamResults` 供 parent 调用)、`classes`(✅ P0-7 已修复classes 通过 `homework/data-access-classes` 获取作业数据,不再反向直查 homework/exams 表)、`exams`V3-8 新增:`exams/stats-service.getExamAnalytics` 调用 `getHomeworkAssignmentsByExamId` / `getGradedSubmissionsByExamId`,合理跨模块调用)
**已知问题**
@@ -614,12 +638,15 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
| `data-access-write.ts` | 285+ | 作业写操作P1-2 新增 10 个写函数从 actions 下沉V3-7 新增 `batchAutoGradeSubmissions` |
| `data-access-classes.ts` | 240+ | 跨模块查询封装P0-7 新增;✅ P1-1 已修复:内部通过 exams/school data-access 获取考试科目信息,不再直查 exams/subjects 表) |
| `stats-service.ts` | 425 | 统计分析(教师趋势/作业分析/学生仪表盘成绩) |
| `actions.ts` | 239+ | 6 个 Server ActionP1-2 已修复,无直接 DB 操作V3-7 新增 `batchAutoGradeSubmissionsAction` |
| `actions.ts` | 239+ | 8 个 Server ActionP1-2 已修复,无直接 DB 操作V3-7 新增 `batchAutoGradeSubmissionsAction`2026-06-24 新增 `getScansAction`/`deleteScanAction`;新增 `ScanAttachment` 类型导出 |
| `types.ts` | 186 | 类型定义 |
| `schema.ts` | 29 | Zod 校验 |
| `components/homework-batch-grading-view.tsx` | - | V3-7 新增批量批改视图use client |
| `components/homework-submission-result.tsx` | - | V3-9 新增:提交后即时反馈页 |
| `components/homework-take-view.tsx` | - | V3-9/V3-12 增强:提交后跳转结果页+移动端触摸优化;✅ P0-竞品:集成限时/监考模式倒计时useExamCountdown+ 到时自动提交 |
| `components/scan-uploader.tsx` | - | 2026-06-24 新增:学生扫描图上传组件(调用 /api/uploadtargetType="homework",导出 `ScanImage` 类型) |
| `components/scan-image-viewer.tsx` | - | 2026-06-24 新增:扫描图查看器(翻页/缩放/旋转/全屏) |
| `components/homework-scan-grading-view.tsx` | - | 2026-06-24 新增教师阅卷式批改视图ResizablePanel 左题目右扫描图 + 上一份/下一份切换) |
| `hooks/use-exam-countdown.ts` | 122 | P0-竞品新增:考试倒计时 Hook每秒更新、紧急状态高亮、到时回调自动提交 |
---
@@ -1311,10 +1338,11 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
**导出函数**
- Data-access`getAllFileAttachments` / `getFileAttachmentsByOwner` / `getFileAttachmentById` / `createFileAttachment` / `updateFileAttachment` / `deleteFileAttachment` / `batchDeleteFileAttachments` / `getFileStats`(✅ P2 已修复7 个读函数使用 `React.cache()` 包装实现请求级 memoization`getFileAttachment` / `getFileAttachmentsByTarget` / `getFileAttachmentsByUploader` / `getAllFileAttachments` / `getFileAttachmentsWithFilters` / `getFileStats` / `getFileAttachmentsByIds`
- Types✅ 2026-06-24 更新):`FileTargetType` 新增 `"homework"` 枚举值,现为 `"exam" | "textbook" | "question" | "announcement" | "homework"`用于学生答题扫描图附件targetId = homeworkSubmissions.id`app/api/upload/route.ts``VALID_TARGET_TYPES` 同步新增 `"homework"`
**依赖关系**
- 依赖:`shared/*``@/auth`
- 被依赖:`app/api/upload` / `app/api/files/[id]` / `app/api/files/batch-delete`
- 被依赖:`app/api/upload` / `app/api/files/[id]` / `app/api/files/batch-delete` / `homework`(✅ 2026-06-24 新增:`homework/actions.deleteScanAction` 调用 `deleteFileAttachment` 删除扫描图 DB 记录;`homework/components/scan-uploader` 调用 `/api/upload` 上传扫描图targetType="homework"
**已知问题**
- ✅ P2-13 已修复:~~所有函数 try-catch 吞错误返回空数组/null~~ 所有 catch 块已添加 `console.error` 输出错误上下文

View File

@@ -1544,6 +1544,23 @@
"audit/components/data-change-log-table.tsx"
]
},
{
"name": "ResizablePanel",
"file": "components/ui/resizable-panel.tsx",
"props": "{ minLeft?: number, minRight?: number, initialLeft?: number, left: ReactNode, right: ReactNode, className?: string }",
"purpose": "2026-06-24 新增:自实现可拖拽分栏容器(左右两栏 + 中间分隔条 pointer 拖拽调整宽度无新依赖。用于试卷富文本编辑器左编辑右预览、阅卷式批改左题目右扫描图等场景。pointerdown/move/up 事件驱动document.body cursor=userSelect=none 防选中clamp 到 minLeft/minRight 范围。",
"internalDeps": [
"cn",
"useState",
"useRef",
"useCallback",
"useEffect"
],
"usedBy": [
"exams/components/exam-rich-form.tsx",
"homework/components/homework-scan-grading-view.tsx"
]
},
{
"name": "StatusBadge",
"file": "components/ui/status-badge.tsx",
@@ -2337,7 +2354,8 @@
"createdAt"
],
"usedBy": [
"files"
"files",
"homework"
]
},
"gradeRecords": {
@@ -2906,6 +2924,44 @@
"usedBy": [
"management/grade/dashboard"
]
},
{
"name": "autoMarkExamAction",
"permission": "EXAM_AI_GENERATE",
"signature": "(prevState: ActionState<AutoMarkResult> | null, formData: FormData) => Promise<ActionState<AutoMarkResult>>",
"purpose": "2026-06-24 新增AI 自动标记试卷文本。将粘贴的试卷文本交给 AI 解析为带题目块/分组/填空/加点字标记的 Tiptap JSONContent 文档,教师可在富文本编辑器中基于此结果继续微调。返回 { doc, title } 供编辑器载入。",
"deps": [
"requirePermission",
"shared/lib/ai.createAiChatCompletion",
"shared/lib/action-utils.handleActionError",
"exams/ai-pipeline/parse.parseAiResponse",
"exams/actions.buildTiptapDocFromAiResponse",
"exams/actions.extractTitleFromAiResponse",
"AutoMarkSchema"
],
"usedBy": [
"exams/components/exam-rich-form.tsx"
]
},
{
"name": "createExamFromRichEditorAction",
"permission": "EXAM_CREATE",
"signature": "(prevState: ActionState<string> | null, formData: FormData) => Promise<ActionState<string>>",
"purpose": "2026-06-24 新增:从富文本编辑器保存试卷草稿。将 Tiptap JSONContent 通过 editor/editor-to-structure.editorDocToStructure 转换为 questions + structure 后复用 persistAiGeneratedExamDraft 持久化。revalidatePath('/teacher/exams/all')。",
"deps": [
"requirePermission",
"shared/lib/action-utils.handleActionError",
"shared/lib/action-utils.safeJsonParse",
"exams/editor/editor-to-structure.editorDocToStructure",
"exams/data-access.persistAiGeneratedExamDraft",
"exams/actions.prepareExamCreateContext",
"exams/actions.parseExamModeConfig",
"RichExamCreateSchema",
"revalidatePath"
],
"usedBy": [
"exams/components/exam-rich-form.tsx"
]
}
],
"dataAccess": [
@@ -3294,6 +3350,37 @@
"grades/components/batch-grade-entry",
"teacher/grades/entry/page.tsx"
]
},
{
"name": "AutoMarkResult",
"type": "interface",
"file": "actions.ts",
"definition": "{ doc: unknown; title: string }",
"purpose": "2026-06-24 新增AI 自动标记结果。doc 为 Tiptap JSONContent 文档可直接载入富文本编辑器title 为解析出的试卷标题(如有)",
"usedBy": [
"exams/actions.autoMarkExamAction",
"exams/components/exam-rich-form.tsx"
]
},
{
"name": "AutoMarkSchema",
"type": "const",
"file": "actions.ts",
"definition": "z.object({ sourceText: z.string().min(1), aiProviderId: z.string().optional() })",
"purpose": "2026-06-24 新增autoMarkExamAction 的 Zod 输入校验 schema",
"usedBy": [
"exams/actions.autoMarkExamAction"
]
},
{
"name": "RichExamCreateSchema",
"type": "const",
"file": "actions.ts",
"definition": "z.object({ title, subject, grade, difficulty: z.coerce.number().int().min(1).max(5), totalScore: z.coerce.number().int().min(1), durationMin: z.coerce.number().int().min(1), scheduledAt: z.string().optional().nullable(), editorDoc: z.string().min(1) })",
"purpose": "2026-06-24 新增createExamFromRichEditorAction 的 Zod 输入校验 schema。editorDoc 为 Tiptap JSONContent 的 JSON 字符串",
"usedBy": [
"exams/actions.createExamFromRichEditorAction"
]
}
],
"components": [
@@ -3510,6 +3597,11 @@
"types": [
"ExamAnalyticsSummary"
]
},
{
"name": "ExamRichForm",
"file": "exam-rich-form.tsx",
"purpose": "2026-06-24 新增富文本编辑器试卷创建表单use client。使用 ResizablePanel 左编辑右预览布局,集成 autoMarkExamAction 一键 AI 标记(粘贴试卷文本 → AI 解析为 Tiptap doc 载入编辑器)+ createExamFromRichEditorAction 保存草稿。调用 getSubjectsAction/getGradesAction 填充科目年级选项。包含 difficulty/totalScore/durationMin/scheduledAt 等基本信息字段。"
}
],
"hooks": [
@@ -3569,7 +3661,211 @@
"exams/components/exam-analytics-dashboard.tsx"
]
}
]
],
"editor": {
"_description": "2026-06-24 新增:基于 Tiptap 的试卷富文本编辑器子模块src/modules/exams/editor/)。提供自定义扩展(题目块/分组/填空/加点字/图片、Tiptap JSONContent ↔ ExamStructure 双向转换、浮动选择工具栏。Barrel 导出位于 editor/index.ts。",
"components": [
{
"name": "ExamRichEditor",
"file": "editor/exam-rich-editor.tsx",
"type": "component",
"purpose": "富文本编辑器主体use client + forwardRef。基于 Tiptap StarterKit + 自定义扩展DottedMark/BlankNode/ImageNode/QuestionBlock/GroupBlock。通过 ExamRichEditorHandle 暴露 getJSON() 供父组件获取 JSONContent。集成 SelectionToolbar 浮动工具栏。",
"usedBy": [
"exams/components/exam-rich-form.tsx"
]
},
{
"name": "ExamRichEditorHandle",
"file": "editor/exam-rich-editor.tsx",
"type": "interface",
"definition": "{ getJSON: () => EditorJSONContent | null }",
"purpose": "ExamRichEditor 的 ref 句柄类型,暴露 getJSON 方法获取当前编辑器内容的 Tiptap JSONContent",
"usedBy": [
"exams/components/exam-rich-form.tsx"
]
},
{
"name": "SelectionToolbar",
"file": "editor/selection-toolbar.tsx",
"type": "component",
"purpose": "浮动选择工具栏use client。当编辑器内有文本选中时显示提供 DottedMark加点字等标记的快捷应用按钮。",
"usedBy": [
"exams/editor/exam-rich-editor.tsx"
]
}
],
"extensions": [
{
"name": "DottedMark",
"file": "editor/extensions/dotted-mark.ts",
"type": "tiptap-extension",
"purpose": "加点字标记扩展Mark。用于拼音注音题中加点的字渲染为带下点样式的 span。",
"usedBy": [
"exams/editor/exam-rich-editor.tsx",
"exams/editor/selection-toolbar.tsx"
]
},
{
"name": "BlankNode",
"file": "editor/extensions/blank-node.tsx",
"type": "tiptap-extension",
"purpose": "填空空位节点扩展Node原子节点。渲染为可填写的空位占位符含 id/answer/score 属性。",
"usedBy": [
"exams/editor/exam-rich-editor.tsx"
]
},
{
"name": "ImageNode",
"file": "editor/extensions/image-node.ts",
"type": "tiptap-extension",
"purpose": "图片节点扩展Node。含 fileId/url/alt 属性,用于在题目中嵌入图片。",
"usedBy": [
"exams/editor/exam-rich-editor.tsx"
]
},
{
"name": "GroupBlock",
"file": "editor/extensions/group-block.tsx",
"type": "tiptap-extension",
"purpose": "大题分组节点扩展Node块级。含 title 属性,用于「一、选择题」「二、填空题」等大题分组。",
"usedBy": [
"exams/editor/exam-rich-editor.tsx"
]
},
{
"name": "QuestionBlock",
"file": "editor/extensions/question-block.tsx",
"type": "tiptap-extension",
"purpose": "题目块节点扩展Node块级。含 typeQuestionBlockType: single_choice|multiple_choice|judgment|text|composite/score 等 attrsQuestionBlockAttrs用于包裹一道题目。",
"usedBy": [
"exams/editor/exam-rich-editor.tsx"
]
},
{
"name": "QuestionBlockType",
"file": "editor/extensions/question-block.tsx",
"type": "type",
"definition": "\"single_choice\" | \"multiple_choice\" | \"judgment\" | \"text\" | \"composite\"",
"purpose": "题目块类型枚举",
"usedBy": [
"exams/editor/extensions/question-block.tsx"
]
},
{
"name": "QuestionBlockAttrs",
"file": "editor/extensions/question-block.tsx",
"type": "interface",
"definition": "{ type: QuestionBlockType; score: number; questionId?: string }",
"purpose": "题目块属性接口",
"usedBy": [
"exams/editor/extensions/question-block.tsx"
]
}
],
"types": [
{
"name": "RichQuestionType",
"file": "editor/exam-rich-editor-types.ts",
"type": "type",
"definition": "\"single_choice\" | \"multiple_choice\" | \"judgment\" | \"text\" | \"composite\"",
"purpose": "富文本编辑器题目类型枚举",
"usedBy": [
"exams/editor/*",
"exams/editor/editor-to-structure.ts",
"exams/editor/structure-to-editor.ts"
]
},
{
"name": "RichQuestionContent",
"file": "editor/exam-rich-editor-types.ts",
"type": "interface",
"definition": "{ text: string; options?: Array<{ id, text, isCorrect? }>; blanks?: Array<{ id, answer?, score? }>; images?: Array<{ fileId, url, alt? }>; subQuestions?: Array<{ id, text, answer?, score? }>; correctAnswer?: unknown }",
"purpose": "富文本编辑器题目内容结构(题干文本+选项+填空+图片+子题+正确答案)",
"usedBy": [
"exams/editor/exam-rich-editor-types.ts.EditorQuestion",
"exams/editor/editor-to-structure.ts",
"exams/editor/structure-to-editor.ts"
]
},
{
"name": "EditorQuestion",
"file": "editor/exam-rich-editor-types.ts",
"type": "interface",
"definition": "{ id: string; type: RichQuestionType; score: number; content: RichQuestionContent }",
"purpose": "富文本编辑器题目结构",
"usedBy": [
"exams/editor/exam-rich-editor-types.ts.EditorDoc",
"exams/editor/editor-to-structure.ts",
"exams/editor/structure-to-editor.ts"
]
},
{
"name": "EditorStructureNode",
"file": "editor/exam-rich-editor-types.ts",
"type": "interface",
"definition": "{ id: string; type: \"group\" | \"question\"; title?: string; questionId?: string; score?: number; children?: EditorStructureNode[] }",
"purpose": "富文本编辑器结构节点递归group|question",
"usedBy": [
"exams/editor/exam-rich-editor-types.ts.EditorDoc",
"exams/editor/editor-to-structure.ts",
"exams/editor/structure-to-editor.ts"
]
},
{
"name": "EditorDoc",
"file": "editor/exam-rich-editor-types.ts",
"type": "interface",
"definition": "{ title: string; questions: EditorQuestion[]; structure: EditorStructureNode[] }",
"purpose": "富文本编辑器文档结构(标题+题目列表+结构树),由 editorDocToStructure 产出",
"usedBy": [
"exams/editor/editor-to-structure.ts",
"exams/editor/structure-to-editor.ts",
"exams/components/exam-rich-form.tsx"
]
},
{
"name": "EditorJSONContent",
"file": "editor/exam-rich-editor-types.ts",
"type": "type",
"definition": "JSONContent (from @tiptap/react)",
"purpose": "Tiptap JSONContent 别名,用于编辑器与试卷结构互转",
"usedBy": [
"exams/editor/exam-rich-editor.tsx",
"exams/components/exam-rich-form.tsx"
]
}
],
"converters": [
{
"name": "editorDocToStructure",
"file": "editor/editor-to-structure.ts",
"type": "function",
"signature": "(doc: EditorJSONContent, fallbackTitle: string) => EditorDoc",
"purpose": "将 Tiptap JSONContent 转换为 EditorDoc 结构title + questions + structure。遍历 doc.nodes识别 GroupBlock/QuestionBlock/BlankNode/DottedMark/ImageNode 等自定义节点,提取题目内容与结构树。供 createExamFromRichEditorAction 持久化使用。",
"deps": [
"@paralleldrive/cuid2.createId",
"exams/editor/exam-rich-editor-types.ts"
],
"usedBy": [
"exams/actions.createExamFromRichEditorAction",
"exams/components/exam-rich-form.tsx"
]
},
{
"name": "structureToEditorDoc",
"file": "editor/structure-to-editor.ts",
"type": "function",
"signature": "(doc: EditorDoc) => EditorJSONContent",
"purpose": "将 EditorDoc 结构转换回 Tiptap JSONContent供编辑器回填已保存的试卷内容。",
"deps": [
"exams/editor/exam-rich-editor-types.ts"
],
"usedBy": [
"exams/editor/exam-rich-editor.tsx"
]
}
]
}
}
},
"homework": {
@@ -3659,6 +3955,39 @@
"usedBy": [
"homework-batch-grading-view.tsx"
]
},
{
"name": "getScansAction",
"permission": "HOMEWORK_GRADE | HOMEWORK_SUBMIT",
"signature": "(submissionId: string) => Promise<ActionState<ScanAttachment[]>>",
"purpose": "2026-06-24 新增:获取某次提交的所有答题扫描图。扫描图存储在 fileAttachments 表中 targetType=\"homework\"/targetId=submissionId。支持两类访问者教师 HOMEWORK_GRADE仅可访问自己创建的作业的提交通过 getHomeworkSubmissionForGrading 校验 creatorId===ctx.userId 或 dataScope.type===\"all\"/ 学生 HOMEWORK_SUBMIT仅可访问自己的提交通过 getHomeworkSubmissionForPermission 校验 studentId===ctx.userId。按 createdAt asc 排序page 从 1 开始递增。",
"deps": [
"requirePermission",
"shared/lib/action-utils.handleActionError",
"data-access-write.getHomeworkSubmissionForGrading",
"data-access-write.getHomeworkSubmissionForPermission",
"shared/db",
"shared/db.schema.fileAttachments",
"drizzle-orm.eq/and/asc"
],
"usedBy": [
"homework/components/homework-scan-grading-view.tsx"
]
},
{
"name": "deleteScanAction",
"permission": "HOMEWORK_SUBMIT",
"signature": "(submissionId: string, fileId: string) => Promise<ActionState<null>>",
"purpose": "2026-06-24 新增:删除答题扫描图。仅允许提交者本人删除(通过 getHomeworkSubmissionForPermission 校验 studentId===ctx.userId且仅在提交状态为 started 时允许(已提交/已批改的提交锁定不可修改)。调用 files/data-access.deleteFileAttachment 删除 DB 记录(磁盘文件由 files 模块处理)。",
"deps": [
"requirePermission",
"shared/lib/action-utils.handleActionError",
"data-access-write.getHomeworkSubmissionForPermission",
"files/data-access.deleteFileAttachment"
],
"usedBy": [
"homework/components/scan-uploader.tsx"
]
}
],
"dataAccess": [
@@ -4153,6 +4482,29 @@
"usedBy": [
"student/dashboard"
]
},
{
"name": "ScanAttachment",
"type": "interface",
"file": "actions.ts",
"definition": "{ fileId: string; url: string; filename: string; originalName: string; page: number }",
"purpose": "2026-06-24 新增:扫描图附件结构(由 getScansAction 返回。page 为页码(按 createdAt asc 排序,从 1 开始递增)。",
"usedBy": [
"homework/actions.getScansAction",
"homework/components/homework-scan-grading-view.tsx"
]
},
{
"name": "ScanImage",
"type": "interface",
"file": "components/scan-uploader.tsx",
"definition": "{ fileId: string; url: string; filename: string; originalName?: string; page: number }",
"purpose": "2026-06-24 新增:扫描图 UI 结构(由 ScanUploader 导出,供 ScanImageViewer 复用)。与 ScanAttachment 字段一致,但 originalName 可选。",
"usedBy": [
"homework/components/scan-uploader.tsx",
"homework/components/scan-image-viewer.tsx",
"homework/components/homework-scan-grading-view.tsx"
]
}
],
"components": [
@@ -4215,6 +4567,62 @@
"name": "HomeworkSubmissionResult",
"file": "homework-submission-result.tsx",
"purpose": "V3-9 新增:提交后即时反馈页。学生提交后立即看到分数汇总(总分/满分、得分率 Progress、对错分布正确/错误/部分正确/待批改)、错题预览(题目文本、学生答案、正确答案)。对标智学网/猿题库提交后反馈。"
},
{
"name": "ScanUploader",
"file": "scan-uploader.tsx",
"purpose": "2026-06-24 新增学生扫描图上传组件use client。学生在纸上作答后按页拍摄上传调用 /api/upload 上传图片targetType=\"homework\"/targetId=submissionId返回 fileId+url。上传后将 fileId 列表通过 onChange 暴露给父组件。支持增删/排序onDeleteScan 回调用于删除 fileAttachments 记录(调用 deleteScanAction。已提交disabled时禁止操作。导出 ScanImage 类型供 ScanImageViewer 复用。",
"deps": [
"shared/components/ui/button",
"shared/lib/utils.cn",
"next-intl.useTranslations",
"sonner.toast",
"lucide-react (Upload/X/Loader2/ImageIcon)"
],
"usedBy": [
"homework/components/homework-scan-grading-view.tsx"
]
},
{
"name": "ScanImageViewer",
"file": "scan-image-viewer.tsx",
"purpose": "2026-06-24 新增扫描图查看器use client。用于阅卷式批改时查看学生答题图片。支持翻页上一页/下一页+页码、缩放ZoomIn/ZoomOut0.25 步进1-3 范围、旋转RotateCw90° 步进、全屏Maximize2。切换页面时重置缩放与旋转。",
"deps": [
"shared/components/ui/button",
"shared/lib/utils.cn",
"lucide-react (ChevronLeft/ChevronRight/ZoomIn/ZoomOut/Maximize2/RotateCw)",
"homework/components/scan-uploader.ScanImage"
],
"usedBy": [
"homework/components/homework-scan-grading-view.tsx"
]
},
{
"name": "HomeworkScanGradingView",
"file": "homework-scan-grading-view.tsx",
"purpose": "2026-06-24 新增教师阅卷式批改视图use client。使用 ResizablePanel 左侧题目+批改表单、右侧 ScanImageViewer 扫描图查看器。集成 getScansAction 拉取扫描图、gradeHomeworkSubmissionAction 批改。支持上一份/下一份提交快速切换prevSubmissionId/nextSubmissionId。显示学生姓名、作业标题、提交时间、状态、总分。左侧每题显示题干、学生答案、得分输入、反馈输入。",
"deps": [
"shared/components/ui/button",
"shared/components/ui/card",
"shared/components/ui/input",
"shared/components/ui/label",
"shared/components/ui/textarea",
"shared/components/ui/badge",
"shared/components/ui/scroll-area",
"shared/components/ui/resizable-panel.ResizablePanel",
"shared/lib/utils.formatDate",
"shared/lib/utils.cn",
"next-intl.useTranslations",
"sonner.toast",
"homework/actions.gradeHomeworkSubmissionAction",
"homework/actions.getScansAction",
"homework/components/question-renderer.QuestionRenderer",
"homework/components/scan-image-viewer.ScanImageViewer",
"lucide-react (Save/ChevronLeft/ChevronRight/User/Clock)"
],
"usedBy": [
"app/(dashboard)/teacher/homework/submissions/[submissionId]/scan-grading/page.tsx"
]
}
],
"hooks": [
@@ -9055,10 +9463,15 @@
"name": "FileTargetType",
"type": "type",
"file": "types.ts",
"definition": "\"exam\" | \"textbook\" | \"question\" | \"announcement\"",
"definition": "\"exam\" | \"textbook\" | \"question\" | \"announcement\" | \"homework\"",
"purpose": "文件附件关联资源类型枚举。2026-06-24 新增 \"homework\" 枚举值用于学生答题扫描图附件targetId = homeworkSubmissions.id",
"usedBy": [
"types.FileAttachment.targetType",
"file-upload.tsx"
"file-upload.tsx",
"app/api/upload/route.ts (VALID_TARGET_TYPES)",
"homework/actions.getScansAction",
"homework/actions.deleteScanAction",
"homework/components/scan-uploader.tsx"
]
},
{
@@ -16352,7 +16765,8 @@
"exams",
"classes",
"school",
"users"
"users",
"files"
],
"uses": {
"shared": [
@@ -16384,6 +16798,9 @@
"users": [
"data-access.getUserWithRole",
"data-access.getUserNamesByIds"
],
"files": [
"data-access.deleteFileAttachment"
]
}
},
@@ -18514,6 +18931,19 @@
],
"permission": "exam:create"
},
"/teacher/exams/new": {
"component": "NewExamPage + ExamRichForm",
"type": "server",
"module": "exams",
"actions": [
"autoMarkExamAction",
"createExamFromRichEditorAction",
"getSubjectsAction",
"getGradesAction"
],
"permission": "exam:create",
"description": "2026-06-24 新增:富文本编辑器试卷创建页面。基于 Tiptap 的所见即所得试卷编辑器,支持 AI 自动标记(粘贴试卷文本 → AI 解析为 Tiptap doc、手动编辑题目块/分组/填空/加点字/图片、左编辑右预览分栏布局ResizablePanel。保存时将 Tiptap JSONContent 转换为 questions + structure 后持久化为试卷草稿。"
},
"/teacher/questions": {
"component": "QuestionDataTable",
"type": "server",
@@ -18645,6 +19075,20 @@
],
"permission": "homework:grade"
},
"/teacher/homework/submissions/[submissionId]/scan-grading": {
"component": "HomeworkScanGradingPage + HomeworkScanGradingView",
"type": "server",
"module": "homework",
"actions": [
"gradeHomeworkSubmissionAction",
"getScansAction"
],
"dataAccess": [
"homework/data-access.getHomeworkSubmissionDetails"
],
"permission": "homework:grade",
"description": "2026-06-24 新增:教师阅卷式批改页面。左侧显示题目+批改表单右侧显示学生答题扫描图ScanImageViewer支持翻页/缩放/旋转/全屏)。使用 ResizablePanel 可拖拽分栏。支持上一份/下一份提交快速切换。适用于学生纸上作答后上传扫描图的场景。"
},
"/teacher/exams": {
"component": "重定向",
"type": "server",