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:
@@ -1,11 +1,9 @@
|
||||
/**
|
||||
* Universal domain API 单元测试(portal-shell spec §5.6、§9.9)
|
||||
*
|
||||
* 覆盖:
|
||||
* - useGrades:归一化返回 Grade[](从 { grades: [...] } 拍平)
|
||||
* - useNotifications:归一化返回 NotificationList(从 { notifications: { items, total } } 拍平)
|
||||
* - useGrades:enabled=false 时跳过查询
|
||||
* - useAttendance:归一化返回 AttendanceStats
|
||||
* P1-2(ARCHITECTURE.md §10):6 个假契约查询已下线
|
||||
* (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,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
307
apps/portal-shell/src/lib/api/dashboard.ts
Normal file
307
apps/portal-shell/src/lib/api/dashboard.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
172
apps/portal-shell/src/lib/api/operations/dashboard.graphql.ts
Normal file
172
apps/portal-shell/src/lib/api/operations/dashboard.graphql.ts
Normal 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
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -1,69 +1,16 @@
|
||||
// Universal domain GraphQL documents
|
||||
// Extracted from widgets/universal/*/index.tsx
|
||||
// Related: spec section 2.2
|
||||
//
|
||||
// P1-2(ARCHITECTURE.md §10):6 个假契约查询已下线(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-7(codegen 恢复后统一处理)。
|
||||
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
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -3,67 +3,20 @@
|
||||
/**
|
||||
* Universal domain API
|
||||
*
|
||||
* 涵盖 7 个 universal widget 的查询函数:
|
||||
* - useGrades / useHomework / useSchedule / useAttendance / useExams
|
||||
* - useNotifications / useAnnouncements
|
||||
* P1-2(ARCHITECTURE.md §10):6 个假契约查询已下线
|
||||
* (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-7(codegen 恢复后统一处理)。
|
||||
*
|
||||
* 关联: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,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user