用户端页面构建完成---待测试
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Card, Button, Radio, Checkbox, Input, message, Progress } from 'antd';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { Card, Button, Radio, Checkbox, Input, message, Progress, Modal } from 'antd';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { useUser, useQuiz } from '../contexts';
|
||||
import { quizAPI } from '../services/api';
|
||||
@@ -32,14 +32,14 @@ const QuizPage = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const { user } = useUser();
|
||||
const { questions, setQuestions, currentQuestionIndex, setCurrentQuestionIndex, answers, setAnswer, clearQuiz } = useQuiz();
|
||||
const { questions, setQuestions, currentQuestionIndex, setCurrentQuestionIndex, answers, setAnswer, setAnswers, clearQuiz } = useQuiz();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [timeLeft, setTimeLeft] = useState<number | null>(null);
|
||||
const [timeLimit, setTimeLimit] = useState<number | null>(null);
|
||||
const [subjectId, setSubjectId] = useState<string>('');
|
||||
const [taskId, setTaskId] = useState<string>('');
|
||||
const [showAnalysis, setShowAnalysis] = useState(false);
|
||||
const lastTickSavedAtMsRef = useRef<number>(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (!user) {
|
||||
@@ -49,22 +49,49 @@ const QuizPage = () => {
|
||||
}
|
||||
|
||||
const state = location.state as LocationState;
|
||||
|
||||
|
||||
clearQuiz();
|
||||
|
||||
if (state?.questions) {
|
||||
// 如果已经有题目数据(来自科目选择页面)
|
||||
const nextSubjectId = state.subjectId || '';
|
||||
const nextTaskId = state.taskId || '';
|
||||
const nextTimeLimit = state.timeLimit || 60;
|
||||
|
||||
setQuestions(state.questions);
|
||||
setTimeLimit(state.timeLimit || 60);
|
||||
setTimeLeft((state.timeLimit || 60) * 60); // 转换为秒
|
||||
setSubjectId(state.subjectId || '');
|
||||
setTaskId(state.taskId || '');
|
||||
setAnswers({});
|
||||
setCurrentQuestionIndex(0);
|
||||
} else {
|
||||
// 兼容旧版本,直接生成题目
|
||||
generateQuiz();
|
||||
setTimeLimit(nextTimeLimit);
|
||||
setTimeLeft(nextTimeLimit * 60);
|
||||
setSubjectId(nextSubjectId);
|
||||
setTaskId(nextTaskId);
|
||||
|
||||
const progressKey = buildProgressKey(user.id, nextSubjectId, nextTaskId);
|
||||
setActiveProgress(user.id, progressKey);
|
||||
saveProgress(progressKey, {
|
||||
questions: state.questions,
|
||||
answers: {},
|
||||
currentQuestionIndex: 0,
|
||||
timeLeftSeconds: nextTimeLimit * 60,
|
||||
timeLimitMinutes: nextTimeLimit,
|
||||
subjectId: nextSubjectId,
|
||||
taskId: nextTaskId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 清除之前的答题状态
|
||||
clearQuiz();
|
||||
const restored = restoreActiveProgress(user.id);
|
||||
if (restored) {
|
||||
setQuestions(restored.questions);
|
||||
setAnswers(restored.answers);
|
||||
setCurrentQuestionIndex(restored.currentQuestionIndex);
|
||||
setTimeLimit(restored.timeLimitMinutes);
|
||||
setTimeLeft(restored.timeLeftSeconds);
|
||||
setSubjectId(restored.subjectId);
|
||||
setTaskId(restored.taskId);
|
||||
return;
|
||||
}
|
||||
|
||||
generateQuiz();
|
||||
}, [user, navigate, location]);
|
||||
|
||||
// 倒计时逻辑
|
||||
@@ -85,16 +112,29 @@ const QuizPage = () => {
|
||||
return () => clearInterval(timer);
|
||||
}, [timeLeft]);
|
||||
|
||||
useEffect(() => {
|
||||
setShowAnalysis(false);
|
||||
}, [currentQuestionIndex]);
|
||||
|
||||
const generateQuiz = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await quizAPI.generateQuiz(user!.id);
|
||||
setQuestions(response.data.questions);
|
||||
setCurrentQuestionIndex(0);
|
||||
setAnswers({});
|
||||
setTimeLimit(60);
|
||||
setTimeLeft(60 * 60);
|
||||
setSubjectId('');
|
||||
setTaskId('');
|
||||
|
||||
const progressKey = buildProgressKey(user!.id, '', '');
|
||||
setActiveProgress(user!.id, progressKey);
|
||||
saveProgress(progressKey, {
|
||||
questions: response.data.questions,
|
||||
answers: {},
|
||||
currentQuestionIndex: 0,
|
||||
timeLeftSeconds: 60 * 60,
|
||||
timeLimitMinutes: 60,
|
||||
subjectId: '',
|
||||
taskId: '',
|
||||
});
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '生成试卷失败');
|
||||
} finally {
|
||||
@@ -134,6 +174,48 @@ const QuizPage = () => {
|
||||
setAnswer(questionId, value);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!user) return;
|
||||
if (!questions.length) return;
|
||||
if (timeLeft === null) return;
|
||||
|
||||
const progressKey = getActiveProgressKey(user.id);
|
||||
if (!progressKey) return;
|
||||
|
||||
saveProgress(progressKey, {
|
||||
questions,
|
||||
answers,
|
||||
currentQuestionIndex,
|
||||
timeLeftSeconds: timeLeft,
|
||||
timeLimitMinutes: timeLimit || 60,
|
||||
subjectId,
|
||||
taskId,
|
||||
});
|
||||
}, [user, questions, answers, currentQuestionIndex, subjectId, taskId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!user) return;
|
||||
if (!questions.length) return;
|
||||
if (timeLeft === null) return;
|
||||
|
||||
const now = Date.now();
|
||||
if (now - lastTickSavedAtMsRef.current < 5000) return;
|
||||
lastTickSavedAtMsRef.current = now;
|
||||
|
||||
const progressKey = getActiveProgressKey(user.id);
|
||||
if (!progressKey) return;
|
||||
|
||||
saveProgress(progressKey, {
|
||||
questions,
|
||||
answers,
|
||||
currentQuestionIndex,
|
||||
timeLeftSeconds: timeLeft,
|
||||
timeLimitMinutes: timeLimit || 60,
|
||||
subjectId,
|
||||
taskId,
|
||||
});
|
||||
}, [user, questions.length, timeLeft]);
|
||||
|
||||
const handleNext = () => {
|
||||
if (currentQuestionIndex < questions.length - 1) {
|
||||
setCurrentQuestionIndex(currentQuestionIndex + 1);
|
||||
@@ -178,6 +260,7 @@ const QuizPage = () => {
|
||||
});
|
||||
|
||||
message.success('答题提交成功!');
|
||||
clearActiveProgress(user!.id);
|
||||
navigate(`/result/${response.data.recordId}`);
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '提交失败');
|
||||
@@ -186,6 +269,22 @@ const QuizPage = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleGiveUp = () => {
|
||||
if (!user) return;
|
||||
Modal.confirm({
|
||||
title: '确认弃考?',
|
||||
content: '弃考将清空本次答题进度与计时信息,且不计入考试次数。',
|
||||
okText: '确认弃考',
|
||||
cancelText: '继续答题',
|
||||
okButtonProps: { danger: true },
|
||||
onOk: () => {
|
||||
clearActiveProgress(user.id);
|
||||
clearQuiz();
|
||||
navigate('/tasks');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const checkAnswer = (question: Question, userAnswer: string | string[]): boolean => {
|
||||
if (!userAnswer) return false;
|
||||
|
||||
@@ -293,16 +392,21 @@ const QuizPage = () => {
|
||||
第 {currentQuestionIndex + 1} 题 / 共 {questions.length} 题
|
||||
</p>
|
||||
</div>
|
||||
{timeLeft !== null && (
|
||||
<div className="text-right">
|
||||
<div className="text-sm text-gray-500">剩余时间</div>
|
||||
<div className={`text-2xl font-bold tabular-nums ${
|
||||
timeLeft < 300 ? 'text-red-600' : 'text-mars-600'
|
||||
}`}>
|
||||
{formatTime(timeLeft)}
|
||||
<div className="flex items-center space-x-3">
|
||||
<Button danger onClick={handleGiveUp}>
|
||||
弃考
|
||||
</Button>
|
||||
{timeLeft !== null && (
|
||||
<div className="text-right">
|
||||
<div className="text-sm text-gray-500">剩余时间</div>
|
||||
<div className={`text-2xl font-bold tabular-nums ${
|
||||
timeLeft < 300 ? 'text-red-600' : 'text-mars-600'
|
||||
}`}>
|
||||
{formatTime(timeLeft)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Progress
|
||||
@@ -321,9 +425,6 @@ const QuizPage = () => {
|
||||
<span className={`inline-block px-3 py-1 rounded-full text-sm font-medium ${getTagColor(currentQuestion.type)}`}>
|
||||
{questionTypeMap[currentQuestion.type]}
|
||||
</span>
|
||||
<span className="text-sm text-gray-500 font-medium">
|
||||
{currentQuestion.score} 分
|
||||
</span>
|
||||
</div>
|
||||
{currentQuestion.category && (
|
||||
<div className="mb-3">
|
||||
@@ -341,23 +442,6 @@ const QuizPage = () => {
|
||||
{renderQuestion(currentQuestion)}
|
||||
</div>
|
||||
|
||||
{String(currentQuestion.analysis ?? '').trim() ? (
|
||||
<div className="mb-6">
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() => setShowAnalysis((v) => !v)}
|
||||
className="p-0 h-auto text-mars-600 hover:text-mars-700"
|
||||
>
|
||||
{showAnalysis ? '收起解析' : '查看解析'}
|
||||
</Button>
|
||||
{showAnalysis ? (
|
||||
<div className="mt-2 p-4 rounded-lg border border-gray-100 bg-gray-50 text-gray-700 whitespace-pre-wrap">
|
||||
{currentQuestion.analysis}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<div className="flex justify-between items-center pt-6 border-t border-gray-100">
|
||||
<Button
|
||||
@@ -399,3 +483,72 @@ const QuizPage = () => {
|
||||
};
|
||||
|
||||
export default QuizPage;
|
||||
|
||||
const QUIZ_PROGRESS_ACTIVE_PREFIX = 'quiz_progress_active_v1:';
|
||||
const QUIZ_PROGRESS_PREFIX = 'quiz_progress_v1:';
|
||||
|
||||
type QuizProgressV1 = {
|
||||
questions: Question[];
|
||||
answers: Record<string, string | string[]>;
|
||||
currentQuestionIndex: number;
|
||||
timeLeftSeconds: number;
|
||||
timeLimitMinutes: number;
|
||||
subjectId: string;
|
||||
taskId: string;
|
||||
savedAt: string;
|
||||
};
|
||||
|
||||
const buildProgressKey = (userId: string, subjectId: string, taskId: string) => {
|
||||
const scope = taskId ? `task:${taskId}` : `subject:${subjectId || 'none'}`;
|
||||
return `${QUIZ_PROGRESS_PREFIX}${userId}:${scope}`;
|
||||
};
|
||||
|
||||
const setActiveProgress = (userId: string, progressKey: string) => {
|
||||
localStorage.setItem(`${QUIZ_PROGRESS_ACTIVE_PREFIX}${userId}`, progressKey);
|
||||
};
|
||||
|
||||
const removeActiveProgress = (userId: string) => {
|
||||
localStorage.removeItem(`${QUIZ_PROGRESS_ACTIVE_PREFIX}${userId}`);
|
||||
};
|
||||
|
||||
const getActiveProgressKey = (userId: string) => {
|
||||
return localStorage.getItem(`${QUIZ_PROGRESS_ACTIVE_PREFIX}${userId}`) || '';
|
||||
};
|
||||
|
||||
const saveProgress = (progressKey: string, input: Omit<QuizProgressV1, 'savedAt'>) => {
|
||||
const payload: QuizProgressV1 = {
|
||||
...input,
|
||||
savedAt: new Date().toISOString(),
|
||||
};
|
||||
localStorage.setItem(progressKey, JSON.stringify(payload));
|
||||
};
|
||||
|
||||
const restoreActiveProgress = (userId: string): QuizProgressV1 | null => {
|
||||
const progressKey = getActiveProgressKey(userId);
|
||||
if (!progressKey) return null;
|
||||
|
||||
const raw = localStorage.getItem(progressKey);
|
||||
if (!raw) return null;
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as QuizProgressV1;
|
||||
if (!parsed || !Array.isArray(parsed.questions)) return null;
|
||||
if (!parsed.answers || typeof parsed.answers !== 'object') return null;
|
||||
if (typeof parsed.currentQuestionIndex !== 'number') return null;
|
||||
if (typeof parsed.timeLeftSeconds !== 'number') return null;
|
||||
if (typeof parsed.timeLimitMinutes !== 'number') return null;
|
||||
if (typeof parsed.subjectId !== 'string') return null;
|
||||
if (typeof parsed.taskId !== 'string') return null;
|
||||
return parsed;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const clearActiveProgress = (userId: string) => {
|
||||
const progressKey = getActiveProgressKey(userId);
|
||||
if (progressKey) {
|
||||
localStorage.removeItem(progressKey);
|
||||
}
|
||||
removeActiveProgress(userId);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user