feat(portal-shell): homework 模块 7 页迁移(教师域 §9.1 B2)

§9.1 教师域 homework 模块完整迁移(继 exams 之后第二个 B2 模块):

7 页路由结构(与旧 teacher-portal 同构):
- /shell/teacher/homework:列表页(?classId/status/q 筛选)
- /shell/teacher/homework/new:布置作业表单页
- /shell/teacher/homework/[id]:详情 + 内联批改(含提交列表 + recordGrade 表单)
- /shell/teacher/homework/submissions:跨作业提交评审列表
- /shell/teacher/homework/submissions/[submissionId]:单份提交批改 + AI 建议 + 上下份导航
- /shell/teacher/homework/submissions/[submissionId]/scan-grading:扫描批改工作台(三栏)
- /shell/teacher/homework/assignments/[id]/submissions:按作业批量批改 + 统计 + AI 批量评分

数据契约(混合):
-  homework(id: ID!) 真实查询(schema 已就绪,详情页用)
-  列表/mutation/submissions/grading/aiBatchGrading 全部 @contract-pending MSW 兜底
  · 9 个 hook 走 MSW,待后端补齐 mutation 后切换真实 fetcher

§11.3 DoD 11 项验收:
1. route-permissions:EXACT + PREFIX 表 /shell/teacher/homework 已配置
2. 页面模板:list/new 用 ListPageShell/FormPageShell;detail/grading 用 DetailPageShell;
   scan-grading 用 WorkbenchPageShell(三栏,未使用 emptyNode)
3. 三态:loading(Skeleton)/error(errorNode 或 errorSummary)/empty(emptyNode) 全实现
4. lib/api hooks:homework.ts 10 个 hooks(useHomework 真实 + 9 个 MSW)
5. @contract-pending MSW:graphql-data.ts 扩展 6 块 mock + 10 个 switch case
6. i18n:homework 节点扩展 8 个分区共 130+ keys(list/detail/new/submissions/grading/
   scan/assignment/error)中英对齐
7. lint:0 errors(4 warnings 在 __generated__)
8. lint:tokens:0 errors
9. notify:mutation 反馈走 @/shared/lib/notify(非 sonner 直引)
10. vitest:transformations 纯函数单测齐全,全量 323/323 通过(新增 ~50 测试)
11. typecheck:0 errors(noUncheckedIndexedAccess 安全访问)

附带修复:
- 修复 2 处遗留 broken link:
  · widgets/sidebar/quick-actions: /homework/new → /shell/teacher/homework/new
  · widgets/topbar/global-search: /homework → /shell/teacher/homework
- scripts/check-page-count.ts baseline 同步 13 → 26(与 exams 6 + homework 7 一致)

剩余模块:grades(5)+lesson-plans(6)+questions(1)+textbooks(2)+attendance(4)+classes(3)+
students(1)+course-plans(2)+elective(3)+error-book(1)+diagnostic(2)+analytics(2)+ai-*(3)+
knowledge-graph(1)+practice(1)+schedule-changes(1)+leave(1) 共 39 页。
This commit is contained in:
SpecialX
2026-07-22 18:11:15 +08:00
parent dca25fc42f
commit 081cb5fbc3
28 changed files with 4810 additions and 6 deletions

View File

