443 lines
17 KiB
TypeScript
443 lines
17 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useEffect } from 'react'
|
|
import Link from 'next/link'
|
|
import {
|
|
Users,
|
|
Package,
|
|
ShoppingCart,
|
|
DollarSign,
|
|
TrendingUp,
|
|
Eye,
|
|
BarChart3,
|
|
Settings,
|
|
Plus
|
|
} from 'lucide-react'
|
|
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, PieChart, Pie, Cell } from 'recharts'
|
|
|
|
interface StatsData {
|
|
overview: {
|
|
totalUsers: number
|
|
totalOrders: number
|
|
totalComponents: number
|
|
totalRevenue: number
|
|
}
|
|
topUsers: Array<{
|
|
id: string
|
|
username: string
|
|
name: string
|
|
email: string
|
|
totalSpending: number
|
|
orderCount: number
|
|
}>
|
|
topComponents: Array<{
|
|
id: string
|
|
name: string
|
|
brand: string
|
|
price: number
|
|
typeName: string
|
|
totalSales: number
|
|
totalRevenue: number
|
|
}>
|
|
recentOrders: Array<{
|
|
id: string
|
|
orderNumber: string
|
|
totalAmount: number
|
|
status: string
|
|
createdAt: string
|
|
user: {
|
|
username: string
|
|
name?: string
|
|
}
|
|
orderItems: Array<{
|
|
component: {
|
|
name: string
|
|
}
|
|
}>
|
|
}>
|
|
}
|
|
|
|
const COLORS = ['#3B82F6', '#EF4444', '#10B981', '#F59E0B', '#8B5CF6', '#EC4899']
|
|
|
|
export default function AdminDashboard() {
|
|
const [stats, setStats] = useState<StatsData | null>(null)
|
|
const [isLoading, setIsLoading] = useState(true)
|
|
const [activeTab, setActiveTab] = useState('overview')
|
|
|
|
useEffect(() => {
|
|
checkAdminAccess()
|
|
loadStats()
|
|
}, [])
|
|
|
|
const checkAdminAccess = () => {
|
|
const user = JSON.parse(localStorage.getItem('user') || 'null')
|
|
if (!user || !user.isAdmin) {
|
|
alert('权限不足,需要管理员权限')
|
|
window.location.href = '/'
|
|
return
|
|
}
|
|
}
|
|
|
|
const loadStats = async () => {
|
|
try {
|
|
const token = localStorage.getItem('token')
|
|
const response = await fetch('/api/admin/stats', {
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`
|
|
}
|
|
})
|
|
|
|
if (response.ok) {
|
|
const data = await response.json()
|
|
setStats(data)
|
|
} else if (response.status === 403) {
|
|
alert('权限不足')
|
|
window.location.href = '/'
|
|
}
|
|
} catch (error) {
|
|
console.error('加载统计数据失败:', error)
|
|
} finally {
|
|
setIsLoading(false)
|
|
}
|
|
}
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
|
<div className="text-center">
|
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
|
|
<p className="text-gray-600">加载中...</p>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gray-50">
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
{/* Header */}
|
|
<div className="flex justify-between items-center mb-8">
|
|
<div>
|
|
<h1 className="text-3xl font-bold text-gray-900">管理后台</h1>
|
|
<p className="text-gray-600">系统管理与数据统计</p>
|
|
</div>
|
|
<div className="flex space-x-4">
|
|
<Link
|
|
href="/admin/components"
|
|
className="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors"
|
|
>
|
|
<Plus className="h-4 w-4 inline mr-2" />
|
|
添加商品
|
|
</Link>
|
|
<Link
|
|
href="/admin/orders"
|
|
className="border border-blue-600 text-blue-600 px-4 py-2 rounded-lg hover:bg-blue-50 transition-colors"
|
|
>
|
|
<Eye className="h-4 w-4 inline mr-2" />
|
|
查看订单
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Overview Cards */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
|
<div className="bg-white rounded-lg shadow-sm p-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-gray-600 text-sm">总用户数</p>
|
|
<p className="text-2xl font-bold text-gray-900">{stats?.overview.totalUsers || 0}</p>
|
|
</div>
|
|
<Users className="h-12 w-12 text-blue-600" />
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-white rounded-lg shadow-sm p-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-gray-600 text-sm">总订单数</p>
|
|
<p className="text-2xl font-bold text-gray-900">{stats?.overview.totalOrders || 0}</p>
|
|
</div>
|
|
<ShoppingCart className="h-12 w-12 text-green-600" />
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-white rounded-lg shadow-sm p-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-gray-600 text-sm">商品总数</p>
|
|
<p className="text-2xl font-bold text-gray-900">{stats?.overview.totalComponents || 0}</p>
|
|
</div>
|
|
<Package className="h-12 w-12 text-purple-600" />
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-white rounded-lg shadow-sm p-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-gray-600 text-sm">总营收</p>
|
|
<p className="text-2xl font-bold text-gray-900">¥{stats?.overview.totalRevenue?.toFixed(2) || '0.00'}</p>
|
|
</div>
|
|
<DollarSign className="h-12 w-12 text-red-600" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Tab Navigation */}
|
|
<div className="mb-8">
|
|
<div className="border-b border-gray-200">
|
|
<nav className="-mb-px flex space-x-8">
|
|
<button
|
|
onClick={() => setActiveTab('overview')}
|
|
className={`py-2 px-1 border-b-2 font-medium text-sm ${
|
|
activeTab === 'overview'
|
|
? 'border-blue-500 text-blue-600'
|
|
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
|
}`}
|
|
>
|
|
<BarChart3 className="h-4 w-4 inline mr-2" />
|
|
数据统计
|
|
</button>
|
|
<button
|
|
onClick={() => setActiveTab('users')}
|
|
className={`py-2 px-1 border-b-2 font-medium text-sm ${
|
|
activeTab === 'users'
|
|
? 'border-blue-500 text-blue-600'
|
|
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
|
}`}
|
|
>
|
|
<Users className="h-4 w-4 inline mr-2" />
|
|
用户排行
|
|
</button>
|
|
<button
|
|
onClick={() => setActiveTab('products')}
|
|
className={`py-2 px-1 border-b-2 font-medium text-sm ${
|
|
activeTab === 'products'
|
|
? 'border-blue-500 text-blue-600'
|
|
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
|
}`}
|
|
>
|
|
<Package className="h-4 w-4 inline mr-2" />
|
|
商品排行
|
|
</button>
|
|
<button
|
|
onClick={() => setActiveTab('orders')}
|
|
className={`py-2 px-1 border-b-2 font-medium text-sm ${
|
|
activeTab === 'orders'
|
|
? 'border-blue-500 text-blue-600'
|
|
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
|
}`}
|
|
>
|
|
<ShoppingCart className="h-4 w-4 inline mr-2" />
|
|
最近订单
|
|
</button>
|
|
</nav>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Tab Content */}
|
|
{activeTab === 'overview' && (
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
|
{/* Top Users Chart */}
|
|
<div className="bg-white rounded-lg shadow-sm p-6">
|
|
<h3 className="text-lg font-semibold text-gray-900 mb-4">用户消费金额排行</h3>
|
|
<ResponsiveContainer width="100%" height={300}>
|
|
<BarChart data={stats?.topUsers.slice(0, 5) || []}>
|
|
<CartesianGrid strokeDasharray="3 3" />
|
|
<XAxis dataKey="name" />
|
|
<YAxis />
|
|
<Tooltip formatter={(value) => [`¥${value}`, '消费金额']} />
|
|
<Bar dataKey="totalSpending" fill="#3B82F6" />
|
|
</BarChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
|
|
{/* Top Components Chart */}
|
|
<div className="bg-white rounded-lg shadow-sm p-6">
|
|
<h3 className="text-lg font-semibold text-gray-900 mb-4">商品销量排行</h3>
|
|
<ResponsiveContainer width="100%" height={300}>
|
|
<BarChart data={stats?.topComponents.slice(0, 5) || []}>
|
|
<CartesianGrid strokeDasharray="3 3" />
|
|
<XAxis dataKey="name" />
|
|
<YAxis />
|
|
<Tooltip formatter={(value) => [`${value}件`, '销量']} />
|
|
<Bar dataKey="totalSales" fill="#EF4444" />
|
|
</BarChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{activeTab === 'users' && (
|
|
<div className="bg-white rounded-lg shadow-sm overflow-hidden">
|
|
<div className="px-6 py-4 border-b border-gray-200">
|
|
<h3 className="text-lg font-semibold text-gray-900">用户消费排行榜</h3>
|
|
</div>
|
|
<div className="overflow-x-auto">
|
|
<table className="min-w-full divide-y divide-gray-200">
|
|
<thead className="bg-gray-50">
|
|
<tr>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
排名
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
用户
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
邮箱
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
订单数
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
消费金额
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="bg-white divide-y divide-gray-200">
|
|
{stats?.topUsers.map((user, index) => (
|
|
<tr key={user.id}>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
|
#{index + 1}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
|
{user.name}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
{user.email}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
|
{user.orderCount}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-red-600">
|
|
¥{user.totalSpending.toFixed(2)}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{activeTab === 'products' && (
|
|
<div className="bg-white rounded-lg shadow-sm overflow-hidden">
|
|
<div className="px-6 py-4 border-b border-gray-200">
|
|
<h3 className="text-lg font-semibold text-gray-900">商品销量排行榜</h3>
|
|
</div>
|
|
<div className="overflow-x-auto">
|
|
<table className="min-w-full divide-y divide-gray-200">
|
|
<thead className="bg-gray-50">
|
|
<tr>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
排名
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
商品
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
品牌
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
类型
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
销量
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
营收
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="bg-white divide-y divide-gray-200">
|
|
{stats?.topComponents.map((component, index) => (
|
|
<tr key={component.id}>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
|
#{index + 1}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
|
{component.name}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
{component.brand}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
{component.typeName}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
|
{component.totalSales}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-red-600">
|
|
¥{component.totalRevenue.toFixed(2)}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{activeTab === 'orders' && (
|
|
<div className="bg-white rounded-lg shadow-sm overflow-hidden">
|
|
<div className="px-6 py-4 border-b border-gray-200">
|
|
<h3 className="text-lg font-semibold text-gray-900">最近订单</h3>
|
|
</div>
|
|
<div className="overflow-x-auto">
|
|
<table className="min-w-full divide-y divide-gray-200">
|
|
<thead className="bg-gray-50">
|
|
<tr>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
订单号
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
用户
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
商品数量
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
订单金额
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
状态
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
创建时间
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="bg-white divide-y divide-gray-200">
|
|
{stats?.recentOrders.map((order) => (
|
|
<tr key={order.id}>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
|
{order.orderNumber}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
|
{order.user.name || order.user.username}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
{order.orderItems.length} 件
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-red-600">
|
|
¥{order.totalAmount}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<span className="inline-flex px-2 py-1 text-xs font-medium rounded-full bg-blue-100 text-blue-800">
|
|
{order.status}
|
|
</span>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
{new Date(order.createdAt).toLocaleDateString('zh-CN')}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|