mirror of
https://github.com/handsomezhuzhu/QQuiz.git
synced 2026-02-20 20:10:14 +00:00
错题本乱序和顺序刷题
This commit is contained in:
@@ -3,6 +3,7 @@ import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-d
|
|||||||
import { Toaster } from 'react-hot-toast'
|
import { Toaster } from 'react-hot-toast'
|
||||||
import { AuthProvider } from './context/AuthContext'
|
import { AuthProvider } from './context/AuthContext'
|
||||||
import { ProtectedRoute } from './components/ProtectedRoute'
|
import { ProtectedRoute } from './components/ProtectedRoute'
|
||||||
|
import Layout from './components/Layout'
|
||||||
|
|
||||||
// Auth Pages
|
// Auth Pages
|
||||||
import Login from './pages/Login'
|
import Login from './pages/Login'
|
||||||
@@ -56,88 +57,34 @@ function App() {
|
|||||||
<Route path="/login" element={<Login />} />
|
<Route path="/login" element={<Login />} />
|
||||||
<Route path="/register" element={<Register />} />
|
<Route path="/register" element={<Register />} />
|
||||||
|
|
||||||
{/* Protected Routes */}
|
{/* Protected Routes with Layout */}
|
||||||
<Route
|
<Route element={<ProtectedRoute><Layout /></ProtectedRoute>}>
|
||||||
path="/dashboard"
|
<Route path="/dashboard" element={<Dashboard />} />
|
||||||
element={
|
<Route path="/exams" element={<ExamList />} />
|
||||||
<ProtectedRoute>
|
<Route path="/exams/:examId" element={<ExamDetail />} />
|
||||||
<Dashboard />
|
<Route path="/quiz/:examId" element={<QuizPlayer />} />
|
||||||
</ProtectedRoute>
|
<Route path="/mistakes" element={<MistakeList />} />
|
||||||
}
|
<Route path="/mistake-quiz" element={<MistakePlayer />} />
|
||||||
/>
|
<Route path="/questions" element={<QuestionBank />} />
|
||||||
|
|
||||||
<Route
|
{/* Admin Only Routes */}
|
||||||
path="/exams"
|
<Route
|
||||||
element={
|
path="/admin"
|
||||||
<ProtectedRoute>
|
element={
|
||||||
<ExamList />
|
<ProtectedRoute adminOnly>
|
||||||
</ProtectedRoute>
|
<AdminPanel />
|
||||||
}
|
</ProtectedRoute>
|
||||||
/>
|
}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/exams/:examId"
|
path="/admin/settings"
|
||||||
element={
|
element={
|
||||||
<ProtectedRoute>
|
<ProtectedRoute adminOnly>
|
||||||
<ExamDetail />
|
<AdminSettings />
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
</Route>
|
||||||
<Route
|
|
||||||
path="/quiz/:examId"
|
|
||||||
element={
|
|
||||||
<ProtectedRoute>
|
|
||||||
<QuizPlayer />
|
|
||||||
</ProtectedRoute>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/mistakes"
|
|
||||||
element={
|
|
||||||
<ProtectedRoute>
|
|
||||||
<MistakeList />
|
|
||||||
</ProtectedRoute>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/mistake-quiz"
|
|
||||||
element={
|
|
||||||
<ProtectedRoute>
|
|
||||||
<MistakePlayer />
|
|
||||||
</ProtectedRoute>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/questions"
|
|
||||||
element={
|
|
||||||
<ProtectedRoute>
|
|
||||||
<QuestionBank />
|
|
||||||
</ProtectedRoute>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Admin Only Routes */}
|
|
||||||
<Route
|
|
||||||
path="/admin"
|
|
||||||
element={
|
|
||||||
<ProtectedRoute adminOnly>
|
|
||||||
<AdminPanel />
|
|
||||||
</ProtectedRoute>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/admin/settings"
|
|
||||||
element={
|
|
||||||
<ProtectedRoute adminOnly>
|
|
||||||
<AdminSettings />
|
|
||||||
</ProtectedRoute>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Default Route */}
|
{/* Default Route */}
|
||||||
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* Main Layout Component with Navigation
|
* Main Layout Component with Navigation
|
||||||
*/
|
*/
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { Link, useNavigate, useLocation } from 'react-router-dom'
|
import { Link, useNavigate, useLocation, Outlet } from 'react-router-dom'
|
||||||
import { useAuth } from '../context/AuthContext'
|
import { useAuth } from '../context/AuthContext'
|
||||||
import {
|
import {
|
||||||
BookOpen,
|
BookOpen,
|
||||||
@@ -12,10 +12,11 @@ import {
|
|||||||
Settings,
|
Settings,
|
||||||
LogOut,
|
LogOut,
|
||||||
Menu,
|
Menu,
|
||||||
X
|
X,
|
||||||
|
Shield
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
|
|
||||||
export const Layout = ({ children }) => {
|
export const Layout = () => {
|
||||||
const { user, logout, isAdmin } = useAuth()
|
const { user, logout, isAdmin } = useAuth()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
@@ -33,6 +34,7 @@ export const Layout = ({ children }) => {
|
|||||||
]
|
]
|
||||||
|
|
||||||
if (isAdmin) {
|
if (isAdmin) {
|
||||||
|
navigation.push({ name: '管理面板', href: '/admin', icon: Shield })
|
||||||
navigation.push({ name: '系统设置', href: '/admin/settings', icon: Settings })
|
navigation.push({ name: '系统设置', href: '/admin/settings', icon: Settings })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,11 +65,10 @@ export const Layout = ({ children }) => {
|
|||||||
key={item.name}
|
key={item.name}
|
||||||
to={item.href}
|
to={item.href}
|
||||||
onClick={() => setMobileMenuOpen(false)}
|
onClick={() => setMobileMenuOpen(false)}
|
||||||
className={`flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${
|
className={`flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${isActive(item.href)
|
||||||
isActive(item.href)
|
|
||||||
? 'bg-primary-50 text-primary-600'
|
? 'bg-primary-50 text-primary-600'
|
||||||
: 'text-gray-700 hover:bg-gray-100'
|
: 'text-gray-700 hover:bg-gray-100'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<item.icon className="h-5 w-5" />
|
<item.icon className="h-5 w-5" />
|
||||||
<span>{item.name}</span>
|
<span>{item.name}</span>
|
||||||
@@ -105,11 +106,10 @@ export const Layout = ({ children }) => {
|
|||||||
<Link
|
<Link
|
||||||
key={item.name}
|
key={item.name}
|
||||||
to={item.href}
|
to={item.href}
|
||||||
className={`flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${
|
className={`flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${isActive(item.href)
|
||||||
isActive(item.href)
|
|
||||||
? 'bg-primary-50 text-primary-600'
|
? 'bg-primary-50 text-primary-600'
|
||||||
: 'text-gray-700 hover:bg-gray-100'
|
: 'text-gray-700 hover:bg-gray-100'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<item.icon className="h-5 w-5" />
|
<item.icon className="h-5 w-5" />
|
||||||
<span>{item.name}</span>
|
<span>{item.name}</span>
|
||||||
@@ -132,7 +132,7 @@ export const Layout = ({ children }) => {
|
|||||||
|
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<div className="flex-1 lg:pl-64">
|
<div className="flex-1 lg:pl-64">
|
||||||
{children}
|
<Outlet />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -106,42 +106,21 @@ export const AdminPanel = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-100">
|
<div className="p-4 md:p-8">
|
||||||
{/* Header */}
|
<div className="mb-6">
|
||||||
<div className="bg-white shadow">
|
<h1 className="text-2xl md:text-3xl font-bold text-gray-900">管理员面板</h1>
|
||||||
<div className="max-w-7xl mx-auto px-4 py-6">
|
<p className="text-gray-600 mt-1">系统统计与用户管理</p>
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<button onClick={() => navigate(-1)} className="p-2 hover:bg-gray-100 rounded-lg">
|
|
||||||
<ArrowLeft className="h-6 w-6 text-gray-600" />
|
|
||||||
</button>
|
|
||||||
<Shield className="h-8 w-8 text-primary-600" />
|
|
||||||
<div>
|
|
||||||
<h1 className="text-2xl font-bold text-gray-900">管理员面板</h1>
|
|
||||||
<p className="text-gray-600">{user?.username}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={() => navigate('/admin/settings')}
|
|
||||||
className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<Settings className="h-5 w-5" />
|
|
||||||
系统设置
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tabs */}
|
{/* Tabs */}
|
||||||
<div className="max-w-7xl mx-auto px-4 py-6">
|
<div className="mb-6">
|
||||||
<div className="flex gap-4 border-b border-gray-200 mb-6">
|
<div className="flex gap-4 border-b border-gray-200 mb-6">
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveTab('stats')}
|
onClick={() => setActiveTab('stats')}
|
||||||
className={`pb-3 px-4 font-medium border-b-2 transition-colors ${
|
className={`pb-3 px-4 font-medium border-b-2 transition-colors ${activeTab === 'stats'
|
||||||
activeTab === 'stats'
|
? 'border-primary-600 text-primary-600'
|
||||||
? 'border-primary-600 text-primary-600'
|
: 'border-transparent text-gray-600 hover:text-gray-900'
|
||||||
: 'border-transparent text-gray-600 hover:text-gray-900'
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<BarChart3 className="h-5 w-5" />
|
<BarChart3 className="h-5 w-5" />
|
||||||
@@ -150,11 +129,10 @@ export const AdminPanel = () => {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveTab('users')}
|
onClick={() => setActiveTab('users')}
|
||||||
className={`pb-3 px-4 font-medium border-b-2 transition-colors ${
|
className={`pb-3 px-4 font-medium border-b-2 transition-colors ${activeTab === 'users'
|
||||||
activeTab === 'users'
|
? 'border-primary-600 text-primary-600'
|
||||||
? 'border-primary-600 text-primary-600'
|
: 'border-transparent text-gray-600 hover:text-gray-900'
|
||||||
: 'border-transparent text-gray-600 hover:text-gray-900'
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Users className="h-5 w-5" />
|
<Users className="h-5 w-5" />
|
||||||
@@ -265,13 +243,14 @@ export const AdminPanel = () => {
|
|||||||
className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 flex items-center gap-2"
|
className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 flex items-center gap-2"
|
||||||
>
|
>
|
||||||
<Plus className="h-5 w-5" />
|
<Plus className="h-5 w-5" />
|
||||||
创建用户
|
<span className="hidden md:inline">创建用户</span>
|
||||||
|
<span className="md:hidden">新建</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Users Table */}
|
{/* Users Table */}
|
||||||
<div className="bg-white rounded-xl shadow overflow-hidden">
|
<div className="bg-white rounded-xl shadow overflow-hidden overflow-x-auto">
|
||||||
<table className="w-full">
|
<table className="w-full">
|
||||||
<thead className="bg-gray-50">
|
<thead className="bg-gray-50">
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
@@ -117,31 +117,14 @@ export const AdminSettings = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-100">
|
<div className="p-4 md:p-8">
|
||||||
{/* Header */}
|
<div className="mb-6">
|
||||||
<div className="bg-white shadow">
|
<h1 className="text-2xl md:text-3xl font-bold text-gray-900">系统设置</h1>
|
||||||
<div className="max-w-5xl mx-auto px-4 py-6">
|
<p className="text-gray-600 mt-1">配置系统参数与 AI 接口</p>
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<button
|
|
||||||
onClick={() => navigate(-1)}
|
|
||||||
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
|
||||||
title="返回"
|
|
||||||
>
|
|
||||||
<ArrowLeft className="h-6 w-6 text-gray-600" />
|
|
||||||
</button>
|
|
||||||
<Settings className="h-8 w-8 text-primary-600" />
|
|
||||||
<div>
|
|
||||||
<h1 className="text-2xl font-bold text-gray-900">系统设置</h1>
|
|
||||||
<p className="text-gray-600">管理员:{user?.username}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className="max-w-5xl mx-auto px-4 py-8 space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Basic Settings */}
|
{/* Basic Settings */}
|
||||||
<div className="bg-white rounded-xl shadow-md p-6 space-y-6">
|
<div className="bg-white rounded-xl shadow-md p-6 space-y-6">
|
||||||
<h2 className="text-xl font-bold text-gray-900 mb-4">基础设置</h2>
|
<h2 className="text-xl font-bold text-gray-900 mb-4">基础设置</h2>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import React, { useState, useEffect } from 'react'
|
|||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { examAPI, mistakeAPI } from '../api/client'
|
import { examAPI, mistakeAPI } from '../api/client'
|
||||||
import { useAuth } from '../context/AuthContext'
|
import { useAuth } from '../context/AuthContext'
|
||||||
import Layout from '../components/Layout'
|
|
||||||
import {
|
import {
|
||||||
FolderOpen, XCircle, TrendingUp, BookOpen, ArrowRight, Settings, Shield
|
FolderOpen, XCircle, TrendingUp, BookOpen, ArrowRight, Settings, Shield
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
@@ -58,7 +58,7 @@ export const Dashboard = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<>
|
||||||
<div className="p-4 md:p-8">
|
<div className="p-4 md:p-8">
|
||||||
{/* Welcome */}
|
{/* Welcome */}
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
@@ -179,35 +179,8 @@ export const Dashboard = () => {
|
|||||||
)}
|
)}
|
||||||
</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>
|
</div>
|
||||||
</Layout>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react'
|
import React, { useState, useEffect, useRef } from 'react'
|
||||||
import { useParams, useNavigate } from 'react-router-dom'
|
import { useParams, useNavigate } from 'react-router-dom'
|
||||||
import { examAPI, questionAPI } from '../api/client'
|
import { examAPI, questionAPI } from '../api/client'
|
||||||
import Layout from '../components/Layout'
|
|
||||||
import ParsingProgress from '../components/ParsingProgress'
|
import ParsingProgress from '../components/ParsingProgress'
|
||||||
import {
|
import {
|
||||||
ArrowLeft, Upload, Play, Loader, FileText, AlertCircle, RefreshCw, ArrowRight
|
ArrowLeft, Upload, Play, Loader, FileText, AlertCircle, RefreshCw, ArrowRight
|
||||||
@@ -155,22 +154,18 @@ export const ExamDetail = () => {
|
|||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<div className="flex items-center justify-center h-screen">
|
||||||
<div className="flex items-center justify-center h-screen">
|
<Loader className="h-8 w-8 animate-spin text-primary-600" />
|
||||||
<Loader className="h-8 w-8 animate-spin text-primary-600" />
|
</div>
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!exam) {
|
if (!exam) {
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<div className="flex flex-col items-center justify-center h-screen">
|
||||||
<div className="flex flex-col items-center justify-center h-screen">
|
<AlertCircle className="h-16 w-16 text-gray-300 mb-4" />
|
||||||
<AlertCircle className="h-16 w-16 text-gray-300 mb-4" />
|
<p className="text-gray-600">题库不存在</p>
|
||||||
<p className="text-gray-600">题库不存在</p>
|
</div>
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,7 +175,7 @@ export const ExamDetail = () => {
|
|||||||
const quizProgress = calculateProgress(exam.current_index, exam.total_questions)
|
const quizProgress = calculateProgress(exam.current_index, exam.total_questions)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<>
|
||||||
<div className="p-4 md:p-8">
|
<div className="p-4 md:p-8">
|
||||||
{/* Back Button */}
|
{/* Back Button */}
|
||||||
<button
|
<button
|
||||||
@@ -374,7 +369,7 @@ export const ExamDetail = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Layout>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
import React, { useState, useEffect } from 'react'
|
import React, { useState, useEffect } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { examAPI } from '../api/client'
|
import { examAPI } from '../api/client'
|
||||||
import Layout from '../components/Layout'
|
|
||||||
import {
|
import {
|
||||||
Plus, FolderOpen, Loader, AlertCircle, Trash2, Upload
|
Plus, FolderOpen, Loader, AlertCircle, Trash2, Upload
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
@@ -131,16 +130,14 @@ export const ExamList = () => {
|
|||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<div className="flex items-center justify-center h-screen">
|
||||||
<div className="flex items-center justify-center h-screen">
|
<Loader className="h-8 w-8 animate-spin text-primary-600" />
|
||||||
<Loader className="h-8 w-8 animate-spin text-primary-600" />
|
</div>
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<>
|
||||||
<div className="p-4 md:p-8">
|
<div className="p-4 md:p-8">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex flex-col md:flex-row md:items-center md:justify-between mb-6">
|
<div className="flex flex-col md:flex-row md:items-center md:justify-between mb-6">
|
||||||
@@ -150,10 +147,11 @@ export const ExamList = () => {
|
|||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowCreateModal(true)}
|
onClick={() => setShowCreateModal(true)}
|
||||||
className="mt-4 md:mt-0 bg-primary-600 text-white px-6 py-3 rounded-lg font-medium hover:bg-primary-700 transition-colors flex items-center gap-2 justify-center"
|
className="mt-4 md:mt-0 bg-primary-600 text-white px-4 py-2 rounded-lg font-medium hover:bg-primary-700 transition-colors flex items-center gap-2 justify-center text-sm md:text-base"
|
||||||
>
|
>
|
||||||
<Plus className="h-5 w-5" />
|
<Plus className="h-4 w-4 md:h-5 md:w-5" />
|
||||||
创建题库
|
<span className="hidden md:inline">创建题库</span>
|
||||||
|
<span className="md:hidden">新建</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -337,7 +335,7 @@ export const ExamList = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Layout>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,8 @@
|
|||||||
import React, { useState, useEffect } from 'react'
|
import React, { useState, useEffect } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { mistakeAPI } from '../api/client'
|
import { mistakeAPI } from '../api/client'
|
||||||
import Layout from '../components/Layout'
|
|
||||||
import Pagination from '../components/Pagination'
|
import Pagination from '../components/Pagination'
|
||||||
import { XCircle, Loader, Trash2, BookOpen, Play } from 'lucide-react'
|
import { XCircle, Loader, Trash2, BookOpen, Play, ChevronRight } from 'lucide-react'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
import { getQuestionTypeText, formatRelativeTime } from '../utils/helpers'
|
import { getQuestionTypeText, formatRelativeTime } from '../utils/helpers'
|
||||||
|
|
||||||
@@ -62,16 +61,14 @@ export const MistakeList = () => {
|
|||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<div className="flex items-center justify-center h-screen">
|
||||||
<div className="flex items-center justify-center h-screen">
|
<Loader className="h-8 w-8 animate-spin text-primary-600" />
|
||||||
<Loader className="h-8 w-8 animate-spin text-primary-600" />
|
</div>
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<>
|
||||||
<div className="p-4 md:p-8">
|
<div className="p-4 md:p-8">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex flex-col md:flex-row md:items-center justify-between mb-6 gap-4">
|
<div className="flex flex-col md:flex-row md:items-center justify-between mb-6 gap-4">
|
||||||
@@ -83,10 +80,11 @@ export const MistakeList = () => {
|
|||||||
{mistakes.length > 0 && (
|
{mistakes.length > 0 && (
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowModeModal(true)}
|
onClick={() => setShowModeModal(true)}
|
||||||
className="bg-primary-600 text-white px-6 py-2 rounded-lg font-medium hover:bg-primary-700 transition-colors flex items-center justify-center gap-2"
|
className="bg-primary-600 text-white px-4 py-2 rounded-lg font-medium hover:bg-primary-700 transition-colors flex items-center justify-center gap-2 text-sm md:text-base"
|
||||||
>
|
>
|
||||||
<Play className="h-5 w-5" />
|
<Play className="h-4 w-4 md:h-5 md:w-5" />
|
||||||
开始刷错题
|
<span className="hidden md:inline">开始刷错题</span>
|
||||||
|
<span className="md:hidden">刷题</span>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -237,7 +235,7 @@ export const MistakeList = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Layout>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
import React, { useState, useEffect } from 'react'
|
import React, { useState, useEffect } from 'react'
|
||||||
import { useNavigate, useLocation } from 'react-router-dom'
|
import { useNavigate, useLocation } from 'react-router-dom'
|
||||||
import { mistakeAPI, questionAPI } from '../api/client'
|
import { mistakeAPI, questionAPI } from '../api/client'
|
||||||
import Layout from '../components/Layout'
|
|
||||||
import {
|
import {
|
||||||
ArrowLeft, ArrowRight, Check, X, Loader, Trash2, AlertCircle
|
ArrowLeft, ArrowRight, Check, X, Loader, Trash2, AlertCircle
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
@@ -13,16 +12,17 @@ import { getQuestionTypeText } from '../utils/helpers'
|
|||||||
|
|
||||||
export const MistakePlayer = () => {
|
export const MistakePlayer = () => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const searchParams = new URLSearchParams(location.search)
|
const searchParams = new URLSearchParams(location.search)
|
||||||
const mode = searchParams.get('mode') || 'sequential'
|
const mode = searchParams.get('mode') || 'sequential'
|
||||||
|
|
||||||
|
console.log('MistakePlayer mounted, mode:', mode)
|
||||||
|
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [mistake, setMistake] = useState(null)
|
const [mistake, setMistake] = useState(null)
|
||||||
const [currentIndex, setCurrentIndex] = useState(0)
|
const [currentIndex, setCurrentIndex] = useState(0)
|
||||||
const [total, setTotal] = useState(0)
|
const [total, setTotal] = useState(0)
|
||||||
const [randomIds, setRandomIds] = useState([]) // For random mode
|
const [randomMistakes, setRandomMistakes] = useState([]) // Store full mistake objects
|
||||||
|
|
||||||
const [submitting, setSubmitting] = useState(false)
|
const [submitting, setSubmitting] = useState(false)
|
||||||
const [userAnswer, setUserAnswer] = useState('')
|
const [userAnswer, setUserAnswer] = useState('')
|
||||||
@@ -31,7 +31,7 @@ export const MistakePlayer = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadMistake()
|
loadMistake()
|
||||||
}, [currentIndex])
|
}, [currentIndex, mode])
|
||||||
|
|
||||||
const loadMistake = async () => {
|
const loadMistake = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -41,49 +41,26 @@ export const MistakePlayer = () => {
|
|||||||
|
|
||||||
if (mode === 'random') {
|
if (mode === 'random') {
|
||||||
// Random Mode Logic
|
// Random Mode Logic
|
||||||
if (randomIds.length === 0) {
|
if (randomMistakes.length === 0) {
|
||||||
// First load: fetch all mistakes to get IDs
|
// First load: fetch all mistakes
|
||||||
// Note: fetching up to 1000 for now. For larger datasets, we need a specific API to get just IDs.
|
|
||||||
const response = await mistakeAPI.getList(0, 1000)
|
const response = await mistakeAPI.getList(0, 1000)
|
||||||
const allMistakes = response.data.mistakes
|
const allMistakes = response.data.mistakes
|
||||||
setTotal(response.data.total)
|
setTotal(response.data.total)
|
||||||
|
|
||||||
if (allMistakes.length > 0) {
|
if (allMistakes.length > 0) {
|
||||||
// Shuffle IDs
|
// Shuffle mistakes
|
||||||
const ids = allMistakes.map(m => m.id)
|
const shuffled = [...allMistakes]
|
||||||
for (let i = ids.length - 1; i > 0; i--) {
|
for (let i = shuffled.length - 1; i > 0; i--) {
|
||||||
const j = Math.floor(Math.random() * (i + 1));
|
const j = Math.floor(Math.random() * (i + 1));
|
||||||
[ids[i], ids[j]] = [ids[j], ids[i]];
|
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
||||||
}
|
}
|
||||||
setRandomIds(ids)
|
setRandomMistakes(shuffled)
|
||||||
|
currentMistake = shuffled[0]
|
||||||
// Get first mistake from shuffled list
|
|
||||||
// We need to find the full mistake object from our initial fetch
|
|
||||||
// (Since we fetched all, we have it)
|
|
||||||
const firstId = ids[0]
|
|
||||||
currentMistake = allMistakes.find(m => m.id === firstId)
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Subsequent loads: use stored random IDs
|
// Subsequent loads: use stored mistakes
|
||||||
// We need to fetch the specific mistake details if we don't have them cached
|
if (currentIndex < randomMistakes.length) {
|
||||||
// But wait, mistakeAPI.getList is pagination based.
|
currentMistake = randomMistakes[currentIndex]
|
||||||
// We can't easily "get mistake by ID" using getList without knowing its index.
|
|
||||||
// However, we have `mistakeAPI.remove` but not `getById`.
|
|
||||||
// Actually, the `mistake` object contains the `question`.
|
|
||||||
// If we only have the ID, we might need an API to get mistake by ID.
|
|
||||||
// But since we fetched ALL mistakes initially (up to 1000), we can just store the whole objects in randomIds?
|
|
||||||
// No, that's too much memory if many.
|
|
||||||
|
|
||||||
// Let's assume for now we fetched all and stored them in a "cache" or just store the list of objects if < 1000.
|
|
||||||
// For simplicity and performance on small datasets:
|
|
||||||
// If randomIds contains objects, use them.
|
|
||||||
|
|
||||||
// REVISION: Let's just store the full shuffled list of mistakes in state if < 1000.
|
|
||||||
// If > 1000, this approach needs backend support for "random fetch".
|
|
||||||
// Given the user requirements, let's assume < 1000 for now.
|
|
||||||
|
|
||||||
if (currentIndex < randomIds.length) {
|
|
||||||
currentMistake = randomIds[currentIndex]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -101,15 +78,10 @@ export const MistakePlayer = () => {
|
|||||||
currentMistake.question.options = ['A. 正确', 'B. 错误']
|
currentMistake.question.options = ['A. 正确', 'B. 错误']
|
||||||
}
|
}
|
||||||
setMistake(currentMistake)
|
setMistake(currentMistake)
|
||||||
|
console.log('Mistake loaded:', currentMistake)
|
||||||
setResult(null)
|
setResult(null)
|
||||||
setUserAnswer('')
|
setUserAnswer('')
|
||||||
setMultipleAnswers([])
|
setMultipleAnswers([])
|
||||||
|
|
||||||
// If we just initialized random mode, update state
|
|
||||||
if (mode === 'random' && randomIds.length === 0) {
|
|
||||||
// This part is tricky because state updates are async.
|
|
||||||
// We handled the initialization above.
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
setMistake(null)
|
setMistake(null)
|
||||||
}
|
}
|
||||||
@@ -118,6 +90,7 @@ export const MistakePlayer = () => {
|
|||||||
toast.error('加载错题失败')
|
toast.error('加载错题失败')
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
|
console.log('Loading finished')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,8 +149,8 @@ export const MistakePlayer = () => {
|
|||||||
// If we remove the last item, we need to go back one step or show empty
|
// If we remove the last item, we need to go back one step or show empty
|
||||||
if (mode === 'random') {
|
if (mode === 'random') {
|
||||||
// Remove from random list
|
// Remove from random list
|
||||||
const newRandomList = randomIds.filter(m => m.id !== mistake.id)
|
const newRandomList = randomMistakes.filter(m => m.id !== mistake.id)
|
||||||
setRandomIds(newRandomList)
|
setRandomMistakes(newRandomList)
|
||||||
setTotal(newRandomList.length)
|
setTotal(newRandomList.length)
|
||||||
|
|
||||||
if (currentIndex >= newRandomList.length && newRandomList.length > 0) {
|
if (currentIndex >= newRandomList.length && newRandomList.length > 0) {
|
||||||
@@ -219,35 +192,46 @@ export const MistakePlayer = () => {
|
|||||||
|
|
||||||
if (loading && !mistake) {
|
if (loading && !mistake) {
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<div className="flex items-center justify-center min-h-[50vh]">
|
||||||
<div className="flex items-center justify-center h-screen">
|
<Loader className="h-8 w-8 animate-spin text-primary-600" />
|
||||||
<Loader className="h-8 w-8 animate-spin text-primary-600" />
|
</div>
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!mistake) {
|
if (!mistake) {
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<div className="flex flex-col items-center justify-center min-h-[50vh]">
|
||||||
<div className="flex flex-col items-center justify-center h-screen">
|
<AlertCircle className="h-16 w-16 text-gray-300 mb-4" />
|
||||||
<AlertCircle className="h-16 w-16 text-gray-300 mb-4" />
|
<p className="text-gray-600">错题本为空</p>
|
||||||
<p className="text-gray-600">错题本为空</p>
|
<button
|
||||||
<button
|
onClick={() => navigate('/mistakes')}
|
||||||
onClick={() => navigate('/mistakes')}
|
className="mt-4 text-primary-600 hover:text-primary-700 font-medium"
|
||||||
className="mt-4 text-primary-600 hover:text-primary-700 font-medium"
|
>
|
||||||
>
|
返回错题列表
|
||||||
返回错题列表
|
</button>
|
||||||
</button>
|
</div>
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const question = mistake.question
|
const question = mistake.question
|
||||||
|
|
||||||
|
if (!question) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center min-h-[50vh]">
|
||||||
|
<AlertCircle className="h-16 w-16 text-red-300 mb-4" />
|
||||||
|
<p className="text-gray-600">题目数据缺失</p>
|
||||||
|
<button
|
||||||
|
onClick={() => navigate('/mistakes')}
|
||||||
|
className="mt-4 text-primary-600 hover:text-primary-700 font-medium"
|
||||||
|
>
|
||||||
|
返回错题列表
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<>
|
||||||
<div className="max-w-4xl mx-auto p-4 md:p-8">
|
<div className="max-w-4xl mx-auto p-4 md:p-8">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between mb-6">
|
<div className="flex items-center justify-between mb-6">
|
||||||
@@ -421,7 +405,7 @@ export const MistakePlayer = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
import React, { useState, useEffect } from 'react'
|
import React, { useState, useEffect } from 'react'
|
||||||
import { questionAPI } from '../api/client'
|
import { questionAPI } from '../api/client'
|
||||||
import Layout from '../components/Layout'
|
|
||||||
import Pagination from '../components/Pagination'
|
import Pagination from '../components/Pagination'
|
||||||
import { FileText, Loader, Search } from 'lucide-react'
|
import { FileText, Loader, Search } from 'lucide-react'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
@@ -44,16 +43,14 @@ export const QuestionBank = () => {
|
|||||||
|
|
||||||
if (loading && questions.length === 0) {
|
if (loading && questions.length === 0) {
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<div className="flex items-center justify-center h-screen">
|
||||||
<div className="flex items-center justify-center h-screen">
|
<Loader className="h-8 w-8 animate-spin text-primary-600" />
|
||||||
<Loader className="h-8 w-8 animate-spin text-primary-600" />
|
</div>
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<>
|
||||||
<div className="p-4 md:p-8">
|
<div className="p-4 md:p-8">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
@@ -152,7 +149,7 @@ export const QuestionBank = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
import React, { useState, useEffect } from 'react'
|
import React, { useState, useEffect } from 'react'
|
||||||
import { useParams, useNavigate, useSearchParams } from 'react-router-dom'
|
import { useParams, useNavigate, useSearchParams } from 'react-router-dom'
|
||||||
import { examAPI, questionAPI, mistakeAPI } from '../api/client'
|
import { examAPI, questionAPI, mistakeAPI } from '../api/client'
|
||||||
import Layout from '../components/Layout'
|
|
||||||
import {
|
import {
|
||||||
ArrowLeft, ArrowRight, Check, X, Loader, BookmarkPlus, BookmarkX, AlertCircle
|
ArrowLeft, ArrowRight, Check, X, Loader, BookmarkPlus, BookmarkX, AlertCircle
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
@@ -159,27 +158,23 @@ export const QuizPlayer = () => {
|
|||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<div className="flex items-center justify-center h-screen">
|
||||||
<div className="flex items-center justify-center h-screen">
|
<Loader className="h-8 w-8 animate-spin text-primary-600" />
|
||||||
<Loader className="h-8 w-8 animate-spin text-primary-600" />
|
</div>
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!question) {
|
if (!question) {
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<div className="flex flex-col items-center justify-center h-screen">
|
||||||
<div className="flex flex-col items-center justify-center h-screen">
|
<AlertCircle className="h-16 w-16 text-gray-300 mb-4" />
|
||||||
<AlertCircle className="h-16 w-16 text-gray-300 mb-4" />
|
<p className="text-gray-600">没有更多题目了</p>
|
||||||
<p className="text-gray-600">没有更多题目了</p>
|
</div>
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<>
|
||||||
<div className="max-w-4xl mx-auto p-4 md:p-8">
|
<div className="max-w-4xl mx-auto p-4 md:p-8">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between mb-6">
|
<div className="flex items-center justify-between mb-6">
|
||||||
@@ -212,8 +207,8 @@ export const QuizPlayer = () => {
|
|||||||
<button
|
<button
|
||||||
onClick={handleToggleMistake}
|
onClick={handleToggleMistake}
|
||||||
className={`flex items-center gap-2 px-3 py-2 rounded-lg transition-colors ${inMistakeBook
|
className={`flex items-center gap-2 px-3 py-2 rounded-lg transition-colors ${inMistakeBook
|
||||||
? 'bg-yellow-100 text-yellow-700 hover:bg-yellow-200'
|
? 'bg-yellow-100 text-yellow-700 hover:bg-yellow-200'
|
||||||
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{inMistakeBook ? (
|
{inMistakeBook ? (
|
||||||
@@ -260,8 +255,8 @@ export const QuizPlayer = () => {
|
|||||||
}}
|
}}
|
||||||
disabled={!!result}
|
disabled={!!result}
|
||||||
className={`w-full text-left p-4 rounded-lg border-2 transition-all ${isSelected
|
className={`w-full text-left p-4 rounded-lg border-2 transition-all ${isSelected
|
||||||
? 'border-primary-500 bg-primary-50'
|
? 'border-primary-500 bg-primary-50'
|
||||||
: 'border-gray-200 hover:border-gray-300'
|
: 'border-gray-200 hover:border-gray-300'
|
||||||
} ${result ? 'cursor-not-allowed opacity-75' : 'cursor-pointer'}`}
|
} ${result ? 'cursor-not-allowed opacity-75' : 'cursor-pointer'}`}
|
||||||
>
|
>
|
||||||
<span className="text-gray-900">{option}</span>
|
<span className="text-gray-900">{option}</span>
|
||||||
@@ -367,7 +362,7 @@ export const QuizPlayer = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user