feat: 更新考试相关页面,优化任务状态处理,添加用户任务接口测试

This commit is contained in:
2025-12-30 11:10:03 +08:00
parent 57101fac37
commit 7fff13afb7
9 changed files with 234 additions and 50 deletions

View File

@@ -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 {

View File

@@ -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">

View File

@@ -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>