You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

249 lines
9.1 KiB

import { useState, useEffect } from 'react'
import { Users, Zap, Activity as ActivityIcon, Database, Loader2 } from 'lucide-react'
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card'
import { apiClient } from '@/services/api'
import type { DashboardStats, Activity } from '@/types'
// Fallback data when API is not available
const fallbackStats: DashboardStats = {
totalUsers: 1234,
activeUsers: 856,
systemLoad: 32,
dbStatus: '正常',
userGrowth: 65, // Single value for chart
}
const fallbackActivities: Activity[] = [
{ id: 1, user: 'john@example.com', action: '登录系统', time: '5 分钟前', status: 'success' },
{ id: 2, user: 'jane@example.com', action: '更新资料', time: '15 分钟前', status: 'success' },
{ id: 3, user: 'admin@example.com', action: '创建用户', time: '1 小时前', status: 'success' },
{ id: 4, user: 'bob@example.com', action: '修改密码', time: '2 小时前', status: 'success' },
{ id: 5, user: 'alice@example.com', action: '登录失败', time: '3 小时前', status: 'error' },
]
// Chart data (12 months)
const chartData = [65, 72, 68, 80, 75, 85, 82, 90, 88, 95, 92, 100]
export function DashboardPage() {
const [isLoading, setIsLoading] = useState(true)
const [stats, setStats] = useState<DashboardStats | null>(null)
const [activities, setActivities] = useState<Activity[]>([])
const [error, setError] = useState<string | null>(null)
useEffect(() => {
loadDashboardData()
}, [])
const loadDashboardData = async () => {
try {
setIsLoading(true)
setError(null)
// 调用仪表盘 API 获取真实统计数据
const [statsResponse, activitiesResponse] = await Promise.all([
apiClient.getDashboardStats().catch(() => null),
apiClient.getRecentActivities(5).catch(() => null),
])
if (statsResponse?.success && statsResponse.data) {
setStats(statsResponse.data)
} else {
setStats(fallbackStats)
}
if (activitiesResponse?.success && activitiesResponse.data) {
setActivities(activitiesResponse.data)
} else {
setActivities(fallbackActivities)
}
} catch (err) {
console.error('加载仪表盘数据失败:', err)
setError('加载数据失败')
setStats(fallbackStats)
setActivities(fallbackActivities)
} finally {
setIsLoading(false)
}
}
// Format number with commas
const formatNumber = (num: number): string => {
return num.toLocaleString('zh-CN')
}
// Calculate growth percentage (mock calculation)
const calculateGrowth = (current: number): string => {
const growth = Math.floor(Math.random() * 20) - 5
return growth >= 0 ? `+${growth}%` : `${growth}%`
}
const statsConfig = [
{
title: '总用户数',
value: stats ? formatNumber(stats.totalUsers) : '-',
change: calculateGrowth(stats?.totalUsers || 0),
icon: Users,
color: 'from-sky-500 to-blue-600',
},
{
title: '活跃用户',
value: stats ? formatNumber(stats.activeUsers) : '-',
change: calculateGrowth(stats?.activeUsers || 0),
icon: ActivityIcon,
color: 'from-green-500 to-emerald-600',
},
{
title: '系统负载',
value: stats ? `${stats.systemLoad}%` : '-',
change: '-5%',
icon: Zap,
color: 'from-amber-500 to-orange-600',
},
{
title: '数据库状态',
value: stats?.dbStatus || '正常',
change: '稳定',
icon: Database,
color: 'from-purple-500 to-violet-600',
},
]
if (isLoading) {
return (
<div className="flex items-center justify-center h-96">
<Loader2 className="h-12 w-12 animate-spin text-sky-400" />
</div>
)
}
return (
<div className="space-y-6">
{error && (
<div className="p-4 bg-red-500/10 text-red-400 rounded-lg flex justify-between items-center">
<span>{error}</span>
<button onClick={loadDashboardData} className="underline hover:text-red-300">
</button>
</div>
)}
{/* Stats Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 animate-fade-in" style={{ animationDelay: '0.1s' }}>
{statsConfig.map((stat, index) => (
<Card key={index} className="overflow-hidden hover:border-gray-700 transition-colors">
<CardContent className="p-6">
<div className="flex items-start justify-between">
<div className="space-y-1">
<p className="text-sm font-medium text-gray-400 font-body">{stat.title}</p>
<p className="text-3xl font-bold text-white font-display">{stat.value}</p>
<p className="text-xs text-green-400 font-body">{stat.change}</p>
</div>
<div className={`p-3 rounded-lg bg-gradient-to-br ${stat.color}`}>
<stat.icon className="h-5 w-5 text-white" />
</div>
</div>
</CardContent>
</Card>
))}
</div>
{/* Main Content Grid */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Chart */}
<Card className="lg:col-span-2 animate-fade-in" style={{ animationDelay: '0.2s' }}>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent>
<div className="h-64 flex items-end justify-between gap-2 px-4">
{chartData.map((height, i) => (
<div
key={i}
className="flex-1 max-w-8 bg-gradient-to-t from-sky-600 to-sky-400 rounded-t-sm transition-all duration-300 hover:from-sky-500 hover:to-sky-300 relative group"
style={{ height: `${height}%` }}
>
<div className="absolute -top-8 left-1/2 -translate-x-1/2 bg-gray-800 text-xs text-white px-2 py-1 rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap">
{height}%
</div>
</div>
))}
</div>
<div className="flex justify-between mt-4 px-4 text-xs text-gray-500 font-body">
<span>1</span>
<span>2</span>
<span>3</span>
<span>4</span>
<span>5</span>
<span>6</span>
<span>7</span>
<span>8</span>
<span>9</span>
<span>10</span>
<span>11</span>
<span>12</span>
</div>
</CardContent>
</Card>
{/* Recent Activity */}
<Card className="animate-fade-in" style={{ animationDelay: '0.3s' }}>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
{activities.map((activity) => (
<div
key={activity.id}
className="flex items-start gap-3 p-3 rounded-lg bg-gray-800/50 hover:bg-gray-800 transition-colors"
>
<div
className={`w-2 h-2 rounded-full mt-2 flex-shrink-0 ${
activity.status === 'success' ? 'bg-green-500' : 'bg-red-500'
}`}
/>
<div className="flex-1 min-w-0">
<p className="text-sm text-gray-300 font-body truncate">
{activity.user}
</p>
<p className="text-xs text-gray-500 font-body">{activity.action}</p>
</div>
<span className="text-xs text-gray-600 font-body whitespace-nowrap">
{activity.time}
</span>
</div>
))}
</div>
</CardContent>
</Card>
</div>
{/* Quick Actions */}
<Card className="animate-fade-in" style={{ animationDelay: '0.4s' }}>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{[
{ label: '添加用户', icon: Users, action: 'users' },
{ label: '系统设置', icon: Zap, action: 'settings' },
{ label: '数据备份', icon: Database, action: 'backup' },
{ label: '查看日志', icon: ActivityIcon, action: 'logs' },
].map((item, index) => (
<button
key={index}
className="flex flex-col items-center gap-2 p-4 rounded-lg border border-gray-800 hover:border-sky-500/50 hover:bg-gray-800/50 transition-all duration-200 group"
>
<item.icon className="h-6 w-6 text-gray-500 group-hover:text-sky-400 transition-colors" />
<span className="text-sm text-gray-400 group-hover:text-gray-200 font-body transition-colors">
{item.label}
</span>
</button>
))}
</div>
</CardContent>
</Card>
</div>
)
}