Files
AOUN/src/pages/admin/index.tsx
2025-11-28 22:44:54 +08:00

225 lines
11 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.
import { GetServerSideProps } from 'next';
import AdminLayout from '@/components/admin/AdminLayout';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Users, FileText, ShoppingBag, DollarSign } from 'lucide-react';
import { verifyToken } from '@/lib/auth';
import { User, Article, Order } from '@/models';
import { BarChart, Bar, XAxis, YAxis, ResponsiveContainer } from 'recharts';
// 定义统计数据接口
interface DashboardStats {
totalUsers: number;
totalArticles: number;
totalOrders: number;
totalRevenue: number;
}
export default function AdminDashboard({ stats }: { stats: DashboardStats }) {
// 模拟图表数据 (实际应从 API 获取)
const data = [
{ name: 'Jan', total: Math.floor(Math.random() * 5000) + 1000 },
{ name: 'Feb', total: Math.floor(Math.random() * 5000) + 1000 },
{ name: 'Mar', total: Math.floor(Math.random() * 5000) + 1000 },
{ name: 'Apr', total: Math.floor(Math.random() * 5000) + 1000 },
{ name: 'May', total: Math.floor(Math.random() * 5000) + 1000 },
{ name: 'Jun', total: Math.floor(Math.random() * 5000) + 1000 },
{ name: 'Jul', total: Math.floor(Math.random() * 5000) + 1000 },
{ name: 'Aug', total: Math.floor(Math.random() * 5000) + 1000 },
{ name: 'Sep', total: Math.floor(Math.random() * 5000) + 1000 },
{ name: 'Oct', total: Math.floor(Math.random() * 5000) + 1000 },
{ name: 'Nov', total: Math.floor(Math.random() * 5000) + 1000 },
{ name: 'Dec', total: Math.floor(Math.random() * 5000) + 1000 },
];
return (
<AdminLayout>
<div className="space-y-6">
{/* ... (existing header and stats cards) ... */}
<div>
<h2 className="text-3xl font-bold tracking-tight"></h2>
<p className="text-muted-foreground">
</p>
</div>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"></CardTitle>
<Users className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stats.totalUsers}</div>
<p className="text-xs text-muted-foreground">
+20.1%
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"></CardTitle>
<FileText className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stats.totalArticles}</div>
<p className="text-xs text-muted-foreground">
+15
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"></CardTitle>
<ShoppingBag className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stats.totalOrders}</div>
<p className="text-xs text-muted-foreground">
+19%
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"></CardTitle>
<DollarSign className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">¥{stats.totalRevenue.toFixed(2)}</div>
<p className="text-xs text-muted-foreground">
+10.5%
</p>
</CardContent>
</Card>
</div>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-7">
<Card className="col-span-4">
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent className="pl-2">
<ResponsiveContainer width="100%" height={350}>
<BarChart data={data}>
<XAxis
dataKey="name"
stroke="#888888"
fontSize={12}
tickLine={false}
axisLine={false}
/>
<YAxis
stroke="#888888"
fontSize={12}
tickLine={false}
axisLine={false}
tickFormatter={(value) => `¥${value}`}
/>
<Bar dataKey="total" fill="#000000" radius={[4, 4, 0, 0]} />
</BarChart>
</ResponsiveContainer>
</CardContent>
</Card>
<Card className="col-span-3">
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-8">
{/* 模拟动态数据 */}
<div className="flex items-center">
<span className="relative flex h-2 w-2 mr-2">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-sky-400 opacity-75"></span>
<span className="relative inline-flex rounded-full h-2 w-2 bg-sky-500"></span>
</span>
<div className="ml-4 space-y-1">
<p className="text-sm font-medium leading-none"></p>
<p className="text-sm text-muted-foreground">
zhangsan@example.com
</p>
</div>
<div className="ml-auto font-medium text-xs text-muted-foreground"></div>
</div>
<div className="flex items-center">
<span className="relative flex h-2 w-2 mr-2">
<span className="relative inline-flex rounded-full h-2 w-2 bg-green-500"></span>
</span>
<div className="ml-4 space-y-1">
<p className="text-sm font-medium leading-none"></p>
<p className="text-sm text-muted-foreground">
¥299.00 -
</p>
</div>
<div className="ml-auto font-medium text-xs text-muted-foreground">5</div>
</div>
</div>
</CardContent>
</Card>
</div>
</div>
</AdminLayout>
);
}
export const getServerSideProps: GetServerSideProps = async (context) => {
const { req } = context;
const token = req.cookies.token;
// 1. 验证 Token
if (!token) {
return {
redirect: {
destination: '/auth/login?redirect=/admin',
permanent: false,
},
};
}
const user = verifyToken(token);
if (!user || user.role !== 'admin') {
return {
redirect: {
destination: '/', // 非管理员跳转首页
permanent: false,
},
};
}
// 2. 获取统计数据 (SSR)
// 使用 withDatabase 确保数据库连接
// 注意getServerSideProps 中不能直接使用 withDatabase 高阶函数包裹,需要手动调用连接逻辑或确保全局连接
// 这里我们假设 mongoose 已经在 api 路由或其他地方连接过,或者我们在 _app.tsx 中处理了连接
// 为了稳妥,我们在 lib/dbConnect.ts 中应该有一个缓存连接的逻辑,这里简单起见,我们直接查询
// 更好的做法是把数据获取逻辑封装成 service
// 由于 withDatabase 是 API 路由的高阶函数,这里我们直接使用 mongoose
// 但为了避免连接问题,我们最好在 models/index.ts 导出时确保连接,或者在这里手动 connect
// 鉴于项目结构,我们假设 models 已经可用。
// 修正:在 getServerSideProps 中直接操作数据库需要确保连接。
// 我们临时引入 mongoose 并连接。
const mongoose = require('mongoose');
if (mongoose.connection.readyState === 0) {
await mongoose.connect(process.env.MONGODB_URI);
}
const totalUsers = await User.countDocuments();
const totalArticles = await Article.countDocuments();
const totalOrders = await Order.countDocuments();
// 计算总收入
const orders = await Order.find({ : 'paid' }, '支付金额');
const totalRevenue = orders.reduce((acc: number, order: any) => acc + (order. || 0), 0);
return {
props: {
stats: {
totalUsers,
totalArticles,
totalOrders,
totalRevenue
}
},
};
};