feat: 更新考试相关页面,优化任务状态处理,添加用户任务接口测试
This commit is contained in:
@@ -347,11 +347,17 @@ const QuizPage = () => {
|
||||
subjectId: subjectId || undefined,
|
||||
taskId: taskId || undefined,
|
||||
answers: answersData
|
||||
});
|
||||
}) as any;
|
||||
|
||||
const payload = response?.data ?? response;
|
||||
const recordId = payload?.recordId;
|
||||
if (!recordId) {
|
||||
throw new Error('提交成功但未返回记录ID');
|
||||
}
|
||||
|
||||
message.success('答题提交成功!');
|
||||
clearActiveProgress(user!.id);
|
||||
navigate(`/result/${response.data.recordId}`);
|
||||
navigate(`/result/${recordId}`);
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '提交失败');
|
||||
} finally {
|
||||
|
||||
@@ -32,6 +32,55 @@ interface QuizAnswer {
|
||||
questionAnalysis?: string;
|
||||
}
|
||||
|
||||
const StatusIcon = ({ status }: { status: QuizRecord['status'] }) => {
|
||||
const colorClass =
|
||||
status === '优秀' ? 'text-green-500' : status === '合格' ? 'text-blue-500' : 'text-red-500';
|
||||
|
||||
const renderInner = () => {
|
||||
if (status === '优秀') {
|
||||
// 五角星(实心)
|
||||
return (
|
||||
<path
|
||||
d="M12 3.6l2.63 5.33 5.89.86-4.26 4.15 1.01 5.86L12 17.69 6.73 19.8l1.01-5.86-4.26-4.15 5.89-.86L12 3.6z"
|
||||
fill="currentColor"
|
||||
stroke="none"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (status === '合格') {
|
||||
// 对号(空心)
|
||||
return <path d="M7.5 12.2l3 3L16.8 9" />;
|
||||
}
|
||||
|
||||
// 感叹号(空心)
|
||||
return (
|
||||
<>
|
||||
<path d="M12 7v6" />
|
||||
<path d="M12 16.8h.01" />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
width="32"
|
||||
height="32"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
className={colorClass}
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
{renderInner()}
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
const ResultPage = () => {
|
||||
const { id: recordId } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
@@ -53,12 +102,14 @@ const ResultPage = () => {
|
||||
const fetchResultDetail = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await quizAPI.getRecordDetail(recordId!);
|
||||
setRecord(response.record);
|
||||
setAnswers(response.answers);
|
||||
const response = await quizAPI.getRecordDetail(recordId!) as any;
|
||||
const payload = response?.data ?? response;
|
||||
setRecord(payload?.record ?? null);
|
||||
setAnswers(Array.isArray(payload?.answers) ? payload.answers : []);
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '获取答题结果失败');
|
||||
navigate('/');
|
||||
setRecord(null);
|
||||
setAnswers([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -203,19 +254,7 @@ const ResultPage = () => {
|
||||
<Card className="shadow-lg mb-6 rounded-xl border-t-4 border-t-mars-500" bodyStyle={{ padding: '12px' }}>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex-shrink-0 mt-0.5">
|
||||
{record.status === '优秀' ? (
|
||||
<svg viewBox="64 64 896 896" focusable="false" data-icon="check-circle" width="32" height="32" fill="currentColor" aria-hidden="true" className="text-green-500">
|
||||
<path d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm193.5 301.7l-210.6 292a31.8 31.8 0 01-51.7 0L318.5 484.9c-3.8-5.3 0-12.7 6.5-12.7h46.9c10.2 0 19.9 4.9 25.9 13.3l71.2 98.8 157.2-218c6-8.3 15.6-13.3 25.9-13.3H699c6.5 0 10.3 7.4 6.5 12.7z"></path>
|
||||
</svg>
|
||||
) : record.status === '合格' ? (
|
||||
<svg viewBox="64 64 896 896" focusable="false" data-icon="check-circle" width="32" height="32" fill="currentColor" aria-hidden="true" className="text-blue-500">
|
||||
<path d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm193.5 301.7l-210.6 292a31.8 31.8 0 01-51.7 0L318.5 484.9c-3.8-5.3 0-12.7 6.5-12.7h46.9c10.2 0 19.9 4.9 25.9 13.3l71.2 98.8 157.2-218c6-8.3 15.6-13.3 25.9-13.3H699c6.5 0 10.3 7.4 6.5 12.7z"></path>
|
||||
</svg>
|
||||
) : (
|
||||
<svg viewBox="64 64 896 896" focusable="false" data-icon="close-circle" width="32" height="32" fill="currentColor" aria-hidden="true" className="text-red-500">
|
||||
<path d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm193.5 301.7l-210.6 292a31.8 31.8 0 01-51.7 0L318.5 484.9c-3.8-5.3 0-12.7 6.5-12.7h46.9c10.2 0 19.9 4.9 25.9 13.3l71.2 98.8 157.2-218c6-8.3 15.6-13.3 25.9-13.3H699c6.5 0 10.3 7.4 6.5 12.7z"></path>
|
||||
</svg>
|
||||
)}
|
||||
<StatusIcon status={record.status} />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-sm font-semibold text-gray-800 mb-0.5">
|
||||
|
||||
@@ -66,19 +66,21 @@ export const SubjectSelectionPage: React.FC = () => {
|
||||
};
|
||||
|
||||
const getTaskStatus = (task: ExamTask) => {
|
||||
const now = new Date();
|
||||
const startAt = new Date(task.startAt);
|
||||
const endAt = new Date(task.endAt);
|
||||
const nowMs = Date.now();
|
||||
const startMs = new Date(task.startAt).getTime();
|
||||
const endMs = new Date(task.endAt).getTime();
|
||||
const usedAttempts = Number(task.usedAttempts) || 0;
|
||||
const maxAttempts = Number(task.maxAttempts) || 3;
|
||||
|
||||
if (now < startAt) {
|
||||
return 'notStarted';
|
||||
} else if (now >= startAt && now <= endAt && usedAttempts < maxAttempts) {
|
||||
return 'ongoing';
|
||||
} else {
|
||||
// 日期解析失败时,按“已完成/不可开始”处理,避免误分组
|
||||
if (!Number.isFinite(startMs) || !Number.isFinite(endMs)) {
|
||||
return 'completed';
|
||||
}
|
||||
|
||||
if (nowMs < startMs) return 'notStarted';
|
||||
if (nowMs > endMs) return 'completed';
|
||||
if (usedAttempts >= maxAttempts) return 'completed';
|
||||
return 'ongoing';
|
||||
};
|
||||
|
||||
const getTasksByStatus = (status: 'ongoing' | 'completed' | 'notStarted') => {
|
||||
@@ -163,7 +165,10 @@ export const SubjectSelectionPage: React.FC = () => {
|
||||
<div>
|
||||
<div className="flex items-center mb-3 border-b border-gray-200 pb-1">
|
||||
<BookOutlined className="text-lg mr-2 text-mars-400" />
|
||||
<Title level={4} className="!mb-0 !text-gray-700 !text-base">我的考试任务</Title>
|
||||
<div>
|
||||
<Title level={4} className="!mb-0 !text-gray-700 !text-base">我的考试任务</Title>
|
||||
{user && <Text className="text-sm text-gray-500">欢迎,{user.name}</Text>}
|
||||
</div>
|
||||
<Button
|
||||
type="default"
|
||||
size="small"
|
||||
@@ -260,14 +265,17 @@ export const SubjectSelectionPage: React.FC = () => {
|
||||
const usedAttempts = Number(task.usedAttempts) || 0;
|
||||
const maxAttempts = Number(task.maxAttempts) || 3;
|
||||
const attemptsExhausted = usedAttempts >= maxAttempts;
|
||||
const endMs = new Date(task.endAt).getTime();
|
||||
const isExpired = Number.isFinite(endMs) ? endMs < Date.now() : true;
|
||||
const isDisabled = attemptsExhausted || isExpired;
|
||||
return (
|
||||
<Button
|
||||
key={task.id}
|
||||
type="default"
|
||||
block
|
||||
disabled={attemptsExhausted}
|
||||
className={`h-auto py-3 px-4 text-left border-l-4 border-l-gray-400 hover:border-l-gray-500 hover:shadow-md transition-all duration-300 ${attemptsExhausted ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||
onClick={() => !attemptsExhausted && startQuiz(task.id)}
|
||||
disabled={isDisabled}
|
||||
className={`h-auto py-3 px-4 text-left border-l-4 border-l-gray-400 hover:border-l-gray-500 hover:shadow-md transition-all duration-300 ${isDisabled ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||
onClick={() => !isDisabled && startQuiz(task.id)}
|
||||
>
|
||||
<div className="flex justify-between items-center w-full">
|
||||
<div className="flex-1">
|
||||
@@ -294,7 +302,6 @@ export const SubjectSelectionPage: React.FC = () => {
|
||||
{typeof task.bestScore === 'number' ? (
|
||||
<Tag color="green" className="text-xs">最高分 {task.bestScore} 分</Tag>
|
||||
) : null}
|
||||
{attemptsExhausted ? <Tag color="red" className="text-xs">次数用尽</Tag> : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
@@ -304,9 +311,13 @@ export const SubjectSelectionPage: React.FC = () => {
|
||||
{new Date(task.startAt).toLocaleDateString()} - {new Date(task.endAt).toLocaleDateString()}
|
||||
</Text>
|
||||
</div>
|
||||
{!attemptsExhausted && (
|
||||
{attemptsExhausted ? (
|
||||
<div className="text-red-600">
|
||||
<Text className="text-xs px-2 py-1 rounded border border-red-200 bg-red-50">次数用尽</Text>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-gray-400">
|
||||
<Text className="text-xs px-2 py-1 rounded border border-gray-200 bg-gray-50">点击开始考试</Text>
|
||||
<Text className="text-xs px-2 py-1 rounded border border-gray-200 bg-gray-50">已结束</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user