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:
384
apps/portal-shell/src/lib/api/attendance.ts
Normal file
384
apps/portal-shell/src/lib/api/attendance.ts
Normal file
@@ -0,0 +1,384 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* Attendance domain API(ARCHITECTURE.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. useSaveAttendanceSheet(mutation):❌ 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-pending,MSW 提供形状) */
|
||||
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-pending,MSW 兜底)。
|
||||
*
|
||||
* 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-pending,MSW 兜底)。
|
||||
*
|
||||
* 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-pending,MSW 兜底)。
|
||||
*
|
||||
* 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-pending,MSW 兜底)。
|
||||
*
|
||||
* 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-pending,MSW 兜底)。
|
||||
*
|
||||
* 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 };
|
||||
}
|
||||
320
apps/portal-shell/src/lib/api/classes.ts
Normal file
320
apps/portal-shell/src/lib/api/classes.ts
Normal file
@@ -0,0 +1,320 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* Classes domain API(ARCHITECTURE.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-pending,MSW 兜底)。
|
||||
*
|
||||
* 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-pending,MSW 兜底)。
|
||||
*
|
||||
* 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-pending,MSW 兜底)。
|
||||
*
|
||||
* 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-pending,MSW 兜底)。
|
||||
*
|
||||
* 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,
|
||||
};
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
159
apps/portal-shell/src/lib/api/operations/attendance.graphql.ts
Normal file
159
apps/portal-shell/src/lib/api/operations/attendance.graphql.ts
Normal 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
|
||||
}
|
||||
}
|
||||
`;
|
||||
134
apps/portal-shell/src/lib/api/operations/classes.graphql.ts
Normal file
134
apps/portal-shell/src/lib/api/operations/classes.graphql.ts
Normal 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-pending);ClassInfo 类型无 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
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -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";
|
||||
|
||||
62
apps/portal-shell/src/lib/api/operations/students.graphql.ts
Normal file
62
apps/portal-shell/src/lib/api/operations/students.graphql.ts
Normal 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
|
||||
}
|
||||
}
|
||||
`;
|
||||
148
apps/portal-shell/src/lib/api/students.ts
Normal file
148
apps/portal-shell/src/lib/api/students.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* Students domain API(ARCHITECTURE.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-pending,MSW 提供形状) =====
|
||||
|
||||
/**
|
||||
* 学生实体(列表项与单查同构,@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-pending,MSW 兜底)。
|
||||
*
|
||||
* 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-pending,MSW 兜底)。
|
||||
*
|
||||
* 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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user