www_site/components/admin/gate.tsx

97 lines
3.6 KiB
TypeScript
Raw Normal View History

"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 与审计" },
2026-09-11 08:52:29 +00:00
{ 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);
2026-09-11 09:29:55 +00:00
const link = item.href.startsWith("/cms") ? <a
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}</a> : (
<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>
);
2026-09-11 09:29:55 +00:00
return 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>
);
}