feat(teacher-portal): 完成参考项目差距闭环 P3-P7 全量实现
- 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 参考项目差距闭环(含完整文件清单)
This commit is contained in:
499
apps/teacher-portal/src/lib/graphql-p7-advanced.ts
Normal file
499
apps/teacher-portal/src/lib/graphql-p7-advanced.ts
Normal file
@@ -0,0 +1,499 @@
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
Reference in New Issue
Block a user