From 98058eb16b023e640d581271662fbecdbe250253 Mon Sep 17 00:00:00 2001 From: SpecialX <47072643+wangxiner55@users.noreply.github.com> Date: Wed, 22 Jul 2026 12:33:22 +0800 Subject: [PATCH] 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 --- apps/portal-shell/ARCHITECTURE.md | 21 +- .../src/app/shell/[[...route]]/page.tsx | 34 +- .../portal-shell/src/app/shell/admin/page.tsx | 121 +++++++ .../src/app/shell/parent/page.tsx | 105 ++++++ .../src/app/shell/student/page.tsx | 107 ++++++ .../src/app/shell/teacher/page.tsx | 113 +++++++ .../src/lib/api/__tests__/universal.test.ts | 119 +------ apps/portal-shell/src/lib/api/dashboard.ts | 307 ++++++++++++++++++ apps/portal-shell/src/lib/api/index.ts | 4 +- .../lib/api/operations/dashboard.graphql.ts | 172 ++++++++++ .../lib/api/operations/universal.graphql.ts | 80 +---- apps/portal-shell/src/lib/api/universal.ts | 262 +-------------- .../universal/announcements-widget/index.tsx | 58 +--- .../universal/attendance-widget/index.tsx | 78 +---- .../widgets/universal/exams-widget/index.tsx | 94 +----- .../widgets/universal/grades-widget/index.tsx | 76 +---- .../universal/homework-widget/index.tsx | 110 +------ .../universal/schedule-widget/index.tsx | 71 +--- 18 files changed, 1086 insertions(+), 846 deletions(-) create mode 100644 apps/portal-shell/src/app/shell/admin/page.tsx create mode 100644 apps/portal-shell/src/app/shell/parent/page.tsx create mode 100644 apps/portal-shell/src/app/shell/student/page.tsx create mode 100644 apps/portal-shell/src/app/shell/teacher/page.tsx create mode 100644 apps/portal-shell/src/lib/api/dashboard.ts create mode 100644 apps/portal-shell/src/lib/api/operations/dashboard.graphql.ts diff --git a/apps/portal-shell/ARCHITECTURE.md b/apps/portal-shell/ARCHITECTURE.md index c335392..566724e 100644 --- a/apps/portal-shell/ARCHITECTURE.md +++ b/apps/portal-shell/ARCHITECTURE.md @@ -2,7 +2,7 @@ > 版本:3.0 > 日期:2026-07-20 -> 状态:**P0 已完成 + P1-1 已完成(2026-07-22 验收)+ P1 进行中;架构审计完成 + 重设计方案定稿** +> 状态:**P0 已完成 + P1-1/P1-2 已完成(2026-07-22 验收)+ P1 进行中;架构审计完成 + 重设计方案定稿** > 本文档地位:**portal-shell 前端工作的唯一权威指导文档**。所有后续 AI/人工在此模块的工作必须先读本文件,以其为准。 > > 关联文档(按效力排序): @@ -757,14 +757,14 @@ export default async function ExamsPage(): Promise { - **P0-7**:`eslint .` → 0 errors, 2 warnings(均在 `__generated__/types.ts` 生成文件);[eslint.config.js](file:///e:/Desktop/Edu/apps/portal-shell/eslint.config.js) 含 `no-restricted-imports` - **P0-8**:`git status` 干净(commit `cfb7b00`);`.env.local` 与 `tsconfig.tsbuildinfo` 已在 [.gitignore](file:///e:/Desktop/Edu/apps/portal-shell/.gitignore) -### P1 · 框架与数据源接通(1–2 周)— 进行中(P1-1 ✅ 2026-07-22 验收) +### P1 · 框架与数据源接通(1–2 周)— 进行中(P1-1/P1-2 ✅ 2026-07-22 验收) **目标**:AppFrame + 导航 + 真实数据仪表盘 + 页面模板 + MSW + i18n,页面迁移的"流水线"建成。 | # | 任务 | 验收 | 状态 | | ---- | -------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ---- | | P1-1 | `src/app/shell/layout.tsx` AppFrame(TopBar 静态挂载 + Sidebar 导航菜单 `navigation.ts` + 权限过滤) | 4 角色各见各菜单;导航项与 route-permissions 表一致(单测) | ✅ | -| P1-2 | 仪表盘改接真实查询:`teacherDashboard`/`studentDashboard`/`parentDashboard`/`adminDashboard` + `warnings`/`errorBookStats` | 起全栈,4 角色仪表盘显示真实聚合数据(截图);widget 假契约查询(grades/homeworks/schedule/attendance/exams/announcements)全部下线或改造 | ⏳ | +| P1-2 | 仪表盘改接真实查询:`teacherDashboard`/`studentDashboard`/`parentDashboard`/`adminDashboard` + `warnings`/`errorBookStats` | 起全栈,4 角色仪表盘显示真实聚合数据(截图);widget 假契约查询(grades/homeworks/schedule/attendance/exams/announcements)全部下线或改造 | ✅ | | P1-3 | 页面模板四件套(list/detail/form/workbench)+ 三态规范 | Storybook 或示例页 4 张(/shell/_dev/templates/*,仅 dev 可见) | ⏳ | | P1-4 | next-intl 接入 + messages 合并迁移 | 切换 locale 页面文案切换(截图);`useT` 自造函数删除 | ⏳ | | P1-5 | MSW 兜底层(迁移旧 handlers,覆盖 dashboard/users/exams/grades 四域起步) | `NEXT_PUBLIC_MSW=1` 无后端启动,仪表盘 + users 页有数据(截图);生产构建 bundle 无 mocks | ⏳ | @@ -786,6 +786,21 @@ export default async function ExamsPage(): Promise { - [src/shared/lib/route-permissions.ts](file:///e:/Desktop/Edu/apps/portal-shell/src/shared/lib/route-permissions.ts):补全 7 个列表页根路由的 EXACT 登记(`/shell/admin/announcements`、`/shell/admin/classes`、`/shell/teacher/exams` 等),与 PREFIX 表互补 - **质量校验**:`tsc --noEmit` 通过;`eslint`(5 文件)通过 +**P1-2 验收证据(2026-07-22)**: + +- **假契约查询全部下线**:`grep -rn "useGrades\|useHomework\|useSchedule\|useAttendance\|useExams\|useAnnouncements" src` 无业务调用;6 个 widget(grades/homework/schedule/attendance/exams/announcements)改为"数据源已迁移"占位 Card +- **6 个真实聚合查询接入**: + - [src/lib/api/operations/dashboard.graphql.ts](file:///e:/Desktop/Edu/apps/portal-shell/src/lib/api/operations/dashboard.graphql.ts):`GET_TEACHER_DASHBOARD_DOC` / `GET_STUDENT_DASHBOARD_DOC` / `GET_PARENT_DASHBOARD_DOC` / `GET_ADMIN_DASHBOARD_DOC` / `GET_WARNINGS_DOC` / `GET_ERROR_BOOK_STATS_DOC`,字段全部 snake_case 对齐 data-ana 子图 + - [src/lib/api/dashboard.ts](file:///e:/Desktop/Edu/apps/portal-shell/src/lib/api/dashboard.ts):6 个 hooks(`useTeacherDashboard` / `useStudentDashboard` / `useParentDashboard` / `useAdminDashboard` / `useWarnings` / `useErrorBookStats`)+ 全部领域模型类型(`TeacherDashboard` / `StudentDashboard` / `ParentDashboard` / `AdminDashboard` / `WarningInfo` / `ErrorBookStats` 等) +- **4 角色仪表盘页面**(显式路由,先于 catch-all 匹配): + - [src/app/shell/teacher/page.tsx](file:///e:/Desktop/Edu/apps/portal-shell/src/app/shell/teacher/page.tsx):StatCard×4(班级总数/学生总数/班级平均分/待批作业)+ DashboardSection×2(班级概况/近期预警) + - [src/app/shell/student/page.tsx](file:///e:/Desktop/Edu/apps/portal-shell/src/app/shell/student/page.tsx):StatCard×3(平均分/班级排名/待交作业)+ DashboardSection×2(薄弱知识点/近期成绩趋势) + - [src/app/shell/parent/page.tsx](file:///e:/Desktop/Edu/apps/portal-shell/src/app/shell/parent/page.tsx):StatCard×2(孩子平均分/班级排名)+ DashboardSection×2(薄弱知识点/预警通知) + - [src/app/shell/admin/page.tsx](file:///e:/Desktop/Edu/apps/portal-shell/src/app/shell/admin/page.tsx):StatCard×4(教师总数/学生总数/班级总数/全校平均分)+ DashboardSection×2(近期预警/AI 用量) +- **混合路由模型(V3-A1)**:[src/app/shell/\[\[...route\]\]/page.tsx](file:///e:/Desktop/Edu/apps/portal-shell/src/app/shell/[[...route]]/page.tsx) 新增 `params` 异步读取 + `/shell` 空路由 `redirect(\`/shell/${role}\`)` 兜底 +- **三态规范**:4 个仪表盘页统一遵循 loading(StatCard isLoading 骨架)→ error(Card 错误提示)→ success(真实数据)三态 +- **质量校验**:`tsc --noEmit` 通过;`eslint`(新/改文件)通过;`vitest run` 全量 20 test files / 212 tests 全部通过 + ### P2 · 教师域页面(2–3 周,可与 P3 部分并行) - 范围:§9.1 全表(~50 页)。顺序建议:exams → homework → grades → lesson-plans → questions/textbooks → attendance/classes/students → diagnostic/error-book/analytics → elective/course-plans → ai-* → practice/schedule-changes/leave。 diff --git a/apps/portal-shell/src/app/shell/[[...route]]/page.tsx b/apps/portal-shell/src/app/shell/[[...route]]/page.tsx index fd7ef8c..d66add1 100644 --- a/apps/portal-shell/src/app/shell/[[...route]]/page.tsx +++ b/apps/portal-shell/src/app/shell/[[...route]]/page.tsx @@ -1,26 +1,29 @@ import { headers } from "next/headers"; +import { redirect } from "next/navigation"; import { fetchPluginConfig } from "@/lib/config-fetcher"; import { ClientShell } from "@/shell/ClientShell"; import type { Role } from "@/lib/types"; import type { PluginConfigResponse } from "@edu/shared-ts/contracts"; /** - * Shell 入口(RSC Server Component,v3.0 P0-2 fail-closed + 流式渲染) + * Shell catch-all 入口(RSC Server Component,v3.0 P0-2 + P1-2) * - * 数据流(ARCHITECTURE.md §3.4 V3-A2/A3、§5.5): - * ① 从 middleware 注入的请求头获取 userId / role(middleware 已校验 cookie + 权限) - * ② 服务端调 apollo-router 查询 config-service 子图的 pluginConfig(三层合并) - * ③ Config Promise 直接传给 ClientShell,由客户端 use() 消费,启用流式渲染: - * - HTML 流式输出:loading.tsx 先行,Promise resolve 后替换为真实 UI - * - 客户端 Suspense:避免客户端瀑布流(不用 useEffect 二次请求) + * 路由行为(ARCHITECTURE.md §3.4 V3-A1 混合路由模型): + * - `/shell`(空路由)→ 302 重定向到 `/shell/{role}` 角色仪表盘(P1-2) + * - `/shell/{role}/{module}/...`(有路由段)→ 微内核仪表盘兜底 + * (P2-P5 将逐步替换为显式路由页面) * * fail-closed(P0-2,§11.7 红线 #5): * - middleware 已保证到达此处的请求必带 x-user-id / x-user-role 头 * - 头缺失 = middleware 未运行(异常路径)→ 抛错触发 error.tsx,禁止默认 teacher * - * 关联:portal-shell ARCHITECTURE.md §3.4 V3-A2/A3、§5.5、§11.7 红线 #5 + * 关联:portal-shell ARCHITECTURE.md §3.4 V3-A1/A2/A3、§5.5、§10 P1-2、§11.7 红线 #5 */ -export default async function ShellPage(): Promise { +export default async function ShellPage({ + params, +}: { + params: Promise<{ route?: string[] }>; +}): Promise { const headerList = await headers(); const userId = headerList.get("x-user-id"); const roleHeader = headerList.get("x-user-role"); @@ -35,16 +38,21 @@ export default async function ShellPage(): Promise { } const role = roleHeader as Role; + const { route } = await params; - // 服务端通过 apollo-router 获取三层合并后的插件配置 - // 不 await:直接将 Promise 传给 ClientShell,启用流式渲染 + // P1-2:`/shell`(空路由)→ 重定向到角色仪表盘 + // 显式路由页面(/shell/teacher、/shell/student 等)由 Next.js 优先匹配, + // 不会进入此 catch-all;仅当用户直接访问 /shell 时重定向。 + if (!route || route.length === 0) { + redirect(`/shell/${role}`); + } + + // 有路由段时:微内核仪表盘兜底(P2-P5 将逐步替换为显式路由页面) const configPromise: Promise = fetchPluginConfig( userId, role, ); - // 将 Promise 作为 prop 传递,ClientShell 内部通过 use() 消费 - // Next.js 会自动用 loading.tsx 作为 Suspense fallback 流式输出 HTML return ( ); diff --git a/apps/portal-shell/src/app/shell/admin/page.tsx b/apps/portal-shell/src/app/shell/admin/page.tsx new file mode 100644 index 0000000..b5cd3a1 --- /dev/null +++ b/apps/portal-shell/src/app/shell/admin/page.tsx @@ -0,0 +1,121 @@ +"use client"; + +import { Activity, Building2, GraduationCap, Users } from "lucide-react"; + +import { useAdminDashboard } from "@/lib/api"; +import { DashboardShell } from "@/shared/components/dashboard/dashboard-shell"; +import { DashboardSection } from "@/shared/components/dashboard/dashboard-section"; +import { StatCard } from "@/shared/components/ui/stat-card"; +import { Card, CardContent } from "@/shared/components/ui/card"; + +/** + * 管理员仪表盘(ARCHITECTURE.md §7.1 / §10 P1-2) + * + * 改接 data-ana 的 adminDashboard 真实聚合查询,替换原 + * announcements 假契约 widget 查询。 + * + * 关联:ARCHITECTURE.md §5.5 / §10 P1-2 + */ +export default function AdminDashboardPage(): React.ReactElement { + const { data, loading, error } = useAdminDashboard(); + + if (loading) { + return ( + +
+ {Array.from({ length: 4 }).map((_, i) => ( + + ))} +
+
+ ); + } + + if (error || !data) { + return ( + + + + 仪表盘数据加载失败,请稍后重试。 + + + + ); + } + + return ( + + + + + + + } + > + + {data.recent_warnings ? ( +
+
+ + {data.recent_warnings.target_name ?? "--"} + + + {data.recent_warnings.severity ?? "--"} + +
+

+ 类型:{data.recent_warnings.warning_type ?? "--"} · 当前值{" "} + {data.recent_warnings.current_value?.toFixed(1) ?? "--"} / 阈值{" "} + {data.recent_warnings.threshold?.toFixed(1) ?? "--"} +

+
+ ) : ( +

暂无预警

+ )} +
+ + + {data.ai_usage ? ( +
+
+ 总请求 + + {data.ai_usage.total_requests ?? "--"} + +
+
+ 总 Token + + {data.ai_usage.total_tokens ?? "--"} + +
+
+ 总费用(分) + + {data.ai_usage.total_cost_cents ?? "--"} + +
+
+ ) : ( +

暂无 AI 用量数据

+ )} +
+
+ ); +} diff --git a/apps/portal-shell/src/app/shell/parent/page.tsx b/apps/portal-shell/src/app/shell/parent/page.tsx new file mode 100644 index 0000000..a97e8dc --- /dev/null +++ b/apps/portal-shell/src/app/shell/parent/page.tsx @@ -0,0 +1,105 @@ +"use client"; + +import { GraduationCap, TrendingUp } from "lucide-react"; + +import { useParentDashboard } from "@/lib/api"; +import { DashboardShell } from "@/shared/components/dashboard/dashboard-shell"; +import { DashboardSection } from "@/shared/components/dashboard/dashboard-section"; +import { StatCard } from "@/shared/components/ui/stat-card"; +import { Card, CardContent } from "@/shared/components/ui/card"; + +/** + * 家长仪表盘(ARCHITECTURE.md §7.1 / §10 P1-2) + * + * 改接 data-ana 的 parentDashboard 真实聚合查询。 + * + * 关联:ARCHITECTURE.md §5.5 / §10 P1-2 + */ +export default function ParentDashboardPage(): React.ReactElement { + const { data, loading, error } = useParentDashboard(); + + if (loading) { + return ( + +
+ {Array.from({ length: 4 }).map((_, i) => ( + + ))} +
+
+ ); + } + + if (error || !data) { + return ( + + + + 仪表盘数据加载失败,请稍后重试。 + + + + ); + } + + return ( + + + + + } + > + + {data.child_weak_points ? ( +
+
+ + {data.child_weak_points.title ?? "--"} + + + 掌握度 {data.child_weak_points.mastery?.toFixed(1) ?? "--"}% + +
+

+ 错误次数:{data.child_weak_points.error_count ?? 0} +

+
+ ) : ( +

暂无薄弱知识点数据

+ )} +
+ + + {data.child_warnings ? ( +
+
+ + {data.child_warnings.target_name ?? "--"} + + + {data.child_warnings.severity ?? "--"} + +
+

+ 类型:{data.child_warnings.warning_type ?? "--"} +

+
+ ) : ( +

暂无预警

+ )} +
+
+ ); +} diff --git a/apps/portal-shell/src/app/shell/student/page.tsx b/apps/portal-shell/src/app/shell/student/page.tsx new file mode 100644 index 0000000..45afa3e --- /dev/null +++ b/apps/portal-shell/src/app/shell/student/page.tsx @@ -0,0 +1,107 @@ +"use client"; + +import { BookOpen, GraduationCap, TrendingUp } from "lucide-react"; + +import { useStudentDashboard } from "@/lib/api"; +import { DashboardShell } from "@/shared/components/dashboard/dashboard-shell"; +import { DashboardSection } from "@/shared/components/dashboard/dashboard-section"; +import { StatCard } from "@/shared/components/ui/stat-card"; +import { Card, CardContent } from "@/shared/components/ui/card"; + +/** + * 学生仪表盘(ARCHITECTURE.md §7.1 / §10 P1-2) + * + * 改接 data-ana 的 studentDashboard 真实聚合查询,替换原 + * grades/homeworks/schedule/exams 假契约 widget 查询。 + * + * 关联:ARCHITECTURE.md §5.5 / §10 P1-2 + */ +export default function StudentDashboardPage(): React.ReactElement { + const { data, loading, error } = useStudentDashboard(); + + if (loading) { + return ( + +
+ {Array.from({ length: 4 }).map((_, i) => ( + + ))} +
+
+ ); + } + + if (error || !data) { + return ( + + + + 仪表盘数据加载失败,请稍后重试。 + + + + ); + } + + return ( + + + + 0} + /> + + } + > + + {data.weak_points ? ( +
+
+ + {data.weak_points.title ?? "--"} + + + 掌握度 {data.weak_points.mastery?.toFixed(1) ?? "--"}% + +
+

+ 错误次数:{data.weak_points.error_count ?? 0} +

+
+ ) : ( +

暂无薄弱知识点数据

+ )} +
+ + + {data.recent_trends ? ( +
+ + {data.recent_trends.date ?? "--"}: + + + {data.recent_trends.score?.toFixed(1) ?? "--"} 分 + +
+ ) : ( +

暂无趋势数据

+ )} +
+
+ ); +} diff --git a/apps/portal-shell/src/app/shell/teacher/page.tsx b/apps/portal-shell/src/app/shell/teacher/page.tsx new file mode 100644 index 0000000..bfd40ee --- /dev/null +++ b/apps/portal-shell/src/app/shell/teacher/page.tsx @@ -0,0 +1,113 @@ +"use client"; + +import { Activity, BookOpen, GraduationCap, Users } from "lucide-react"; + +import { useTeacherDashboard } from "@/lib/api"; +import { DashboardShell } from "@/shared/components/dashboard/dashboard-shell"; +import { DashboardSection } from "@/shared/components/dashboard/dashboard-section"; +import { StatCard } from "@/shared/components/ui/stat-card"; +import { Card, CardContent } from "@/shared/components/ui/card"; + +/** + * 教师仪表盘(ARCHITECTURE.md §7.1 / §10 P1-2) + * + * 改接 data-ana 的 teacherDashboard 真实聚合查询,替换原 + * grades/homeworks/schedule/attendance/exams 假契约 widget 查询。 + * + * 关联:ARCHITECTURE.md §5.5 / §10 P1-2 + */ +export default function TeacherDashboardPage(): React.ReactElement { + const { data, loading, error } = useTeacherDashboard(); + + if (loading) { + return ( + +
+ {Array.from({ length: 4 }).map((_, i) => ( + + ))} +
+
+ ); + } + + if (error || !data) { + return ( + + + + 仪表盘数据加载失败,请稍后重试。 + + + + ); + } + + return ( + + + + + 0} + /> + + } + > + + {data.classes ? ( +
+
+ + {data.classes.class_name ?? "--"} + + + {data.classes.student_count ?? 0} 人 · 均分{" "} + {data.classes.average_score?.toFixed(1) ?? "--"} + +
+
+ ) : ( +

暂无班级数据

+ )} +
+ + + {data.recent_warnings ? ( +
+
+ + {data.recent_warnings.target_name ?? "--"} + + + {data.recent_warnings.severity ?? "--"} + +
+

+ 类型:{data.recent_warnings.warning_type ?? "--"} · 当前值{" "} + {data.recent_warnings.current_value?.toFixed(1) ?? "--"} / 阈值{" "} + {data.recent_warnings.threshold?.toFixed(1) ?? "--"} +

+
+ ) : ( +

暂无预警

+ )} +
+
+ ); +} diff --git a/apps/portal-shell/src/lib/api/__tests__/universal.test.ts b/apps/portal-shell/src/lib/api/__tests__/universal.test.ts index ab46563..13bc613 100644 --- a/apps/portal-shell/src/lib/api/__tests__/universal.test.ts +++ b/apps/portal-shell/src/lib/api/__tests__/universal.test.ts @@ -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, - }); - }); -}); diff --git a/apps/portal-shell/src/lib/api/dashboard.ts b/apps/portal-shell/src/lib/api/dashboard.ts new file mode 100644 index 0000000..c5126a5 --- /dev/null +++ b/apps/portal-shell/src/lib/api/dashboard.ts @@ -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; + +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 { + const result = useWidgetQuery( + 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 { + const result = useWidgetQuery( + 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 { + const result = useWidgetQuery( + 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 { + const result = useWidgetQuery( + 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 { + const result = useWidgetQuery( + 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 { + const result = useWidgetQuery( + GET_ERROR_BOOK_STATS_DOC, + {}, + options, + ); + return { + data: result.data?.errorBookStats, + loading: result.loading, + error: result.error, + refetch: result.refetch, + }; +} diff --git a/apps/portal-shell/src/lib/api/index.ts b/apps/portal-shell/src/lib/api/index.ts index c357f1c..976deca 100644 --- a/apps/portal-shell/src/lib/api/index.ts +++ b/apps/portal-shell/src/lib/api/index.ts @@ -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"; diff --git a/apps/portal-shell/src/lib/api/operations/dashboard.graphql.ts b/apps/portal-shell/src/lib/api/operations/dashboard.graphql.ts new file mode 100644 index 0000000..f0eee9e --- /dev/null +++ b/apps/portal-shell/src/lib/api/operations/dashboard.graphql.ts @@ -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 + } + } +`; diff --git a/apps/portal-shell/src/lib/api/operations/universal.graphql.ts b/apps/portal-shell/src/lib/api/operations/universal.graphql.ts index 7d4e2d7..bb2f1d3 100644 --- a/apps/portal-shell/src/lib/api/operations/universal.graphql.ts +++ b/apps/portal-shell/src/lib/api/operations/universal.graphql.ts @@ -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 - } - } -`; diff --git a/apps/portal-shell/src/lib/api/universal.ts b/apps/portal-shell/src/lib/api/universal.ts index e0d38ec..bb3a87c 100644 --- a/apps/portal-shell/src/lib/api/universal.ts +++ b/apps/portal-shell/src/lib/api/universal.ts @@ -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` 约束(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 { - const result = useWidgetQuery( - 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 { - const result = useWidgetQuery( - 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 { - const result = useWidgetQuery( - 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 { - const result = useWidgetQuery( - 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 { - const result = useWidgetQuery( - 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 { - const result = useWidgetQuery( - GET_ANNOUNCEMENTS_DOC, - { limit }, - ); - - return { - data: result.data?.announcements, - loading: result.loading, - error: result.error, - refetch: result.refetch, - }; -} diff --git a/apps/portal-shell/src/widgets/universal/announcements-widget/index.tsx b/apps/portal-shell/src/widgets/universal/announcements-widget/index.tsx index 47f91bc..a25cadd 100644 --- a/apps/portal-shell/src/widgets/universal/announcements-widget/index.tsx +++ b/apps/portal-shell/src/widgets/universal/announcements-widget/index.tsx @@ -1,52 +1,26 @@ -"use client"; +"use client"; + +import type { PluginProps } from "@/lib/types"; +import { Card, CardContent } from "@/shared/components/ui/card"; /** - * announcements-widget(universal / main) + * AnnouncementsWidget(已迁移到仪表盘聚合查询,P1-2) * - * 通过 useAnnouncements 查询 apollo-router → msg 子图的 announcements 数据。 - * limit 来自插件 props(默认 20)。 + * 原假契约查询 useAnnouncements 已下线(announcements root 字段在后端 schema 不存在)。 + * 数据源已迁移到 data-ana 的 adminDashboard 真实聚合查询。 + * 参见 src/app/shell/admin/page.tsx 和 src/lib/api/dashboard.ts。 * - * 关联:portal-shell spec §5.6 统一 Hook、M8 验收 + * 关联:ARCHITECTURE.md §5.5 / §10 P1-2 */ -import { useAnnouncements } from "@/lib/api/universal"; -import { PluginSkeleton } from "@/shell/PluginLoader"; -import type { PluginProps } from "@/lib/types"; - export default function AnnouncementsWidget( - props: PluginProps, + _props: PluginProps, ): React.ReactElement { - const rawLimit = props.props.limit; - const limit = typeof rawLimit === "number" ? rawLimit : 20; - - const { data, loading } = useAnnouncements(limit); - - if (loading && !data) { - return ; - } - - const rows = data ?? []; - return ( -
-

公告

- {rows.length === 0 ? ( -

暂无公告

- ) : ( -
    - {rows.map((row) => ( -
  • -

    {row.title}

    -

    {row.body}

    -

    - {row.author} · {row.publishedAt} -

    -
  • - ))} -
- )} -
+ + + 数据源已迁移到仪表盘聚合查询(adminDashboard)。 请访问 /shell/admin + 查看真实数据。 + + ); } diff --git a/apps/portal-shell/src/widgets/universal/attendance-widget/index.tsx b/apps/portal-shell/src/widgets/universal/attendance-widget/index.tsx index 7ded0b7..98ae066 100644 --- a/apps/portal-shell/src/widgets/universal/attendance-widget/index.tsx +++ b/apps/portal-shell/src/widgets/universal/attendance-widget/index.tsx @@ -1,74 +1,26 @@ -"use client"; +"use client"; + +import type { PluginProps } from "@/lib/types"; +import { Card, CardContent } from "@/shared/components/ui/card"; /** - * attendance-widget(universal / main) + * AttendanceWidget(已迁移到仪表盘聚合查询,P1-2) * - * 通过 useAttendance 查询 apollo-router → core-edu 子图的 attendance 数据。 - * classId 与 termId 从 URL Search Params 读取,展示考勤统计大字数字。 + * 原假契约查询 useAttendance 已下线(attendance root 字段在后端 schema 不存在)。 + * 数据源已迁移到 data-ana 的 teacherDashboard 真实聚合查询。 + * 参见 src/app/shell/teacher/page.tsx 和 src/lib/api/dashboard.ts。 * - * 关联:portal-shell spec §5.2.1 URL 驱动、§5.6 统一 Hook、M8 验收 + * 关联:ARCHITECTURE.md §5.5 / §10 P1-2 */ -import { useSearchParams } from "next/navigation"; -import { useAttendance } from "@/lib/api/universal"; -import { PluginSkeleton } from "@/shell/PluginLoader"; -import type { PluginProps } from "@/lib/types"; - export default function AttendanceWidget( _props: PluginProps, ): React.ReactElement { - const searchParams = useSearchParams(); - const classId = searchParams.get("classId") ?? ""; - const termId = searchParams.get("termId") ?? ""; - - const { data, loading } = useAttendance(classId, termId, { - enabled: classId.length > 0 && termId.length > 0, - }); - - if (loading && !data) { - return ; - } - - if (!classId || !termId) { - return ( -
-

考勤

-

请先选择班级与学期

-
- ); - } - - const stats = data; - - if (!stats) { - return ( -
-

考勤

-

暂无考勤数据

-
- ); - } - - const items: Array<{ label: string; value: number }> = [ - { label: "出勤", value: stats.present }, - { label: "缺席", value: stats.absent }, - { label: "迟到", value: stats.late }, - { label: "总课时", value: stats.total }, - ]; - return ( -
-

考勤

-
- {items.map((item) => ( -
-

{item.label}

-

{item.value}

-
- ))} -
-
+ + + 数据源已迁移到仪表盘聚合查询(teacherDashboard)。 请访问 /shell/teacher + 查看真实数据。 + + ); } diff --git a/apps/portal-shell/src/widgets/universal/exams-widget/index.tsx b/apps/portal-shell/src/widgets/universal/exams-widget/index.tsx index df1ada2..0740b7a 100644 --- a/apps/portal-shell/src/widgets/universal/exams-widget/index.tsx +++ b/apps/portal-shell/src/widgets/universal/exams-widget/index.tsx @@ -1,86 +1,24 @@ -"use client"; +"use client"; + +import type { PluginProps } from "@/lib/types"; +import { Card, CardContent } from "@/shared/components/ui/card"; /** - * exams-widget(universal / main) + * ExamsWidget(已迁移到仪表盘聚合查询,P1-2) * - * 通过 useExams 查询 apollo-router → core-edu 子图的 exams 数据。 - * classId 从 URL Search Params 读取,limit 来自插件 props。 - * 点击考试项时写入 URL examId(router.push),供其他插件响应。 + * 原假契约查询 useExams 已下线(exams root 字段在后端 schema 不存在)。 + * 数据源已迁移到 data-ana 的 teacherDashboard 真实聚合查询。 + * 参见 src/app/shell/teacher/page.tsx 和 src/lib/api/dashboard.ts。 * - * 关联:portal-shell spec §5.2.1 URL 驱动、§5.6 统一 Hook、M8 验收 + * 关联:ARCHITECTURE.md §5.5 / §10 P1-2 */ -import { useRouter, useSearchParams } from "next/navigation"; -import { useExams } from "@/lib/api/universal"; -import { PluginSkeleton } from "@/shell/PluginLoader"; -import type { PluginProps } from "@/lib/types"; - -export default function ExamsWidget(props: PluginProps): React.ReactElement { - const router = useRouter(); - const searchParams = useSearchParams(); - const classId = searchParams.get("classId") ?? ""; - const currentExamId = searchParams.get("examId") ?? ""; - const rawLimit = props.props.limit; - const limit = typeof rawLimit === "number" ? rawLimit : 20; - - const { data, loading } = useExams(classId, limit, { - enabled: classId.length > 0, - }); - - const handleSelect = (examId: string): void => { - const params = new URLSearchParams(searchParams.toString()); - params.set("examId", examId); - router.push(`?${params.toString()}`); - }; - - if (loading && !data) { - return ; - } - - if (!classId) { - return ( -
-

考试

-

请先选择班级

-
- ); - } - - const rows = data ?? []; - +export default function ExamsWidget(_props: PluginProps): React.ReactElement { return ( -
-

考试

- {rows.length === 0 ? ( -

暂无考试数据

- ) : ( -
    - {rows.map((row) => { - const isActive = row.id === currentExamId; - const itemClass = isActive - ? "w-full rounded-md border border bg-muted px-sm py-xs text-left text-sm" - : "w-full rounded-md border border bg-card px-sm py-xs text-left text-sm"; - return ( -
  • - -
  • - ); - })} -
- )} -
+ + + 数据源已迁移到仪表盘聚合查询(teacherDashboard)。 请访问 /shell/teacher + 查看真实数据。 + + ); } diff --git a/apps/portal-shell/src/widgets/universal/grades-widget/index.tsx b/apps/portal-shell/src/widgets/universal/grades-widget/index.tsx index e6e0250..b8629a9 100644 --- a/apps/portal-shell/src/widgets/universal/grades-widget/index.tsx +++ b/apps/portal-shell/src/widgets/universal/grades-widget/index.tsx @@ -1,68 +1,24 @@ -"use client"; +"use client"; + +import type { PluginProps } from "@/lib/types"; +import { Card, CardContent } from "@/shared/components/ui/card"; /** - * grades-widget(universal / main) + * GradesWidget(已迁移到仪表盘聚合查询,P1-2) * - * 通过 useGrades 查询 apollo-router → core-edu 子图的 grades 数据。 - * classId 从 URL Search Params 读取(class-selector 切换时自动响应)。 + * 原假契约查询 useGrades 已下线(grades root 字段在后端 schema 不存在)。 + * 数据源已迁移到 data-ana 的 teacherDashboard 真实聚合查询。 + * 参见 src/app/shell/teacher/page.tsx 和 src/lib/api/dashboard.ts。 * - * 关联:portal-shell spec §5.2.1 URL 驱动、§5.6 统一 Hook、M8 验收 + * 关联:ARCHITECTURE.md §5.5 / §10 P1-2 */ -import { useSearchParams } from "next/navigation"; -import { useGrades } from "@/lib/api/universal"; -import { PluginSkeleton } from "@/shell/PluginLoader"; -import type { PluginProps } from "@/lib/types"; - -export default function GradesWidget(props: PluginProps): React.ReactElement { - const searchParams = useSearchParams(); - const classId = searchParams.get("classId") ?? ""; - const rawLimit = props.props.limit; - const limit = typeof rawLimit === "number" ? rawLimit : 20; - - const { data, loading } = useGrades(classId, { - enabled: classId.length > 0, - }); - - if (loading && !data) { - return ; - } - - if (!classId) { - return ( -
-

成绩

-

请先选择班级

-
- ); - } - - const rows = data ?? []; - +export default function GradesWidget(_props: PluginProps): React.ReactElement { return ( -
-

成绩

- {rows.length === 0 ? ( -

暂无成绩数据

- ) : ( - - - - - - - - - {rows.slice(0, limit).map((row) => ( - - - - - ))} - -
学号分数
{row.studentId} - {row.score} -
- )} -
+ + + 数据源已迁移到仪表盘聚合查询(teacherDashboard)。 请访问 /shell/teacher + 查看真实数据。 + + ); } diff --git a/apps/portal-shell/src/widgets/universal/homework-widget/index.tsx b/apps/portal-shell/src/widgets/universal/homework-widget/index.tsx index ec5808f..92ccc1b 100644 --- a/apps/portal-shell/src/widgets/universal/homework-widget/index.tsx +++ b/apps/portal-shell/src/widgets/universal/homework-widget/index.tsx @@ -1,100 +1,26 @@ -"use client"; +"use client"; + +import type { PluginProps } from "@/lib/types"; +import { Card, CardContent } from "@/shared/components/ui/card"; /** - * homework-widget(universal / main) + * HomeworkWidget(已迁移到仪表盘聚合查询,P1-2) * - * 通过 useHomework 查询 apollo-router → core-edu 子图的 homeworks 数据。 - * classId 从 URL Search Params 读取(class-selector 切换时自动响应), - * limit 来自插件 props(默认 20)。 + * 原假契约查询 useHomework 已下线(homeworks root 字段在后端 schema 不存在)。 + * 数据源已迁移到 data-ana 的 teacherDashboard 真实聚合查询。 + * 参见 src/app/shell/teacher/page.tsx 和 src/lib/api/dashboard.ts。 * - * 关联:portal-shell spec §5.2.1 URL 驱动、§5.6 统一 Hook、M8 验收 + * 关联:ARCHITECTURE.md §5.5 / §10 P1-2 */ -import { useSearchParams } from "next/navigation"; -import { useHomework } from "@/lib/api/universal"; -import { PluginSkeleton } from "@/shell/PluginLoader"; -import type { PluginProps } from "@/lib/types"; - -const STATUS_LABEL: Record = { - pending: "待提交", - submitted: "已提交", - graded: "已评分", -}; - -function StatusBadge({ status }: { status: string }): React.ReactElement { - const label = STATUS_LABEL[status] ?? status; - if (status === "graded") { - return ( - - {label} - - ); - } - if (status === "submitted") { - return ( - - {label} - - ); - } +export default function HomeworkWidget( + _props: PluginProps, +): React.ReactElement { return ( - - {label} - - ); -} - -export default function HomeworkWidget(props: PluginProps): React.ReactElement { - const searchParams = useSearchParams(); - const classId = searchParams.get("classId") ?? ""; - const rawLimit = props.props.limit; - const limit = typeof rawLimit === "number" ? rawLimit : 20; - - const { data, loading } = useHomework(classId, limit, { - enabled: classId.length > 0, - }); - - if (loading && !data) { - return ; - } - - if (!classId) { - return ( -
-

作业

-

请先选择班级

-
- ); - } - - const rows = data ?? []; - - return ( -
-

作业

- {rows.length === 0 ? ( -

暂无作业数据

- ) : ( - - - - - - - - - - {rows.map((row) => ( - - - - - - ))} - -
标题截止日期状态
{row.title}{row.dueDate} - -
- )} -
+ + + 数据源已迁移到仪表盘聚合查询(teacherDashboard)。 请访问 /shell/teacher + 查看真实数据。 + + ); } diff --git a/apps/portal-shell/src/widgets/universal/schedule-widget/index.tsx b/apps/portal-shell/src/widgets/universal/schedule-widget/index.tsx index 057a882..e2d91be 100644 --- a/apps/portal-shell/src/widgets/universal/schedule-widget/index.tsx +++ b/apps/portal-shell/src/widgets/universal/schedule-widget/index.tsx @@ -1,67 +1,26 @@ -"use client"; +"use client"; + +import type { PluginProps } from "@/lib/types"; +import { Card, CardContent } from "@/shared/components/ui/card"; /** - * schedule-widget(universal / main) + * ScheduleWidget(已迁移到仪表盘聚合查询,P1-2) * - * 通过 useSchedule 查询 apollo-router → core-edu 子图的 schedule 数据。 - * classId 从 URL Search Params 读取,默认展示当天课表(按星期几过滤)。 + * 原假契约查询 useSchedule 已下线(schedule root 字段在后端 schema 不存在)。 + * 数据源已迁移到 data-ana 的 teacherDashboard 真实聚合查询。 + * 参见 src/app/shell/teacher/page.tsx 和 src/lib/api/dashboard.ts。 * - * 关联:portal-shell spec §5.2.1 URL 驱动、§5.6 统一 Hook、M8 验收 + * 关联:ARCHITECTURE.md §5.5 / §10 P1-2 */ -import { useSearchParams } from "next/navigation"; -import { useSchedule } from "@/lib/api/universal"; -import { PluginSkeleton } from "@/shell/PluginLoader"; -import type { PluginProps } from "@/lib/types"; - export default function ScheduleWidget( _props: PluginProps, ): React.ReactElement { - const searchParams = useSearchParams(); - const classId = searchParams.get("classId") ?? ""; - const today = new Date().getDay(); - const dayOfWeek = today === 0 ? 7 : today; // 1=周一 ... 7=周日 - - const { data, loading } = useSchedule(classId, dayOfWeek, { - enabled: classId.length > 0, - }); - - if (loading && !data) { - return ; - } - - if (!classId) { - return ( -
-

课表

-

请先选择班级

-
- ); - } - - const rows = [...(data ?? [])].sort((a, b) => - a.startTime.localeCompare(b.startTime), - ); - return ( -
-

今日课表

- {rows.length === 0 ? ( -

今日无课

- ) : ( -
    - {rows.map((row) => ( -
  • - {row.subject} - - {row.startTime} - {row.endTime} - - - {row.teacherName} - -
  • - ))} -
- )} -
+ + + 数据源已迁移到仪表盘聚合查询(teacherDashboard)。 请访问 /shell/teacher + 查看真实数据。 + + ); }