feat(portal-shell): attendance + classes + students 模块 8 页迁移(教师域 §9.1 B2)

§9.1 line 632-634 教师域:
- /shell/teacher/attendance (列表) / /sheet (表单) / /report (报表) / /stats (统计) — 4 页
- /shell/teacher/classes (列表) / /[id] (详情) / /schedule (课表) — 3 页
- /shell/teacher/students (列表) — 1 页
契约:
- attendance 全  → MSW 兜底
- classes 🟡 classInfo(id)  真实单查 + 列表  MSW
- students 全  → MSW 兜底

新增文件:
- src/lib/api/{attendance,classes,students}.ts (14 hooks 合计)
- src/lib/api/operations/{attendance,classes,students}.graphql.ts (14 documents)
- src/features/teacher/{attendance,classes,students}/ (clients + transformations + tests)
- src/app/shell/teacher/{attendance,classes,students}/ (8 page.tsx + 3 loading + 3 error)

修改文件:
- src/mocks/graphql-data.ts (12 mock 数据 + handler cases)
- src/messages/{zh-CN,en}.json (attendance/classes/students i18n 命名空间)
- src/lib/api/{index,operations/index}.ts (导出 attendance/classes/students)
- src/shared/lib/route-permissions.ts (attendance/classes/students 路由权限)
- scripts/check-page-count.ts (baseline 40 → 48)

DoD 验收(§11.3 11 项):
- typecheck 0 errors
- lint 0 errors
- vitest 566 tests passed
- lint:tokens 0 errors
- check:pages 48 PASS
- route-permissions 已声明
- 三态齐备
- @contract-pending + MSW 兜底
- i18n zh-CN + en 同步

设计决策:
- classes/[id] 走真实 classInfo(id) 查询
- 无 STUDENT_* 权限点,students 路由复用 CLASS_READ/CLASS_MANAGE

关联:ARCHITECTURE.md §5.3 / §5.4 / §5.5 / §9.1 / §10 P2 / §11.3 / §11.4
契约工单:docs/architecture/issues/contracts/core-edu_contract.md
This commit is contained in:
SpecialX
2026-07-22 21:58:07 +08:00
parent 33ebb9a652
commit 73e09ca29a
41 changed files with 5876 additions and 8 deletions

View File

