Files
soul/app/admin/page.tsx
卡若 b60edb3d47 feat: 完整重构小程序匹配功能 + 修复UI对齐 + 文章数据API
主要更新:
1. 按H5网页端完全重构匹配功能(match页面)
   - 4种匹配类型: 创业合伙/资源对接/导师顾问/团队招募
   - 资源对接等类型弹出手机号/微信号输入框
   - 去掉重新匹配按钮,改为返回按钮

2. 修复所有卡片对齐和宽度问题
   - 目录页附录卡片居中
   - 首页阅读进度卡片满宽度
   - 我的页面菜单卡片对齐
   - 推广中心分享卡片统一宽度

3. 修复目录页图标和文字对齐
   - section-icon固定40rpx宽高
   - section-title与图标垂直居中

4. 更新真实完整文章标题(62篇)
   - 从book目录读取真实markdown文件名
   - 替换之前的简化标题

5. 新增文章数据API
   - /api/db/chapters - 获取完整书籍结构
   - 支持按ID获取单篇文章内容
2026-01-21 15:49:12 +08:00

167 lines
6.8 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 { useStore } from "@/lib/store"
import { Users, BookOpen, ShoppingBag, TrendingUp, RefreshCw, ChevronRight } from "lucide-react"
export default function AdminDashboard() {
const router = useRouter()
const { getAllUsers, getAllPurchases } = useStore()
const [mounted, setMounted] = useState(false)
const [users, setUsers] = useState<any[]>([])
const [purchases, setPurchases] = useState<any[]>([])
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)
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: `¥${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) => (
<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]">
{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>
)
}