Files
QQuiz/frontend/src/pages/Dashboard.jsx
2025-12-12 23:16:05 +08:00

207 lines
7.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Dashboard Page
*/
import React, { useState, useEffect } from 'react'
import { useNavigate } from 'react-router-dom'
import { examAPI, mistakeAPI } from '../api/client'
import { useAuth } from '../context/AuthContext'
import Layout from '../components/Layout'
import {
FolderOpen, XCircle, TrendingUp, BookOpen, ArrowRight, Settings, Shield
} from 'lucide-react'
import { getStatusColor, getStatusText, formatRelativeTime, calculateProgress } from '../utils/helpers'
export const Dashboard = () => {
const { user, isAdmin } = useAuth()
const navigate = useNavigate()
const [stats, setStats] = useState({
totalExams: 0,
totalQuestions: 0,
completedQuestions: 0,
mistakeCount: 0
})
const [recentExams, setRecentExams] = useState([])
const [loading, setLoading] = useState(true)
useEffect(() => {
loadDashboardData()
}, [])
const loadDashboardData = async () => {
try {
const [examsRes, mistakesRes] = await Promise.all([
examAPI.getList(0, 5),
mistakeAPI.getList(0, 1)
])
const exams = examsRes.data.exams
// Calculate stats
const totalQuestions = exams.reduce((sum, e) => sum + e.total_questions, 0)
const completedQuestions = exams.reduce((sum, e) => sum + e.current_index, 0)
setStats({
totalExams: exams.length,
totalQuestions,
completedQuestions,
mistakeCount: mistakesRes.data.total
})
setRecentExams(exams)
} catch (error) {
console.error('Failed to load dashboard:', error)
} finally {
setLoading(false)
}
}
return (
<Layout>
<div className="p-4 md:p-8">
{/* Welcome */}
<div className="mb-8">
<h1 className="text-2xl md:text-3xl font-bold text-gray-900">
欢迎回来{user?.username}
</h1>
<p className="text-gray-600 mt-1">继续你的学习之旅</p>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
<div
className="bg-white rounded-xl shadow-sm p-6 cursor-pointer hover:shadow-md transition-shadow"
onClick={() => navigate('/exams')}
>
<div className="flex items-center gap-3 mb-2">
<div className="bg-primary-100 p-2 rounded-lg">
<FolderOpen className="h-5 w-5 text-primary-600" />
</div>
<span className="text-2xl font-bold text-gray-900">{stats.totalExams}</span>
</div>
<p className="text-sm text-gray-600">题库总数</p>
</div>
<div className="bg-white rounded-xl shadow-sm p-6">
<div className="flex items-center gap-3 mb-2">
<div className="bg-blue-100 p-2 rounded-lg">
<BookOpen className="h-5 w-5 text-blue-600" />
</div>
<span className="text-2xl font-bold text-gray-900">{stats.totalQuestions}</span>
</div>
<p className="text-sm text-gray-600">题目总数</p>
</div>
<div className="bg-white rounded-xl shadow-sm p-6">
<div className="flex items-center gap-3 mb-2">
<div className="bg-green-100 p-2 rounded-lg">
<TrendingUp className="h-5 w-5 text-green-600" />
</div>
<span className="text-2xl font-bold text-gray-900">{stats.completedQuestions}</span>
</div>
<p className="text-sm text-gray-600">已完成</p>
</div>
<div
className="bg-white rounded-xl shadow-sm p-6 cursor-pointer hover:shadow-md transition-shadow"
onClick={() => navigate('/mistakes')}
>
<div className="flex items-center gap-3 mb-2">
<div className="bg-red-100 p-2 rounded-lg">
<XCircle className="h-5 w-5 text-red-600" />
</div>
<span className="text-2xl font-bold text-gray-900">{stats.mistakeCount}</span>
</div>
<p className="text-sm text-gray-600">错题数量</p>
</div>
</div>
{/* Recent Exams */}
<div className="bg-white rounded-xl shadow-sm p-6">
<div className="flex items-center justify-between mb-6">
<h2 className="text-xl font-bold text-gray-900">最近的题库</h2>
<button
onClick={() => navigate('/exams')}
className="text-primary-600 hover:text-primary-700 flex items-center gap-1 text-sm font-medium"
>
查看全部
<ArrowRight className="h-4 w-4" />
</button>
</div>
{recentExams.length === 0 ? (
<div className="text-center py-8">
<FolderOpen className="h-12 w-12 text-gray-300 mx-auto mb-3" />
<p className="text-gray-500">还没有题库快去创建一个吧</p>
</div>
) : (
<div className="space-y-3">
{recentExams.map((exam) => (
<div
key={exam.id}
onClick={() => navigate(`/exams/${exam.id}`)}
className="border border-gray-200 rounded-lg p-4 hover:border-primary-300 hover:bg-primary-50 transition-all cursor-pointer"
>
<div className="flex items-start justify-between mb-2">
<h3 className="font-semibold text-gray-900">{exam.title}</h3>
<span className={`px-2 py-1 text-xs font-medium rounded-full ${getStatusColor(exam.status)}`}>
{getStatusText(exam.status)}
</span>
</div>
<div className="flex items-center justify-between text-sm text-gray-600">
<span>
{exam.current_index} / {exam.total_questions}
</span>
<span>{formatRelativeTime(exam.updated_at)}</span>
</div>
{exam.total_questions > 0 && (
<div className="w-full bg-gray-200 rounded-full h-2 mt-3">
<div
className="bg-primary-600 h-2 rounded-full transition-all"
style={{ width: `${calculateProgress(exam.current_index, exam.total_questions)}%` }}
></div>
</div>
)}
</div>
))}
</div>
)}
</div>
{/* Admin Quick Access */}
{isAdmin && (
<div className="mt-6 bg-gradient-to-r from-primary-500 to-primary-600 rounded-xl shadow-sm p-6 text-white">
<div className="flex items-center justify-between">
<div>
<h3 className="font-semibold mb-1">管理员功能</h3>
<p className="text-sm text-primary-100">用户管理系统统计配置设置</p>
</div>
<div className="flex gap-2">
<button
onClick={() => navigate('/admin')}
className="bg-white text-primary-600 px-4 py-2 rounded-lg font-medium hover:bg-primary-50 transition-colors flex items-center gap-2"
>
<Shield className="h-5 w-5" />
管理面板
</button>
<button
onClick={() => navigate('/admin/settings')}
className="bg-white/90 text-primary-600 px-4 py-2 rounded-lg font-medium hover:bg-white transition-colors flex items-center gap-2"
>
<Settings className="h-5 w-5" />
系统设置
</button>
</div>
</div>
</div>
)}
</div>
</Layout>
)
}
export default Dashboard