feat(student-portal): 完整实现 student-portal 微前端
包含 src 全部实现、Dockerfile、配置文件、contracts 包等
This commit is contained in:
86
apps/student-portal/src/mocks/browser.ts
Normal file
86
apps/student-portal/src/mocks/browser.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* MSW 浏览器端 setup(ai14)
|
||||
*
|
||||
* 开发环境下在浏览器中启动 MSW Service Worker 拦截 GraphQL 请求。
|
||||
* 通过 NEXT_PUBLIC_API_MOCKING=enabled 控制是否启用。
|
||||
*
|
||||
* 重要:setupWorker 仅在浏览器环境调用,SSR 时不执行。
|
||||
* worker 实例延迟初始化,避免 Next.js prerender 时报错。
|
||||
*/
|
||||
|
||||
import { handlers } from "./handlers";
|
||||
|
||||
/** MSW worker 类型(延迟初始化)*/
|
||||
type MSWWorker = Awaited<
|
||||
ReturnType<(typeof import("msw/browser"))["setupWorker"]>
|
||||
>;
|
||||
|
||||
/** MSW worker 单例(延迟初始化,仅在浏览器环境)*/
|
||||
let workerInstance: MSWWorker | null = null;
|
||||
|
||||
/** MSW worker 是否已启动 */
|
||||
let isStarted = false;
|
||||
|
||||
/**
|
||||
* 获取或创建 MSW worker 实例
|
||||
*
|
||||
* 仅在浏览器环境调用,SSR 时返回 null。
|
||||
* 使用动态 import 避免 Next.js prerender 时加载 msw/browser。
|
||||
*/
|
||||
async function getOrCreateWorker(): Promise<MSWWorker | null> {
|
||||
if (typeof window === "undefined") return null;
|
||||
if (workerInstance) return workerInstance;
|
||||
|
||||
const { setupWorker } = await import("msw/browser");
|
||||
workerInstance = setupWorker(...handlers);
|
||||
return workerInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化浏览器 MSW mock
|
||||
*
|
||||
* 仅在 NEXT_PUBLIC_API_MOCKING=enabled 时启动。
|
||||
* 启动后拦截 /api/v1/student/graphql 的请求并返回 mock 响应。
|
||||
*
|
||||
* @returns Promise<void>,启动完成后 resolve
|
||||
*/
|
||||
export async function initMocks(): Promise<void> {
|
||||
const isMockingEnabled = process.env.NEXT_PUBLIC_API_MOCKING === "enabled";
|
||||
|
||||
if (!isMockingEnabled || isStarted) {
|
||||
return;
|
||||
}
|
||||
|
||||
const worker = await getOrCreateWorker();
|
||||
if (!worker) return;
|
||||
|
||||
await worker.start({
|
||||
onUnhandledRequest: "bypass",
|
||||
quiet: false,
|
||||
serviceWorker: {
|
||||
url: "/mockServiceWorker.js",
|
||||
},
|
||||
});
|
||||
|
||||
isStarted = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 MSW worker 实例(测试用)
|
||||
* @returns worker 实例或 null(非浏览器环境)
|
||||
*/
|
||||
export function getWorker(): MSWWorker | null {
|
||||
return workerInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止 MSW worker
|
||||
* @returns Promise<void>
|
||||
*/
|
||||
export async function stopMocks(): Promise<void> {
|
||||
if (!isStarted || !workerInstance) {
|
||||
return;
|
||||
}
|
||||
await workerInstance.stop();
|
||||
isStarted = false;
|
||||
}
|
||||
88
apps/student-portal/src/mocks/fixtures/chapters.json
Normal file
88
apps/student-portal/src/mocks/fixtures/chapters.json
Normal file
@@ -0,0 +1,88 @@
|
||||
[
|
||||
{
|
||||
"id": "ch-001",
|
||||
"textbookId": "tb-001",
|
||||
"name": "第一章 函数与导数",
|
||||
"level": 1,
|
||||
"parentId": null,
|
||||
"order": 1,
|
||||
"children": [
|
||||
{
|
||||
"id": "ch-001-01",
|
||||
"textbookId": "tb-001",
|
||||
"name": "1.1 函数的概念与性质",
|
||||
"level": 2,
|
||||
"parentId": "ch-001",
|
||||
"order": 1,
|
||||
"children": [
|
||||
{
|
||||
"id": "ch-001-01-01",
|
||||
"textbookId": "tb-001",
|
||||
"name": "1.1.1 函数的定义",
|
||||
"level": 3,
|
||||
"parentId": "ch-001-01",
|
||||
"order": 1,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "ch-001-01-02",
|
||||
"textbookId": "tb-001",
|
||||
"name": "1.1.2 函数的单调性",
|
||||
"level": 3,
|
||||
"parentId": "ch-001-01",
|
||||
"order": 2,
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "ch-001-02",
|
||||
"textbookId": "tb-001",
|
||||
"name": "1.2 导数的概念",
|
||||
"level": 2,
|
||||
"parentId": "ch-001",
|
||||
"order": 2,
|
||||
"children": [
|
||||
{
|
||||
"id": "ch-001-02-01",
|
||||
"textbookId": "tb-001",
|
||||
"name": "1.2.1 导数的定义",
|
||||
"level": 3,
|
||||
"parentId": "ch-001-02",
|
||||
"order": 1,
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "ch-002",
|
||||
"textbookId": "tb-001",
|
||||
"name": "第二章 三角函数",
|
||||
"level": 1,
|
||||
"parentId": null,
|
||||
"order": 2,
|
||||
"children": [
|
||||
{
|
||||
"id": "ch-002-01",
|
||||
"textbookId": "tb-001",
|
||||
"name": "2.1 任意角的三角函数",
|
||||
"level": 2,
|
||||
"parentId": "ch-002",
|
||||
"order": 1,
|
||||
"children": [
|
||||
{
|
||||
"id": "ch-002-01-01",
|
||||
"textbookId": "tb-001",
|
||||
"name": "2.1.1 三角函数定义",
|
||||
"level": 3,
|
||||
"parentId": "ch-002-01",
|
||||
"order": 1,
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
20
apps/student-portal/src/mocks/fixtures/currentUser.json
Normal file
20
apps/student-portal/src/mocks/fixtures/currentUser.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"id": "student-001",
|
||||
"name": "张小明",
|
||||
"studentNo": "2024001",
|
||||
"grade": "高三(2)班",
|
||||
"avatar": null,
|
||||
"roles": ["student"],
|
||||
"permissions": [
|
||||
"STUDENT_DASHBOARD_VIEW",
|
||||
"HOMEWORK_READ_OWN",
|
||||
"HOMEWORK_SUBMIT",
|
||||
"EXAMS_READ_OWN",
|
||||
"EXAMS_TAKE",
|
||||
"EXAMS_RESULT_VIEW",
|
||||
"GRADES_READ_OWN",
|
||||
"ATTENDANCE_READ_OWN",
|
||||
"LEARNING_PATH_VIEW"
|
||||
],
|
||||
"dataScope": "L0"
|
||||
}
|
||||
68
apps/student-portal/src/mocks/fixtures/examDetail.json
Normal file
68
apps/student-portal/src/mocks/fixtures/examDetail.json
Normal file
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"id": "exam-001",
|
||||
"name": "高三数学期中考试",
|
||||
"subject": { "id": "subject-math", "name": "数学" },
|
||||
"status": "not_started",
|
||||
"startsAt": "2026-07-15T09:00:00.000Z",
|
||||
"expiresAt": "2026-07-15T11:00:00.000Z",
|
||||
"durationSeconds": 7200,
|
||||
"totalScore": 100,
|
||||
"questionCount": 5,
|
||||
"questions": [
|
||||
{
|
||||
"id": "q-001",
|
||||
"type": "single-choice",
|
||||
"content": "已知函数 f(x) = x² + 2x,求 f(3) 的值。",
|
||||
"options": [
|
||||
{ "id": "opt-a", "text": "9" },
|
||||
{ "id": "opt-b", "text": "15" },
|
||||
{ "id": "opt-c", "text": "12" },
|
||||
{ "id": "opt-d", "text": "18" }
|
||||
],
|
||||
"score": 0,
|
||||
"maxScore": 20
|
||||
},
|
||||
{
|
||||
"id": "q-002",
|
||||
"type": "single-choice",
|
||||
"content": "若 sin θ = 1/2,且 θ 为锐角,则 cos θ 的值为?",
|
||||
"options": [
|
||||
{ "id": "opt-a", "text": "1/2" },
|
||||
{ "id": "opt-b", "text": "√3/2" },
|
||||
{ "id": "opt-c", "text": "√2/2" },
|
||||
{ "id": "opt-d", "text": "1" }
|
||||
],
|
||||
"score": 0,
|
||||
"maxScore": 20
|
||||
},
|
||||
{
|
||||
"id": "q-003",
|
||||
"type": "multiple-choice",
|
||||
"content": "下列哪些是偶函数?(多选)",
|
||||
"options": [
|
||||
{ "id": "opt-a", "text": "f(x) = x²" },
|
||||
{ "id": "opt-b", "text": "f(x) = x³" },
|
||||
{ "id": "opt-c", "text": "f(x) = |x|" },
|
||||
{ "id": "opt-d", "text": "f(x) = cos x" }
|
||||
],
|
||||
"score": 0,
|
||||
"maxScore": 20
|
||||
},
|
||||
{
|
||||
"id": "q-004",
|
||||
"type": "short-answer",
|
||||
"content": "简述导数的几何意义。",
|
||||
"options": [],
|
||||
"score": 0,
|
||||
"maxScore": 20
|
||||
},
|
||||
{
|
||||
"id": "q-005",
|
||||
"type": "short-answer",
|
||||
"content": "求不等式 x² - 5x + 6 < 0 的解集。",
|
||||
"options": [],
|
||||
"score": 0,
|
||||
"maxScore": 20
|
||||
}
|
||||
]
|
||||
}
|
||||
38
apps/student-portal/src/mocks/fixtures/homeworkDetail.json
Normal file
38
apps/student-portal/src/mocks/fixtures/homeworkDetail.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"id": "hw-001",
|
||||
"title": "数学作业第五章",
|
||||
"subject": { "id": "subject-math", "name": "数学" },
|
||||
"status": "pending",
|
||||
"dueAt": "2026-07-12T23:59:59.000Z",
|
||||
"assignedAt": "2026-07-08T08:00:00.000Z",
|
||||
"questionCount": 3,
|
||||
"totalScore": 100,
|
||||
"questions": [
|
||||
{
|
||||
"id": "hw-q-001",
|
||||
"type": "single-choice",
|
||||
"content": "函数 f(x) = 2x + 3 在 x = 2 处的函数值为?",
|
||||
"options": [
|
||||
{ "id": "opt-a", "text": "5" },
|
||||
{ "id": "opt-b", "text": "7" },
|
||||
{ "id": "opt-c", "text": "9" },
|
||||
{ "id": "opt-d", "text": "11" }
|
||||
],
|
||||
"maxScore": 30
|
||||
},
|
||||
{
|
||||
"id": "hw-q-002",
|
||||
"type": "short-answer",
|
||||
"content": "解方程:x² - 4 = 0",
|
||||
"options": [],
|
||||
"maxScore": 40
|
||||
},
|
||||
{
|
||||
"id": "hw-q-003",
|
||||
"type": "short-answer",
|
||||
"content": "证明:三角形内角和为 180 度。",
|
||||
"options": [],
|
||||
"maxScore": 30
|
||||
}
|
||||
]
|
||||
}
|
||||
10
apps/student-portal/src/mocks/fixtures/learningPath.json
Normal file
10
apps/student-portal/src/mocks/fixtures/learningPath.json
Normal file
@@ -0,0 +1,10 @@
|
||||
[
|
||||
{ "id": "kp-001", "name": "函数概念", "knowledgePointId": "kp-001", "status": "completed", "mastery": 0.9, "dependencies": [], "recommendedOrder": 1 },
|
||||
{ "id": "kp-002", "name": "函数性质", "knowledgePointId": "kp-002", "status": "completed", "mastery": 0.85, "dependencies": ["kp-001"], "recommendedOrder": 2 },
|
||||
{ "id": "kp-003", "name": "导数概念", "knowledgePointId": "kp-003", "status": "in-progress", "mastery": 0.6, "dependencies": ["kp-001", "kp-002"], "recommendedOrder": 3 },
|
||||
{ "id": "kp-004", "name": "导数运算", "knowledgePointId": "kp-004", "status": "available", "mastery": 0.3, "dependencies": ["kp-003"], "recommendedOrder": 4 },
|
||||
{ "id": "kp-005", "name": "三角函数定义", "knowledgePointId": "kp-005", "status": "completed", "mastery": 0.95, "dependencies": [], "recommendedOrder": 5 },
|
||||
{ "id": "kp-006", "name": "三角恒等变换", "knowledgePointId": "kp-006", "status": "in-progress", "mastery": 0.55, "dependencies": ["kp-005"], "recommendedOrder": 6 },
|
||||
{ "id": "kp-007", "name": "不等式", "knowledgePointId": "kp-007", "status": "available", "mastery": 0.2, "dependencies": ["kp-001"], "recommendedOrder": 7 },
|
||||
{ "id": "kp-008", "name": "数列", "knowledgePointId": "kp-008", "status": "locked", "mastery": 0, "dependencies": ["kp-002"], "recommendedOrder": 8 }
|
||||
]
|
||||
12
apps/student-portal/src/mocks/fixtures/myAttendance.json
Normal file
12
apps/student-portal/src/mocks/fixtures/myAttendance.json
Normal file
@@ -0,0 +1,12 @@
|
||||
[
|
||||
{ "id": "att-001", "date": "2026-07-01", "status": "present", "subject": { "id": "subject-math", "name": "数学" } },
|
||||
{ "id": "att-002", "date": "2026-07-02", "status": "present", "subject": { "id": "subject-chinese", "name": "语文" } },
|
||||
{ "id": "att-003", "date": "2026-07-03", "status": "late", "subject": { "id": "subject-english", "name": "英语" }, "lateMinutes": 10 },
|
||||
{ "id": "att-004", "date": "2026-07-04", "status": "present", "subject": { "id": "subject-physics", "name": "物理" } },
|
||||
{ "id": "att-005", "date": "2026-07-07", "status": "absent", "subject": { "id": "subject-chemistry", "name": "化学" }, "reason": "病假" },
|
||||
{ "id": "att-006", "date": "2026-07-08", "status": "present", "subject": { "id": "subject-math", "name": "数学" } },
|
||||
{ "id": "att-007", "date": "2026-07-09", "status": "present", "subject": { "id": "subject-chinese", "name": "语文" } },
|
||||
{ "id": "att-008", "date": "2026-07-10", "status": "late", "subject": { "id": "subject-english", "name": "英语" }, "lateMinutes": 5 },
|
||||
{ "id": "att-009", "date": "2026-07-11", "status": "present", "subject": { "id": "subject-physics", "name": "物理" } },
|
||||
{ "id": "att-010", "date": "2026-07-12", "status": "present", "subject": { "id": "subject-chemistry", "name": "化学" } }
|
||||
]
|
||||
10
apps/student-portal/src/mocks/fixtures/myClasses.json
Normal file
10
apps/student-portal/src/mocks/fixtures/myClasses.json
Normal file
@@ -0,0 +1,10 @@
|
||||
[
|
||||
{
|
||||
"id": "class-001",
|
||||
"name": "高三(2)班",
|
||||
"homeroomTeacher": "李老师",
|
||||
"studentCount": 45,
|
||||
"grade": "高三",
|
||||
"year": 2026
|
||||
}
|
||||
]
|
||||
26
apps/student-portal/src/mocks/fixtures/myExams.json
Normal file
26
apps/student-portal/src/mocks/fixtures/myExams.json
Normal file
@@ -0,0 +1,26 @@
|
||||
[
|
||||
{
|
||||
"id": "exam-001",
|
||||
"name": "高三数学期中考试",
|
||||
"subject": { "id": "subject-math", "name": "数学" },
|
||||
"status": "not_started",
|
||||
"startsAt": "2026-07-15T09:00:00.000Z",
|
||||
"expiresAt": "2026-07-15T11:00:00.000Z",
|
||||
"durationSeconds": 7200,
|
||||
"questionCount": 5,
|
||||
"totalScore": 100
|
||||
},
|
||||
{
|
||||
"id": "exam-002",
|
||||
"name": "高三语文月考",
|
||||
"subject": { "id": "subject-chinese", "name": "语文" },
|
||||
"status": "submitted",
|
||||
"startsAt": "2026-07-05T09:00:00.000Z",
|
||||
"expiresAt": "2026-07-05T11:00:00.000Z",
|
||||
"durationSeconds": 7200,
|
||||
"questionCount": 4,
|
||||
"totalScore": 100,
|
||||
"submittedAt": "2026-07-05T10:45:00.000Z",
|
||||
"submissionId": "submission-002"
|
||||
}
|
||||
]
|
||||
62
apps/student-portal/src/mocks/fixtures/myGrades.json
Normal file
62
apps/student-portal/src/mocks/fixtures/myGrades.json
Normal file
@@ -0,0 +1,62 @@
|
||||
[
|
||||
{
|
||||
"id": "grade-001",
|
||||
"examId": "exam-002",
|
||||
"examName": "高三语文月考",
|
||||
"subject": { "id": "subject-chinese", "name": "语文" },
|
||||
"score": 85,
|
||||
"maxScore": 100,
|
||||
"grade": "良好",
|
||||
"rank": 8,
|
||||
"submittedAt": "2026-07-05T10:45:00.000Z",
|
||||
"gradedAt": "2026-07-07T14:00:00.000Z"
|
||||
},
|
||||
{
|
||||
"id": "grade-002",
|
||||
"examId": "exam-003",
|
||||
"examName": "高三英语模拟考",
|
||||
"subject": { "id": "subject-english", "name": "英语" },
|
||||
"score": 92,
|
||||
"maxScore": 100,
|
||||
"grade": "优秀",
|
||||
"rank": 3,
|
||||
"submittedAt": "2026-06-28T10:00:00.000Z",
|
||||
"gradedAt": "2026-06-30T09:00:00.000Z"
|
||||
},
|
||||
{
|
||||
"id": "grade-003",
|
||||
"examId": "exam-004",
|
||||
"examName": "高三物理单元测验",
|
||||
"subject": { "id": "subject-physics", "name": "物理" },
|
||||
"score": 78,
|
||||
"maxScore": 100,
|
||||
"grade": "及格",
|
||||
"rank": 15,
|
||||
"submittedAt": "2026-06-20T10:00:00.000Z",
|
||||
"gradedAt": "2026-06-22T09:00:00.000Z"
|
||||
},
|
||||
{
|
||||
"id": "grade-004",
|
||||
"examId": "exam-005",
|
||||
"examName": "高三化学月考",
|
||||
"subject": { "id": "subject-chemistry", "name": "化学" },
|
||||
"score": 88,
|
||||
"maxScore": 100,
|
||||
"grade": "良好",
|
||||
"rank": 6,
|
||||
"submittedAt": "2026-06-10T10:00:00.000Z",
|
||||
"gradedAt": "2026-06-12T09:00:00.000Z"
|
||||
},
|
||||
{
|
||||
"id": "grade-005",
|
||||
"examId": "exam-006",
|
||||
"examName": "高三数学月考",
|
||||
"subject": { "id": "subject-math", "name": "数学" },
|
||||
"score": 95,
|
||||
"maxScore": 100,
|
||||
"grade": "优秀",
|
||||
"rank": 2,
|
||||
"submittedAt": "2026-05-28T10:00:00.000Z",
|
||||
"gradedAt": "2026-05-30T09:00:00.000Z"
|
||||
}
|
||||
]
|
||||
36
apps/student-portal/src/mocks/fixtures/myHomework.json
Normal file
36
apps/student-portal/src/mocks/fixtures/myHomework.json
Normal file
@@ -0,0 +1,36 @@
|
||||
[
|
||||
{
|
||||
"id": "hw-001",
|
||||
"title": "数学作业第五章",
|
||||
"subject": { "id": "subject-math", "name": "数学" },
|
||||
"status": "pending",
|
||||
"dueAt": "2026-07-12T23:59:59.000Z",
|
||||
"assignedAt": "2026-07-08T08:00:00.000Z",
|
||||
"questionCount": 10,
|
||||
"totalScore": 100
|
||||
},
|
||||
{
|
||||
"id": "hw-002",
|
||||
"title": "语文阅读理解练习",
|
||||
"subject": { "id": "subject-chinese", "name": "语文" },
|
||||
"status": "submitted",
|
||||
"dueAt": "2026-07-10T23:59:59.000Z",
|
||||
"assignedAt": "2026-07-06T08:00:00.000Z",
|
||||
"submittedAt": "2026-07-09T20:30:00.000Z",
|
||||
"questionCount": 8,
|
||||
"totalScore": 100
|
||||
},
|
||||
{
|
||||
"id": "hw-003",
|
||||
"title": "英语单元测试卷",
|
||||
"subject": { "id": "subject-english", "name": "英语" },
|
||||
"status": "graded",
|
||||
"dueAt": "2026-07-05T23:59:59.000Z",
|
||||
"assignedAt": "2026-07-01T08:00:00.000Z",
|
||||
"submittedAt": "2026-07-04T21:00:00.000Z",
|
||||
"gradedAt": "2026-07-06T10:00:00.000Z",
|
||||
"score": 88,
|
||||
"totalScore": 100,
|
||||
"questionCount": 12
|
||||
}
|
||||
]
|
||||
12
apps/student-portal/src/mocks/fixtures/myNotifications.json
Normal file
12
apps/student-portal/src/mocks/fixtures/myNotifications.json
Normal file
@@ -0,0 +1,12 @@
|
||||
[
|
||||
{ "id": "ntf-001", "type": "homework", "title": "新作业发布", "content": "数学作业第五章已发布,截止日期 7月12日", "read": false, "createdAt": "2026-07-08T08:00:00.000Z" },
|
||||
{ "id": "ntf-002", "type": "exam", "title": "考试即将开始", "content": "高三数学期中考试将于7月15日开始", "read": false, "createdAt": "2026-07-09T10:00:00.000Z" },
|
||||
{ "id": "ntf-003", "type": "grade", "title": "成绩已发布", "content": "您的语文月考成绩为85分", "read": false, "createdAt": "2026-07-07T14:00:00.000Z" },
|
||||
{ "id": "ntf-004", "type": "system", "title": "系统维护通知", "content": "系统将于本周日 2:00-4:00 进行维护", "read": true, "createdAt": "2026-07-06T16:00:00.000Z" },
|
||||
{ "id": "ntf-005", "type": "homework", "title": "作业即将截止", "content": "语文阅读理解练习将于明天截止", "read": false, "createdAt": "2026-07-09T20:00:00.000Z" },
|
||||
{ "id": "ntf-006", "type": "grade", "title": "成绩已发布", "content": "您的英语模拟考成绩为92分", "read": true, "createdAt": "2026-06-30T09:00:00.000Z" },
|
||||
{ "id": "ntf-007", "type": "exam", "title": "考试结果发布", "content": "高三语文月考已结束,查看详情", "read": true, "createdAt": "2026-07-05T11:00:00.000Z" },
|
||||
{ "id": "ntf-008", "type": "system", "title": "隐私政策更新", "content": "平台隐私政策已更新,请查阅", "read": true, "createdAt": "2026-07-01T10:00:00.000Z" },
|
||||
{ "id": "ntf-009", "type": "homework", "title": "作业已批改", "content": "英语单元测试卷已批改,得分88分", "read": true, "createdAt": "2026-07-06T10:00:00.000Z" },
|
||||
{ "id": "ntf-010", "type": "system", "title": "学期注册提醒", "content": "请于7月20日前完成新学期注册", "read": false, "createdAt": "2026-07-10T08:00:00.000Z" }
|
||||
]
|
||||
9
apps/student-portal/src/mocks/fixtures/myTrend.json
Normal file
9
apps/student-portal/src/mocks/fixtures/myTrend.json
Normal file
@@ -0,0 +1,9 @@
|
||||
[
|
||||
{ "date": "2026-07-04", "score": 88, "examName": "英语单元测试" },
|
||||
{ "date": "2026-07-05", "score": 85, "examName": "语文月考" },
|
||||
{ "date": "2026-06-20", "score": 78, "examName": "物理单元测验" },
|
||||
{ "date": "2026-06-10", "score": 88, "examName": "化学月考" },
|
||||
{ "date": "2026-05-28", "score": 95, "examName": "数学月考" },
|
||||
{ "date": "2026-05-15", "score": 82, "examName": "语文模拟考" },
|
||||
{ "date": "2026-05-01", "score": 80, "examName": "英语模拟考" }
|
||||
]
|
||||
38
apps/student-portal/src/mocks/fixtures/myWeakness.json
Normal file
38
apps/student-portal/src/mocks/fixtures/myWeakness.json
Normal file
@@ -0,0 +1,38 @@
|
||||
[
|
||||
{
|
||||
"id": "wp-001",
|
||||
"questionId": "q-weak-001",
|
||||
"questionContent": "求复合函数的导数",
|
||||
"subject": { "id": "subject-math", "name": "数学" },
|
||||
"knowledgePointId": "kp-004",
|
||||
"knowledgePointName": "导数运算",
|
||||
"mastery": 0.3,
|
||||
"wrongCount": 5,
|
||||
"lastWrongAt": "2026-07-08T10:00:00.000Z",
|
||||
"recommendedAction": "practice"
|
||||
},
|
||||
{
|
||||
"id": "wp-002",
|
||||
"questionId": "q-weak-002",
|
||||
"questionContent": "三角恒等变换应用",
|
||||
"subject": { "id": "subject-math", "name": "数学" },
|
||||
"knowledgePointId": "kp-006",
|
||||
"knowledgePointName": "三角恒等变换",
|
||||
"mastery": 0.4,
|
||||
"wrongCount": 3,
|
||||
"lastWrongAt": "2026-07-06T14:00:00.000Z",
|
||||
"recommendedAction": "review"
|
||||
},
|
||||
{
|
||||
"id": "wp-003",
|
||||
"questionId": "q-weak-003",
|
||||
"questionContent": "不等式解集求解",
|
||||
"subject": { "id": "subject-math", "name": "数学" },
|
||||
"knowledgePointId": "kp-007",
|
||||
"knowledgePointName": "不等式",
|
||||
"mastery": 0.2,
|
||||
"wrongCount": 4,
|
||||
"lastWrongAt": "2026-07-09T09:00:00.000Z",
|
||||
"recommendedAction": "practice"
|
||||
}
|
||||
]
|
||||
4
apps/student-portal/src/mocks/fixtures/serverTime.json
Normal file
4
apps/student-portal/src/mocks/fixtures/serverTime.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"timestamp": "2026-07-10T12:00:00.000Z",
|
||||
"offset": 0
|
||||
}
|
||||
20
apps/student-portal/src/mocks/fixtures/studentDashboard.json
Normal file
20
apps/student-portal/src/mocks/fixtures/studentDashboard.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"upcomingHomework": [
|
||||
{ "id": "hw-001", "title": "数学作业第五章", "dueAt": "2026-07-12T23:59:59.000Z", "subject": { "id": "subject-math", "name": "数学" }, "status": "pending" },
|
||||
{ "id": "hw-004", "title": "物理实验报告", "dueAt": "2026-07-13T23:59:59.000Z", "subject": { "id": "subject-physics", "name": "物理" }, "status": "pending" },
|
||||
{ "id": "hw-005", "title": "英语听力练习", "dueAt": "2026-07-14T23:59:59.000Z", "subject": { "id": "subject-english", "name": "英语" }, "status": "pending" }
|
||||
],
|
||||
"upcomingExams": [
|
||||
{ "id": "exam-001", "name": "高三数学期中考试", "startsAt": "2026-07-15T09:00:00.000Z", "expiresAt": "2026-07-15T11:00:00.000Z", "durationSeconds": 7200, "subject": { "id": "subject-math", "name": "数学" }, "status": "not_started" },
|
||||
{ "id": "exam-007", "name": "高三英语期末考", "startsAt": "2026-07-18T09:00:00.000Z", "expiresAt": "2026-07-18T11:00:00.000Z", "durationSeconds": 7200, "subject": { "id": "subject-english", "name": "英语" }, "status": "not_started" }
|
||||
],
|
||||
"recentGrades": [
|
||||
{ "id": "grade-001", "examName": "高三语文月考", "score": 85, "maxScore": 100, "grade": "良好", "submittedAt": "2026-07-05T10:45:00.000Z" },
|
||||
{ "id": "grade-002", "examName": "高三英语模拟考", "score": 92, "maxScore": 100, "grade": "优秀", "submittedAt": "2026-06-28T10:00:00.000Z" },
|
||||
{ "id": "grade-003", "examName": "高三物理单元测验", "score": 78, "maxScore": 100, "grade": "及格", "submittedAt": "2026-06-20T10:00:00.000Z" }
|
||||
],
|
||||
"attendanceRate": 0.95,
|
||||
"learningStreakDays": 12,
|
||||
"avgScore": 85.0,
|
||||
"classRank": 5
|
||||
}
|
||||
7
apps/student-portal/src/mocks/fixtures/textbooks.json
Normal file
7
apps/student-portal/src/mocks/fixtures/textbooks.json
Normal file
@@ -0,0 +1,7 @@
|
||||
[
|
||||
{ "id": "tb-001", "name": "高三数学(上)", "subject": { "id": "subject-math", "name": "数学" }, "version": "人教版 2024", "author": "人民教育出版社" },
|
||||
{ "id": "tb-002", "name": "高三语文", "subject": { "id": "subject-chinese", "name": "语文" }, "version": "人教版 2024", "author": "人民教育出版社" },
|
||||
{ "id": "tb-003", "name": "高三英语", "subject": { "id": "subject-english", "name": "英语" }, "version": "外研版 2024", "author": "外语教学与研究出版社" },
|
||||
{ "id": "tb-004", "name": "高三物理", "subject": { "id": "subject-physics", "name": "物理" }, "version": "人教版 2024", "author": "人民教育出版社" },
|
||||
{ "id": "tb-005", "name": "高三化学", "subject": { "id": "subject-chemistry", "name": "化学" }, "version": "鲁科版 2024", "author": "山东科学技术出版社" }
|
||||
]
|
||||
484
apps/student-portal/src/mocks/handlers.ts
Normal file
484
apps/student-portal/src/mocks/handlers.ts
Normal file
@@ -0,0 +1,484 @@
|
||||
/**
|
||||
* MSW 请求处理器(ai14)
|
||||
*
|
||||
* 拦截 POST /api/v1/student/graphql,按 operationName 分发到对应的 mock 响应。
|
||||
* 所有响应包裹在 ActionState 信封中:
|
||||
* { success: true, errors: [], data: <fixture>, extensions: { degraded: false } }
|
||||
*
|
||||
* GraphQL 响应结构:
|
||||
* { data: { <operationName>: ActionState<...> } }
|
||||
*
|
||||
* 支持的 24 个操作(16 查询 + 8 变更)详见 types.ts OperationName。
|
||||
*/
|
||||
|
||||
import { http, HttpResponse } from "msw";
|
||||
import type {
|
||||
ActionState,
|
||||
MutationOperationName,
|
||||
QueryOperationName,
|
||||
} from "../lib/graphql/types";
|
||||
import { ERROR_CODES } from "../lib/graphql/types";
|
||||
|
||||
// 查询 fixture 数据
|
||||
import currentUserFixture from "./fixtures/currentUser.json";
|
||||
import myClassesFixture from "./fixtures/myClasses.json";
|
||||
import myExamsFixture from "./fixtures/myExams.json";
|
||||
import examDetailFixture from "./fixtures/examDetail.json";
|
||||
import myHomeworkFixture from "./fixtures/myHomework.json";
|
||||
import homeworkDetailFixture from "./fixtures/homeworkDetail.json";
|
||||
import myGradesFixture from "./fixtures/myGrades.json";
|
||||
import myAttendanceFixture from "./fixtures/myAttendance.json";
|
||||
import textbooksFixture from "./fixtures/textbooks.json";
|
||||
import chaptersFixture from "./fixtures/chapters.json";
|
||||
import learningPathFixture from "./fixtures/learningPath.json";
|
||||
import studentDashboardFixture from "./fixtures/studentDashboard.json";
|
||||
import myWeaknessFixture from "./fixtures/myWeakness.json";
|
||||
import myTrendFixture from "./fixtures/myTrend.json";
|
||||
import myNotificationsFixture from "./fixtures/myNotifications.json";
|
||||
import serverTimeFixture from "./fixtures/serverTime.json";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// GraphQL 请求/响应类型
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** GraphQL 请求体 */
|
||||
interface GraphQLRequest {
|
||||
query: string;
|
||||
variables?: Record<string, unknown>;
|
||||
operationName?: string;
|
||||
}
|
||||
|
||||
/** GraphQL 顶层响应 */
|
||||
interface GraphQLResponseData {
|
||||
data: Record<string, unknown> | null;
|
||||
errors?: Array<{ message: string; extensions?: Record<string, unknown> }>;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ActionState 信封构造工具
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* 构造成功的 ActionState 响应
|
||||
* @param data 业务数据
|
||||
* @returns ActionState 信封
|
||||
*/
|
||||
function ok<T>(data: T): ActionState<T> {
|
||||
return {
|
||||
success: true,
|
||||
errors: [],
|
||||
data,
|
||||
extensions: { degraded: false },
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造失败的 ActionState 响应
|
||||
* @param code 错误码
|
||||
* @param message 错误消息
|
||||
* @returns ActionState 信封(data 为 null)
|
||||
*/
|
||||
function fail<T>(code: string, message: string): ActionState<T> {
|
||||
return {
|
||||
success: false,
|
||||
errors: [{ code, message }],
|
||||
data: null,
|
||||
extensions: { degraded: false },
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造降级的 ActionState 响应(部分上游不可用)
|
||||
* @param data 部分业务数据
|
||||
* @param reason 降级原因
|
||||
* @returns ActionState 信封(degraded: true)
|
||||
*/
|
||||
function degraded<T>(data: T, reason: string): ActionState<T> {
|
||||
return {
|
||||
success: true,
|
||||
errors: [],
|
||||
data,
|
||||
extensions: { degraded: true, degradedReason: reason },
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// GraphQL 响应构造
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* 构造 GraphQL 成功响应(单操作)
|
||||
* @param operationName 操作名
|
||||
* @param actionState ActionState 信封
|
||||
* @returns HttpResponse JSON
|
||||
*/
|
||||
function graphqlOk(
|
||||
operationName: string,
|
||||
actionState: ActionState<unknown>,
|
||||
): Response {
|
||||
const body: GraphQLResponseData = {
|
||||
data: { [operationName]: actionState },
|
||||
};
|
||||
return HttpResponse.json(body);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造 GraphQL 错误响应(顶层错误,如认证失败)
|
||||
* @param message 错误消息
|
||||
* @param code 错误码
|
||||
* @returns HttpResponse JSON
|
||||
*/
|
||||
function graphqlError(message: string, code: string): Response {
|
||||
const body: GraphQLResponseData = {
|
||||
data: null,
|
||||
errors: [{ message, extensions: { code } }],
|
||||
};
|
||||
return HttpResponse.json(body, { status: 200 });
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// operationName 提取
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* 从 GraphQL 请求中提取 operationName
|
||||
* 优先使用 body.operationName,缺失时从 query 字符串解析
|
||||
* @param body GraphQL 请求体
|
||||
* @returns 操作名或 null
|
||||
*/
|
||||
function extractOperationName(body: GraphQLRequest): string | null {
|
||||
if (body.operationName) {
|
||||
return body.operationName;
|
||||
}
|
||||
// 从 query 字符串解析:query/mutation OperationName( ...
|
||||
const match = body.query.match(/(?:query|mutation)\s+(\w+)/);
|
||||
return match?.[1] ?? null;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 查询处理器(16 个)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* 处理查询操作
|
||||
* @param operationName 操作名
|
||||
* @param variables 查询变量
|
||||
* @returns HttpResponse
|
||||
*/
|
||||
function handleQuery(
|
||||
operationName: QueryOperationName,
|
||||
variables: Record<string, unknown>,
|
||||
): Response {
|
||||
switch (operationName) {
|
||||
case "currentUser":
|
||||
return graphqlOk("currentUser", ok(currentUserFixture));
|
||||
|
||||
case "myClasses":
|
||||
return graphqlOk("myClasses", ok(myClassesFixture));
|
||||
|
||||
case "myExams":
|
||||
return graphqlOk("myExams", ok(myExamsFixture));
|
||||
|
||||
case "examDetail": {
|
||||
const examId = variables["examId"] ?? "exam-001";
|
||||
// 根据请求的 examId 返回对应数据,默认返回 examDetail fixture
|
||||
const detail =
|
||||
examId === "exam-001"
|
||||
? examDetailFixture
|
||||
: { ...examDetailFixture, id: examId };
|
||||
return graphqlOk("examDetail", ok(detail));
|
||||
}
|
||||
|
||||
case "myHomework":
|
||||
return graphqlOk("myHomework", ok(myHomeworkFixture));
|
||||
|
||||
case "homeworkDetail": {
|
||||
const homeworkId = variables["homeworkId"] ?? "hw-001";
|
||||
const detail =
|
||||
homeworkId === "hw-001"
|
||||
? homeworkDetailFixture
|
||||
: { ...homeworkDetailFixture, id: homeworkId };
|
||||
return graphqlOk("homeworkDetail", ok(detail));
|
||||
}
|
||||
|
||||
case "myGrades":
|
||||
return graphqlOk("myGrades", ok(myGradesFixture));
|
||||
|
||||
case "myAttendance":
|
||||
return graphqlOk("myAttendance", ok(myAttendanceFixture));
|
||||
|
||||
case "textbooks":
|
||||
return graphqlOk("textbooks", ok(textbooksFixture));
|
||||
|
||||
case "chapters": {
|
||||
const textbookId = variables["textbookId"] ?? "tb-001";
|
||||
const chapters =
|
||||
textbookId === "tb-001"
|
||||
? chaptersFixture
|
||||
: chaptersFixture.filter((c) => c.textbookId === textbookId);
|
||||
return graphqlOk("chapters", ok(chapters));
|
||||
}
|
||||
|
||||
case "learningPath":
|
||||
return graphqlOk("learningPath", ok(learningPathFixture));
|
||||
|
||||
case "studentDashboard":
|
||||
return graphqlOk("studentDashboard", ok(studentDashboardFixture));
|
||||
|
||||
case "myWeakness":
|
||||
return graphqlOk("myWeakness", ok(myWeaknessFixture));
|
||||
|
||||
case "myTrend":
|
||||
return graphqlOk("myTrend", ok(myTrendFixture));
|
||||
|
||||
case "myNotifications": {
|
||||
const first = (variables["first"] as number | undefined) ?? 10;
|
||||
const items = myNotificationsFixture.slice(0, first);
|
||||
const unreadCount = items.filter((n) => !n.read).length;
|
||||
const result = {
|
||||
items,
|
||||
totalCount: myNotificationsFixture.length,
|
||||
unreadCount,
|
||||
};
|
||||
return graphqlOk("myNotifications", ok(result));
|
||||
}
|
||||
|
||||
case "serverTime":
|
||||
// 返回当前时间以支持倒计时校正
|
||||
return graphqlOk(
|
||||
"serverTime",
|
||||
ok({
|
||||
timestamp: new Date().toISOString(),
|
||||
offset: 0,
|
||||
}),
|
||||
);
|
||||
|
||||
default: {
|
||||
// 未覆盖的查询操作名,返回错误
|
||||
return graphqlError(
|
||||
`未实现的查询: ${operationName}`,
|
||||
ERROR_CODES.BFF_STUDENT_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 变更处理器(8 个)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* 处理变更操作
|
||||
* @param operationName 操作名
|
||||
* @param variables 变更变量
|
||||
* @returns HttpResponse
|
||||
*/
|
||||
function handleMutation(
|
||||
operationName: MutationOperationName,
|
||||
variables: Record<string, unknown>,
|
||||
): Response {
|
||||
switch (operationName) {
|
||||
case "submitHomework": {
|
||||
const homeworkId = variables["homeworkId"] ?? "hw-001";
|
||||
const result = {
|
||||
submissionId: `submission-${Date.now()}`,
|
||||
submittedAt: new Date().toISOString(),
|
||||
};
|
||||
void homeworkId;
|
||||
return graphqlOk("submitHomework", ok(result));
|
||||
}
|
||||
|
||||
case "submitExam": {
|
||||
const examId = variables["examId"] ?? "exam-001";
|
||||
const result = {
|
||||
submissionId: `submission-${Date.now()}`,
|
||||
submittedAt: new Date().toISOString(),
|
||||
score: 85,
|
||||
maxScore: 100,
|
||||
};
|
||||
void examId;
|
||||
return graphqlOk("submitExam", ok(result));
|
||||
}
|
||||
|
||||
case "saveExamDraft": {
|
||||
const result = {
|
||||
savedAt: new Date().toISOString(),
|
||||
ok: true,
|
||||
};
|
||||
return graphqlOk("saveExamDraft", ok(result));
|
||||
}
|
||||
|
||||
case "markAsRead": {
|
||||
const notificationId = variables["notificationId"] ?? "";
|
||||
const result = {
|
||||
notificationId: notificationId as string,
|
||||
read: true,
|
||||
};
|
||||
return graphqlOk("markAsRead", ok(result));
|
||||
}
|
||||
|
||||
case "markAllAsRead": {
|
||||
const result = {
|
||||
updatedCount: myNotificationsFixture.filter((n) => !n.read).length,
|
||||
};
|
||||
return graphqlOk("markAllAsRead", ok(result));
|
||||
}
|
||||
|
||||
case "recordExamViolation": {
|
||||
const result = {
|
||||
recorded: true,
|
||||
violationCount: 1,
|
||||
};
|
||||
return graphqlOk("recordExamViolation", ok(result));
|
||||
}
|
||||
|
||||
case "recordPasteEvent": {
|
||||
const result = {
|
||||
recorded: true,
|
||||
};
|
||||
return graphqlOk("recordPasteEvent", ok(result));
|
||||
}
|
||||
|
||||
case "updateNotificationPreference": {
|
||||
const type = variables["type"] ?? "system";
|
||||
const enabled = variables["enabled"] ?? true;
|
||||
const result = {
|
||||
type: type as string,
|
||||
enabled: enabled as boolean,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
return graphqlOk("updateNotificationPreference", ok(result));
|
||||
}
|
||||
|
||||
default: {
|
||||
// 未覆盖的变更操作名,返回错误
|
||||
return graphqlError(
|
||||
`未实现的变更: ${operationName}`,
|
||||
ERROR_CODES.BFF_STUDENT_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 主处理器
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** 查询操作名集合 */
|
||||
const QUERY_NAMES = new Set<QueryOperationName>([
|
||||
"currentUser",
|
||||
"myClasses",
|
||||
"myExams",
|
||||
"examDetail",
|
||||
"myHomework",
|
||||
"homeworkDetail",
|
||||
"myGrades",
|
||||
"myAttendance",
|
||||
"textbooks",
|
||||
"chapters",
|
||||
"learningPath",
|
||||
"studentDashboard",
|
||||
"myWeakness",
|
||||
"myTrend",
|
||||
"myNotifications",
|
||||
"serverTime",
|
||||
]);
|
||||
|
||||
/** 变更操作名集合 */
|
||||
const MUTATION_NAMES = new Set<MutationOperationName>([
|
||||
"submitHomework",
|
||||
"submitExam",
|
||||
"saveExamDraft",
|
||||
"markAsRead",
|
||||
"markAllAsRead",
|
||||
"recordExamViolation",
|
||||
"recordPasteEvent",
|
||||
"updateNotificationPreference",
|
||||
]);
|
||||
|
||||
/**
|
||||
* 判断是否为查询操作名
|
||||
* @param name 操作名
|
||||
* @returns 是否查询
|
||||
*/
|
||||
function isQueryName(name: string): name is QueryOperationName {
|
||||
return (QUERY_NAMES as Set<string>).has(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为变更操作名
|
||||
* @param name 操作名
|
||||
* @returns 是否变更
|
||||
*/
|
||||
function isMutationName(name: string): name is MutationOperationName {
|
||||
return (MUTATION_NAMES as Set<string>).has(name);
|
||||
}
|
||||
|
||||
/** MSW 请求处理器列表 */
|
||||
export const handlers = [
|
||||
http.post(
|
||||
"*/api/v1/student/graphql",
|
||||
async ({ request }): Promise<Response> => {
|
||||
let body: GraphQLRequest;
|
||||
try {
|
||||
body = (await request.json()) as GraphQLRequest;
|
||||
} catch {
|
||||
return graphqlError(
|
||||
"请求体解析失败",
|
||||
ERROR_CODES.BFF_STUDENT_VALIDATION_FAILED,
|
||||
);
|
||||
}
|
||||
|
||||
const operationName = extractOperationName(body);
|
||||
if (!operationName) {
|
||||
return graphqlError(
|
||||
"无法识别 operationName",
|
||||
ERROR_CODES.BFF_STUDENT_VALIDATION_FAILED,
|
||||
);
|
||||
}
|
||||
|
||||
const variables = body.variables ?? {};
|
||||
|
||||
// 按操作类型分发
|
||||
if (isQueryName(operationName)) {
|
||||
return handleQuery(operationName, variables);
|
||||
}
|
||||
|
||||
if (isMutationName(operationName)) {
|
||||
return handleMutation(operationName, variables);
|
||||
}
|
||||
|
||||
// 未知操作名
|
||||
return graphqlError(
|
||||
`未知操作: ${operationName}`,
|
||||
ERROR_CODES.BFF_STUDENT_VALIDATION_FAILED,
|
||||
);
|
||||
},
|
||||
),
|
||||
];
|
||||
|
||||
/** 导出 fixture 数据供测试使用 */
|
||||
export const fixtures = {
|
||||
currentUser: currentUserFixture,
|
||||
myClasses: myClassesFixture,
|
||||
myExams: myExamsFixture,
|
||||
examDetail: examDetailFixture,
|
||||
myHomework: myHomeworkFixture,
|
||||
homeworkDetail: homeworkDetailFixture,
|
||||
myGrades: myGradesFixture,
|
||||
myAttendance: myAttendanceFixture,
|
||||
textbooks: textbooksFixture,
|
||||
chapters: chaptersFixture,
|
||||
learningPath: learningPathFixture,
|
||||
studentDashboard: studentDashboardFixture,
|
||||
myWeakness: myWeaknessFixture,
|
||||
myTrend: myTrendFixture,
|
||||
myNotifications: myNotificationsFixture,
|
||||
serverTime: serverTimeFixture,
|
||||
};
|
||||
|
||||
/** 导出工具函数供测试使用 */
|
||||
export { ok, fail, degraded, graphqlOk, graphqlError };
|
||||
|
||||
/** 导出操作名集合供测试断言 */
|
||||
export { QUERY_NAMES, MUTATION_NAMES };
|
||||
20
apps/student-portal/src/mocks/server.ts
Normal file
20
apps/student-portal/src/mocks/server.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* MSW 服务端 setup(ai14)
|
||||
*
|
||||
* 用于 Node.js 环境(Vitest 集成测试 / SSR)拦截 GraphQL 请求。
|
||||
* 通过 setupServer 注册 handler,在测试 beforeEach 中启动、afterEach 中停止。
|
||||
*
|
||||
* 用法:
|
||||
* import { server } from "@/mocks/server";
|
||||
* beforeAll(() => server.listen());
|
||||
* afterEach(() => server.resetHandlers());
|
||||
* afterAll(() => server.close());
|
||||
*/
|
||||
|
||||
import { setupServer } from "msw/node";
|
||||
import { handlers } from "./handlers";
|
||||
|
||||
/** MSW Node.js server 单例 */
|
||||
export const server = setupServer(...handlers);
|
||||
|
||||
export { handlers };
|
||||
Reference in New Issue
Block a user