feat(portal-shell): 教师域考试管理页面迁移(P2)

按 ARCHITECTURE.md §9.1/§10 P2 要求,迁移教师域 exams 模块:
- 列表页 /shell/teacher/exams(ListPageShell + URL 状态 + 客户端二次筛选)
- 详情页 /shell/teacher/exams/[id](DetailPageShell + 真实 exam(id) 查询)
- 新建页 /shell/teacher/exams/new(FormPageShell + MSW 兜底)
- 纯函数 transformations.ts + 19 个 vitest 单测
- @contract-pending:exams(classId) 列表查询、createExam mutation 走 MSW
- 三态 UI(loading/error/empty)+ 路由级 loading.tsx/error.tsx
- i18n:zh-CN/en 双语补全,无硬编码中文
- MSW handlers 支持 variables 透传

§11.3 DoD 验收:
- lint: 0 errors(4 个 __generated__ 预存警告)
- typecheck: 0 errors
- test: 250/250 passed(含 19 个新增 transformations 测试)
- lint:tokens: 0 errors
This commit is contained in:
SpecialX
2026-07-22 17:02:05 +08:00
parent 843c3c0144
commit d066da563f
19 changed files with 1578 additions and 10 deletions

View File

@@ -59,10 +59,12 @@ export async function POST(req: NextRequest): Promise<NextResponse> {
if (MSW_ENABLED) {
const body = (await req.json().catch(() => ({}))) as {
operationName?: string;
variables?: Record<string, unknown>;
};
return NextResponse.json(graphqlResponse(body.operationName), {
headers: { "Cache-Control": "no-store" },
});
return NextResponse.json(
graphqlResponse(body.operationName, body.variables),
{ headers: { "Cache-Control": "no-store" } },
);
}
const cookieHeader = req.headers.get("cookie");

View File

@@ -0,0 +1,22 @@
import { Suspense } from "react";
import { ExamDetailClient } from "@/features/teacher/exams/exam-detail-client";
import { DetailPageSkeleton } from "@/shared/components/page-templates";
/**
* 考试详情页ARCHITECTURE.md §7.3 详情页 / §9.1 / §10 P2
*
* Server Component 入口:仅负责 Suspense 边界包裹。
* 业务逻辑在 ExamDetailClientclient component中。
*
* 数据契约:单查 exam(id: ID!) ✅ schema 真实字段core-edu 子图)
*
* 关联ARCHITECTURE.md §5.5 / §7.3 / §9.1 / §10 P2 / §11.3
*/
export default function ExamDetailPage(): React.ReactElement {
return (
<Suspense fallback={<DetailPageSkeleton />}>
<ExamDetailClient />
</Suspense>
);
}

View File

@@ -0,0 +1,33 @@
"use client";
/**
* 考试路由错误边界ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD
* Next.js Route Segment error.tsx捕获子树未处理异常。
*/
import { useEffect } from "react";
import { Button } from "@/shared/components/ui/button";
export default function ExamsError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}): React.ReactElement {
useEffect(() => {
console.error("[portal-shell] exams route error:", error);
}, [error]);
return (
<div className="flex flex-col items-center justify-center gap-4 rounded-xl border border-destructive/30 bg-destructive/5 p-10">
<h2 className="text-lg font-semibold text-destructive"></h2>
<p className="text-sm text-muted-foreground">
{error.message || "未知错误"}
</p>
<Button onClick={reset} variant="outline">
</Button>
</div>
);
}

View File

@@ -0,0 +1,9 @@
import { ListPageSkeleton } from "@/shared/components/page-templates";
/**
* 考试列表页加载骨架ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD
* Next.js Route Segment loading.tsx自动包裹页面渲染期间。
*/
export default function ExamsLoading(): React.ReactElement {
return <ListPageSkeleton rows={5} />;
}

View File

@@ -0,0 +1,23 @@
import { Suspense } from "react";
import { NewExamClient } from "@/features/teacher/exams/new-exam-client";
import { FormPageSkeleton } from "@/shared/components/page-templates";
/**
* 新建考试表单页ARCHITECTURE.md §7.3 表单页 / §9.1 / §10 P2
*
* Server Component 入口:仅负责 Suspense 边界包裹useSearchParams 要求)。
* 业务逻辑在 NewExamClientclient component中。
*
* 数据契约mutation createExam(input) ❌ schema 无 Mutation → MSW 兜底(@contract-pending
* 契约工单docs/architecture/issues/contracts/core-edu_contract.md#create-exam-mutation
*
* 关联ARCHITECTURE.md §5.3 / §5.4 / §7.3 / §9.1 / §10 P2 / §11.3 / §11.4
*/
export default function NewExamPage(): React.ReactElement {
return (
<Suspense fallback={<FormPageSkeleton />}>
<NewExamClient />
</Suspense>
);
}

View File

@@ -0,0 +1,23 @@
import { Suspense } from "react";
import { ExamsListClient } from "@/features/teacher/exams/exams-list-client";
import { ListPageSkeleton } from "@/shared/components/page-templates";
/**
* 考试管理列表页ARCHITECTURE.md §7.3 列表页 / §9.1 / §10 P2
*
* Server Component 入口:仅负责 Suspense 边界包裹useSearchParams 要求)。
* 业务逻辑在 ExamsListClientclient component中。
*
* 数据契约:列表查询 exams(classId) ❌ schema 无此字段 → MSW 兜底(@contract-pending
* 契约工单docs/architecture/issues/contracts/core-edu_contract.md#exams-list
*
* 关联ARCHITECTURE.md §5.3 / §5.4 / §7.3 / §9.1 / §10 P2 / §11.3 / §11.4
*/
export default function ExamsListPage(): React.ReactElement {
return (
<Suspense fallback={<ListPageSkeleton rows={5} />}>
<ExamsListClient />
</Suspense>
);
}