147 lines
4.5 KiB
TypeScript
147 lines
4.5 KiB
TypeScript
"use client";
|
||
|
||
/**
|
||
* Dashboard 页面 - 教师仪表盘
|
||
*
|
||
* 数据来源(ARB-001 §1.2):GraphQL DashboardQuery
|
||
* - 并行聚合:iam.me + iam.viewports + iam.permissions + classes.byTeacher
|
||
* - 返回:user + classes + viewports + stats
|
||
*
|
||
* 维护者:ai13(teacher-portal)
|
||
*/
|
||
|
||
import { useQuery } from "urql";
|
||
import Link from "next/link";
|
||
import { Loading, Empty } from "@edu/ui-components";
|
||
import { DashboardQuery } from "@/lib/graphql";
|
||
import type { Class } from "@/lib/graphql";
|
||
|
||
export default function DashboardPage() {
|
||
const [result] = useQuery({ query: DashboardQuery });
|
||
|
||
if (result.fetching) {
|
||
return (
|
||
<div className="px-10 py-10">
|
||
<Loading lines={5} />
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (result.error) {
|
||
return (
|
||
<div className="px-10 py-10">
|
||
<header className="mb-8">
|
||
<h1 className="text-3xl font-serif text-ink">欢迎</h1>
|
||
</header>
|
||
<div className="rule-thin mb-8" />
|
||
<div className="mark-left py-2 mb-4 border-l-2 border-danger pl-md">
|
||
<p className="text-sm px-3 text-danger">
|
||
仪表盘加载失败:{result.error.message}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
const data = result.data?.dashboard;
|
||
if (!data) {
|
||
return (
|
||
<div className="px-10 py-10">
|
||
<Empty title="暂无数据" description="仪表盘数据尚未就绪" />
|
||
</div>
|
||
);
|
||
}
|
||
|
||
const { user, classes, stats } = data;
|
||
|
||
return (
|
||
<div className="px-10 py-10">
|
||
<header className="mb-8">
|
||
<h1 className="text-3xl font-serif text-ink">欢迎,{user.name}</h1>
|
||
<p className="mt-1 text-sm text-ink-muted">
|
||
{user.email} · 角色:{user.roles.join(", ") || "无"} · 数据范围:
|
||
{user.dataScope}
|
||
</p>
|
||
</header>
|
||
|
||
<div className="rule-thin mb-8" />
|
||
|
||
{/* 统计卡片 */}
|
||
<section className="grid grid-cols-3 gap-6 mb-10">
|
||
<div className="p-6 border border-rule rounded-card">
|
||
<p className="text-tiny uppercase tracking-wide text-ink-muted">
|
||
班级总数
|
||
</p>
|
||
<p className="mt-3 text-4xl font-serif text-ink">{classes.length}</p>
|
||
</div>
|
||
<div className="p-6 border border-rule rounded-card">
|
||
<p className="text-tiny uppercase tracking-wide text-ink-muted">
|
||
考试总数
|
||
</p>
|
||
<p className="mt-3 text-4xl font-serif text-ink">
|
||
{stats.totalExams}
|
||
</p>
|
||
</div>
|
||
<div className="p-6 border border-rule rounded-card">
|
||
<p className="text-tiny uppercase tracking-wide text-ink-muted">
|
||
待批改
|
||
</p>
|
||
<p className="mt-3 text-4xl font-serif text-accent">
|
||
{stats.pendingGrading}
|
||
</p>
|
||
</div>
|
||
</section>
|
||
|
||
{/* 班级快览 */}
|
||
<section>
|
||
<div className="flex items-baseline justify-between mb-4">
|
||
<h2 className="text-xl font-serif text-ink">
|
||
我的班级
|
||
<span className="ml-2 text-sm font-sans text-ink-muted">
|
||
{classes.length} 个
|
||
</span>
|
||
</h2>
|
||
<Link
|
||
href="/classes"
|
||
className="text-tiny uppercase tracking-wide text-accent hover:opacity-70"
|
||
>
|
||
查看全部
|
||
</Link>
|
||
</div>
|
||
<div className="rule-thin mb-6" />
|
||
|
||
{classes.length === 0 ? (
|
||
<Empty title="暂无班级" description="请联系管理员分配班级" />
|
||
) : (
|
||
<ul className="space-y-0">
|
||
{(classes as Class[]).slice(0, 5).map((cls) => (
|
||
<li
|
||
key={cls.id}
|
||
className="py-4 grid grid-cols-12 gap-4 items-baseline border-b border-rule"
|
||
>
|
||
<div className="col-span-8">
|
||
<h3 className="text-lg font-serif text-ink">{cls.name}</h3>
|
||
<p className="mt-1 text-tiny text-ink-muted">
|
||
年级:{cls.gradeId}
|
||
</p>
|
||
</div>
|
||
<div className="col-span-2 text-sm text-ink-muted">
|
||
{cls.studentCount} 名学生
|
||
</div>
|
||
<div className="col-span-2 text-right">
|
||
<Link
|
||
href={`/students?classId=${cls.id}`}
|
||
className="text-tiny uppercase tracking-wide text-accent hover:opacity-70"
|
||
>
|
||
查看学生
|
||
</Link>
|
||
</div>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
)}
|
||
</section>
|
||
</div>
|
||
);
|
||
}
|