Files
Edu/apps/portal-shell/src/lib/api/universal.ts
SpecialX 2910a90271 feat(portal-shell): extract domain API layer and migrate 31 widgets
Task 4-10 of portal-shell data abstraction plan (M1-M2).

Add 7 domain API modules under src/lib/api/ (parent/admin/teacher/
student/universal/sidebar/topbar), each exposing semantic hooks that
wrap useWidgetQuery/useWidgetMutation and return flattened domain
models. Widget code now imports from @/lib/api instead of inlining
gql literals.

- 31 widgets migrated (gql literal count in widgets: 0)
- 7 test files (85 cases, all passing)
- topbar.useNotifications renamed to useNotificationBell to avoid
  barrel export collision with universal.useNotifications
- typecheck + lint (0 errors) + test (85/85) verified
2026-07-17 13:07:24 +08:00

322 lines
6.6 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";
/**
* Universal domain API
*
* 涵盖 7 个 universal widget 的查询函数:
* - useGrades / useHomework / useSchedule / useAttendance / useExams
* - useNotifications / useAnnouncements
*
* widget 通过 `import { useGrades } from "@/lib/api"` 调用,
* 不再各自内嵌 gql/接口/手写类型。
*
* universal domain 全部为查询,无 mutation。
*
* 关联spec §2.2、§5.6 统一 Hook、M8 验收
*/
import type { FetchPolicy } from "@apollo/client";
import {
GET_GRADES_DOC,
GET_HOMEWORKS_DOC,
GET_SCHEDULE_DOC,
GET_ATTENDANCE_DOC,
GET_EXAMS_DOC,
GET_NOTIFICATIONS_LIST_DOC,
GET_ANNOUNCEMENTS_DOC,
} from "@/lib/api/operations/universal.graphql";
import { useWidgetQuery } from "@/lib/useWidgetQuery";
import type { Pagination, UseQueryResult } from "./types";
// ===== 领域模型类型 =====
export interface Grade {
studentId: string;
score: number;
}
export interface Homework {
id: string;
title: string;
dueDate: string;
status: string;
}
export interface ScheduleItem {
id: string;
subject: string;
startTime: string;
endTime: string;
teacherName: string;
}
export interface AttendanceStats {
present: number;
absent: number;
late: number;
total: number;
}
export interface Exam {
id: string;
name: string;
examDate: string;
subject: string;
maxScore: number;
}
export interface Notification {
id: string;
title: string;
body: string;
createdAt: string;
type: string;
}
export interface NotificationList {
items: Notification[];
total: number;
}
export interface Announcement {
id: string;
title: string;
body: string;
author: string;
publishedAt: string;
}
// ===== 查询选项(透传 useWidgetQuery但 fallbackData 由本层处理) =====
export interface UniversalQueryOptions {
/** 是否启用查询false 时跳过) */
enabled?: boolean;
/** 轮询间隔ms */
pollInterval?: number;
/** Apollo fetchPolicy */
fetchPolicy?: FetchPolicy;
}
// ===== 内部 Query 类型codegen skipDocumentsValidation用 inline 类型) =====
// 注意Vars 使用 type alias 而非 interface以满足 useWidgetQuery 的
// `TVars extends Record<string, unknown>` 约束known-issues §2.17 TS2344
interface GradesQueryData {
grades: Grade[];
}
type GradesQueryVars = {
classId: string;
};
interface HomeworksQueryData {
homeworks: Homework[];
}
type HomeworksQueryVars = {
classId: string;
limit: number;
};
interface ScheduleQueryData {
schedule: ScheduleItem[];
}
type ScheduleQueryVars = {
classId: string;
dayOfWeek: number;
};
interface AttendanceQueryData {
attendance: AttendanceStats;
}
type AttendanceQueryVars = {
classId: string;
termId: string;
};
interface ExamsQueryData {
exams: Exam[];
}
type ExamsQueryVars = {
classId: string;
limit: number;
};
interface NotificationsListQueryData {
notifications: NotificationList;
}
type NotificationsListQueryVars = {
limit: number;
offset: number;
};
interface AnnouncementsQueryData {
announcements: Announcement[];
}
type AnnouncementsQueryVars = {
limit: number;
};
// ===== Hooks =====
/**
* 查询班级成绩列表。
*
* 关联portal-shell spec §5.6 统一 Hook、M8 验收
*/
export function useGrades(
classId: string,
options?: UniversalQueryOptions,
): UseQueryResult<Grade[]> {
const result = useWidgetQuery<GradesQueryData, GradesQueryVars>(
GET_GRADES_DOC,
{ classId },
options,
);
return {
data: result.data?.grades,
loading: result.loading,
error: result.error,
refetch: result.refetch,
};
}
/**
* 查询班级作业列表,可指定返回条数。
*
* 关联portal-shell spec §5.6 统一 Hook、M8 验收
*/
export function useHomework(
classId: string,
limit: number,
options?: UniversalQueryOptions,
): UseQueryResult<Homework[]> {
const result = useWidgetQuery<HomeworksQueryData, HomeworksQueryVars>(
GET_HOMEWORKS_DOC,
{ classId, limit },
options,
);
return {
data: result.data?.homeworks,
loading: result.loading,
error: result.error,
refetch: result.refetch,
};
}
/**
* 查询班级当日课表(按星期几过滤)。
*
* 关联portal-shell spec §5.6 统一 Hook、M8 验收
*/
export function useSchedule(
classId: string,
dayOfWeek: number,
options?: UniversalQueryOptions,
): UseQueryResult<ScheduleItem[]> {
const result = useWidgetQuery<ScheduleQueryData, ScheduleQueryVars>(
GET_SCHEDULE_DOC,
{ classId, dayOfWeek },
options,
);
return {
data: result.data?.schedule,
loading: result.loading,
error: result.error,
refetch: result.refetch,
};
}
/**
* 查询班级学期考勤统计。
*
* 关联portal-shell spec §5.6 统一 Hook、M8 验收
*/
export function useAttendance(
classId: string,
termId: string,
options?: UniversalQueryOptions,
): UseQueryResult<AttendanceStats> {
const result = useWidgetQuery<AttendanceQueryData, AttendanceQueryVars>(
GET_ATTENDANCE_DOC,
{ classId, termId },
options,
);
return {
data: result.data?.attendance,
loading: result.loading,
error: result.error,
refetch: result.refetch,
};
}
/**
* 查询班级考试列表,可指定返回条数。
*
* 关联portal-shell spec §5.6 统一 Hook、M8 验收
*/
export function useExams(
classId: string,
limit: number,
options?: UniversalQueryOptions,
): UseQueryResult<Exam[]> {
const result = useWidgetQuery<ExamsQueryData, ExamsQueryVars>(
GET_EXAMS_DOC,
{ classId, limit },
options,
);
return {
data: result.data?.exams,
loading: result.loading,
error: result.error,
refetch: result.refetch,
};
}
/**
* 查询通知列表(分页),返回 items + total。
*
* 关联portal-shell spec §5.6 统一 Hook、M8 验收
*/
export function useNotifications(
pagination: Pagination,
): UseQueryResult<NotificationList> {
const result = useWidgetQuery<
NotificationsListQueryData,
NotificationsListQueryVars
>(GET_NOTIFICATIONS_LIST_DOC, {
limit: pagination.limit,
offset: pagination.offset,
});
return {
data: result.data?.notifications,
loading: result.loading,
error: result.error,
refetch: result.refetch,
};
}
/**
* 查询公告列表,可指定返回条数。
*
* 关联portal-shell spec §5.6 统一 Hook、M8 验收
*/
export function useAnnouncements(
limit: number,
): UseQueryResult<Announcement[]> {
const result = useWidgetQuery<AnnouncementsQueryData, AnnouncementsQueryVars>(
GET_ANNOUNCEMENTS_DOC,
{ limit },
);
return {
data: result.data?.announcements,
loading: result.loading,
error: result.error,
refetch: result.refetch,
};
}