@@ -0,0 +1,384 @@
"use client";
/**
* Attendance domain APIARCHITECTURE.md §5.1 / §5.3 / §9.1 教师域考勤模块)
*
* 全部操作(@contract-pending
* 1. useAttendanceRecords列表查询❌ schema 无 attendanceRecords(...) → MSW 兜底
* 2. useAttendanceSheet点名表查询❌ schema 无 attendanceSheet(...) → MSW 兜底
* 3. useAttendanceReport报表查询❌ schema 无 attendanceReport(...) → MSW 兜底
* 4. useAttendanceStats统计查询❌ schema 无 attendanceStats(...) → MSW 兜底
* 5. useSaveAttendanceSheetmutation❌ schema 无 Mutation → MSW 兜底
*
* 契约工单docs/architecture/issues/contracts/classes_contract.md#attendance
* 后端补齐后:重跑 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 {
GET_ATTENDANCE_RECORDS_DOC,
GET_ATTENDANCE_REPORT_DOC,
GET_ATTENDANCE_SHEET_DOC,
GET_ATTENDANCE_STATS_DOC,
SAVE_ATTENDANCE_SHEET_DOC,
} from "./operations/attendance.graphql";
import type { UseQueryResult } from "./types";
// ===== 数据类型 =====
/** 考勤状态枚举(对齐业务约定) */
export const ATTENDANCE_STATUS = {
PRESENT: "present",
ABSENT: "absent",
LATE: "late",
LEAVE: "leave",
} as const;
/** 考勤记录实体(列表项,@contract-pendingMSW 提供形状) */
export interface AttendanceRecord {
id: string;
studentId: string;
studentName: string;
classId: string;
className: string;
date: string;
status: string;
remark: string | null;
recordedBy: string;
createdAt: string;
updatedAt: string;
}
/** 点名表条目(单个学生当日出勤) */
export interface AttendanceSheetEntry {
studentId: string;
studentName: string;
status: string;
remark: string | null;
}
/** 点名表整体(某班级某日全部学生出勤) */
export interface AttendanceSheet {
classId: string;
className: string;
date: string;
entries: AttendanceSheetEntry[];
}
/** 考勤报表汇总 */
export interface AttendanceReportSummary {
total: number;
present: number;
absent: number;
late: number;
leave: number;
attendanceRate: number;
}
/** 考勤报表行项(按学生聚合) */
export interface AttendanceReportItem {
studentId: string;
studentName: string;
present: number;
absent: number;
late: number;
leave: number;
attendanceRate: number;
}
/** 考勤报表整体 */
export interface AttendanceReport {
classId: string;
className: string;
range: string;
summary: AttendanceReportSummary;
items: AttendanceReportItem[];
}
/** 状态分布项 */
export interface AttendanceStatusDistribution {
status: string;
count: number;
ratio: number;
}
/** 趋势项 */
export interface AttendanceTrendItem {
date: string;
attendanceRate: number;
}
/** 班级排名项 */
export interface AttendanceClassRankingItem {
classId: string;
className: string;
attendanceRate: number;
totalStudents: number;
}
/** 考勤统计整体 */
export interface AttendanceStats {
classId: string | null;
overallAttendanceRate: number;
totalRecords: number;
statusDistribution: AttendanceStatusDistribution[];
trend: AttendanceTrendItem[];
classRanking: AttendanceClassRankingItem[];
}
// ===== 响应类型(@contract-pending 假契约形状MSW 返回此结构) =====
interface AttendanceRecordsResponse {
attendanceRecords: {
items: AttendanceRecord[];
total: number;
};
}
interface AttendanceSheetResponse {
attendanceSheet: AttendanceSheet | null;
}
interface AttendanceReportResponse {
attendanceReport: AttendanceReport | null;
}
interface AttendanceStatsResponse {
attendanceStats: AttendanceStats | null;
}
interface SaveAttendanceSheetResponse {
saveAttendanceSheet: {
classId: string;
date: string;
savedCount: number;
} | null;
}
// ===== 筛选与输入类型 =====
export interface AttendanceRecordsFilter {
classId?: string;
date?: string;
status?: string;
q?: string;
limit?: number;
offset?: number;
}
export interface AttendanceStatsFilter {
classId?: string;
startDate?: string;
endDate?: string;
}
/** 点名表保存输入条目 */
export interface AttendanceEntryInput {
studentId: string;
status: string;
remark?: string;
}
export interface SaveAttendanceSheetInput {
classId: string;
date: string;
entries: AttendanceEntryInput[];
}
// ===== 查询选项 =====
export interface AttendanceQueryOptions {
enabled?: boolean;
pollInterval?: number;
fetchPolicy?: FetchPolicy;
}
// ===== Hooks =====
/**
* 查询考勤记录列表(@contract-pendingMSW 兜底)。
*
* schema 无 attendanceRecords(...) 根字段,由 MSW handlers 返回 mock 数据。
* 后端补齐列表查询后切换到真实 fetcher页面无需改动。
*
* 关联ARCHITECTURE.md §5.4 / §9.1 列表页 / §11.4 契约工单
*/
export function useAttendanceRecords(
filter: AttendanceRecordsFilter,
options?: AttendanceQueryOptions,
): UseQueryResult<{ items: AttendanceRecord[]; total: number }> {
const result = useWidgetQuery<
AttendanceRecordsResponse,
{
classId?: string;
date?: string;
status?: string;
q?: string;
limit?: number;
offset?: number;
}
>(
GET_ATTENDANCE_RECORDS_DOC,
{
classId: filter.classId,
date: filter.date,
status: filter.status,
q: filter.q,
limit: filter.limit,
offset: filter.offset,
},
{
enabled: options?.enabled ?? true,
fetchPolicy: options?.fetchPolicy,
pollInterval: options?.pollInterval,
},
);
return {
data: result.data?.attendanceRecords,
loading: result.loading,
error: result.error,
refetch: result.refetch,
};
}
/**
* 查询某班级某日点名表(@contract-pendingMSW 兜底)。
*
* schema 无 attendanceSheet(classId, date) 根字段,由 MSW handlers 返回 mock 数据。
*
* 关联ARCHITECTURE.md §5.4 / §9.1 点名表页 / §11.4 契约工单
*/
export function useAttendanceSheet(
classId: string,
date: string,
options?: AttendanceQueryOptions,
): UseQueryResult<AttendanceSheet | null> {
const result = useWidgetQuery<
AttendanceSheetResponse,
{ classId: string; date: string }
>(
GET_ATTENDANCE_SHEET_DOC,
{ classId, date },
{
...options,
enabled: options?.enabled ?? (classId.length > 0 && date.length > 0),
},
);
return {
data: result.data?.attendanceSheet ?? null,
loading: result.loading,
error: result.error,
refetch: result.refetch,
};
}
/**
* 查询某班级考勤报表(@contract-pendingMSW 兜底)。
*
* schema 无 attendanceReport(classId, range) 根字段,由 MSW handlers 返回 mock 数据。
*
* 关联ARCHITECTURE.md §5.4 / §9.1 报表页 / §11.4 契约工单
*/
export function useAttendanceReport(
classId: string,
startDate?: string,
endDate?: string,
options?: AttendanceQueryOptions,
): UseQueryResult<AttendanceReport | null> {
const result = useWidgetQuery<
AttendanceReportResponse,
{ classId: string; startDate?: string; endDate?: string }
>(
GET_ATTENDANCE_REPORT_DOC,
{ classId, startDate, endDate },
{
...options,
enabled: options?.enabled ?? classId.length > 0,
},
);
return {
data: result.data?.attendanceReport ?? null,
loading: result.loading,
error: result.error,
refetch: result.refetch,
};
}
/**
* 查询考勤统计(@contract-pendingMSW 兜底)。
*
* schema 无 attendanceStats(...) 根字段,由 MSW handlers 返回 mock 数据。
*
* 关联ARCHITECTURE.md §5.4 / §9.1 统计页 / §11.4 契约工单
*/
export function useAttendanceStats(
filter: AttendanceStatsFilter,
options?: AttendanceQueryOptions,
): UseQueryResult<AttendanceStats | null> {
const result = useWidgetQuery<
AttendanceStatsResponse,
{ classId?: string; startDate?: string; endDate?: string }
>(
GET_ATTENDANCE_STATS_DOC,
{
classId: filter.classId,
startDate: filter.startDate,
endDate: filter.endDate,
},
{
enabled: options?.enabled ?? true,
fetchPolicy: options?.fetchPolicy,
pollInterval: options?.pollInterval,
},
);
return {
data: result.data?.attendanceStats ?? null,
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 useSaveAttendanceSheet(): {
run: (input: SaveAttendanceSheetInput) => Promise<{
classId: string;
date: string;
savedCount: number;
}>;
loading: boolean;
error: unknown;
} {
const {
run: rawRun,
loading,
error,
} = useWidgetMutation<
SaveAttendanceSheetResponse,
{ input: SaveAttendanceSheetInput }
>(SAVE_ATTENDANCE_SHEET_DOC);
const run = async (
input: SaveAttendanceSheetInput,
): Promise<{ classId: string; date: string; savedCount: number }> => {
const data = await rawRun({ input });
if (!data?.saveAttendanceSheet) {
throw new ApiError("Failed to save attendance sheet", "INTERNAL_ERROR");
}
return data.saveAttendanceSheet;
};
return { run, loading, error };
}

View File

@@ -0,0 +1,320 @@
"use client";
/**
* Classes domain APIARCHITECTURE.md §5.1 / §5.3 / §9.1 教师域班级模块)
*
* 混合契约:
* 1. useClassInfo按 id 单查):✅ 真实查询 classInfo(id: ID!)schema 已就绪
* 2. useClasses列表查询❌ schema 无 classes(...) 根字段 → MSW 兜底(@contract-pending
* 3. useClassSchedule课表❌ schema 无 classSchedule(classId) → MSW 兜底(@contract-pending
* 4. useClassStudents学生名单❌ schema 无 classStudents(classId) → MSW 兜底(@contract-pending
* 5. useClassTeachers任课老师❌ schema 无 classTeachers(classId) → MSW 兜底(@contract-pending
*
* 契约工单docs/architecture/issues/contracts/classes_contract.md
* 后端补齐后:重跑 normalize + codegen → 关闭 skipDocumentsValidation → 切换 fetcher → 删 mock
*
* 关联ARCHITECTURE.md §5.3 契约纪律 / §5.4 MSW 兜底 / §5.5 后端已就绪查询 / §9.1 / §11.4 契约工单
*/
import type { FetchPolicy } from "@apollo/client";
import { useWidgetQuery } from "../useWidgetQuery";
import {
GET_CLASS_INFO_DOC,
GET_CLASS_SCHEDULE_DOC,
GET_CLASS_STUDENTS_DOC,
GET_CLASS_TEACHERS_DOC,
GET_CLASSES_DOC,
} from "./operations/classes.graphql";
import type { UseQueryResult } from "./types";
// ===== 数据类型(对齐 schema ClassInfo 类型)=====
/**
* 班级实体(对齐 combined-schema.graphql ClassInfo 类型classes 子图)
*
* 字段命名 camelCase与 schema 一致)。
* schema ClassInfo: id / name / gradeId / headTeacherId / description / createdAt / updatedAt
*/
export interface ClassInfo {
id: string;
name: string;
gradeId: string;
headTeacherId: string | null;
description: string | null;
createdAt: string;
updatedAt: string;
}
/**
* 班级列表项(@contract-pending 扩展字段MSW 提供)
*
* schema ClassInfo 无 headTeacherName/studentCount/subjectCount 字段,
* 列表项中的这些字段由 MSW mock 扩展提供,后端补齐列表契约时同步对齐。
*/
export interface ClassListItem {
id: string;
name: string;
gradeId: string;
headTeacherId: string | null;
/** @contract-pending 列表扩展字段MSW 提供,后端补齐后对齐 */
headTeacherName: string | null;
description: string | null;
/** @contract-pending 列表扩展字段MSW 提供,后端补齐后对齐 */
studentCount: number;
/** @contract-pending 列表扩展字段MSW 提供,后端补齐后对齐 */
subjectCount: number;
createdAt: string;
updatedAt: string;
}
/** 课表条目 */
export interface ClassScheduleItem {
id: string;
weekday: number;
period: number;
subjectId: string;
subjectName: string;
teacherId: string;
teacherName: string;
classroom: string | null;
startTime: string;
endTime: string;
}
/** 班级课表整体(@contract-pending */
export interface ClassSchedule {
classId: string;
className: string;
weekRange: string;
items: ClassScheduleItem[];
}
/** 班级学生名单项(@contract-pending */
export interface ClassStudent {
id: string;
studentNo: string;
name: string;
gender: string;
classId: string;
className: string;
gradeId: string;
enrolledAt: string;
}
/** 班级任课老师项(@contract-pending */
export interface ClassTeacher {
id: string;
name: string;
subjectId: string;
subjectName: string;
role: string;
}
// ===== 响应类型 =====
/** 单查响应(真实 schema */
interface ClassInfoResponse {
classInfo: ClassInfo | null;
}
/** 列表查询响应(@contract-pending 假契约形状MSW 返回此结构) */
interface ClassesListResponse {
classes: {
items: ClassListItem[];
total: number;
};
}
/** 课表查询响应(@contract-pending */
interface ClassScheduleResponse {
classSchedule: ClassSchedule | null;
}
/** 学生名单响应(@contract-pending */
interface ClassStudentsResponse {
classStudents: {
items: ClassStudent[];
total: number;
};
}
/** 任课老师响应(@contract-pending */
interface ClassTeachersResponse {
classTeachers: {
items: ClassTeacher[];
total: number;
};
}
// ===== 筛选类型 =====
export interface ClassesListFilter {
gradeId?: string;
subjectId?: string;
q?: string;
limit?: number;
offset?: number;
}
// ===== 查询选项 =====
export interface ClassQueryOptions {
enabled?: boolean;
pollInterval?: number;
fetchPolicy?: FetchPolicy;
}
// ===== Hooks =====
/**
* 按 id 查询班级详情(真实 schema✅ 契约已就绪)。
*
* 关联ARCHITECTURE.md §5.5 后端已就绪查询 / §9.1 详情页
*/
export function useClassInfo(
id: string,
options?: ClassQueryOptions,
): UseQueryResult<ClassInfo | null> {
const result = useWidgetQuery<ClassInfoResponse, { id: string }>(
GET_CLASS_INFO_DOC,
{ id },
{
...options,
enabled: options?.enabled ?? id.length > 0,
},
);
return {
data: result.data?.classInfo ?? null,
loading: result.loading,
error: result.error,
refetch: result.refetch,
};
}
/**
* 查询班级列表(@contract-pendingMSW 兜底)。
*
* schema 无 classes(...) 根字段,由 MSW handlers 返回 mock 数据。
* 后端补齐列表查询后切换到真实 fetcher页面无需改动。
*
* 关联ARCHITECTURE.md §5.4 / §9.1 列表页 / §11.4 契约工单
*/
export function useClasses(
filter: ClassesListFilter,
options?: ClassQueryOptions,
): UseQueryResult<{ items: ClassListItem[]; total: number }> {
const result = useWidgetQuery<
ClassesListResponse,
{
gradeId?: string;
subjectId?: string;
q?: string;
limit?: number;
offset?: number;
}
>(
GET_CLASSES_DOC,
{
gradeId: filter.gradeId,
subjectId: filter.subjectId,
q: filter.q,
limit: filter.limit,
offset: filter.offset,
},
{
enabled: options?.enabled ?? true,
fetchPolicy: options?.fetchPolicy,
pollInterval: options?.pollInterval,
},
);
return {
data: result.data?.classes,
loading: result.loading,
error: result.error,
refetch: result.refetch,
};
}
/**
* 查询班级课表(@contract-pendingMSW 兜底)。
*
* schema 无 classSchedule(classId) 根字段ClassInfo 类型也无 schedule 字段。
* 详情页课表通过 MSW 返回 mock 数据,后端补齐后切换 fetcher。
*
* 关联ARCHITECTURE.md §5.4 / §9.1 课表页 / §11.4 契约工单
*/
export function useClassSchedule(
classId: string,
options?: ClassQueryOptions,
): UseQueryResult<ClassSchedule | null> {
const result = useWidgetQuery<ClassScheduleResponse, { classId: string }>(
GET_CLASS_SCHEDULE_DOC,
{ classId },
{
...options,
enabled: options?.enabled ?? classId.length > 0,
},
);
return {
data: result.data?.classSchedule ?? null,
loading: result.loading,
error: result.error,
refetch: result.refetch,
};
}
/**
* 查询班级学生名单(@contract-pendingMSW 兜底)。
*
* schema 无 classStudents(classId) 根字段ClassInfo 类型也无 students 字段。
* 详情页学生名单通过 MSW 返回 mock 数据,后端补齐后切换 fetcher。
*
* 关联ARCHITECTURE.md §5.4 / §9.1 详情页 / §11.4 契约工单
*/
export function useClassStudents(
classId: string,
options?: ClassQueryOptions,
): UseQueryResult<{ items: ClassStudent[]; total: number }> {
const result = useWidgetQuery<ClassStudentsResponse, { classId: string }>(
GET_CLASS_STUDENTS_DOC,
{ classId },
{
...options,
enabled: options?.enabled ?? classId.length > 0,
},
);
return {
data: result.data?.classStudents,
loading: result.loading,
error: result.error,
refetch: result.refetch,
};
}
/**
* 查询班级任课老师(@contract-pendingMSW 兜底)。
*
* schema 无 classTeachers(classId) 根字段ClassInfo 类型也无 teachers 字段。
* 详情页任课老师通过 MSW 返回 mock 数据,后端补齐后切换 fetcher。
*
* 关联ARCHITECTURE.md §5.4 / §9.1 详情页 / §11.4 契约工单
*/
export function useClassTeachers(
classId: string,
options?: ClassQueryOptions,
): UseQueryResult<{ items: ClassTeacher[]; total: number }> {
const result = useWidgetQuery<ClassTeachersResponse, { classId: string }>(
GET_CLASS_TEACHERS_DOC,
{ classId },
{
...options,
enabled: options?.enabled ?? classId.length > 0,
},
);
return {
data: result.data?.classTeachers,
loading: result.loading,
error: result.error,
refetch: result.refetch,
};
}

View File

@@ -20,6 +20,9 @@ export * from "./grades";
export * from "./lesson-plans";
export * from "./questions";
export * from "./textbooks";
export * from "./attendance";
export * from "./classes";
export * from "./students";
export * from "./student";
export * from "./parent";
export * from "./admin";

View File

@@ -0,0 +1,159 @@
// Attendance domain GraphQL documents (ARCHITECTURE.md §5.3 契约纪律 / §9.1)
//
// 拆分原则:
// - 全部操作:❌ schema 无 attendance / attendanceRecords / attendanceSheet / attendanceReport /
// attendanceStats 根字段,也无 Mutation 类型
// → 走 MSW 兜底(@contract-pending等待后端补齐契约
//
// 契约工单docs/architecture/issues/contracts/classes_contract.md#attendance
// 关联ARCHITECTURE.md §5.3 / §5.4 / §9.1 / §11.4
import { gql } from "@apollo/client";
// ── 假契约查询(@contract-pending─────────────────────────────
// 列表查询schema 无 attendanceRecords(...) 根字段
// 页面通过 MSW 兜底获取列表数据,后端补齐后切换 fetcher 指向真实查询
// 契约工单classes_contract.md#attendance-records-list
export const GET_ATTENDANCE_RECORDS_DOC = gql`
query GetAttendanceRecords(
$classId: ID
$date: String
$status: String
$q: String
$limit: Int
$offset: Int
) {
attendanceRecords(
classId: $classId
date: $date
status: $status
q: $q
limit: $limit
offset: $offset
) {
items {
id
studentId
studentName
classId
className
date
status
remark
recordedBy
createdAt
updatedAt
}
total
}
}
`;
// ── 点名表查询(@contract-pending─────────────────────────────
// schema 无 attendanceSheet(classId, date) 根字段
// 点名表页通过 MSW 兜底获取某班级某日全部学生出勤,后端补齐后切换 fetcher
// 契约工单classes_contract.md#attendance-sheet
export const GET_ATTENDANCE_SHEET_DOC = gql`
query GetAttendanceSheet($classId: ID!, $date: String!) {
attendanceSheet(classId: $classId, date: $date) {
classId
className
date
entries {
studentId
studentName
status
remark
}
}
}
`;
// ── 考勤报表查询(@contract-pending─────────────────────────────
// schema 无 attendanceReport(classId, range) 根字段
// 报表页通过 MSW 兜底获取按班级的考勤报表,后端补齐后切换 fetcher
// 契约工单classes_contract.md#attendance-report
export const GET_ATTENDANCE_REPORT_DOC = gql`
query GetAttendanceReport(
$classId: ID!
$startDate: String
$endDate: String
) {
attendanceReport(
classId: $classId
startDate: $startDate
endDate: $endDate
) {
classId
className
range
summary {
total
present
absent
late
leave
attendanceRate
}
items {
studentId
studentName
present
absent
late
leave
attendanceRate
}
}
}
`;
// ── 考勤统计查询(@contract-pending─────────────────────────────
// schema 无 attendanceStats(...) 根字段
// 统计页通过 MSW 兜底获取考勤聚合统计,后端补齐后切换 fetcher
// 契约工单classes_contract.md#attendance-stats
export const GET_ATTENDANCE_STATS_DOC = gql`
query GetAttendanceStats($classId: ID, $startDate: String, $endDate: String) {
attendanceStats(
classId: $classId
startDate: $startDate
endDate: $endDate
) {
classId
overallAttendanceRate
totalRecords
statusDistribution {
status
count
ratio
}
trend {
date
attendanceRate
}
classRanking {
classId
className
attendanceRate
totalStudents
}
}
}
`;
// ── 假契约变更(@contract-pending─────────────────────────────
// 保存点名表schema 无 Mutation 类型
// 页面通过 MSW 兜底提交,后端补齐 mutation 后切换 fetcher
// 契约工单classes_contract.md#save-attendance-sheet-mutation
export const SAVE_ATTENDANCE_SHEET_DOC = gql`
mutation SaveAttendanceSheet(
$classId: ID!
$date: String!
$entries: [AttendanceEntryInput!]!
) {
saveAttendanceSheet(classId: $classId, date: $date, entries: $entries) {
classId
date
savedCount
}
}
`;

View File

@@ -0,0 +1,134 @@
// Classes domain GraphQL documents (ARCHITECTURE.md §5.3 契约纪律 / §9.1)
//
// 拆分原则:
// - GetClassInfo按 id 单查):✅ combined-schema 中真实存在classInfo(id: ID!): ClassInfo
// - GetClasses列表查询❌ schema 无 classes(...) 根字段
// → 走 MSW 兜底(@contract-pending等待后端补齐列表契约
// - GetClassSchedule课表❌ schema 无 classSchedule(classId) 根字段
// → 走 MSW 兜底(@contract-pending
// - GetClassStudents / GetClassTeachers❌ schema 无对应根字段
// → 走 MSW 兜底(@contract-pendingClassInfo 类型无 students/teachers 字段
//
// 契约工单docs/architecture/issues/contracts/classes_contract.md
// 关联ARCHITECTURE.md §5.3 / §5.4 / §5.5 / §9.1 / §11.4
import { gql } from "@apollo/client";
// ── 真实查询classInfo(id) 单查 ─────────────────────────────────
// 字段全部对齐 combined-schema.graphql 中 ClassInfo 类型classes 子图)
// ClassInfo: id / name / gradeId / headTeacherId / description / createdAt / updatedAt
export const GET_CLASS_INFO_DOC = gql`
query GetClassInfo($id: ID!) {
classInfo(id: $id) {
id
name
gradeId
headTeacherId
description
createdAt
updatedAt
}
}
`;
// ── 假契约查询(@contract-pending─────────────────────────────
// 列表查询schema 无 classes(...) 根字段
// 页面通过 MSW 兜底获取列表数据,后端补齐后切换 fetcher 指向真实查询
// 契约工单classes_contract.md#classes-list
export const GET_CLASSES_DOC = gql`
query GetClasses(
$gradeId: String
$subjectId: String
$q: String
$limit: Int
$offset: Int
) {
classes(
gradeId: $gradeId
subjectId: $subjectId
q: $q
limit: $limit
offset: $offset
) {
items {
id
name
gradeId
headTeacherId
headTeacherName
description
studentCount
subjectCount
createdAt
updatedAt
}
total
}
}
`;
// ── 班级课表查询(@contract-pending───────────────────────────
// schema 无 classSchedule(classId) 根字段ClassInfo 类型也无 schedule 字段
// 详情页课表通过 MSW 返回 mock 数据,后端补齐后切换 fetcher
// 契约工单classes_contract.md#class-schedule
export const GET_CLASS_SCHEDULE_DOC = gql`
query GetClassSchedule($classId: ID!) {
classSchedule(classId: $classId) {
classId
className
weekRange
items {
id
weekday
period
subjectId
subjectName
teacherId
teacherName
classroom
startTime
endTime
}
}
}
`;
// ── 班级学生名单查询(@contract-pending───────────────────────
// schema 无 classStudents(classId) 根字段ClassInfo 类型也无 students 字段
// 详情页学生名单通过 MSW 返回 mock 数据,后端补齐后切换 fetcher
// 契约工单classes_contract.md#class-students
export const GET_CLASS_STUDENTS_DOC = gql`
query GetClassStudents($classId: ID!) {
classStudents(classId: $classId) {
items {
id
studentNo
name
gender
classId
className
gradeId
enrolledAt
}
total
}
}
`;
// ── 班级任课老师查询(@contract-pending───────────────────────
// schema 无 classTeachers(classId) 根字段ClassInfo 类型也无 teachers 字段
// 详情页任课老师通过 MSW 返回 mock 数据,后端补齐后切换 fetcher
// 契约工单classes_contract.md#class-teachers
export const GET_CLASS_TEACHERS_DOC = gql`
query GetClassTeachers($classId: ID!) {
classTeachers(classId: $classId) {
items {
id
name
subjectId
subjectName
role
}
total
}
}
`;

View File

@@ -10,6 +10,9 @@ export * from "./grades.graphql";
export * from "./lesson-plans.graphql";
export * from "./questions.graphql";
export * from "./textbooks.graphql";
export * from "./attendance.graphql";
export * from "./classes.graphql";
export * from "./students.graphql";
export * from "./student.graphql";
export * from "./parent.graphql";
export * from "./admin.graphql";

View File

@@ -0,0 +1,62 @@
// Students domain GraphQL documents (ARCHITECTURE.md §5.3 契约纪律 / §9.1)
//
// 拆分原则:
// - 全部操作:❌ schema 无 students(...) 列表 / student(id) 单查根字段
// → 走 MSW 兜底(@contract-pending等待后端补齐契约
//
// 契约工单docs/architecture/issues/contracts/classes_contract.md#students
// 关联ARCHITECTURE.md §5.3 / §5.4 / §9.1 / §11.4
import { gql } from "@apollo/client";
// ── 假契约查询(@contract-pending─────────────────────────────
// 列表查询schema 无 students(...) 根字段
// 页面通过 MSW 兜底获取列表数据,后端补齐后切换 fetcher 指向真实查询
// 契约工单classes_contract.md#students-list
export const GET_STUDENTS_DOC = gql`
query GetStudents(
$classId: ID
$gradeId: String
$q: String
$limit: Int
$offset: Int
) {
students(
classId: $classId
gradeId: $gradeId
q: $q
limit: $limit
offset: $offset
) {
items {
id
studentNo
name
gender
classId
className
gradeId
enrolledAt
}
total
}
}
`;
// ── 单查(@contract-pending─────────────────────────────────────
// schema 无 student(id) 根字段
// 详情/弹窗通过 MSW 兜底获取单条学生,后端补齐后切换 fetcher
// 契约工单classes_contract.md#student-by-id
export const GET_STUDENT_DOC = gql`
query GetStudent($id: ID!) {
student(id: $id) {
id
studentNo
name
gender
classId
className
gradeId
enrolledAt
}
}
`;

View File

@@ -0,0 +1,148 @@
"use client";
/**
* Students domain APIARCHITECTURE.md §5.1 / §5.3 / §9.1 教师域学生模块)
*
* 全部操作(@contract-pending
* 1. useStudents列表查询❌ schema 无 students(...) → MSW 兜底
* 2. useStudent单查❌ schema 无 student(id) → MSW 兜底
*
* 契约工单docs/architecture/issues/contracts/classes_contract.md#students
* 后端补齐后:重跑 normalize + codegen → 关闭 skipDocumentsValidation → 切换 fetcher → 删 mock
*
* 关联ARCHITECTURE.md §5.3 契约纪律 / §5.4 MSW 兜底 / §9.1 / §11.4 契约工单
*/
import type { FetchPolicy } from "@apollo/client";
import { useWidgetQuery } from "../useWidgetQuery";
import {
GET_STUDENT_DOC,
GET_STUDENTS_DOC,
} from "./operations/students.graphql";
import type { UseQueryResult } from "./types";
// ===== 数据类型(@contract-pendingMSW 提供形状) =====
/**
* 学生实体(列表项与单查同构,@contract-pending
*
* schema 无 Student 类型,字段形状由 MSW mock 定义。
* 后端补齐后对齐真实 schema。
*/
export interface Student {
id: string;
studentNo: string;
name: string;
gender: string;
classId: string;
className: string;
gradeId: string;
enrolledAt: string;
}
/** 列表项(与 Student 同构) */
export type StudentListItem = Student;
// ===== 响应类型(@contract-pending 假契约形状MSW 返回此结构) =====
interface StudentsListResponse {
students: {
items: StudentListItem[];
total: number;
};
}
interface StudentResponse {
student: Student | null;
}
// ===== 筛选类型 =====
export interface StudentsListFilter {
classId?: string;
gradeId?: string;
q?: string;
limit?: number;
offset?: number;
}
// ===== 查询选项 =====
export interface StudentQueryOptions {
enabled?: boolean;
pollInterval?: number;
fetchPolicy?: FetchPolicy;
}
// ===== Hooks =====
/**
* 查询学生列表(@contract-pendingMSW 兜底)。
*
* schema 无 students(...) 根字段,由 MSW handlers 返回 mock 数据。
* 后端补齐列表查询后切换到真实 fetcher页面无需改动。
*
* 关联ARCHITECTURE.md §5.4 / §9.1 列表页 / §11.4 契约工单
*/
export function useStudents(
filter: StudentsListFilter,
options?: StudentQueryOptions,
): UseQueryResult<{ items: StudentListItem[]; total: number }> {
const result = useWidgetQuery<
StudentsListResponse,
{
classId?: string;
gradeId?: string;
q?: string;
limit?: number;
offset?: number;
}
>(
GET_STUDENTS_DOC,
{
classId: filter.classId,
gradeId: filter.gradeId,
q: filter.q,
limit: filter.limit,
offset: filter.offset,
},
{
enabled: options?.enabled ?? true,
fetchPolicy: options?.fetchPolicy,
pollInterval: options?.pollInterval,
},
);
return {
data: result.data?.students,
loading: result.loading,
error: result.error,
refetch: result.refetch,
};
}
/**
* 按 id 查询单个学生(@contract-pendingMSW 兜底)。
*
* schema 无 student(id) 根字段,由 MSW handlers 返回 mock 数据。
*
* 关联ARCHITECTURE.md §5.4 / §9.1 / §11.4 契约工单
*/
export function useStudent(
id: string,
options?: StudentQueryOptions,
): UseQueryResult<Student | null> {
const result = useWidgetQuery<StudentResponse, { id: string }>(
GET_STUDENT_DOC,
{ id },
{
...options,
enabled: options?.enabled ?? id.length > 0,
},
);
return {
data: result.data?.student ?? null,
loading: result.loading,
error: result.error,
refetch: result.refetch,
};
}