Files
soul/app/admin/page.tsx

185 lines
7.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client"
import { useState, useEffect } from "react"
import { useRouter } from "next/navigation"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Users, BookOpen, ShoppingBag, TrendingUp, RefreshCw, ChevronRight } from "lucide-react"
export default function AdminDashboard() {
const router = useRouter()
const [mounted, setMounted] = useState(false)
const [users, setUsers] = useState<any[]>([])
const [purchases, setPurchases] = useState<any[]>([])
// 从API获取数据任意接口失败时仍保持页面可展示不抛错
async function loadData() {
try {
const usersRes = await fetch('/api/db/users')
const usersData = await usersRes.ok ? usersRes.json().catch(() => ({})) : { success: false }
if (usersData.success && Array.isArray(usersData.users)) {
setUsers(usersData.users)
}
} catch (e) {
console.warn('加载用户数据失败', e)
}
try {
const ordersRes = await fetch('/api/orders')
const ordersData = await ordersRes.ok ? ordersRes.json().catch(() => ({})) : { success: false }
if (ordersData.success && Array.isArray(ordersData.orders)) {
setPurchases(ordersData.orders)
}
} catch (e) {
console.warn('加载订单数据失败', e)
}
}
useEffect(() => {
setMounted(true)
loadData()
}, [])
// 防止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 + (Number(p?.amount) || 0), 0)
const totalUsers = users.length
const totalPurchases = purchases.length
const stats = [
{ title: "总用户数", value: totalUsers, icon: Users, color: "text-blue-400", bg: "bg-blue-500/20", link: "/admin/users" },
{
title: "总收入",
value: `¥${Number(totalRevenue).toFixed(2)}`,
icon: TrendingUp,
color: "text-[#38bdac]",
bg: "bg-[#38bdac]/20",
link: "/admin/orders",
},
{ title: "订单数", value: totalPurchases, icon: ShoppingBag, color: "text-purple-400", bg: "bg-purple-500/20", link: "/admin/orders" },
{
title: "转化率",
value: `${totalUsers > 0 ? (Number(totalPurchases) / Number(totalUsers) * 100).toFixed(1) : 0}%`,
icon: BookOpen,
color: "text-orange-400",
bg: "bg-orange-500/20",
link: "/admin/distribution",
},
]
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) => (
<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)}
>
<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>
<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>
</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">{p?.createdAt ? new Date(p.createdAt).toLocaleString() : "-"}</p>
</div>
<div className="text-right">
<p className="text-sm font-bold text-[#38bdac]">+¥{Number(p?.amount) || 0}</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]">
{u.nickname?.charAt(0) || "?"}
</div>
<div>
<p className="text-sm font-medium text-white">{u.nickname || "匿名用户"}</p>
<p className="text-xs text-gray-500">{u.phone || "-"}</p>
</div>
</div>
<p className="text-xs text-gray-400">
{u?.createdAt ? new Date(u.createdAt).toLocaleDateString() : "-"}
</p>
</div>
))}
{users.length === 0 && <p className="text-gray-500 text-center py-8"></p>}
</div>
</CardContent>
</Card>
</div>
</div>
)
}