- P3 考试/作业/成绩 mutation + 详情页 + 批改界面 + 乐观更新 + 多 Tab 同步 - P4 知识图谱 SVG 可视化 + 学情分析仪表盘 + parent-portal Remote - P5 WebSocket 通知中心 + AI 出题(SSE) + AI 教案 + AI 学情报告 - P6 可观测性硬化:Sentry + WebVitals + OTel + A11y + 性能配置 + Cookie 迁移 - P7 参考项目差距闭环:新增 35 个页面覆盖 13 个缺失模块 - attendance(考勤 4 页)/questions(题库)/textbooks(教材 2 页) - classes/[id] 详情 + classes/schedule 课表 - course-plans(2 页)/diagnostic(2 页)/error-book/practice - exams/[id]/build 组卷 + exams/[id]/analytics 考后分析 - exams/[id]/edit-rich 富文本编辑 + exams/[id]/proctoring 监考 - grades/entry 批量录入 + grades/stats 统计 + grades/analytics 分析 + grades/report-card 报告卡 - homework/submissions 列表 + assignments/[id]/submissions 批量批改 - homework/submissions/[submissionId] 单份批改 + scan-grading 扫描批改 - lesson-plans 编辑器 + library + calendar + heatmap 5 页 - elective 选修课 3 页 /leave 请假 /schedule-changes 调课 - P7 基础设施:61 GraphQL operations + 5 handlers + 13 fixtures + 11 viewports - 集成 browser.ts/server.ts 注册所有 p7 handlers(fallthrough 顺序) - viewports.ts 扩展 11 个新导航项 - 验证:tsc --noEmit 零错误 + eslint 零错误 - 文档:workline.md 新增 §5 P7 参考项目差距闭环(含完整文件清单)
500 lines
11 KiB
TypeScript
500 lines
11 KiB
TypeScript
/**
|
||
* GraphQL operations - P7 扩展(选修课 + 富文本编辑 + 监考 + 扫描批改 + 课标热力图)
|
||
*
|
||
* 维护者:ai13(teacher-portal)
|
||
* 关联:graphql.ts P2/P3 扩展、workline.md §3 P7 交付物
|
||
*
|
||
* schema 尚未在 ARB-001 P2 第一版中定义,开发期通过 MSW 拦截返回 mock 响应;
|
||
* 上游就绪后由 ai03 在 teacher-bff.graphql 扩展,届时合并本文件到 graphql.ts。
|
||
*
|
||
* 查询范围(P7-advanced):
|
||
* - ElectiveCourses / ElectiveCourseDetail:选修课列表 + 详情
|
||
* - CreateElectiveCourse / UpdateElectiveCourse / PublishElectiveCourse / CancelElectiveCourse
|
||
* - ExamRichEditor / SaveExamRichContent:富文本试卷内容
|
||
* - ProctoringStatus / ProctoringEvent:监考状态 + 事件
|
||
* - SubmissionScanGrading / SaveScanGrading:扫描批改
|
||
* - LessonPlanHeatmap:课标覆盖热力图
|
||
*/
|
||
|
||
import { gql } from "urql";
|
||
|
||
// ============ Types(选修课) ============
|
||
|
||
/** 选修课状态 */
|
||
export type ElectiveCourseStatus =
|
||
| "draft"
|
||
| "open"
|
||
| "closed"
|
||
| "cancelled";
|
||
|
||
/** 选课学生项 */
|
||
export interface ElectiveStudent {
|
||
studentId: string;
|
||
studentName: string;
|
||
studentNo: string;
|
||
enrolledAt: string;
|
||
status: "enrolled" | "cancelled";
|
||
}
|
||
|
||
/** 选修课列表项 */
|
||
export interface ElectiveCourseItem {
|
||
id: string;
|
||
title: string;
|
||
subject: string;
|
||
grade: string;
|
||
capacity: number;
|
||
enrolledCount: number;
|
||
teacherId: string;
|
||
teacherName: string;
|
||
semester: string;
|
||
status: ElectiveCourseStatus;
|
||
description: string | null;
|
||
}
|
||
|
||
/** 选修课详情(含学生列表) */
|
||
export interface ElectiveCourseDetail extends ElectiveCourseItem {
|
||
students: ElectiveStudent[];
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
}
|
||
|
||
/** 创建选修课输入 */
|
||
export interface CreateElectiveCourseInput {
|
||
title: string;
|
||
subject: string;
|
||
grade: string;
|
||
capacity: number;
|
||
semester: string;
|
||
description?: string | null;
|
||
}
|
||
|
||
/** 更新选修课输入 */
|
||
export interface UpdateElectiveCourseInput {
|
||
id: string;
|
||
title?: string;
|
||
subject?: string;
|
||
grade?: string;
|
||
capacity?: number;
|
||
semester?: string;
|
||
description?: string | null;
|
||
}
|
||
|
||
/** 创建/更新结果 */
|
||
export interface ElectiveCourseMutationResult {
|
||
id: string;
|
||
status: ElectiveCourseStatus;
|
||
updatedAt: string;
|
||
}
|
||
|
||
// ============ Types(富文本编辑) ============
|
||
|
||
/** 富文本试卷内容(Tiptap examNodes JSON 结构,mock 用 JSON) */
|
||
export interface ExamRichContent {
|
||
examId: string;
|
||
title: string;
|
||
totalScore: number;
|
||
questionCount: number;
|
||
content: unknown; // Tiptap JSON,结构由编辑器决定
|
||
updatedAt: string;
|
||
}
|
||
|
||
/** 保存富文本输入 */
|
||
export interface SaveExamRichContentInput {
|
||
examId: string;
|
||
content: unknown;
|
||
}
|
||
|
||
/** 保存富文本结果 */
|
||
export interface SaveExamRichContentResult {
|
||
examId: string;
|
||
updatedAt: string;
|
||
}
|
||
|
||
// ============ Types(监考) ============
|
||
|
||
/** 监考学生状态 */
|
||
export type ProctoringStudentStatus =
|
||
| "online"
|
||
| "offline"
|
||
| "submitted"
|
||
| "flagged";
|
||
|
||
/** 监考摘要 */
|
||
export interface ProctoringSummary {
|
||
examId: string;
|
||
expectedCount: number;
|
||
onlineCount: number;
|
||
submittedCount: number;
|
||
flaggedCount: number;
|
||
isProctoring: boolean;
|
||
startedAt: string | null;
|
||
}
|
||
|
||
/** 监考学生状态项 */
|
||
export interface ProctoringStudent {
|
||
studentId: string;
|
||
studentName: string;
|
||
studentNo: string;
|
||
status: ProctoringStudentStatus;
|
||
lastEventAt: string | null;
|
||
ipAddress: string | null;
|
||
}
|
||
|
||
/** 监考事件类型 */
|
||
export type ProctoringEventType = "WARNING" | "FLAG" | "NOTE";
|
||
|
||
/** 监考事件项 */
|
||
export interface ProctoringEvent {
|
||
eventId: string;
|
||
examId: string;
|
||
studentId: string;
|
||
studentName: string;
|
||
studentNo: string;
|
||
eventType: ProctoringEventType;
|
||
note: string | null;
|
||
createdAt: string;
|
||
}
|
||
|
||
/** 监考状态完整结构 */
|
||
export interface ProctoringStatus {
|
||
summary: ProctoringSummary;
|
||
students: ProctoringStudent[];
|
||
recentEvents: ProctoringEvent[];
|
||
}
|
||
|
||
/** 提交监考事件输入 */
|
||
export interface ProctoringEventInput {
|
||
examId: string;
|
||
studentId: string;
|
||
eventType: ProctoringEventType;
|
||
note?: string | null;
|
||
}
|
||
|
||
/** 提交监考事件结果 */
|
||
export interface ProctoringEventResult {
|
||
eventId: string;
|
||
createdAt: string;
|
||
}
|
||
|
||
// ============ Types(扫描批改) ============
|
||
|
||
/** 扫描图片项 */
|
||
export interface ScanImage {
|
||
imageId: string;
|
||
url: string;
|
||
page: number;
|
||
}
|
||
|
||
/** 扫描批改单题项 */
|
||
export interface ScanGradingItem {
|
||
questionId: string;
|
||
questionTitle: string;
|
||
questionOrder: number;
|
||
maxScore: number;
|
||
recognizedAnswer: string;
|
||
aiSuggestedScore: number | null;
|
||
aiSuggestion: string | null;
|
||
score: number | null;
|
||
feedback: string | null;
|
||
}
|
||
|
||
/** 扫描批改视图 */
|
||
export interface SubmissionScanGrading {
|
||
submissionId: string;
|
||
homeworkTitle: string;
|
||
studentName: string;
|
||
studentNo: string;
|
||
submittedAt: string;
|
||
images: ScanImage[];
|
||
items: ScanGradingItem[];
|
||
prevSubmissionId: string | null;
|
||
nextSubmissionId: string | null;
|
||
}
|
||
|
||
/** 保存扫描批改单项 */
|
||
export interface SaveScanGradingItem {
|
||
questionId: string;
|
||
score: number;
|
||
feedback?: string | null;
|
||
}
|
||
|
||
/** 保存扫描批改输入 */
|
||
export interface SaveScanGradingInput {
|
||
submissionId: string;
|
||
items: SaveScanGradingItem[];
|
||
}
|
||
|
||
/** 保存扫描批改结果 */
|
||
export interface SaveScanGradingResult {
|
||
submissionId: string;
|
||
totalScore: number;
|
||
savedAt: string;
|
||
}
|
||
|
||
// ============ Types(课标热力图) ============
|
||
|
||
/** 热力图知识点项 */
|
||
export interface HeatmapKnowledgePoint {
|
||
kpId: string;
|
||
name: string;
|
||
chapterName: string;
|
||
}
|
||
|
||
/** 热力图课案项 */
|
||
export interface HeatmapLessonPlan {
|
||
planId: string;
|
||
title: string;
|
||
}
|
||
|
||
/** 热力图覆盖矩阵单元格 */
|
||
export interface HeatmapCell {
|
||
kpId: string;
|
||
planId: string;
|
||
coverage: number; // 0-3 覆盖课案数
|
||
}
|
||
|
||
/** 课标覆盖热力图 */
|
||
export interface LessonPlanHeatmap {
|
||
textbookId: string;
|
||
textbookTitle: string;
|
||
knowledgePoints: HeatmapKnowledgePoint[];
|
||
lessonPlans: HeatmapLessonPlan[];
|
||
cells: HeatmapCell[];
|
||
}
|
||
|
||
// ============ Queries / Mutations(选修课) ============
|
||
|
||
/** 选修课列表查询(按状态筛选,教师范围) */
|
||
export const ElectiveCoursesQuery = gql`
|
||
query ElectiveCourses($status: String, $subject: String) {
|
||
electiveCourses(status: $status, subject: $subject) {
|
||
id
|
||
title
|
||
subject
|
||
grade
|
||
capacity
|
||
enrolledCount
|
||
teacherId
|
||
teacherName
|
||
semester
|
||
status
|
||
description
|
||
}
|
||
}
|
||
`;
|
||
|
||
/** 选修课详情查询(含学生列表) */
|
||
export const ElectiveCourseDetailQuery = gql`
|
||
query ElectiveCourseDetail($id: ID!) {
|
||
electiveCourseDetail(id: $id) {
|
||
id
|
||
title
|
||
subject
|
||
grade
|
||
capacity
|
||
enrolledCount
|
||
teacherId
|
||
teacherName
|
||
semester
|
||
status
|
||
description
|
||
students {
|
||
studentId
|
||
studentName
|
||
studentNo
|
||
enrolledAt
|
||
status
|
||
}
|
||
createdAt
|
||
updatedAt
|
||
}
|
||
}
|
||
`;
|
||
|
||
/** 创建选修课 Mutation */
|
||
export const CreateElectiveCourseMutation = gql`
|
||
mutation CreateElectiveCourse($input: CreateElectiveCourseInput!) {
|
||
createElectiveCourse(input: $input) {
|
||
id
|
||
status
|
||
updatedAt
|
||
}
|
||
}
|
||
`;
|
||
|
||
/** 更新选修课 Mutation */
|
||
export const UpdateElectiveCourseMutation = gql`
|
||
mutation UpdateElectiveCourse($input: UpdateElectiveCourseInput!) {
|
||
updateElectiveCourse(input: $input) {
|
||
id
|
||
status
|
||
updatedAt
|
||
}
|
||
}
|
||
`;
|
||
|
||
/** 发布选修课 Mutation */
|
||
export const PublishElectiveCourseMutation = gql`
|
||
mutation PublishElectiveCourse($id: ID!) {
|
||
publishElectiveCourse(id: $id) {
|
||
id
|
||
status
|
||
updatedAt
|
||
}
|
||
}
|
||
`;
|
||
|
||
/** 取消选修课 Mutation */
|
||
export const CancelElectiveCourseMutation = gql`
|
||
mutation CancelElectiveCourse($id: ID!) {
|
||
cancelElectiveCourse(id: $id) {
|
||
id
|
||
status
|
||
updatedAt
|
||
}
|
||
}
|
||
`;
|
||
|
||
// ============ Queries / Mutations(富文本编辑) ============
|
||
|
||
/** 富文本试卷内容查询 */
|
||
export const ExamRichEditorQuery = gql`
|
||
query ExamRichEditor($examId: ID!) {
|
||
examRichEditor(examId: $examId) {
|
||
examId
|
||
title
|
||
totalScore
|
||
questionCount
|
||
content
|
||
updatedAt
|
||
}
|
||
}
|
||
`;
|
||
|
||
/** 保存富文本内容 Mutation */
|
||
export const SaveExamRichContentMutation = gql`
|
||
mutation SaveExamRichContent($input: SaveExamRichContentInput!) {
|
||
saveExamRichContent(input: $input) {
|
||
examId
|
||
updatedAt
|
||
}
|
||
}
|
||
`;
|
||
|
||
// ============ Queries / Mutations(监考) ============
|
||
|
||
/** 监考状态查询 */
|
||
export const ProctoringStatusQuery = gql`
|
||
query ProctoringStatus($examId: ID!) {
|
||
proctoringStatus(examId: $examId) {
|
||
summary {
|
||
examId
|
||
expectedCount
|
||
onlineCount
|
||
submittedCount
|
||
flaggedCount
|
||
isProctoring
|
||
startedAt
|
||
}
|
||
students {
|
||
studentId
|
||
studentName
|
||
studentNo
|
||
status
|
||
lastEventAt
|
||
ipAddress
|
||
}
|
||
recentEvents {
|
||
eventId
|
||
examId
|
||
studentId
|
||
studentName
|
||
studentNo
|
||
eventType
|
||
note
|
||
createdAt
|
||
}
|
||
}
|
||
}
|
||
`;
|
||
|
||
/** 提交监考事件 Mutation */
|
||
export const ProctoringEventMutation = gql`
|
||
mutation ProctoringEvent($input: ProctoringEventInput!) {
|
||
proctoringEvent(input: $input) {
|
||
eventId
|
||
createdAt
|
||
}
|
||
}
|
||
`;
|
||
|
||
// ============ Queries / Mutations(扫描批改) ============
|
||
|
||
/** 扫描批改视图查询 */
|
||
export const SubmissionScanGradingQuery = gql`
|
||
query SubmissionScanGrading($submissionId: ID!) {
|
||
submissionScanGrading(submissionId: $submissionId) {
|
||
submissionId
|
||
homeworkTitle
|
||
studentName
|
||
studentNo
|
||
submittedAt
|
||
images {
|
||
imageId
|
||
url
|
||
page
|
||
}
|
||
items {
|
||
questionId
|
||
questionTitle
|
||
questionOrder
|
||
maxScore
|
||
recognizedAnswer
|
||
aiSuggestedScore
|
||
aiSuggestion
|
||
score
|
||
feedback
|
||
}
|
||
prevSubmissionId
|
||
nextSubmissionId
|
||
}
|
||
}
|
||
`;
|
||
|
||
/** 保存扫描批改 Mutation */
|
||
export const SaveScanGradingMutation = gql`
|
||
mutation SaveScanGrading($input: SaveScanGradingInput!) {
|
||
saveScanGrading(input: $input) {
|
||
submissionId
|
||
totalScore
|
||
savedAt
|
||
}
|
||
}
|
||
`;
|
||
|
||
// ============ Queries(课标热力图) ============
|
||
|
||
/** 课标覆盖热力图查询(按教材 ID) */
|
||
export const LessonPlanHeatmapQuery = gql`
|
||
query LessonPlanHeatmap($textbookId: ID!) {
|
||
lessonPlanHeatmap(textbookId: $textbookId) {
|
||
textbookId
|
||
textbookTitle
|
||
knowledgePoints {
|
||
kpId
|
||
name
|
||
chapterName
|
||
}
|
||
lessonPlans {
|
||
planId
|
||
title
|
||
}
|
||
cells {
|
||
kpId
|
||
planId
|
||
coverage
|
||
}
|
||
}
|
||
}
|
||
`;
|