211 lines
6.7 KiB
JavaScript
211 lines
6.7 KiB
JavaScript
import React, { useState, useEffect } from 'react';
|
|
import axios from '../../services/axios'
|
|
import { toast } from 'react-toastify';
|
|
import { Link } from 'react-router-dom';
|
|
import { Bar } from 'react-chartjs-2';
|
|
import { Chart, registerables } from 'chart.js';
|
|
|
|
// 注册Chart.js组件
|
|
Chart.register(...registerables);
|
|
|
|
const Dashboard = () => {
|
|
const [stats, setStats] = useState({
|
|
totalUsers: 0,
|
|
activeUsers: 0,
|
|
totalFiles: 0,
|
|
takenFiles: 0,
|
|
availableFiles: 0,
|
|
recentUploads: {},
|
|
recentDownloads: {}
|
|
});
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
const fetchDashboardData = async () => {
|
|
try {
|
|
const [userRes, fileRes] = await Promise.all([
|
|
axios.get('/api/admin/analytics/users'),
|
|
axios.get('/api/admin/analytics/files')
|
|
]);
|
|
|
|
setStats({
|
|
totalUsers: userRes.data.total_users || 0,
|
|
activeUsers: userRes.data.active_users || 0,
|
|
totalFiles: fileRes.data.total_files || 0,
|
|
takenFiles: fileRes.data.taken_files || 0,
|
|
availableFiles: fileRes.data.available_files || 0,
|
|
recentUploads: fileRes.data.upload_stats || {},
|
|
recentDownloads: fileRes.data.take_stats || {}
|
|
});
|
|
} catch (err) {
|
|
console.error('获取统计数据失败:', err);
|
|
toast.error('获取统计数据失败');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
fetchDashboardData();
|
|
}, []);
|
|
|
|
// 准备图表数据
|
|
const getChartData = () => {
|
|
// 确保数据存在且为对象
|
|
const recentUploads = stats.recentUploads || {};
|
|
const recentDownloads = stats.recentDownloads || {};
|
|
|
|
// 确保日期顺序正确
|
|
const uploadDates = Object.keys(recentUploads);
|
|
const downloadDates = Object.keys(recentDownloads);
|
|
const allDates = [...new Set([...uploadDates, ...downloadDates])].sort();
|
|
|
|
return {
|
|
labels: allDates.map(date => new Date(date).toLocaleDateString()),
|
|
datasets: [
|
|
{
|
|
label: '文件上传',
|
|
data: allDates.map(date => recentUploads[date] || 0),
|
|
backgroundColor: 'rgba(54, 162, 235, 0.5)',
|
|
borderColor: 'rgba(54, 162, 235, 1)',
|
|
borderWidth: 1
|
|
},
|
|
{
|
|
label: '文件领取',
|
|
data: allDates.map(date => recentDownloads[date] || 0),
|
|
backgroundColor: 'rgba(75, 192, 192, 0.5)',
|
|
borderColor: 'rgba(75, 192, 192, 1)',
|
|
borderWidth: 1
|
|
}
|
|
]
|
|
};
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="d-flex justify-content-center align-items-center vh-100">
|
|
<div className="spinner-border text-primary" role="status">
|
|
<span className="visually-hidden">Loading...</span>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<div className="d-flex justify-content-between align-items-center mb-4">
|
|
<h1>仪表盘</h1>
|
|
</div>
|
|
|
|
{/* 统计卡片 */}
|
|
<div className="row mb-4">
|
|
<div className="col-md-3 mb-4">
|
|
<div className="stats-card">
|
|
<div className="stats-number">{stats.totalUsers}</div>
|
|
<div className="stats-label">总用户数</div>
|
|
<Link to="/admin/users" className="btn btn-sm btn-admin-outline mt-3">
|
|
查看详情 <i className="bi bi-arrow-right ms-1"></i>
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
<div className="col-md-3 mb-4">
|
|
<div className="stats-card">
|
|
<div className="stats-number">{stats.activeUsers}</div>
|
|
<div className="stats-label">活跃用户</div>
|
|
<Link to="/admin/users" className="btn btn-sm btn-admin-outline mt-3">
|
|
查看详情 <i className="bi bi-arrow-right ms-1"></i>
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
<div className="col-md-3 mb-4">
|
|
<div className="stats-card">
|
|
<div className="stats-number">{stats.totalFiles}</div>
|
|
<div className="stats-label">总文件数</div>
|
|
<Link to="/admin/files" className="btn btn-sm btn-admin-outline mt-3">
|
|
查看详情 <i className="bi bi-arrow-right ms-1"></i>
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
<div className="col-md-3 mb-4">
|
|
<div className="stats-card">
|
|
<div className="stats-number">{stats.availableFiles}</div>
|
|
<div className="stats-label">可领取文件</div>
|
|
<Link to="/admin/files" className="btn btn-sm btn-admin-outline mt-3">
|
|
查看详情 <i className="bi bi-arrow-right ms-1"></i>
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 图表 */}
|
|
<div className="admin-card mb-4">
|
|
<div className="card-header">
|
|
<h5>近7天数据统计</h5>
|
|
</div>
|
|
<div className="card-body">
|
|
<div className="chart-container" style={{ height: '300px' }}>
|
|
<Bar
|
|
data={getChartData()}
|
|
options={{
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: {
|
|
labels: {
|
|
color: '#e8e9ea'
|
|
}
|
|
}
|
|
},
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true,
|
|
ticks: {
|
|
precision: 0,
|
|
color: '#b8bcc8'
|
|
},
|
|
grid: {
|
|
color: 'rgba(100, 255, 218, 0.1)'
|
|
}
|
|
},
|
|
x: {
|
|
ticks: {
|
|
color: '#b8bcc8'
|
|
},
|
|
grid: {
|
|
color: 'rgba(100, 255, 218, 0.1)'
|
|
}
|
|
}
|
|
}
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 快捷操作 */}
|
|
<div className="admin-card">
|
|
<div className="card-header">
|
|
<h5>快捷操作</h5>
|
|
</div>
|
|
<div className="card-body">
|
|
<div className="d-flex flex-wrap gap-3">
|
|
<Link to="/admin/users" className="btn btn-admin">
|
|
<i className="bi bi-people me-2"></i>管理用户
|
|
</Link>
|
|
<Link to="/admin/files/upload" className="btn btn-admin">
|
|
<i className="bi bi-upload me-2"></i>上传新文件
|
|
</Link>
|
|
<Link to="/admin/files" className="btn btn-admin">
|
|
<i className="bi bi-file-earmark me-2"></i>管理文件
|
|
</Link>
|
|
<Link to="/admin/analytics" className="btn btn-admin">
|
|
<i className="bi bi-bar-chart me-2"></i>详细数据分析
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default Dashboard;
|