2025-06-22 11:34:32 +08:00

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>
)
}