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