Files
soul-yongping/app/admin/page.tsx

292 lines
12 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.json()
if (usersData.success && usersData.users) {
setUsers(usersData.users)
}
// 获取订单数据
const ordersRes = await fetch('/api/orders')
const ordersData = await ordersRes.json()
if (ordersData.success && ordersData.orders) {
setPurchases(ordersData.orders)
}
} catch (e) {
console.log('加载数据失败', 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 formatOrderProduct = (p: any) => {
const type = p.productType || ""
const desc = p.description || ""
// 优先使用 description因为它包含完整的商品描述
if (desc) {
// 如果是章节购买,提取章节标题
if (type === "section" && desc.includes("章节")) {
// description 格式可能是:"章节购买-1-2" 或具体章节标题
if (desc.includes("-")) {
const parts = desc.split("-")
if (parts.length >= 3) {
return {
title: `${parts[1]}章 第${parts[2]}`,
subtitle: "《一场Soul的创业实验》"
}
}
}
return {
title: desc,
subtitle: "章节购买"
}
}
// 如果是整本购买
if (type === "fullbook" || desc.includes("全书")) {
return {
title: "《一场Soul的创业实验》",
subtitle: "全书购买"
}
}
// 如果是找伙伴
if (type === "match" || desc.includes("伙伴")) {
return {
title: "找伙伴匹配",
subtitle: "功能服务"
}
}
// 其他情况直接显示 description
return {
title: desc,
subtitle: type === "section" ? "单章" : type === "fullbook" ? "全书" : "其他"
}
}
// 如果没有 descriptionfallback 到原逻辑
if (type === "section") return { title: `章节 ${p.productId || ""}`, subtitle: "单章购买" }
if (type === "fullbook") return { title: "《一场Soul的创业实验》", subtitle: "全书购买" }
if (type === "match") return { title: "找伙伴匹配", subtitle: "功能服务" }
return { title: "未知商品", subtitle: type || "其他" }
}
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 ? ((totalPurchases / 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) => {
const referrer = p.referrerId && users.find((u: any) => u.id === p.referrerId)
const inviteCode = p.referralCode || referrer?.referral_code || referrer?.nickname || p.referrerId?.slice(0, 8)
const product = formatOrderProduct(p)
const buyer = p.userNickname || users.find((u: any) => u.id === p.userId)?.nickname || "匿名用户"
return (
<div
key={p.id}
className="flex items-start justify-between p-4 bg-[#0a1628] rounded-lg border border-gray-700/30 hover:border-[#38bdac]/30 transition-colors"
>
<div className="flex items-start gap-3 flex-1">
{/* 购买者头像 */}
{p.userAvatar ? (
<img
src={p.userAvatar}
alt={buyer}
className="w-9 h-9 rounded-full object-cover flex-shrink-0 mt-0.5"
onError={(e) => {
// 头像加载失败时显示首字母
e.currentTarget.style.display = 'none'
e.currentTarget.nextElementSibling?.classList.remove('hidden')
}}
/>
) : null}
<div
className={`w-9 h-9 rounded-full bg-[#38bdac]/20 flex items-center justify-center text-sm font-medium text-[#38bdac] flex-shrink-0 mt-0.5 ${p.userAvatar ? 'hidden' : ''}`}
>
{buyer.charAt(0)}
</div>
{/* 订单信息 */}
<div className="flex-1 min-w-0">
{/* 购买者 + 商品名称 */}
<div className="flex items-center gap-2 mb-1">
<span className="text-sm text-gray-300">{buyer}</span>
<span className="text-gray-600">·</span>
<span className="text-sm font-medium text-white truncate">{product.title}</span>
</div>
{/* 商品类型 + 时间 */}
<div className="flex items-center gap-2 text-xs text-gray-500">
<span className="px-1.5 py-0.5 bg-gray-700/50 rounded">{product.subtitle}</span>
<span>{new Date(p.createdAt).toLocaleString('zh-CN', {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
})}</span>
</div>
{/* 邀请码 */}
{inviteCode && (
<p className="text-xs text-gray-600 mt-1">: {inviteCode}</p>
)}
</div>
</div>
{/* 金额 */}
<div className="text-right ml-4 flex-shrink-0">
<p className="text-sm font-bold text-[#38bdac]">+¥{Number(p.amount).toFixed(2)}</p>
<p className="text-xs text-gray-500 mt-0.5">{p.paymentMethod || "微信"}</p>
</div>
</div>
)
})}
{purchases.length === 0 && (
<div className="text-center py-12">
<ShoppingBag className="w-12 h-12 text-gray-600 mx-auto mb-3" />
<p className="text-gray-500"></p>
</div>
)}
</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>
)
}