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:
612
apps/portal-shell/src/lib/api/homework.ts
Normal file
612
apps/portal-shell/src/lib/api/homework.ts
Normal file
@@ -0,0 +1,612 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* Homework domain API(ARCHITECTURE.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-pending,MSW 兜底)。
|
||||
*
|
||||
* 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-pending,MSW 兜底)。
|
||||
*
|
||||
* 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-pending,MSW 兜底)。
|
||||
*
|
||||
* 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-pending,MSW 兜底)。
|
||||
*
|
||||
* 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-pending,MSW 兜底)。
|
||||
*
|
||||
* 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-pending,MSW 兜底)。
|
||||
*
|
||||
* 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-pending,MSW 兜底)。
|
||||
*
|
||||
* 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-pending,MSW 兜底)。
|
||||
*
|
||||
* 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-pending,MSW 兜底)。
|
||||
*
|
||||
* 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 };
|
||||
}
|
||||
@@ -15,6 +15,7 @@ export * from "./sidebar";
|
||||
export * from "./topbar";
|
||||
export * from "./teacher";
|
||||
export * from "./exams";
|
||||
export * from "./homework";
|
||||
export * from "./student";
|
||||
export * from "./parent";
|
||||
export * from "./admin";
|
||||
|
||||
261
apps/portal-shell/src/lib/api/operations/homework.graphql.ts
Normal file
261
apps/portal-shell/src/lib/api/operations/homework.graphql.ts
Normal file
@@ -0,0 +1,261 @@
|
||||
// Homework domain GraphQL documents (ARCHITECTURE.md §5.3 契约纪律 / §9.1)
|
||||
//
|
||||
// 拆分原则:
|
||||
// - GetHomework(按 id 单查):✅ combined-schema 中真实存在(homework(id: ID!): Homework)
|
||||
// - 其余 9 个查询/mutation:❌ schema 无对应字段/Mutation 类型
|
||||
// → 走 MSW 兜底(@contract-pending),等待后端补齐契约
|
||||
//
|
||||
// 契约工单:docs/architecture/issues/contracts/core-edu_contract.md
|
||||
// 关联:ARCHITECTURE.md §5.3 / §5.4 / §9.1 / §11.4
|
||||
import { gql } from "@apollo/client";
|
||||
|
||||
// ── 真实查询:homework(id) 单查 ─────────────────────────────────
|
||||
// 字段全部对齐 combined-schema.graphql 中 Homework 类型(core-edu 子图)
|
||||
export const GET_HOMEWORK_DOC = gql`
|
||||
query GetHomework($id: ID!) {
|
||||
homework(id: $id) {
|
||||
id
|
||||
classId
|
||||
subjectId
|
||||
title
|
||||
description
|
||||
dueDate
|
||||
gracePeriod
|
||||
status
|
||||
schoolId
|
||||
createdBy
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// ── 假契约查询(@contract-pending)─────────────────────────────
|
||||
// 列表查询:schema 无 homeworks(classId) 根字段
|
||||
// 页面通过 MSW 兜底获取列表数据,后端补齐后切换 fetcher 指向真实查询
|
||||
// 契约工单:core-edu_contract.md#homework-list
|
||||
export const GET_HOMEWORK_LIST_DOC = gql`
|
||||
query GetHomeworkList(
|
||||
$classId: ID!
|
||||
$status: String
|
||||
$limit: Int
|
||||
$offset: Int
|
||||
) {
|
||||
homeworks(
|
||||
classId: $classId
|
||||
status: $status
|
||||
limit: $limit
|
||||
offset: $offset
|
||||
) {
|
||||
items {
|
||||
id
|
||||
classId
|
||||
subjectId
|
||||
title
|
||||
description
|
||||
dueDate
|
||||
gracePeriod
|
||||
status
|
||||
createdAt
|
||||
}
|
||||
total
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// ── 假契约变更(@contract-pending)─────────────────────────────
|
||||
// 布置作业:schema 无 Mutation 类型
|
||||
// 页面通过 MSW 兜底提交,后端补齐 mutation 后切换 fetcher
|
||||
// 契约工单:core-edu_contract.md#assign-homework-mutation
|
||||
export const ASSIGN_HOMEWORK_DOC = gql`
|
||||
mutation AssignHomework($input: AssignHomeworkInput!) {
|
||||
assignHomework(input: $input) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// ── 跨作业提交列表(@contract-pending)─────────────────────────
|
||||
// schema 无 homeworkSubmissions 根字段 → MSW 兜底
|
||||
// 用于 /shell/teacher/homework/submissions 列表页
|
||||
// 契约工单:core-edu_contract.md#homework-submissions
|
||||
export const GET_HOMEWORK_SUBMISSIONS_DOC = gql`
|
||||
query GetHomeworkSubmissions(
|
||||
$classId: ID
|
||||
$homeworkId: ID
|
||||
$status: String
|
||||
$limit: Int
|
||||
$offset: Int
|
||||
) {
|
||||
homeworkSubmissions(
|
||||
classId: $classId
|
||||
homeworkId: $homeworkId
|
||||
status: $status
|
||||
limit: $limit
|
||||
offset: $offset
|
||||
) {
|
||||
items {
|
||||
id
|
||||
homeworkId
|
||||
homeworkTitle
|
||||
studentId
|
||||
studentName
|
||||
studentNo
|
||||
classId
|
||||
className
|
||||
status
|
||||
submittedAt
|
||||
gradedAt
|
||||
gradedBy
|
||||
totalScore
|
||||
maxScore
|
||||
}
|
||||
total
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// ── 单份提交详情(@contract-pending)───────────────────────────
|
||||
// schema 无 submissionDetail 根字段 → MSW 兜底
|
||||
// 用于 /shell/teacher/homework/submissions/[submissionId] 批改页
|
||||
// 契约工单:core-edu_contract.md#submission-detail
|
||||
export const GET_SUBMISSION_DETAIL_DOC = gql`
|
||||
query GetSubmissionDetail($submissionId: ID!) {
|
||||
submissionDetail(submissionId: $submissionId) {
|
||||
submission {
|
||||
id
|
||||
homeworkId
|
||||
homeworkTitle
|
||||
studentId
|
||||
studentName
|
||||
studentNo
|
||||
classId
|
||||
className
|
||||
status
|
||||
submittedAt
|
||||
gradedAt
|
||||
gradedBy
|
||||
totalScore
|
||||
maxScore
|
||||
feedback
|
||||
}
|
||||
answers {
|
||||
id
|
||||
submissionId
|
||||
questionId
|
||||
questionTitle
|
||||
questionType
|
||||
maxScore
|
||||
answer
|
||||
score
|
||||
teacherComment
|
||||
isCorrect
|
||||
aiSuggestion
|
||||
}
|
||||
navigation {
|
||||
prevId
|
||||
nextId
|
||||
currentIndex
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// ── 批改提交 mutation(@contract-pending)──────────────────────
|
||||
// schema 无 Mutation 类型 → MSW 兜底
|
||||
// 用于单份提交批改页保存评分
|
||||
// 契约工单:core-edu_contract.md#grade-submission
|
||||
export const GRADE_SUBMISSION_DOC = gql`
|
||||
mutation GradeSubmission($input: GradeSubmissionInput!) {
|
||||
gradeSubmission(input: $input) {
|
||||
submissionId
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// ── 保存扫描批改 mutation(@contract-pending)──────────────────
|
||||
// schema 无 Mutation 类型 → MSW 兜底
|
||||
// 用于扫描批改页保存
|
||||
// 契约工单:core-edu_contract.md#save-scan-grading
|
||||
export const SAVE_SCAN_GRADING_DOC = gql`
|
||||
mutation SaveScanGrading($input: SaveScanGradingInput!) {
|
||||
saveScanGrading(input: $input) {
|
||||
submissionId
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// ── AI 批量评分建议(@contract-pending)────────────────────────
|
||||
// schema 无 aiBatchGrading 根字段 → MSW 兜底
|
||||
// 用于按作业批量批改页 AI 评分建议
|
||||
// 契约工单:core-edu_contract.md#ai-batch-grading
|
||||
export const GET_AI_BATCH_GRADING_DOC = gql`
|
||||
query GetAiBatchGrading($homeworkId: ID!) {
|
||||
aiBatchGrading(homeworkId: $homeworkId) {
|
||||
homeworkId
|
||||
suggestions {
|
||||
submissionId
|
||||
studentName
|
||||
suggestedScore
|
||||
confidence
|
||||
reasoning
|
||||
}
|
||||
summary {
|
||||
totalSubmissions
|
||||
processed
|
||||
avgConfidence
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// ── 按作业拉所有提交(@contract-pending)───────────────────────
|
||||
// schema 无 assignmentSubmissions 根字段 → MSW 兜底
|
||||
// 用于 /shell/teacher/homework/assignments/[id]/submissions 批量批改视图
|
||||
// 契约工单:core-edu_contract.md#assignment-submissions
|
||||
export const GET_ASSIGNMENT_SUBMISSIONS_DOC = gql`
|
||||
query GetAssignmentSubmissions($homeworkId: ID!) {
|
||||
assignmentSubmissions(homeworkId: $homeworkId) {
|
||||
homework {
|
||||
id
|
||||
title
|
||||
classId
|
||||
className
|
||||
dueDate
|
||||
maxScore
|
||||
}
|
||||
stats {
|
||||
totalStudents
|
||||
submittedCount
|
||||
gradedCount
|
||||
pendingCount
|
||||
avgScore
|
||||
submissionRate
|
||||
}
|
||||
submissions {
|
||||
id
|
||||
studentId
|
||||
studentName
|
||||
studentNo
|
||||
status
|
||||
submittedAt
|
||||
gradedAt
|
||||
totalScore
|
||||
maxScore
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// ── 详情页内联批改 mutation(@contract-pending)────────────────
|
||||
// schema 无 Mutation 类型 → MSW 兜底
|
||||
// 用于作业详情页内联录入单份提交评分
|
||||
// 契约工单:core-edu_contract.md#record-grade
|
||||
export const RECORD_GRADE_DOC = gql`
|
||||
mutation RecordGrade($input: RecordGradeInput!) {
|
||||
recordGrade(input: $input) {
|
||||
gradeId
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -5,6 +5,7 @@ export * from "./sidebar.graphql";
|
||||
export * from "./topbar.graphql";
|
||||
export * from "./teacher.graphql";
|
||||
export * from "./exams.graphql";
|
||||
export * from "./homework.graphql";
|
||||
export * from "./student.graphql";
|
||||
export * from "./parent.graphql";
|
||||
export * from "./admin.graphql";
|
||||
|
||||
Reference in New Issue
Block a user