feat(portal-shell): wire dashboards to real data-ana queries (P1-2)

- add dashboard.graphql.ts with 6 real aggregate queries
  (teacherDashboard / studentDashboard / parentDashboard /
   adminDashboard / warnings / errorBookStats), snake_case aligned
- add dashboard.ts with 6 hooks + full domain model types
- add 4 role dashboard pages (teacher/student/parent/admin)
  using DashboardShell + StatCard + DashboardSection with
  loading / error / success tri-state
- update [[...route]]/page.tsx to redirect /shell -> /shell/{role}
- retire 6 fake contract queries and hooks (grades/homeworks/
  schedule/attendance/exams/announcements) and mark widget
  placeholders as migrated
- update universal.test.ts to drop retired hook tests
- mark ARCHITECTURE.md P1-2 as completed with acceptance evidence
This commit is contained in:
SpecialX
2026-07-22 12:33:22 +08:00
parent f92fdf8efe
commit 98058eb16b
18 changed files with 1086 additions and 846 deletions

View File

@@ -1,11 +1,9 @@
/**
* Universal domain API 单元测试portal-shell spec §5.6、§9.9
*
* 覆盖:
* - useGrades归一化返回 Grade[](从 { grades: [...] } 拍平)
* - useNotifications:归一化返回 NotificationList从 { notifications: { items, total } } 拍平)
* - useGradesenabled=false 时跳过查询
* - useAttendance归一化返回 AttendanceStats
* P1-2ARCHITECTURE.md §106 个假契约查询已下线
* grades/homeworks/schedule/attendance/exams/announcements
* 本测试仅覆盖保留下来的 useNotifications
*
* 使用 createElement 而非 JSX避免 .ts 文件 JSX 解析错误
* known-issues §2.17.ts 文件不支持 JSX需用 .tsx 或 createElement
@@ -15,12 +13,8 @@ import { createElement, type ReactElement, type ReactNode } from "react";
import { renderHook, waitFor } from "@testing-library/react";
import { MockedProvider } from "@apollo/client/testing";
import type { MockedResponse } from "@apollo/client/testing/core";
import { useGrades, useNotifications, useAttendance } from "../universal";
import {
GET_GRADES_DOC,
GET_NOTIFICATIONS_LIST_DOC,
GET_ATTENDANCE_DOC,
} from "../operations/universal.graphql";
import { useNotifications } from "../universal";
import { GET_NOTIFICATIONS_LIST_DOC } from "../operations/universal.graphql";
function makeWrapper(mocks: MockedResponse[]) {
return function Wrapper({ children }: { children: ReactNode }): ReactElement {
@@ -28,73 +22,6 @@ function makeWrapper(mocks: MockedResponse[]) {
};
}
describe("useGrades", () => {
it("返回归一化的 Grade[](拍平 grades 字段)", async () => {
const mocks: MockedResponse[] = [
{
request: {
query: GET_GRADES_DOC,
variables: { classId: "cls-1" },
},
result: {
data: {
grades: [
{ studentId: "s-1", score: 90 },
{ studentId: "s-2", score: 85 },
],
},
},
},
];
const { result } = renderHook(() => useGrades("cls-1"), {
wrapper: makeWrapper(mocks),
});
await waitFor(() => expect(result.current.loading).toBe(false));
expect(result.current.data).toEqual([
{ studentId: "s-1", score: 90 },
{ studentId: "s-2", score: 85 },
]);
expect(result.current.error).toBeUndefined();
});
it("enabled=false 时不发起查询data 保持 undefined", async () => {
// 故意不配置 mock若 Apollo 仍发起查询MockedProvider 会抛错。
const { result } = renderHook(
() => useGrades("cls-1", { enabled: false }),
{ wrapper: makeWrapper([]) },
);
// 等待一个微任务周期,确认 Apollo 跳过查询
await new Promise((resolve) => setTimeout(resolve, 0));
expect(result.current.loading).toBe(false);
expect(result.current.data).toBeUndefined();
});
it("查询返回空数组时归一化为 []", async () => {
const mocks: MockedResponse[] = [
{
request: {
query: GET_GRADES_DOC,
variables: { classId: "cls-empty" },
},
result: { data: { grades: [] } },
},
];
const { result } = renderHook(() => useGrades("cls-empty"), {
wrapper: makeWrapper(mocks),
});
await waitFor(() => expect(result.current.loading).toBe(false));
expect(result.current.data).toEqual([]);
});
});
describe("useNotifications", () => {
it("返回归一化的 NotificationList含 items + total", async () => {
const mocks: MockedResponse[] = [
@@ -167,39 +94,3 @@ describe("useNotifications", () => {
expect(result.current.data).toBeUndefined();
});
});
describe("useAttendance", () => {
it("返回归一化的 AttendanceStats拍平 attendance 字段)", async () => {
const mocks: MockedResponse[] = [
{
request: {
query: GET_ATTENDANCE_DOC,
variables: { classId: "cls-1", termId: "t-1" },
},
result: {
data: {
attendance: {
present: 30,
absent: 2,
late: 3,
total: 35,
},
},
},
},
];
const { result } = renderHook(() => useAttendance("cls-1", "t-1"), {
wrapper: makeWrapper(mocks),
});
await waitFor(() => expect(result.current.loading).toBe(false));
expect(result.current.data).toEqual({
present: 30,
absent: 2,
late: 3,
total: 35,
});
});
});

View File

@@ -0,0 +1,307 @@
"use client";
/**
* Dashboard domain API (ARCHITECTURE.md §5.5, §10 P1-2)
*
* Real aggregation queries from data-ana subgraph, replacing the 6 fake
* widget contract queries (grades/homeworks/schedule/attendance/exams/
* announcements) that referenced non-existent root fields.
*
* Field naming: snake_case (as exposed by data-ana GraphQL schema).
* ARCHITECTURE.md §5.5 note: "底层字段 snake_case 需在 lib/api 层映射"
* — mapping deferred to P1-7 (codegen恢复后统一处理),当前直传 snake_case。
*
* 关联ARCHITECTURE.md §5.5 后端已就绪查询的立即利用 / §10 P1-2
*/
import type { FetchPolicy } from "@apollo/client";
import {
GET_TEACHER_DASHBOARD_DOC,
GET_STUDENT_DASHBOARD_DOC,
GET_PARENT_DASHBOARD_DOC,
GET_ADMIN_DASHBOARD_DOC,
GET_WARNINGS_DOC,
GET_ERROR_BOOK_STATS_DOC,
} from "@/lib/api/operations/dashboard.graphql";
import { useWidgetQuery } from "@/lib/useWidgetQuery";
import type { UseQueryResult } from "./types";
// ===== 领域模型类型(对齐 combined-schema.graphql data-ana 子图) =====
export interface ClassSummary {
class_id: string;
class_name: string;
student_count: number;
average_score: number;
}
export interface StudentSummary {
student_id: string;
student_name: string;
score: number;
rank_in_class: number;
}
export interface WarningInfo {
warning_id: string;
warning_type: string;
target_id: string;
target_name: string;
threshold: number;
current_value: number;
severity: string;
occurred_at: string;
}
export interface WarningList {
warnings: WarningInfo;
total: number;
}
export interface WeakPoint {
knowledge_point_id: string;
title: string;
mastery: number;
error_count: number;
}
export interface TrendPoint {
date: string;
score: number;
}
export interface AIUsageByProvider {
provider: string;
request_count: string;
total_tokens: string;
cost_cents: string;
}
export interface AIUsageSummary {
total_requests: string;
total_tokens: string;
total_cost_cents: string;
by_provider: AIUsageByProvider;
}
export interface KnowledgePointErrorStats {
knowledge_point_id: string;
title: string;
error_count: number;
question_count: number;
error_rate: number;
}
export interface ErrorBookStats {
student_id: string;
total_error_questions: number;
total_error_count: number;
by_knowledge_point: KnowledgePointErrorStats;
recent_7d_errors: number;
}
export interface TeacherDashboard {
user_id: string;
total_classes: number;
total_students: number;
class_avg_score: number;
pending_homework_count: number;
classes: ClassSummary;
top_students: StudentSummary;
recent_warnings: WarningInfo;
}
export interface StudentDashboard {
user_id: string;
avg_score: number;
class_rank: number;
total_students: number;
weak_points: WeakPoint;
recent_trends: TrendPoint;
pending_homework: number;
}
export interface ParentDashboard {
user_id: string;
student_id: string;
child_avg_score: number;
child_class_rank: number;
total_class_students: number;
child_weak_points: WeakPoint;
child_warnings: WarningInfo;
}
export interface AdminDashboard {
user_id: string;
total_teachers: number;
total_students: number;
total_classes: number;
school_avg_score: number;
recent_warnings: WarningInfo;
ai_usage: AIUsageSummary;
}
// ===== 查询选项 =====
export interface DashboardQueryOptions {
/** 是否启用查询false 时跳过) */
enabled?: boolean;
/** 轮询间隔ms */
pollInterval?: number;
/** Apollo fetchPolicy */
fetchPolicy?: FetchPolicy;
}
// ===== 内部 Query 类型 =====
interface TeacherDashboardQueryData {
teacherDashboard: TeacherDashboard;
}
type DashboardQueryVars = Record<string, never>;
interface StudentDashboardQueryData {
studentDashboard: StudentDashboard;
}
interface ParentDashboardQueryData {
parentDashboard: ParentDashboard;
}
interface AdminDashboardQueryData {
adminDashboard: AdminDashboard;
}
interface WarningsQueryData {
warnings: WarningList;
}
interface ErrorBookStatsQueryData {
errorBookStats: ErrorBookStats;
}
// ===== Hooks =====
/**
* 查询教师仪表盘聚合数据(替换原 grades/homeworks/schedule 等假契约查询)。
*
* 关联ARCHITECTURE.md §5.5 / §10 P1-2
*/
export function useTeacherDashboard(
options?: DashboardQueryOptions,
): UseQueryResult<TeacherDashboard> {
const result = useWidgetQuery<TeacherDashboardQueryData, DashboardQueryVars>(
GET_TEACHER_DASHBOARD_DOC,
{},
options,
);
return {
data: result.data?.teacherDashboard,
loading: result.loading,
error: result.error,
refetch: result.refetch,
};
}
/**
* 查询学生仪表盘聚合数据。
*
* 关联ARCHITECTURE.md §5.5 / §10 P1-2
*/
export function useStudentDashboard(
options?: DashboardQueryOptions,
): UseQueryResult<StudentDashboard> {
const result = useWidgetQuery<StudentDashboardQueryData, DashboardQueryVars>(
GET_STUDENT_DASHBOARD_DOC,
{},
options,
);
return {
data: result.data?.studentDashboard,
loading: result.loading,
error: result.error,
refetch: result.refetch,
};
}
/**
* 查询家长仪表盘聚合数据。
*
* 关联ARCHITECTURE.md §5.5 / §10 P1-2
*/
export function useParentDashboard(
options?: DashboardQueryOptions,
): UseQueryResult<ParentDashboard> {
const result = useWidgetQuery<ParentDashboardQueryData, DashboardQueryVars>(
GET_PARENT_DASHBOARD_DOC,
{},
options,
);
return {
data: result.data?.parentDashboard,
loading: result.loading,
error: result.error,
refetch: result.refetch,
};
}
/**
* 查询管理员仪表盘聚合数据。
*
* 关联ARCHITECTURE.md §5.5 / §10 P1-2
*/
export function useAdminDashboard(
options?: DashboardQueryOptions,
): UseQueryResult<AdminDashboard> {
const result = useWidgetQuery<AdminDashboardQueryData, DashboardQueryVars>(
GET_ADMIN_DASHBOARD_DOC,
{},
options,
);
return {
data: result.data?.adminDashboard,
loading: result.loading,
error: result.error,
refetch: result.refetch,
};
}
/**
* 查询预警列表(跨角色共享)。
*
* 关联ARCHITECTURE.md §5.5 / §10 P1-2
*/
export function useWarnings(
options?: DashboardQueryOptions,
): UseQueryResult<WarningList> {
const result = useWidgetQuery<WarningsQueryData, DashboardQueryVars>(
GET_WARNINGS_DOC,
{},
options,
);
return {
data: result.data?.warnings,
loading: result.loading,
error: result.error,
refetch: result.refetch,
};
}
/**
* 查询错题统计(学生仪表盘子区块)。
*
* 关联ARCHITECTURE.md §5.5 / §10 P1-2
*/
export function useErrorBookStats(
options?: DashboardQueryOptions,
): UseQueryResult<ErrorBookStats> {
const result = useWidgetQuery<ErrorBookStatsQueryData, DashboardQueryVars>(
GET_ERROR_BOOK_STATS_DOC,
{},
options,
);
return {
data: result.data?.errorBookStats,
loading: result.loading,
error: result.error,
refetch: result.refetch,
};
}

