2026-01-09 11:58:08 +08:00
|
|
|
|
"use client"
|
|
|
|
|
|
|
2026-01-21 15:49:12 +08:00
|
|
|
|
import { useState, useEffect } from "react"
|
|
|
|
|
|
import { useRouter } from "next/navigation"
|
2026-01-09 11:58:08 +08:00
|
|
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
|
|
|
|
|
import { useStore } from "@/lib/store"
|
2026-01-21 15:49:12 +08:00
|
|
|
|
import { Users, BookOpen, ShoppingBag, TrendingUp, RefreshCw, ChevronRight } from "lucide-react"
|
2026-01-09 11:58:08 +08:00
|
|
|
|
|
|
|
|
|
|
export default function AdminDashboard() {
|
2026-01-21 15:49:12 +08:00
|
|
|
|
const router = useRouter()
|
2026-01-09 11:58:08 +08:00
|
|
|
|
const { getAllUsers, getAllPurchases } = useStore()
|
2026-01-21 15:49:12 +08:00
|
|
|
|
const [mounted, setMounted] = useState(false)
|
|
|
|
|
|
const [users, setUsers] = useState<any[]>([])
|
|
|
|
|
|
const [purchases, setPurchases] = useState<any[]>([])
|
2026-01-09 11:58:08 +08:00
|
|
|
|
|
2026-01-21 15:49:12 +08:00
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
setMounted(true)
|
|
|
|
|
|
// 客户端加载数据
|
|
|
|
|
|
setUsers(getAllUsers())
|
|
|
|
|
|
setPurchases(getAllPurchases())
|
|
|
|
|
|
}, [getAllUsers, getAllPurchases])
|
|
|
|
|
|
|
|
|
|
|
|
// 防止Hydration错误:服务端渲染时显示加载状态
|
|
|
|
|
|
if (!mounted) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="p-8 max-w-7xl mx-auto">
|
|
|
|
|
|
<h1 className="text-2xl font-bold mb-8 text-white">数据概览</h1>
|
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
|
|
|
|
|
{[1, 2, 3, 4].map((i) => (
|
|
|
|
|
|
<Card key={i} className="bg-[#0f2137] border-gray-700/50 shadow-xl">
|
|
|
|
|
|
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
|
|
|
|
|
<div className="h-4 w-20 bg-gray-700 rounded animate-pulse" />
|
|
|
|
|
|
<div className="w-8 h-8 bg-gray-700 rounded-lg animate-pulse" />
|
|
|
|
|
|
</CardHeader>
|
|
|
|
|
|
<CardContent>
|
|
|
|
|
|
<div className="h-8 w-16 bg-gray-700 rounded animate-pulse" />
|
|
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="flex items-center justify-center py-8">
|
|
|
|
|
|
<RefreshCw className="w-6 h-6 text-[#38bdac] animate-spin" />
|
|
|
|
|
|
<span className="ml-2 text-gray-400">加载中...</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const totalRevenue = purchases.reduce((sum, p) => sum + (p.amount || 0), 0)
|
2026-01-09 11:58:08 +08:00
|
|
|
|
const totalUsers = users.length
|
|
|
|
|
|
const totalPurchases = purchases.length
|
|
|
|
|
|
|
|
|
|
|
|
const stats = [
|
2026-01-21 15:49:12 +08:00
|
|
|
|
{ title: "总用户数", value: totalUsers, icon: Users, color: "text-blue-400", bg: "bg-blue-500/20", link: "/admin/users" },
|
2026-01-09 11:58:08 +08:00
|
|
|
|
{
|
|
|
|
|
|
title: "总收入",
|
|
|
|
|
|
value: `¥${totalRevenue.toFixed(2)}`,
|
|
|
|
|
|
icon: TrendingUp,
|
|
|
|
|
|
color: "text-[#38bdac]",
|
|
|
|
|
|
bg: "bg-[#38bdac]/20",
|
2026-01-21 15:49:12 +08:00
|
|
|
|
link: "/admin/orders",
|
2026-01-09 11:58:08 +08:00
|
|
|
|
},
|
2026-01-21 15:49:12 +08:00
|
|
|
|
{ title: "订单数", value: totalPurchases, icon: ShoppingBag, color: "text-purple-400", bg: "bg-purple-500/20", link: "/admin/orders" },
|
2026-01-09 11:58:08 +08:00
|
|
|
|
{
|
|
|
|
|
|
title: "转化率",
|
|
|
|
|
|
value: `${totalUsers > 0 ? ((totalPurchases / totalUsers) * 100).toFixed(1) : 0}%`,
|
|
|
|
|
|
icon: BookOpen,
|
|
|
|
|
|
color: "text-orange-400",
|
|
|
|
|
|
bg: "bg-orange-500/20",
|
2026-01-21 15:49:12 +08:00
|
|
|
|
link: "/admin/distribution",
|
2026-01-09 11:58:08 +08:00
|
|
|
|
},
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="p-8 max-w-7xl mx-auto">
|
|
|
|
|
|
<h1 className="text-2xl font-bold mb-8 text-white">数据概览</h1>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
|
|
|
|
|
{stats.map((stat, index) => (
|
2026-01-21 15:49:12 +08:00
|
|
|
|
<Card
|
|
|
|
|
|
key={index}
|
|
|
|
|
|
className="bg-[#0f2137] border-gray-700/50 shadow-xl cursor-pointer hover:border-[#38bdac]/50 transition-colors group"
|
|
|
|
|
|
onClick={() => stat.link && router.push(stat.link)}
|
|
|
|
|
|
>
|
2026-01-09 11:58:08 +08:00
|
|
|
|
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
|
|
|
|
|
<CardTitle className="text-sm font-medium text-gray-400">{stat.title}</CardTitle>
|
|
|
|
|
|
<div className={`p-2 rounded-lg ${stat.bg}`}>
|
|
|
|
|
|
<stat.icon className={`w-4 h-4 ${stat.color}`} />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</CardHeader>
|
|
|
|
|
|
<CardContent>
|
2026-01-21 15:49:12 +08:00
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
|
<div className="text-2xl font-bold text-white">{stat.value}</div>
|
|
|
|
|
|
<ChevronRight className="w-5 h-5 text-gray-600 group-hover:text-[#38bdac] transition-colors" />
|
|
|
|
|
|
</div>
|
2026-01-09 11:58:08 +08:00
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
|
|
|
|
|
<Card className="bg-[#0f2137] border-gray-700/50 shadow-xl">
|
|
|
|
|
|
<CardHeader>
|
|
|
|
|
|
<CardTitle className="text-white">最近订单</CardTitle>
|
|
|
|
|
|
</CardHeader>
|
|
|
|
|
|
<CardContent>
|
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
|
{purchases
|
|
|
|
|
|
.slice(-5)
|
|
|
|
|
|
.reverse()
|
|
|
|
|
|
.map((p) => (
|
|
|
|
|
|
<div
|
|
|
|
|
|
key={p.id}
|
|
|
|
|
|
className="flex items-center justify-between p-4 bg-[#0a1628] rounded-lg border border-gray-700/30"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<p className="text-sm font-medium text-white">{p.sectionTitle || "整本购买"}</p>
|
|
|
|
|
|
<p className="text-xs text-gray-500">{new Date(p.createdAt).toLocaleString()}</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="text-right">
|
|
|
|
|
|
<p className="text-sm font-bold text-[#38bdac]">+¥{p.amount}</p>
|
|
|
|
|
|
<p className="text-xs text-gray-400">{p.paymentMethod || "微信支付"}</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
|
|
|
|
|
{purchases.length === 0 && <p className="text-gray-500 text-center py-8">暂无订单数据</p>}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
|
|
<Card className="bg-[#0f2137] border-gray-700/50 shadow-xl">
|
|
|
|
|
|
<CardHeader>
|
|
|
|
|
|
<CardTitle className="text-white">新注册用户</CardTitle>
|
|
|
|
|
|
</CardHeader>
|
|
|
|
|
|
<CardContent>
|
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
|
{users
|
|
|
|
|
|
.slice(-5)
|
|
|
|
|
|
.reverse()
|
|
|
|
|
|
.map((u) => (
|
|
|
|
|
|
<div
|
|
|
|
|
|
key={u.id}
|
|
|
|
|
|
className="flex items-center justify-between p-4 bg-[#0a1628] rounded-lg border border-gray-700/30"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div className="flex items-center gap-3">
|
|
|
|
|
|
<div className="w-10 h-10 rounded-full bg-[#38bdac]/20 flex items-center justify-center text-sm font-medium text-[#38bdac]">
|
2026-01-21 15:49:12 +08:00
|
|
|
|
{u.nickname?.charAt(0) || "?"}
|
2026-01-09 11:58:08 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
2026-01-21 15:49:12 +08:00
|
|
|
|
<p className="text-sm font-medium text-white">{u.nickname || "匿名用户"}</p>
|
|
|
|
|
|
<p className="text-xs text-gray-500">{u.phone || "-"}</p>
|
2026-01-09 11:58:08 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-01-21 15:49:12 +08:00
|
|
|
|
<p className="text-xs text-gray-400">
|
|
|
|
|
|
{u.createdAt ? new Date(u.createdAt).toLocaleDateString() : "-"}
|
|
|
|
|
|
</p>
|
2026-01-09 11:58:08 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
|
|
|
|
|
{users.length === 0 && <p className="text-gray-500 text-center py-8">暂无用户数据</p>}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|