/** * Exam Detail Page - with real-time parsing progress via SSE */ import React, { useState, useEffect, useRef } from 'react' import { useParams, useNavigate } from 'react-router-dom' import { examAPI, questionAPI } from '../api/client' import Layout from '../components/Layout' import ParsingProgress from '../components/ParsingProgress' import { ArrowLeft, Upload, Play, Loader, FileText, AlertCircle, RefreshCw, ArrowRight } from 'lucide-react' import toast from 'react-hot-toast' import { getStatusColor, getStatusText, formatDate, calculateProgress, isValidFileType, getQuestionTypeText } from '../utils/helpers' export const ExamDetail = () => { const { examId } = useParams() const navigate = useNavigate() const [exam, setExam] = useState(null) const [loading, setLoading] = useState(true) const [uploading, setUploading] = useState(false) const [showUploadModal, setShowUploadModal] = useState(false) const [uploadFile, setUploadFile] = useState(null) const [progress, setProgress] = useState(null) const eventSourceRef = useRef(null) useEffect(() => { loadExamDetail() // Cleanup on unmount return () => { if (eventSourceRef.current) { eventSourceRef.current.close() eventSourceRef.current = null } } }, [examId]) const loadExamDetail = async () => { try { const examRes = await examAPI.getDetail(examId) setExam(examRes.data) // Connect to SSE if exam is processing if (examRes.data.status === 'processing') { connectSSE() } } catch (error) { console.error('Failed to load exam:', error) toast.error('加载题库失败') } finally { setLoading(false) } } const connectSSE = () => { // Close existing connection if any if (eventSourceRef.current) { eventSourceRef.current.close() } console.log('[SSE] Connecting to progress stream for exam', examId) const token = localStorage.getItem('token') const url = `/api/exams/${examId}/progress?token=${encodeURIComponent(token)}` const eventSource = new EventSource(url) eventSourceRef.current = eventSource eventSource.onmessage = (event) => { try { const progressData = JSON.parse(event.data) console.log('[SSE] Progress update:', progressData) setProgress(progressData) // Update exam status if completed or failed if (progressData.status === 'completed') { toast.success(progressData.message) setExam(prev => ({ ...prev, status: 'ready' })) loadExamDetail() // Reload to get updated questions eventSource.close() eventSourceRef.current = null } else if (progressData.status === 'failed') { toast.error(progressData.message) setExam(prev => ({ ...prev, status: 'failed' })) eventSource.close() eventSourceRef.current = null } } catch (error) { console.error('[SSE] Failed to parse progress data:', error) } } eventSource.onerror = (error) => { console.error('[SSE] Connection error:', error) eventSource.close() eventSourceRef.current = null } eventSource.onopen = () => { console.log('[SSE] Connection established') } } const handleAppendDocument = async (e) => { e.preventDefault() if (!uploadFile) { toast.error('请选择文件') return } if (!isValidFileType(uploadFile.name)) { toast.error('不支持的文件类型') return } setUploading(true) try { await examAPI.appendDocument(examId, uploadFile) toast.success('文档上传成功,正在解析并去重...') setShowUploadModal(false) setUploadFile(null) setExam(prev => ({ ...prev, status: 'processing' })) // Connect to SSE for real-time progress connectSSE() } catch (error) { console.error('Failed to append document:', error) toast.error('文档上传失败') } finally { setUploading(false) } } const handleStartQuiz = () => { if (exam.current_index >= exam.total_questions) { if (window.confirm('已经完成所有题目,是否从头开始?')) { navigate(`/quiz/${examId}?reset=true`) } } else { navigate(`/quiz/${examId}`) } } if (loading) { return (
) } if (!exam) { return (

题库不存在

) } const isProcessing = exam.status === 'processing' const isReady = exam.status === 'ready' const isFailed = exam.status === 'failed' const quizProgress = calculateProgress(exam.current_index, exam.total_questions) return (
{/* Back Button */} {/* Parsing Progress (only shown when processing) */} {isProcessing && progress && ( )} {/* Header */}

{exam.title}

{getStatusText(exam.status)} {isProcessing && ( 正在处理中... )}
{/* Actions */}
{isReady && exam.total_questions > 0 && ( )}
{/* Stats */}

题目总数

{exam.total_questions}

已完成

{exam.current_index}

剩余

{Math.max(0, exam.total_questions - exam.current_index)}

完成度

{isProcessing ? progress : quizProgress}%

{/* Progress Bar */} {exam.total_questions > 0 && (
)} {/* Info */}

创建时间:{formatDate(exam.created_at)}

最后更新:{formatDate(exam.updated_at)}

{/* Failed Status Warning */} {isFailed && (

文档解析失败

请检查文档格式是否正确,或尝试重新上传。

)} {/* View All Questions Link */}
navigate(`/questions?examId=${examId}`)} >

查看题库所有题目

浏览、搜索和查看该题库中的所有题目详情

{/* Upload Modal */} {showUploadModal && (

添加题目文档

上传新文档后,系统会自动解析题目并去除重复题目。

setUploadFile(e.target.files[0])} required accept=".txt,.pdf,.doc,.docx,.xlsx,.xls" className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent" />

支持:TXT, PDF, DOC, DOCX, XLSX, XLS

)}
) } export default ExamDetail