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,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 (
<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.total_teachers} icon={Users} />
<StatCard
title="学生总数"
value={data.total_students}
icon={GraduationCap}
/>
<StatCard
title="班级总数"
value={data.total_classes}
icon={Building2}
/>
<StatCard
title="全校平均分"
value={data.school_avg_score?.toFixed(1) ?? "--"}
icon={Activity}
/>
</>
}
>
<DashboardSection title="近期预警" variant="list">
{data.recent_warnings ? (
<div className="space-y-2 text-sm">
<div className="flex items-center justify-between">
<span className="font-medium">
{data.recent_warnings.target_name ?? "--"}
</span>
<span className="text-muted-foreground">
{data.recent_warnings.severity ?? "--"}
</span>
</div>
<p className="text-xs text-muted-foreground">
{data.recent_warnings.warning_type ?? "--"} · {" "}
{data.recent_warnings.current_value?.toFixed(1) ?? "--"} / {" "}
{data.recent_warnings.threshold?.toFixed(1) ?? "--"}
</p>
</div>
) : (
<p className="text-sm text-muted-foreground"></p>
)}
</DashboardSection>
<DashboardSection title="AI 用量" variant="card">
{data.ai_usage ? (
<div className="space-y-2 text-sm">
<div className="flex items-center justify-between">
<span className="text-muted-foreground"></span>
<span className="font-medium">
{data.ai_usage.total_requests ?? "--"}
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-muted-foreground"> Token</span>
<span className="font-medium">
{data.ai_usage.total_tokens ?? "--"}
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-muted-foreground"></span>
<span className="font-medium">
{data.ai_usage.total_cost_cents ?? "--"}
</span>
</div>
</div>
) : (
<p className="text-sm text-muted-foreground"> AI </p>
)}
</DashboardSection>
</DashboardShell>
);
}