- lib/auth.ts: localStorage token 存储 + login/logout - app/login: 登录表单页 - components/AppShell: 视口驱动侧边栏 + 路由保护 - app/(app)/layout: 路由组布局套 AppShell - app/(app)/dashboard: 聚合统计卡片页 - app/(app)/classes: 迁移 CRUD 改用真实 JWT - app/page.tsx: 根路径重定向 - known-issues.md: 记录 P2 经验
140 lines
3.9 KiB
TypeScript
140 lines
3.9 KiB
TypeScript
"use client";
|
||
|
||
import { useEffect, useState } from "react";
|
||
import { getToken, getUser, type UserInfo } from "@/lib/auth";
|
||
|
||
interface DashboardData {
|
||
user: { success: boolean; data?: { user: UserInfo } };
|
||
classes: { success: boolean; data?: unknown[] };
|
||
}
|
||
|
||
export default function DashboardPage() {
|
||
const [data, setData] = useState<DashboardData | null>(null);
|
||
const [error, setError] = useState<string | null>(null);
|
||
const [loading, setLoading] = useState(true);
|
||
const user = getUser();
|
||
|
||
useEffect(() => {
|
||
const token = getToken();
|
||
if (!token) return;
|
||
(async () => {
|
||
try {
|
||
const res = await fetch("/api/v1/teacher/dashboard", {
|
||
headers: { Authorization: `Bearer ${token}` },
|
||
});
|
||
const json = await res.json();
|
||
if (json.success) {
|
||
setData(json.data);
|
||
} else {
|
||
setError(json.error?.message || "加载失败");
|
||
}
|
||
} catch (e) {
|
||
setError(e instanceof Error ? e.message : "网络错误");
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
})();
|
||
}, []);
|
||
|
||
const classesCount = Array.isArray(data?.classes?.data)
|
||
? data!.classes.data.length
|
||
: 0;
|
||
|
||
return (
|
||
<div className="px-10 py-10">
|
||
<header className="mb-8">
|
||
<h1
|
||
className="text-3xl"
|
||
style={{ fontFamily: "var(--font-serif)", color: "var(--color-ink)" }}
|
||
>
|
||
欢迎,{user?.name || "老师"}
|
||
</h1>
|
||
<p className="mt-1 text-sm" style={{ color: "var(--color-ink-muted)" }}>
|
||
{user?.email} · 角色:{user?.roles.join(", ") || "无"} · 数据范围:
|
||
{user?.dataScope || "-"}
|
||
</p>
|
||
</header>
|
||
|
||
<div className="rule-thin mb-8" />
|
||
|
||
{loading ? (
|
||
<p className="text-sm" style={{ color: "var(--color-ink-muted)" }}>
|
||
加载中...
|
||
</p>
|
||
) : error ? (
|
||
<div
|
||
className="mark-left py-2 mb-4"
|
||
style={{ borderColor: "var(--color-accent)" }}
|
||
>
|
||
<p className="text-sm px-3" style={{ color: "var(--color-accent)" }}>
|
||
{error}
|
||
</p>
|
||
</div>
|
||
) : (
|
||
<section className="grid grid-cols-3 gap-6">
|
||
<div
|
||
className="p-6 border"
|
||
style={{ borderColor: "var(--color-rule)" }}
|
||
>
|
||
<p
|
||
className="text-xs uppercase tracking-wide"
|
||
style={{ color: "var(--color-ink-muted)" }}
|
||
>
|
||
班级总数
|
||
</p>
|
||
<p
|
||
className="mt-3 text-4xl"
|
||
style={{
|
||
fontFamily: "var(--font-serif)",
|
||
color: "var(--color-ink)",
|
||
}}
|
||
>
|
||
{classesCount}
|
||
</p>
|
||
</div>
|
||
<div
|
||
className="p-6 border"
|
||
style={{ borderColor: "var(--color-rule)" }}
|
||
>
|
||
<p
|
||
className="text-xs uppercase tracking-wide"
|
||
style={{ color: "var(--color-ink-muted)" }}
|
||
>
|
||
权限点
|
||
</p>
|
||
<p
|
||
className="mt-3 text-4xl"
|
||
style={{
|
||
fontFamily: "var(--font-serif)",
|
||
color: "var(--color-ink)",
|
||
}}
|
||
>
|
||
{user?.permissions.length ?? 0}
|
||
</p>
|
||
</div>
|
||
<div
|
||
className="p-6 border"
|
||
style={{ borderColor: "var(--color-rule)" }}
|
||
>
|
||
<p
|
||
className="text-xs uppercase tracking-wide"
|
||
style={{ color: "var(--color-ink-muted)" }}
|
||
>
|
||
数据范围
|
||
</p>
|
||
<p
|
||
className="mt-3 text-2xl"
|
||
style={{
|
||
fontFamily: "var(--font-serif)",
|
||
color: "var(--color-ink)",
|
||
}}
|
||
>
|
||
{user?.dataScope || "-"}
|
||
</p>
|
||
</div>
|
||
</section>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|