92 lines
3.3 KiB
TypeScript
92 lines
3.3 KiB
TypeScript
"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>
|
||
);
|
||
}
|