Files
Edu/apps/teacher-portal/src/mocks/handlers.ts
SpecialX 1eacd1ed87 feat(teacher-portal): 完整实现 teacher-portal 微前端
包含 settings/students/api、graphql、mocks、ui-tokens 设计令牌等
2026-07-10 19:10:20 +08:00

175 lines
4.8 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.
/**
* MSW Handlers - 拦截所有 HTTP / GraphQL 请求返回 mock 数据
*
* 拦截路径contract.md §2.3 + §4.2
* - POST /api/auth/login → 返回固定 JWT + UserInfo
* - POST /api/v1/teacher/graphql → 按 operationName 返回对应 GraphQL 响应
*
* 切换NEXT_PUBLIC_API_MOCKING=enabled 时启用(见 src/mocks/index.ts
*
* 维护者ai13teacher-portal
*/
import { http, HttpResponse } from "msw";
import { mockUser, mockUserInfo, mockToken } from "./fixtures/user";
import { mockViewports } from "./fixtures/viewports";
import { mockClasses, findClass } from "./fixtures/classes";
import { generateMockStudents } from "./fixtures/students";
import { generateMockExams } from "./fixtures/exams";
import { generateMockHomework } from "./fixtures/homework";
import { generateMockGrades } from "./fixtures/grades";
/** ActionState 信封coord-final-decisions F9 + 总裁 §3.4 方案 B */
function envelope<T>(data: T) {
return { success: true, errors: null, data };
}
/** GraphQL 响应体urql 期望 { data: ... } */
function graphqlData<T>(data: T) {
return HttpResponse.json({ data });
}
/**
* 根据 operationName 路由 GraphQL mock 响应
*
* 对应 graphql.ts 的 5 个 P2 Query + 4 个 P3 Query + 1 个 Mutation。
*/
function handleGraphQL(
operationName: string,
variables: Record<string, unknown>,
) {
switch (operationName) {
case "Dashboard":
return graphqlData({
dashboard: {
user: mockUser,
classes: mockClasses,
viewports: mockViewports,
stats: {
totalExams: 8,
pendingGrading: 12,
todayHomework: 3,
},
},
});
case "Viewports":
return graphqlData({ viewports: mockViewports });
case "Me":
return graphqlData({ me: mockUser });
case "Classes":
return graphqlData({ classes: mockClasses });
case "Class": {
const id = variables.id as string;
return graphqlData({ class: findClass(id) });
}
case "ClassStudents": {
const classId = variables.classId as string;
return graphqlData({ classStudents: generateMockStudents(classId) });
}
case "ClassExams": {
const classId = variables.classId as string;
return graphqlData({ classExams: generateMockExams(classId) });
}
case "ClassHomework": {
const classId = variables.classId as string;
return graphqlData({ classHomework: generateMockHomework(classId) });
}
case "ExamGrades": {
const examId = variables.examId as string;
return graphqlData({ examGrades: generateMockGrades(examId) });
}
case "UpdateUser": {
const input = variables.input as { name?: string; email?: string };
return graphqlData({
updateUser: { ...mockUser, ...input },
});
}
default:
return HttpResponse.json(
{
errors: [
{
message: `Mock 未覆盖的 GraphQL operation: ${operationName}`,
extensions: { code: "BFF_TEACHER_NOT_IMPLEMENTED" },
},
],
},
{ status: 200 },
);
}
}
export const handlers = [
// POST /api/auth/login → 登录
http.post("*/api/auth/login", async ({ request }) => {
const body = (await request.json()) as {
email?: string;
password?: string;
};
if (!body.email || !body.password) {
return HttpResponse.json(
{
success: false,
error: { code: "GW_UNAUTHORIZED", message: "邮箱和密码必填" },
},
{ status: 400 },
);
}
return HttpResponse.json(
envelope({
user: mockUserInfo,
tokens: { accessToken: mockToken, refreshToken: "mock-refresh" },
}),
);
}),
// POST /api/v1/teacher/graphql → GraphQL endpoint
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 ?? {};
return handleGraphQL(operationName, variables);
}),
// GET /api/health → liveness
http.get("*/api/health", () =>
HttpResponse.json({ status: "ok", service: "teacher-portal" }),
),
// GET /api/ready → readiness
http.get("*/api/ready", () =>
HttpResponse.json({
status: "ok",
service: "teacher-portal",
checks: { msw: true },
}),
),
];
/**
* 从 query 字符串提取 operationName
* 兜底:当客户端未传 operationName 时从 query 文本提取
*/
function extractOperationName(query?: string): string {
if (!query) return "Unknown";
const match = query.match(/(?:query|mutation)\s+(\w+)/);
return match?.[1] ?? "Unknown";
}