feat: merge admin routes into unified frontend

This commit is contained in:
2026-04-22 11:46:40 +08:00
parent 745f6f07db
commit 691b80a89f
25 changed files with 1144 additions and 35 deletions

View File

@@ -0,0 +1,117 @@
"use client";
import { useQuery } from "@tanstack/react-query";
import clsx from "clsx";
import {
Blocks,
ChartColumnBig,
Coins,
KeySquare,
Link2,
LogOut,
Package2,
Settings2,
Users,
Workflow,
} from "lucide-react";
import Link from "next/link";
import { usePathname, useRouter } from "next/navigation";
import { useEffect } from "react";
import { api } from "@/lib/api";
const navigation = [
{ href: "/admin/dashboard", label: "仪表盘", icon: ChartColumnBig },
{ href: "/admin/users", label: "用户管理", icon: Users },
{ href: "/admin/recharge-orders", label: "充值订单", icon: Coins },
{ href: "/admin/redeem-codes", label: "兑换密钥", icon: KeySquare },
{ href: "/admin/growth-rules", label: "增长奖励", icon: Link2 },
{ href: "/admin/invite-relations", label: "邀请关系", icon: Link2 },
{ href: "/admin/provider-accounts", label: "供应商账号", icon: Workflow },
{ href: "/admin/provider-models", label: "供应商模型", icon: Blocks },
{ href: "/admin/video-models", label: "平台模型", icon: Package2 },
{ href: "/admin/video-model-bindings", label: "模型绑定", icon: Workflow },
{ href: "/admin/pricing-rules", label: "价格规则", icon: Coins },
{ href: "/admin/video-tasks", label: "视频任务", icon: Workflow },
{ href: "/admin/callback-logs", label: "回调日志", icon: ChartColumnBig },
{ href: "/admin/system-config", label: "系统配置", icon: Settings2 },
];
export function AdminShell({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
const router = useRouter();
const meQuery = useQuery({
queryKey: ["admin-me"],
queryFn: () => api.get("/api/v1/admin/auth/me"),
});
useEffect(() => {
if (meQuery.error) {
router.replace("/admin/login");
}
}, [meQuery.error, router]);
if (meQuery.isLoading || !meQuery.data) {
return (
<div className="fullscreen-shell">
<div className="pulse-card">...</div>
</div>
);
}
return (
<div className="shell-grid">
<aside className="shell-sidebar">
<div className="brand-block">
<span className="brand-kicker">AIVIDEO ADMIN</span>
<h1></h1>
<p></p>
</div>
<nav className="sidebar-nav">
{navigation.map((item) => {
const Icon = item.icon;
return (
<Link
key={item.href}
href={item.href}
className={clsx("nav-item", {
active: pathname.startsWith(item.href),
})}
>
<Icon size={18} />
<span>{item.label}</span>
</Link>
);
})}
</nav>
<div className="profile-card">
<div>
<div className="profile-name">
{(meQuery.data as { nickname: string }).nickname}
</div>
<div className="profile-meta">
{(meQuery.data as { username: string }).username}
</div>
</div>
<button
className="ghost-button"
onClick={async () => {
await api.post("/api/v1/admin/auth/logout");
router.replace("/admin/login");
}}
>
<LogOut size={16} />
退
</button>
</div>
</aside>
<main className="shell-main">
<section className="shell-content">{children}</section>
</main>
</div>
);
}

View File

@@ -12,6 +12,7 @@ const statusMap: Record<string, string> = {
unused: "success",
used: "ghost",
disabled: "danger",
rewarded: "success",
};
const statusLabelMap: Record<string, string> = {
@@ -26,6 +27,7 @@ const statusLabelMap: Record<string, string> = {
unused: "未使用",
used: "已使用",
disabled: "已停用",
rewarded: "已奖励",
};
export function StatusBadge({ value }: { value: string }) {