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

@@ -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 (
<DashboardShell title="学生仪表盘" description="学习概览">
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4">
{Array.from({ length: 4 }).map((_, i) => (
<StatCard key={i} title="" value="" isLoading />
))}
</div>
</DashboardShell>
);
}
if (error || !data) {
return (
<DashboardShell title="学生仪表盘" description="学习概览">
<Card>
<CardContent className="py-8 text-center text-muted-foreground">
</CardContent>
</Card>
</DashboardShell>
);
}
return (
<DashboardShell
title="学生仪表盘"
description="学习概览"
stats={
<>
<StatCard
title="平均分"
value={data.avg_score?.toFixed(1) ?? "--"}
icon={GraduationCap}
/>
<StatCard
title="班级排名"
value={`${data.class_rank ?? "--"} / ${data.total_students ?? "--"}`}
icon={TrendingUp}
/>
<StatCard
title="待交作业"
value={data.pending_homework}
icon={BookOpen}
highlight={data.pending_homework > 0}
/>
</>
}
>
<DashboardSection title="薄弱知识点" variant="list">
{data.weak_points ? (
<div className="space-y-2 text-sm">
<div className="flex items-center justify-between">
<span className="font-medium">
{data.weak_points.title ?? "--"}
</span>
<span className="text-muted-foreground">
{data.weak_points.mastery?.toFixed(1) ?? "--"}%
</span>
</div>
<p className="text-xs text-muted-foreground">
{data.weak_points.error_count ?? 0}
</p>
</div>
) : (
<p className="text-sm text-muted-foreground"></p>
)}
</DashboardSection>
<DashboardSection title="近期成绩趋势" variant="chart">
{data.recent_trends ? (
<div className="text-sm">
<span className="text-muted-foreground">
{data.recent_trends.date ?? "--"}
</span>
<span className="font-medium">
{data.recent_trends.score?.toFixed(1) ?? "--"}
</span>
</div>
) : (
<p className="text-sm text-muted-foreground"></p>
)}
</DashboardSection>
</DashboardShell>
);
}