339 lines
13 KiB
TypeScript
339 lines
13 KiB
TypeScript
"use client"
|
||
|
||
import { useState, useEffect } from "react"
|
||
import { useRouter, usePathname } from "next/navigation"
|
||
import Link from "next/link"
|
||
import {
|
||
Users,
|
||
User,
|
||
Radio,
|
||
Building2,
|
||
ShoppingBag,
|
||
FileText,
|
||
Wallet,
|
||
MessageSquare,
|
||
Settings,
|
||
LogOut,
|
||
Gamepad2,
|
||
Hotel,
|
||
CreditCard,
|
||
Bell,
|
||
Search,
|
||
Activity,
|
||
ChevronLeft,
|
||
ChevronRight,
|
||
LayoutDashboard,
|
||
Trophy,
|
||
Monitor,
|
||
History,
|
||
PieChart,
|
||
CircleDollarSign,
|
||
} from "lucide-react"
|
||
import { Input } from "@/components/ui/input"
|
||
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
|
||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||
import { Button } from "@/components/ui/button"
|
||
import { cn } from "@/lib/utils"
|
||
|
||
function AdminLoginForm({ onSuccess }: { onSuccess: () => void; theme?: "light" }) {
|
||
const [username, setUsername] = useState("")
|
||
const [password, setPassword] = useState("")
|
||
const [loading, setLoading] = useState(false)
|
||
|
||
const handleLogin = async () => {
|
||
if (!username.trim() || !password) {
|
||
alert("请输入用户名和密码")
|
||
return
|
||
}
|
||
setLoading(true)
|
||
try {
|
||
const res = await fetch("/api/admin/auth/login", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ username: username.trim(), password }),
|
||
})
|
||
const json = await res.json()
|
||
if (json.success) {
|
||
localStorage.setItem("admin_logged_in", "true")
|
||
onSuccess()
|
||
} else {
|
||
alert(json.message || "用户名或密码错误")
|
||
}
|
||
} catch {
|
||
alert("登录请求失败")
|
||
}
|
||
setLoading(false)
|
||
}
|
||
|
||
return (
|
||
<>
|
||
<div>
|
||
<label className="text-sm mb-1 block text-gray-600">用户名</label>
|
||
<Input
|
||
value={username}
|
||
onChange={(e) => setUsername(e.target.value)}
|
||
placeholder="请输入用户名"
|
||
className="bg-gray-50 border-gray-200 text-gray-900 placeholder:text-gray-400"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="text-sm mb-1 block text-gray-600">密码</label>
|
||
<Input
|
||
type="password"
|
||
value={password}
|
||
onChange={(e) => setPassword(e.target.value)}
|
||
placeholder="请输入密码"
|
||
className="bg-gray-50 border-gray-200 text-gray-900 placeholder:text-gray-400"
|
||
onKeyDown={(e) => e.key === "Enter" && handleLogin()}
|
||
/>
|
||
</div>
|
||
<Button
|
||
onClick={handleLogin}
|
||
disabled={loading}
|
||
className="w-full admin-glass rounded-xl text-gray-800 hover:bg-white/70 border border-white/50"
|
||
>
|
||
{loading ? "登录中..." : "登录后台"}
|
||
</Button>
|
||
</>
|
||
)
|
||
}
|
||
|
||
const menuGroups: { title: string; items: { id: string; label: string; icon: typeof LayoutDashboard; path: string }[] }[] = [
|
||
{
|
||
title: "概览",
|
||
items: [
|
||
{ id: "dashboard", label: "数据概览", icon: LayoutDashboard, path: "/admin" },
|
||
{ id: "screen", label: "大屏", icon: Monitor, path: "/admin/screen" },
|
||
],
|
||
},
|
||
{
|
||
title: "用户与创作者",
|
||
items: [
|
||
{ id: "users", label: "用户管理", icon: Users, path: "/admin/users" },
|
||
{ id: "journey", label: "用户旅程", icon: History, path: "/admin/journey" },
|
||
{ id: "rfm-rules", label: "RFM规则", icon: PieChart, path: "/admin/rfm-rules" },
|
||
{ id: "user-value", label: "用户估值", icon: CircleDollarSign, path: "/admin/user-value" },
|
||
{ id: "streamers", label: "主播管理", icon: Radio, path: "/admin/streamers" },
|
||
{ id: "guilds", label: "公会管理", icon: Building2, path: "/admin/guilds" },
|
||
{ id: "live", label: "直播管理", icon: Gamepad2, path: "/admin/live" },
|
||
{ id: "party", label: "派对群管理", icon: MessageSquare, path: "/admin/party" },
|
||
{ id: "content", label: "内容管理", icon: Trophy, path: "/admin/content" },
|
||
],
|
||
},
|
||
{
|
||
title: "业务与订单",
|
||
items: [
|
||
{ id: "mall", label: "商城管理", icon: ShoppingBag, path: "/admin/mall" },
|
||
{ id: "points", label: "游戏点卡", icon: CreditCard, path: "/admin/points" },
|
||
{ id: "orders", label: "订单管理", icon: FileText, path: "/admin/orders" },
|
||
{ id: "leveling", label: "代练管理", icon: Activity, path: "/admin/leveling" },
|
||
{ id: "pawn", label: "典当管理", icon: Wallet, path: "/admin/pawn" },
|
||
{ id: "accounts", label: "游戏账号", icon: User, path: "/admin/accounts" },
|
||
{ id: "hotel", label: "酒店/网咖", icon: Hotel, path: "/admin/hotel" },
|
||
],
|
||
},
|
||
{
|
||
title: "财务与系统",
|
||
items: [
|
||
{ id: "finance", label: "财务管理", icon: CreditCard, path: "/admin/finance" },
|
||
{ id: "settings", label: "系统设置", icon: Settings, path: "/admin/settings" },
|
||
],
|
||
},
|
||
]
|
||
|
||
function getCurrentMenuId(pathname: string | null): string {
|
||
if (!pathname) return "dashboard"
|
||
if (pathname === "/admin" || pathname === "/admin/") return "dashboard"
|
||
for (const group of menuGroups) {
|
||
for (const item of group.items) {
|
||
if (item.path !== "/admin" && pathname.startsWith(item.path)) return item.id
|
||
if (pathname === item.path) return item.id
|
||
}
|
||
}
|
||
return "dashboard"
|
||
}
|
||
|
||
export default function AdminLayout({ children }: { children: React.ReactNode }) {
|
||
const router = useRouter()
|
||
const pathname = usePathname()
|
||
const [isLoggedIn, setIsLoggedIn] = useState<boolean | null>(null)
|
||
const [sidebarOpen, setSidebarOpen] = useState(true)
|
||
useEffect(() => {
|
||
const loggedIn = localStorage.getItem("admin_logged_in") === "true"
|
||
setIsLoggedIn(loggedIn)
|
||
if (!loggedIn && pathname !== "/admin" && !pathname?.endsWith("/admin")) {
|
||
router.replace("/admin")
|
||
}
|
||
}, [pathname, router])
|
||
|
||
const handleLogout = () => {
|
||
localStorage.removeItem("admin_logged_in")
|
||
setIsLoggedIn(false)
|
||
router.replace("/admin")
|
||
}
|
||
|
||
const currentMenuId = getCurrentMenuId(pathname)
|
||
|
||
if (isLoggedIn === null) {
|
||
return (
|
||
<div className="admin-light-theme admin-bg-archer min-h-screen flex items-center justify-center">
|
||
<div className="text-gray-500">加载中...</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
if (!isLoggedIn) {
|
||
return (
|
||
<div className="admin-light-theme admin-bg-archer min-h-screen flex flex-col items-center justify-center p-4 relative">
|
||
<Card className="admin-glass w-full max-w-md border border-slate-200/80 rounded-2xl overflow-hidden shadow-lg">
|
||
<CardHeader className="text-center">
|
||
<div className="w-16 h-16 rounded-2xl mx-auto mb-4 flex items-center justify-center bg-white/60 border border-white/50">
|
||
<Gamepad2 className="w-8 h-8 text-gray-600" />
|
||
</div>
|
||
<CardTitle className="text-2xl text-gray-900">玩值电竞 · 管理后台</CardTitle>
|
||
<p className="text-sm mt-2 text-gray-500">请使用管理员账号登录(玩值舱)</p>
|
||
</CardHeader>
|
||
<CardContent className="space-y-4">
|
||
<AdminLoginForm onSuccess={() => setIsLoggedIn(true)} />
|
||
<p className="text-center text-xs text-gray-500">默认账号: admin / k123456</p>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<div className="admin-light-theme admin-bg-archer min-h-screen h-screen flex">
|
||
{/* 左侧导航:神射手风格、可折叠、悬浮、苹果毛玻璃 */}
|
||
<div className="flex shrink-0 pt-4 pb-4 pl-4 pr-2">
|
||
<aside
|
||
className={cn(
|
||
"admin-glass-panel flex flex-col transition-[width] duration-200 ease-out overflow-hidden shrink-0 rounded-2xl shadow-lg",
|
||
"h-[calc(100vh-2rem)]",
|
||
sidebarOpen ? "w-64 rounded-r-2xl" : "w-[72px] rounded-r-2xl"
|
||
)}
|
||
>
|
||
<div className="h-14 border-b border-gray-200/60 flex items-center justify-between px-3 shrink-0">
|
||
<div className="flex items-center gap-3 min-w-0">
|
||
<div className="w-9 h-9 rounded-xl flex items-center justify-center shrink-0 bg-white/50 border border-white/50">
|
||
<Gamepad2 className="w-5 h-5 text-gray-600" />
|
||
</div>
|
||
{sidebarOpen && (
|
||
<span className="font-semibold truncate text-sm text-gray-900">玩值电竞</span>
|
||
)}
|
||
</div>
|
||
<button
|
||
type="button"
|
||
onClick={() => setSidebarOpen(!sidebarOpen)}
|
||
className="shrink-0 p-1.5 rounded-lg transition-colors text-gray-500 hover:text-gray-900 hover:bg-gray-100"
|
||
title={sidebarOpen ? "收起侧栏" : "展开侧栏"}
|
||
>
|
||
{sidebarOpen ? <ChevronLeft className="w-4 h-4" /> : <ChevronRight className="w-4 h-4" />}
|
||
</button>
|
||
</div>
|
||
|
||
{sidebarOpen && (
|
||
<div className="p-4 border-b border-gray-200/60">
|
||
<p className="text-xs text-gray-500">用户资产数字化平台</p>
|
||
</div>
|
||
)}
|
||
|
||
<nav className="flex-1 py-3 px-4 overflow-y-auto overflow-x-hidden">
|
||
<ul className="space-y-1">
|
||
{menuGroups.map((group) => (
|
||
<li key={group.title}>
|
||
{sidebarOpen && (
|
||
<div className="px-2 py-1.5 text-[11px] font-medium uppercase tracking-wider text-gray-500">
|
||
{group.title}
|
||
</div>
|
||
)}
|
||
<div className="space-y-1">
|
||
{group.items.map((item) => {
|
||
const isActive = currentMenuId === item.id
|
||
return (
|
||
<Link
|
||
key={item.id}
|
||
href={item.path}
|
||
className={cn(
|
||
"flex items-center gap-3 rounded-xl px-4 py-3 text-sm font-medium transition-all duration-200",
|
||
isActive
|
||
? "bg-gradient-to-r from-violet-500 to-cyan-500 text-white shadow-md"
|
||
: "text-gray-600 hover:bg-white/60 hover:text-gray-900"
|
||
)}
|
||
>
|
||
<item.icon className="w-5 h-5 shrink-0" />
|
||
{sidebarOpen && <span className="truncate">{item.label}</span>}
|
||
</Link>
|
||
)
|
||
})}
|
||
</div>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
</nav>
|
||
|
||
<div className="p-4 border-t border-gray-200/60 shrink-0">
|
||
<div className="flex items-center gap-3 px-2">
|
||
<div className="w-8 h-8 rounded-full flex items-center justify-center text-sm font-bold bg-white/50 text-gray-700 border border-white/50">
|
||
管
|
||
</div>
|
||
{sidebarOpen && (
|
||
<div className="flex-1 min-w-0">
|
||
<p className="text-sm font-medium truncate text-gray-900">平台管理员</p>
|
||
<p className="text-xs truncate text-gray-500">admin@wanzhi.com</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
<button
|
||
type="button"
|
||
onClick={handleLogout}
|
||
className="mt-3 w-full flex items-center gap-3 px-4 py-2.5 rounded-xl transition-colors text-sm text-gray-500 hover:bg-gray-100 hover:text-red-600"
|
||
>
|
||
<LogOut className="w-4 h-4 shrink-0" />
|
||
{sidebarOpen && <span>退出登录</span>}
|
||
</button>
|
||
</div>
|
||
</aside>
|
||
</div>
|
||
|
||
{/* 主内容区:顶栏 + 内容区 */}
|
||
<main className="flex-1 flex flex-col min-w-0 h-screen overflow-hidden">
|
||
<header className="admin-glass h-14 rounded-bl-2xl flex items-center justify-between px-5 shrink-0 border-b border-l border-gray-200/60">
|
||
<div className="flex items-center gap-4">
|
||
<div className="relative w-56">
|
||
<Search className="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none text-gray-500" />
|
||
<Input
|
||
placeholder="搜索..."
|
||
className="w-full pl-9 h-9 text-sm rounded-2xl bg-white border-gray-200 text-gray-800 placeholder:text-gray-500 shadow-sm"
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<button type="button" className="relative p-2 rounded-xl transition-colors text-gray-500 hover:bg-gray-100 hover:text-gray-900" title="通知">
|
||
<Bell className="w-5 h-5" />
|
||
<span className="absolute top-1 right-1 w-4 h-4 bg-amber-500/80 rounded-full text-[10px] text-slate-900 flex items-center justify-center font-medium">
|
||
3
|
||
</span>
|
||
</button>
|
||
<div className="flex items-center gap-3 pl-2 border-l border-gray-200/60">
|
||
{typeof process !== "undefined" && process.env.NEXT_PUBLIC_BUILD_TIME && (
|
||
<span className="text-[11px] font-mono text-gray-500" title="当前镜像构建时间,用于确认已部署最新">
|
||
部署:{process.env.NEXT_PUBLIC_BUILD_TIME}
|
||
</span>
|
||
)}
|
||
<Avatar className="w-8 h-8 border border-white/50 bg-white/50">
|
||
<AvatarFallback className="text-sm bg-white/60 text-gray-700">管</AvatarFallback>
|
||
</Avatar>
|
||
<span className="text-sm font-medium text-gray-700">管理员</span>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<div className="flex-1 overflow-auto p-6 bg-transparent admin-content-area">
|
||
{children}
|
||
</div>
|
||
</main>
|
||
</div>
|
||
)
|
||
}
|