View File

@@ -3,12 +3,14 @@
*
* widget 通过 `import { useParentChildren } from "@/lib/api"` 调用。
* 7 个 domain 文件覆盖全部 31 widget。
* dashboard domain 覆盖 4 角色仪表盘真实聚合查询P1-2
*
* 关联spec §2.2
* 关联spec §2.2、ARCHITECTURE.md §5.5 / §10 P1-2
*/
export * from "./errors";
export * from "./types";
export * from "./universal";
export * from "./dashboard";
export * from "./sidebar";
export * from "./topbar";
export * from "./teacher";

View File

@@ -0,0 +1,172 @@
// Dashboard domain GraphQL documents
// Real aggregation queries from data-ana subgraph (ARCHITECTURE.md §5.5, §10 P1-2)
//
// These queries replace the 6 fake widget contract queries
// (grades/homeworks/schedule/attendance/exams/announcements) that referenced
// non-existent root fields. The dashboard aggregation queries are real and
// implemented by services/data-ana resolvers.
//
// Schema source: combined-schema.graphql (data-ana subgraph section)
// Field naming: snake_case (as exposed by data-ana GraphQL schema)
//
// 关联ARCHITECTURE.md §5.5 后端已就绪查询的立即利用 / §10 P1-2
import { gql } from "@apollo/client";
// ── Teacher Dashboard ──────────────────────────────────────────
export const GET_TEACHER_DASHBOARD_DOC = gql`
query GetTeacherDashboard {
teacherDashboard {
user_id
total_classes
total_students
class_avg_score
pending_homework_count
classes {
class_id
class_name
student_count
average_score
}
top_students {
student_id
student_name
score
rank_in_class
}
recent_warnings {
warning_id
warning_type
target_id
target_name
threshold
current_value
severity
occurred_at
}
}
}
`;
// ── Student Dashboard ──────────────────────────────────────────
export const GET_STUDENT_DASHBOARD_DOC = gql`
query GetStudentDashboard {
studentDashboard {
user_id
avg_score
class_rank
total_students
weak_points {
knowledge_point_id
title
mastery
error_count
}
recent_trends {
date
score
}
pending_homework
}
}
`;
// ── Parent Dashboard ───────────────────────────────────────────
export const GET_PARENT_DASHBOARD_DOC = gql`
query GetParentDashboard {
parentDashboard {
user_id
student_id
child_avg_score
child_class_rank
total_class_students
child_weak_points {
knowledge_point_id
title
mastery
error_count
}
child_warnings {
warning_id
warning_type
target_id
target_name
threshold
current_value
severity
occurred_at
}
}
}
`;
// ── Admin Dashboard ────────────────────────────────────────────
export const GET_ADMIN_DASHBOARD_DOC = gql`
query GetAdminDashboard {
adminDashboard {
user_id
total_teachers
total_students
total_classes
school_avg_score
recent_warnings {
warning_id
warning_type
target_id
target_name
threshold
current_value
severity
occurred_at
}
ai_usage {
total_requests
total_tokens
total_cost_cents
by_provider {
provider
request_count
total_tokens
cost_cents
}
}
}
}
`;
// ── Warnings (shared across dashboards) ────────────────────────
export const GET_WARNINGS_DOC = gql`
query GetWarnings {
warnings {
warnings {
warning_id
warning_type
target_id
target_name
threshold
current_value
severity
occurred_at
}
total
}
}
`;
// ── Error Book Stats ───────────────────────────────────────────
export const GET_ERROR_BOOK_STATS_DOC = gql`
query GetErrorBookStats {
errorBookStats {
student_id
total_error_questions
total_error_count
by_knowledge_point {
knowledge_point_id
title
error_count
question_count
error_rate
}
recent_7d_errors
}
}
`;

