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
322 lines
6.6 KiB
TypeScript
322 lines
6.6 KiB
TypeScript
"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,
|
||
};
|
||
}
|