@@ -0,0 +1,612 @@
"use client";
/**
* Homework domain APIARCHITECTURE.md §5.1 / §5.3 / §9.1 教师域作业模块)
*
* 三类操作:
* 1. useHomework按 id 单查):✅ 真实查询 homework(id: ID!)schema 已就绪
* 2. 列表/提交查询/mutation❌ schema 无对应字段 → MSW 兜底(@contract-pending
*
* 契约工单docs/architecture/issues/contracts/core-edu_contract.md
* 后端补齐后:重跑 normalize + codegen → 关闭 skipDocumentsValidation → 切换 fetcher → 删 mock
*
* 关联ARCHITECTURE.md §5.3 契约纪律 / §5.4 MSW 兜底 / §9.1 / §11.4 契约工单
*/
import type { FetchPolicy } from "@apollo/client";
import { useWidgetMutation } from "../useWidgetMutation";
import { useWidgetQuery } from "../useWidgetQuery";
import { ApiError } from "./errors";
import {
ASSIGN_HOMEWORK_DOC,
GET_AI_BATCH_GRADING_DOC,
GET_ASSIGNMENT_SUBMISSIONS_DOC,
GET_HOMEWORK_DOC,
GET_HOMEWORK_LIST_DOC,
GET_HOMEWORK_SUBMISSIONS_DOC,
GET_SUBMISSION_DETAIL_DOC,
GRADE_SUBMISSION_DOC,
RECORD_GRADE_DOC,
SAVE_SCAN_GRADING_DOC,
} from "./operations/homework.graphql";
import type { UseQueryResult } from "./types";
// ===== 数据类型(对齐 schema Homework 类型)=====
/**
* 作业实体(对齐 combined-schema.graphql Homework 类型core-edu 子图)
*
* 字段命名 camelCase与 schema 一致)。
*/
export interface Homework {
id: string;
classId: string;
subjectId: string;
title: string;
description: string | null;
dueDate: string;
gracePeriod: number;
status: string;
schoolId: string;
createdBy: string;
createdAt: string;
updatedAt: string;
}
/** 作业列表项(轻量字段集,用于列表渲染) */
export interface HomeworkListItem {
id: string;
classId: string;
subjectId: string;
title: string;
description: string | null;
dueDate: string;
gracePeriod: number;
status: string;
createdAt: string;
}
/** 列表查询响应(@contract-pending 假契约形状MSW 返回此结构) */
interface HomeworkListResponse {
homeworks: {
items: HomeworkListItem[];
total: number;
};
}
/** 单查响应(真实 schema */
interface HomeworkResponse {
homework: Homework | null;
}
/** 布置作业输入 */
export interface AssignHomeworkInput {
classId: string;
subjectId: string;
title: string;
description?: string;
dueDate: string;
gracePeriod?: number;
}
/** 布置作业 mutation 响应(@contract-pending */
interface AssignHomeworkResponse {
assignHomework: { id: string } | null;
}
// ===== 提交相关类型(@contract-pending 全 MSW=====
/** 提交状态枚举 */
export type SubmissionStatus =
"SUBMITTED" | "GRADING" | "GRADED" | "RETURNED" | "LATE";
/** 作业提交列表项 */
export interface HomeworkSubmissionItem {
id: string;
homeworkId: string;
homeworkTitle: string;
studentId: string;
studentName: string;
studentNo: string;
classId: string;
className: string;
status: string;
submittedAt: string | null;
gradedAt: string | null;
gradedBy: string | null;
totalScore: number | null;
maxScore: number;
}
/** 提交列表筛选 */
export interface SubmissionsFilter {
classId?: string;
homeworkId?: string;
status?: string;
limit?: number;
offset?: number;
}
/** 提交列表响应 */
interface HomeworkSubmissionsResponse {
homeworkSubmissions: { items: HomeworkSubmissionItem[]; total: number };
}
/** 单题作答 */
export interface SubmissionAnswer {
id: string;
submissionId: string;
questionId: string;
questionTitle: string;
questionType: string;
maxScore: number;
answer: string;
score: number | null;
teacherComment: string | null;
isCorrect: boolean | null;
aiSuggestion: string | null;
}
/** 批改页导航信息 */
export interface SubmissionNavigation {
prevId: string | null;
nextId: string | null;
currentIndex: number;
totalCount: number;
}
/** 单份提交详情 */
export interface SubmissionDetail {
submission: HomeworkSubmissionItem & { feedback: string | null };
answers: SubmissionAnswer[];
navigation: SubmissionNavigation;
}
/** 单份提交详情响应 */
interface SubmissionDetailResponse {
submissionDetail: SubmissionDetail | null;
}
/** 批改提交输入 */
export interface GradeSubmissionInput {
submissionId: string;
answers: Array<{
questionId: string;
score: number;
teacherComment?: string;
}>;
feedback?: string;
}
/** 批改提交响应 */
interface GradeSubmissionResponse {
gradeSubmission: { submissionId: string } | null;
}
/** 保存扫描批改输入 */
export interface SaveScanGradingInput {
submissionId: string;
answers: Array<{
questionId: string;
score: number;
teacherComment?: string;
}>;
feedback?: string;
}
/** 保存扫描批改响应 */
interface SaveScanGradingResponse {
saveScanGrading: { submissionId: string } | null;
}
/** AI 批量评分建议项 */
export interface AiGradingSuggestion {
submissionId: string;
studentName: string;
suggestedScore: number;
confidence: number;
reasoning: string;
}
/** AI 批量评分汇总 */
export interface AiBatchGradingSummary {
totalSubmissions: number;
processed: number;
avgConfidence: number;
}
/** AI 批量评分数据 */
export interface AiBatchGrading {
homeworkId: string;
suggestions: AiGradingSuggestion[];
summary: AiBatchGradingSummary;
}
/** AI 批量评分响应 */
interface AiBatchGradingResponse {
aiBatchGrading: AiBatchGrading | null;
}
/** 按作业汇总统计 */
export interface AssignmentStats {
totalStudents: number;
submittedCount: number;
gradedCount: number;
pendingCount: number;
avgScore: number;
submissionRate: number;
}
/** 按作业的作业摘要 */
export interface AssignmentHomeworkSummary {
id: string;
title: string;
classId: string;
className: string;
dueDate: string;
maxScore: number;
}
/** 按作业的提交列表数据 */
export interface AssignmentSubmissions {
homework: AssignmentHomeworkSummary;
stats: AssignmentStats;
submissions: HomeworkSubmissionItem[];
}
/** 按作业提交列表响应 */
interface AssignmentSubmissionsResponse {
assignmentSubmissions: AssignmentSubmissions | null;
}
/** 详情页内联批改输入 */
export interface RecordGradeInput {
homeworkId: string;
studentId: string;
score: number;
feedback?: string;
}
/** 详情页内联批改响应 */
interface RecordGradeResponse {
recordGrade: { gradeId: string } | null;
}
// ===== 查询选项 =====
export interface HomeworkQueryOptions {
enabled?: boolean;
pollInterval?: number;
fetchPolicy?: FetchPolicy;
}
// ===== Hooks =====
/**
* 按 id 查询作业详情(真实 schema✅ 契约已就绪)。
*
* 关联ARCHITECTURE.md §5.5 后端已就绪查询 / §9.1 详情页
*/
export function useHomework(
id: string,
options?: HomeworkQueryOptions,
): UseQueryResult<Homework | null> {
const result = useWidgetQuery<HomeworkResponse, { id: string }>(
GET_HOMEWORK_DOC,
{ id },
{
...options,
enabled: options?.enabled ?? id.length > 0,
},
);
return {
data: result.data?.homework ?? null,
loading: result.loading,
error: result.error,
refetch: result.refetch,
};
}
/**
* 查询班级下的作业列表(@contract-pendingMSW 兜底)。
*
* schema 无 homeworks(classId) 根字段,由 MSW handlers 返回 mock 数据。
* 后端补齐列表查询后切换到真实 fetcher页面无需改动。
*
* 关联ARCHITECTURE.md §5.4 / §9.1 列表页 / §11.4 契约工单
*/
export function useHomeworkList(
classId: string,
options?: HomeworkQueryOptions & {
status?: string;
limit?: number;
offset?: number;
},
): UseQueryResult<{ items: HomeworkListItem[]; total: number }> {
const result = useWidgetQuery<
HomeworkListResponse,
{
classId: string;
status?: string;
limit?: number;
offset?: number;
}
>(
GET_HOMEWORK_LIST_DOC,
{
classId,
status: options?.status,
limit: options?.limit,
offset: options?.offset,
},
{
enabled: options?.enabled ?? classId.length > 0,
fetchPolicy: options?.fetchPolicy,
pollInterval: options?.pollInterval,
},
);
return {
data: result.data?.homeworks,
loading: result.loading,
error: result.error,
refetch: result.refetch,
};
}
/**
* 布置作业 mutation@contract-pendingMSW 兜底)。
*
* schema 无 Mutation 类型,由 MSW handlers 返回 mock 数据。
* 后端补齐 mutation 后切换到真实 fetcher。
*
* 关联ARCHITECTURE.md §5.4 / §9.1 表单页 / §11.4 契约工单
*/
export function useAssignHomework(): {
run: (input: AssignHomeworkInput) => Promise<{ id: string }>;
loading: boolean;
error: unknown;
} {
const {
run: rawRun,
loading,
error,
} = useWidgetMutation<AssignHomeworkResponse, { input: AssignHomeworkInput }>(
ASSIGN_HOMEWORK_DOC,
);
const run = async (input: AssignHomeworkInput): Promise<{ id: string }> => {
const data = await rawRun({ input });
if (!data?.assignHomework) {
throw new ApiError("Failed to assign homework", "INTERNAL_ERROR");
}
return data.assignHomework;
};
return { run, loading, error };
}
/**
* 查询跨作业提交列表(@contract-pendingMSW 兜底)。
*
* schema 无 homeworkSubmissions 根字段,由 MSW handlers 返回 mock 数据。
* 用于 /shell/teacher/homework/submissions 列表页。
*
* 关联ARCHITECTURE.md §5.4 / §9.1 submissions 列表页 / §11.4 契约工单
*/
export function useHomeworkSubmissions(
filter: SubmissionsFilter,
options?: HomeworkQueryOptions,
): UseQueryResult<{ items: HomeworkSubmissionItem[]; total: number }> {
const result = useWidgetQuery<
HomeworkSubmissionsResponse,
{
classId?: string;
homeworkId?: string;
status?: string;
limit?: number;
offset?: number;
}
>(GET_HOMEWORK_SUBMISSIONS_DOC, filter, {
enabled: options?.enabled ?? true,
fetchPolicy: options?.fetchPolicy,
pollInterval: options?.pollInterval,
});
return {
data: result.data?.homeworkSubmissions,
loading: result.loading,
error: result.error,
refetch: result.refetch,
};
}
/**
* 查询单份提交详情(@contract-pendingMSW 兜底)。
*
* schema 无 submissionDetail 根字段,由 MSW handlers 返回 mock 数据。
* 用于 /shell/teacher/homework/submissions/[submissionId] 批改页。
*
* 关联ARCHITECTURE.md §5.4 / §9.1 批改页 / §11.4 契约工单
*/
export function useSubmissionDetail(
submissionId: string,
options?: HomeworkQueryOptions,
): UseQueryResult<SubmissionDetail | null> {
const result = useWidgetQuery<
SubmissionDetailResponse,
{ submissionId: string }
>(
GET_SUBMISSION_DETAIL_DOC,
{ submissionId },
{
...options,
enabled: options?.enabled ?? submissionId.length > 0,
},
);
return {
data: result.data?.submissionDetail ?? null,
loading: result.loading,
error: result.error,
refetch: result.refetch,
};
}
/**
* 批改提交 mutation@contract-pendingMSW 兜底)。
*
* schema 无 Mutation 类型,由 MSW handlers 返回 mock 数据。
* 用于单份提交批改页保存评分。
*
* 关联ARCHITECTURE.md §5.4 / §9.1 批改页 / §11.4 契约工单
*/
export function useGradeSubmission(): {
run: (input: GradeSubmissionInput) => Promise<{ submissionId: string }>;
loading: boolean;
error: unknown;
} {
const {
run: rawRun,
loading,
error,
} = useWidgetMutation<
GradeSubmissionResponse,
{ input: GradeSubmissionInput }
>(GRADE_SUBMISSION_DOC);
const run = async (
input: GradeSubmissionInput,
): Promise<{ submissionId: string }> => {
const data = await rawRun({ input });
if (!data?.gradeSubmission) {
throw new ApiError("Failed to grade submission", "INTERNAL_ERROR");
}
return data.gradeSubmission;
};
return { run, loading, error };
}
/**
* 保存扫描批改 mutation@contract-pendingMSW 兜底)。
*
* schema 无 Mutation 类型,由 MSW handlers 返回 mock 数据。
* 用于扫描批改页保存。
*
* 关联ARCHITECTURE.md §5.4 / §9.1 扫描批改页 / §11.4 契约工单
*/
export function useSaveScanGrading(): {
run: (input: SaveScanGradingInput) => Promise<{ submissionId: string }>;
loading: boolean;
error: unknown;
} {
const {
run: rawRun,
loading,
error,
} = useWidgetMutation<
SaveScanGradingResponse,
{ input: SaveScanGradingInput }
>(SAVE_SCAN_GRADING_DOC);
const run = async (
input: SaveScanGradingInput,
): Promise<{ submissionId: string }> => {
const data = await rawRun({ input });
if (!data?.saveScanGrading) {
throw new ApiError("Failed to save scan grading", "INTERNAL_ERROR");
}
return data.saveScanGrading;
};
return { run, loading, error };
}
/**
* 查询 AI 批量评分建议(@contract-pendingMSW 兜底)。
*
* schema 无 aiBatchGrading 根字段,由 MSW handlers 返回 mock 数据。
* 用于按作业批量批改页 AI 评分建议。
*
* 关联ARCHITECTURE.md §5.4 / §9.1 批量批改页 / §11.4 契约工单
*/
export function useAiBatchGrading(
homeworkId: string,
options?: HomeworkQueryOptions,
): UseQueryResult<AiBatchGrading | null> {
const result = useWidgetQuery<AiBatchGradingResponse, { homeworkId: string }>(
GET_AI_BATCH_GRADING_DOC,
{ homeworkId },
{
...options,
enabled: options?.enabled ?? homeworkId.length > 0,
},
);
return {
data: result.data?.aiBatchGrading ?? null,
loading: result.loading,
error: result.error,
refetch: result.refetch,
};
}
/**
* 查询按作业拉所有提交(@contract-pendingMSW 兜底)。
*
* schema 无 assignmentSubmissions 根字段,由 MSW handlers 返回 mock 数据。
* 用于 /shell/teacher/homework/assignments/[id]/submissions 批量批改视图。
*
* 关联ARCHITECTURE.md §5.4 / §9.1 批量批改页 / §11.4 契约工单
*/
export function useAssignmentSubmissions(
homeworkId: string,
options?: HomeworkQueryOptions,
): UseQueryResult<AssignmentSubmissions | null> {
const result = useWidgetQuery<
AssignmentSubmissionsResponse,
{ homeworkId: string }
>(
GET_ASSIGNMENT_SUBMISSIONS_DOC,
{ homeworkId },
{
...options,
enabled: options?.enabled ?? homeworkId.length > 0,
},
);
return {
data: result.data?.assignmentSubmissions ?? null,
loading: result.loading,
error: result.error,
refetch: result.refetch,
};
}
/**
* 详情页内联批改 mutation@contract-pendingMSW 兜底)。
*
* schema 无 Mutation 类型,由 MSW handlers 返回 mock 数据。
* 用于作业详情页内联录入单份提交评分。
*
* 关联ARCHITECTURE.md §5.4 / §9.1 详情页 / §11.4 契约工单
*/
export function useRecordGrade(): {
run: (input: RecordGradeInput) => Promise<{ gradeId: string }>;
loading: boolean;
error: unknown;
} {
const {
run: rawRun,
loading,
error,
} = useWidgetMutation<RecordGradeResponse, { input: RecordGradeInput }>(
RECORD_GRADE_DOC,
);
const run = async (input: RecordGradeInput): Promise<{ gradeId: string }> => {
const data = await rawRun({ input });
if (!data?.recordGrade) {
throw new ApiError("Failed to record grade", "INTERNAL_ERROR");
}
return data.recordGrade;
};
return { run, loading, error };
}