View File

@@ -1,69 +1,16 @@
// Universal domain GraphQL documents
// Extracted from widgets/universal/*/index.tsx
// Related: spec section 2.2
//
// P1-2ARCHITECTURE.md §106 个假契约查询已下线grades/homeworks/
// schedule/attendance/exams/announcements root 字段在后端 schema 不存在)。
// 仪表盘改用 data-ana 的真实聚合查询dashboard.graphql.ts
// 仅保留 notifications查询真实字段 notifications(userId),形状待 P1-7 修正)。
import { gql } from "@apollo/client";
// From widgets/universal/grades-widget
export const GET_GRADES_DOC = gql`
query GetGrades($classId: ID!) {
grades(classId: $classId) {
studentId
score
}
}
`;
// From widgets/universal/homework-widget
export const GET_HOMEWORKS_DOC = gql`
query GetHomeworks($classId: ID!, $limit: Int) {
homeworks(classId: $classId, limit: $limit) {
id
title
dueDate
status
}
}
`;
// From widgets/universal/schedule-widget
export const GET_SCHEDULE_DOC = gql`
query GetSchedule($classId: ID!, $dayOfWeek: Int) {
schedule(classId: $classId, dayOfWeek: $dayOfWeek) {
id
subject
startTime
endTime
teacherName
}
}
`;
// From widgets/universal/attendance-widget
export const GET_ATTENDANCE_DOC = gql`
query GetAttendance($classId: ID!, $termId: ID!) {
attendance(classId: $classId, termId: $termId) {
present
absent
late
total
}
}
`;
// From widgets/universal/exams-widget
export const GET_EXAMS_DOC = gql`
query GetExams($classId: ID!, $limit: Int) {
exams(classId: $classId, limit: $limit) {
id
name
examDate
subject
maxScore
}
}
`;
// From widgets/universal/notifications-widget
// 注意notifications(userId) 返回平铺数组,非 {items, total} 包装。
// 形状修正待 P1-7codegen 恢复后统一处理)。
export const GET_NOTIFICATIONS_LIST_DOC = gql`
query GetNotificationsList($limit: Int, $offset: Int) {
notifications(limit: $limit, offset: $offset) {
@@ -78,16 +25,3 @@ export const GET_NOTIFICATIONS_LIST_DOC = gql`
}
}
`;
// From widgets/universal/announcements-widget
export const GET_ANNOUNCEMENTS_DOC = gql`
query GetAnnouncements($limit: Int) {
announcements(limit: $limit) {
id
title
body
author
publishedAt
}
}
`;

View File

@@ -3,67 +3,20 @@
/**
* Universal domain API
*
* 涵盖 7 个 universal widget 的查询函数:
* - useGrades / useHomework / useSchedule / useAttendance / useExams
* - useNotifications / useAnnouncements
* P1-2ARCHITECTURE.md §106 个假契约查询已下线
* grades/homeworks/schedule/attendance/exams/announcements
* 仪表盘改用 data-ana 真实聚合查询dashboard.ts
* 仅保留 notifications真实字段形状待 P1-7 修正)。
*
* widget 通过 `import { useGrades } from "@/lib/api"` 调用,
* 不再各自内嵌 gql/接口/手写类型。
*
* universal domain 全部为查询,无 mutation。
*
* 关联spec §2.2、§5.6 统一 Hook、M8 验收
* 关联spec §2.2、ARCHITECTURE.md §5.5 / §10 P1-2
*/
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 { GET_NOTIFICATIONS_LIST_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;
@@ -77,15 +30,7 @@ export interface NotificationList {
total: number;
}
export interface Announcement {
id: string;
title: string;
body: string;
author: string;
publishedAt: string;
}
// ===== 查询选项(透传 useWidgetQuery但 fallbackData 由本层处理) =====
// ===== 查询选项 =====
export interface UniversalQueryOptions {
/** 是否启用查询false 时跳过) */
@@ -96,48 +41,7 @@ export interface UniversalQueryOptions {
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;
};
// ===== 内部 Query 类型 =====
interface NotificationsListQueryData {
notifications: NotificationList;
@@ -147,137 +51,14 @@ type NotificationsListQueryVars = {
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。
*
* 注意notifications(userId) 后端返回平铺数组,非 {items, total} 包装。
* 形状修正待 P1-7codegen 恢复后统一处理)。
*
* 关联portal-shell spec §5.6 统一 Hook、M8 验收
*/
export function useNotifications(
@@ -298,24 +79,3 @@ export function useNotifications(
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,
};
}