www_site/components/admin/gate.tsx

92 lines
3.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useEffect, useState } from "react";
import { getMe } from "@/lib/admin";
import { PreviewNotice, StatusCard } from "@/components/ui";
type Me = { email: string; role: string; plan: string; status: string };
const nav = [
{ href: "/admin", label: "概览" },
{ href: "/admin/users", label: "用户" },
{ href: "/admin/tts", label: "TTS 与审计" },
{ href: "/cms/admin", label: "CMS 内容管理" },
];
export function AdminGate({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
const [me, setMe] = useState<Me | null>(null);
const [state, setState] = useState<"loading" | "denied" | "ready">("loading");
useEffect(() => {
getMe()
.then((m) => {
setMe(m);
setState(m.role === "admin" ? "ready" : "denied");
})
.catch(() => setState("denied"));
}, []);
if (state === "loading") {
return (
<div className="container-shell py-16 sm:py-24">
<StatusCard title="正在校验管理权限" description="正从业务 API 读取当前管理员会话。" tone="info" />
</div>
);
}
if (state === "denied") {
return (
<div className="container-shell py-16 sm:py-24">
<PreviewNotice>访</PreviewNotice>
<div className="mt-8">
<StatusCard title="无权访问" description="当前会话不是管理员,或未登录。前往账户页登录后,只有 role=admin 的账户才能看到管理功能。" tone="warning" />
<Link className="button-ghost mt-6 inline-flex" href="/account">
</Link>
</div>
</div>
);
}
return (
<>
<div className="container-shell grid gap-6 py-10 lg:grid-cols-[220px_1fr]">
<aside>
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-subtle">Operations</p>
<nav className="mt-4 flex gap-1 lg:flex-col" aria-label="管理后台导航">
{nav.map((item) => {
const active = item.href === "/admin" ? pathname === "/admin" : pathname.startsWith(item.href);
return (
<Link
className={`rounded-lg px-3 py-2 text-sm font-semibold transition-colors ${active ? "bg-blue/10 text-cyan" : "text-muted hover:text-copy"}`}
href={item.href}
key={item.href}
>
{item.label}
</Link>
);
})}
</nav>
<p className="mt-6 rounded-xl border border-line bg-panel/50 p-4 text-xs leading-6 text-subtle">
{me?.email}
</p>
</aside>
<div>{children}</div>
</div>
</>
);
}
export function AdminPageShell({ eyebrow, title, description, children }: { eyebrow: string; title: string; description?: string; children: React.ReactNode }) {
return (
<div>
<p className="eyebrow">{eyebrow}</p>
<h1 className="mt-4 text-3xl font-semibold tracking-[-0.04em] sm:text-4xl">{title}</h1>
{description ? <p className="mt-3 max-w-2xl leading-6 text-muted">{description}</p> : null}
<div className="mt-8">{children}</div>
</div>
);
}