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:
SpecialX
2026-07-13 14:27:04 +08:00
parent f13ca612e6
commit d49d211425
117 changed files with 30867 additions and 108 deletions

View File

@@ -0,0 +1,108 @@
/**
* MSW P4 Handlers - 知识图谱 + 学情分析 mock 拦截
*
* 独立于 handlers.ts 维护,合并时将 p4Handlers 追加到主 handlers 数组,
* 并在主 handleGraphQL 的 default 之前插入以下 case。
*
* 覆盖 operationName对应 graphql-p4.ts
* - KnowledgeGraph → 知识图谱节点 + 边
* - ClassAnalytics → 班级学情分析
* - StudentAnalytics → 单生学情分析
*
* 维护者ai13teacher-portal
*/
import { http, HttpResponse } from "msw";
import { filterKnowledgeGraph } from "./fixtures/knowledge-graph";
import {
mockClassAnalytics,
findStudentAnalytics,
} from "./fixtures/analytics";
/** GraphQL 响应体urql 期望 { data: ... } */
function graphqlData<T>(data: T) {
return HttpResponse.json({ data });
}
/**
* 根据 operationName 路由 P4 GraphQL mock 响应
*
* 返回 null 表示当前 operationName 不属于 P4交回主 handler 处理。
*/
export function handleP4GraphQL(
operationName: string,
variables: Record<string, unknown>,
) {
switch (operationName) {
case "KnowledgeGraph": {
const subject = (variables.subject as string | undefined) ?? null;
return graphqlData({
knowledgeGraph: filterKnowledgeGraph(subject),
});
}
case "ClassAnalytics": {
// classId 传入但 mock 数据固定,返回时回填以保证字段一致
const classId =
(variables.classId as string | undefined) ?? mockClassAnalytics.classId;
return graphqlData({
classAnalytics: { ...mockClassAnalytics, classId },
});
}
case "StudentAnalytics": {
const studentId =
(variables.studentId as string | undefined) ?? "stu-001";
return graphqlData({
studentAnalytics: findStudentAnalytics(studentId),
});
}
default:
return null;
}
}
/**
* P4 独立 handlerPOST /api/v1/teacher/graphql
*
* 合并策略:将本数组追加到 handlers.ts 的 handlers 数组之前MSW 按顺序匹配,
* 先于主 handler 的 default case 命中 P4 operation
*/
export const p4Handlers = [
http.post("*/api/v1/teacher/graphql", async ({ request }) => {
const body = (await request.json()) as {
operationName?: string;
query?: string;
variables?: Record<string, unknown>;
};
const operationName =
body.operationName ?? extractOperationName(body.query);
const variables = body.variables ?? {};
const p4Result = handleP4GraphQL(operationName, variables);
if (p4Result) {
return p4Result;
}
// 非 P4 operation交回后续 handler返回 passthrough 让 MSW 继续匹配)
return HttpResponse.json(
{
errors: [
{
message: `P4 handler 未覆盖的 GraphQL operation: ${operationName}`,
extensions: { code: "BFF_TEACHER_NOT_IMPLEMENTED" },
},
],
},
{ status: 200 },
);
}),
];
/** 从 query 字符串提取 operationName兜底 */
function extractOperationName(query?: string): string {
if (!query) return "Unknown";
const match = query.match(/(?:query|mutation)\s+(\w+)/);
return match?.[1] ?? "Unknown";
}