Files
Edu/apps/portal-shell/src/lib/api/homework.ts
SpecialX 081cb5fbc3 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 页。
2026-07-22 18:11:15 +08:00

613 lines
15 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.
"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 };
}