待测试
This commit is contained in:
@@ -1,13 +1,15 @@
|
||||
import { useState, useEffect, useMemo, useRef, type TouchEventHandler } from 'react';
|
||||
import { Card, Button, Radio, Checkbox, Input, message, Progress, Modal } from 'antd';
|
||||
import { Card, Button, Modal, message } from 'antd';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { useUser, useQuiz } from '../contexts';
|
||||
import { quizAPI } from '../services/api';
|
||||
import { questionTypeMap } from '../utils/validation';
|
||||
import { detectHorizontalSwipe } from '../utils/swipe';
|
||||
import { UserLayout } from '../layouts/UserLayout';
|
||||
|
||||
const { TextArea } = Input;
|
||||
import { QuizHeader } from './quiz/components/QuizHeader';
|
||||
import { QuizProgress } from './quiz/components/QuizProgress';
|
||||
import { OptionList } from './quiz/components/OptionList';
|
||||
import { QuizFooter } from './quiz/components/QuizFooter';
|
||||
|
||||
interface Question {
|
||||
id: string;
|
||||
@@ -216,24 +218,13 @@ const QuizPage = () => {
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const formatTime = (seconds: number) => {
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const remainingSeconds = seconds % 60;
|
||||
return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
const getTagColor = (type: string) => {
|
||||
switch (type) {
|
||||
case 'single':
|
||||
return 'bg-blue-100 text-blue-800';
|
||||
case 'multiple':
|
||||
return 'bg-purple-100 text-purple-800';
|
||||
case 'judgment':
|
||||
return 'bg-orange-100 text-orange-800';
|
||||
case 'text':
|
||||
return 'bg-green-100 text-green-800';
|
||||
default:
|
||||
return 'bg-gray-100 text-gray-800';
|
||||
case 'single': return 'bg-orange-100 text-orange-800 border-orange-200';
|
||||
case 'multiple': return 'bg-blue-100 text-blue-800 border-blue-200';
|
||||
case 'judgment': return 'bg-purple-100 text-purple-800 border-purple-200';
|
||||
case 'text': return 'bg-green-100 text-green-800 border-green-200';
|
||||
default: return 'bg-gray-100 text-gray-800 border-gray-200';
|
||||
}
|
||||
};
|
||||
|
||||
@@ -322,7 +313,6 @@ const QuizPage = () => {
|
||||
setSubmitting(true);
|
||||
|
||||
if (!forceSubmit) {
|
||||
// 检查是否所有题目都已回答
|
||||
const unansweredQuestions = questions.filter(q => !answers[q.id]);
|
||||
if (unansweredQuestions.length > 0) {
|
||||
message.warning(`还有 ${unansweredQuestions.length} 道题未作答`);
|
||||
@@ -330,7 +320,6 @@ const QuizPage = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 准备答案数据
|
||||
const answersData = questions.map(question => {
|
||||
const isCorrect = checkAnswer(question, answers[question.id]);
|
||||
return {
|
||||
@@ -387,72 +376,6 @@ const QuizPage = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const renderQuestion = (question: Question) => {
|
||||
const currentAnswer = answers[question.id];
|
||||
|
||||
switch (question.type) {
|
||||
case 'single':
|
||||
return (
|
||||
<Radio.Group
|
||||
value={currentAnswer as string}
|
||||
onChange={(e) => handleAnswerChange(question.id, e.target.value)}
|
||||
className="w-full"
|
||||
>
|
||||
{question.options?.map((option, index) => (
|
||||
<Radio key={index} value={option} className="block mb-3 p-4 min-h-12 rounded-lg border border-gray-200 hover:bg-gray-50 hover:border-mars-200 transition-colors">
|
||||
{String.fromCharCode(65 + index)}. {option}
|
||||
</Radio>
|
||||
))}
|
||||
</Radio.Group>
|
||||
);
|
||||
|
||||
case 'multiple':
|
||||
return (
|
||||
<Checkbox.Group
|
||||
value={currentAnswer as string[] || []}
|
||||
onChange={(checkedValues) => handleAnswerChange(question.id, checkedValues)}
|
||||
className="w-full"
|
||||
>
|
||||
{question.options?.map((option, index) => (
|
||||
<Checkbox key={index} value={option} className="block mb-3 p-4 min-h-12 rounded-lg border border-gray-200 hover:bg-gray-50 hover:border-mars-200 transition-colors w-full">
|
||||
{String.fromCharCode(65 + index)}. {option}
|
||||
</Checkbox>
|
||||
))}
|
||||
</Checkbox.Group>
|
||||
);
|
||||
|
||||
case 'judgment':
|
||||
return (
|
||||
<Radio.Group
|
||||
value={currentAnswer as string}
|
||||
onChange={(e) => handleAnswerChange(question.id, e.target.value)}
|
||||
className="w-full"
|
||||
>
|
||||
<Radio value="正确" className="block mb-3 p-4 min-h-12 rounded-lg border border-gray-200 hover:bg-gray-50 hover:border-mars-200 transition-colors">
|
||||
正确
|
||||
</Radio>
|
||||
<Radio value="错误" className="block mb-3 p-4 min-h-12 rounded-lg border border-gray-200 hover:bg-gray-50 hover:border-mars-200 transition-colors">
|
||||
错误
|
||||
</Radio>
|
||||
</Radio.Group>
|
||||
);
|
||||
|
||||
case 'text':
|
||||
return (
|
||||
<TextArea
|
||||
rows={6}
|
||||
value={currentAnswer as string || ''}
|
||||
onChange={(e) => handleAnswerChange(question.id, e.target.value)}
|
||||
placeholder="请输入您的答案..."
|
||||
className="rounded-lg border-gray-300 focus:border-mars-500 focus:ring-mars-500"
|
||||
/>
|
||||
);
|
||||
|
||||
default:
|
||||
return <div>未知题型</div>;
|
||||
}
|
||||
};
|
||||
|
||||
const answeredCount = useMemo(() => {
|
||||
return questions.reduce((count, q) => (answers[q.id] ? count + 1 : count), 0);
|
||||
}, [questions, answers]);
|
||||
@@ -478,7 +401,6 @@ const QuizPage = () => {
|
||||
}
|
||||
|
||||
const currentQuestion = questions[currentQuestionIndex];
|
||||
const progress = ((currentQuestionIndex + 1) / questions.length) * 100;
|
||||
|
||||
const handleTouchStart: TouchEventHandler = (e) => {
|
||||
if (e.touches.length !== 1) return;
|
||||
@@ -514,187 +436,129 @@ const QuizPage = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<UserLayout>
|
||||
<div className="max-w-4xl mx-auto pb-24">
|
||||
{/* 头部信息 */}
|
||||
<div className="bg-white rounded-xl shadow-sm p-4 md:p-6 mb-4 md:mb-6 border border-gray-100">
|
||||
<div className="flex flex-col md:flex-row md:justify-between md:items-center gap-3 mb-4">
|
||||
<div>
|
||||
<h1 className="text-xl md:text-2xl font-bold text-gray-900">在线答题</h1>
|
||||
<p className="text-gray-700 mt-1">
|
||||
第 {currentQuestionIndex + 1} 题 / 共 {questions.length} 题
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center justify-between md:justify-end gap-3">
|
||||
<Button danger onClick={handleGiveUp} className="h-12 px-4 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-red-500">
|
||||
弃考
|
||||
</Button>
|
||||
{timeLeft !== null && (
|
||||
<div className="text-right">
|
||||
<div className="text-sm text-gray-700">剩余时间</div>
|
||||
<div className={`text-2xl font-bold tabular-nums ${
|
||||
timeLeft < 300 ? 'text-red-600' : 'text-mars-600'
|
||||
}`}>
|
||||
{formatTime(timeLeft)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Progress
|
||||
percent={Math.round(progress)}
|
||||
strokeColor="#008C8C"
|
||||
trailColor="#f0fcfc"
|
||||
showInfo={false}
|
||||
className="mt-2"
|
||||
/>
|
||||
</div>
|
||||
<div className="min-h-screen bg-gray-50 flex flex-col">
|
||||
<QuizHeader
|
||||
current={currentQuestionIndex + 1}
|
||||
total={questions.length}
|
||||
timeLeft={timeLeft}
|
||||
onGiveUp={handleGiveUp}
|
||||
/>
|
||||
|
||||
<QuizProgress
|
||||
current={currentQuestionIndex + 1}
|
||||
total={questions.length}
|
||||
/>
|
||||
|
||||
{/* 题目卡片 */}
|
||||
<Card className="shadow-sm border border-gray-100 rounded-xl">
|
||||
<div className="flex-1 overflow-y-auto pb-24 safe-area-bottom">
|
||||
<div className="max-w-4xl mx-auto p-4">
|
||||
<div
|
||||
onTouchStart={handleTouchStart}
|
||||
onTouchEnd={handleTouchEnd}
|
||||
className="mb-6 md:mb-8"
|
||||
className={`
|
||||
transition-all duration-300 ease-out
|
||||
${enterAnimationOn
|
||||
? 'opacity-100 translate-x-0'
|
||||
: navDirection === 'next'
|
||||
? 'opacity-0 translate-x-4'
|
||||
: 'opacity-0 -translate-x-4'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<span className={`inline-block px-3 py-1 rounded-full text-sm font-medium ${getTagColor(currentQuestion.type)}`}>
|
||||
{questionTypeMap[currentQuestion.type]}
|
||||
</span>
|
||||
</div>
|
||||
{currentQuestion.category && (
|
||||
<div className="mb-3">
|
||||
<span className="inline-block px-2 py-1 bg-gray-50 text-gray-700 text-xs rounded border border-gray-100">
|
||||
{currentQuestion.category}
|
||||
<div className="bg-white rounded-xl shadow-sm border border-gray-100 p-5 md:p-8 min-h-[400px]">
|
||||
<div className="mb-6">
|
||||
<span className={`inline-block px-3 py-1 rounded-md text-sm font-medium border ${getTagColor(currentQuestion.type)}`}>
|
||||
{questionTypeMap[currentQuestion.type]}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<h2
|
||||
ref={questionHeadingRef}
|
||||
tabIndex={-1}
|
||||
className={`text-lg md:text-xl font-medium text-gray-900 leading-relaxed transition-all duration-200 ease-out ${
|
||||
enterAnimationOn
|
||||
? 'opacity-100 translate-x-0'
|
||||
: navDirection === 'next'
|
||||
? 'opacity-0 translate-x-2'
|
||||
: 'opacity-0 -translate-x-2'
|
||||
}`}
|
||||
>
|
||||
{currentQuestion.content}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`mb-6 md:mb-8 transition-all duration-200 ease-out ${
|
||||
enterAnimationOn
|
||||
? 'opacity-100 translate-x-0'
|
||||
: navDirection === 'next'
|
||||
? 'opacity-0 translate-x-2'
|
||||
: 'opacity-0 -translate-x-2'
|
||||
}`}
|
||||
>
|
||||
{renderQuestion(currentQuestion)}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<div className="fixed left-0 right-0 bottom-0 z-20">
|
||||
<div className="bg-white/95 backdrop-blur border-t border-gray-200">
|
||||
<div className="max-w-4xl mx-auto px-4 py-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<Button
|
||||
onClick={handlePrevious}
|
||||
disabled={currentQuestionIndex === 0}
|
||||
className="h-12 min-w-12 px-4 rounded-lg hover:border-mars-500 hover:text-mars-600 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-mars-500"
|
||||
>
|
||||
上一题
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={() => setAnswerSheetOpen(true)}
|
||||
className="h-12 flex-1 rounded-lg border-gray-200 text-gray-900 hover:border-mars-400 hover:text-mars-600 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-mars-500"
|
||||
>
|
||||
答题卡({answeredCount}/{questions.length})
|
||||
</Button>
|
||||
|
||||
{currentQuestionIndex === questions.length - 1 ? (
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => handleSubmit()}
|
||||
loading={submitting}
|
||||
className="h-12 min-w-12 px-4 rounded-lg bg-mars-600 hover:bg-mars-700 border-none shadow-sm focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-mars-500"
|
||||
>
|
||||
提交
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleNext}
|
||||
className="h-12 min-w-12 px-4 rounded-lg bg-mars-500 hover:bg-mars-600 border-none shadow-sm focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-mars-500"
|
||||
>
|
||||
下一题
|
||||
</Button>
|
||||
{currentQuestion.category && (
|
||||
<span className="ml-2 inline-block px-2 py-1 bg-gray-50 text-gray-600 text-xs rounded border border-gray-100">
|
||||
{currentQuestion.category}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<h2
|
||||
ref={questionHeadingRef}
|
||||
tabIndex={-1}
|
||||
className="text-lg md:text-xl font-medium text-gray-900 leading-relaxed mb-8 outline-none"
|
||||
>
|
||||
{currentQuestion.content}
|
||||
</h2>
|
||||
|
||||
<OptionList
|
||||
type={currentQuestion.type}
|
||||
options={currentQuestion.options}
|
||||
value={answers[currentQuestion.id]}
|
||||
onChange={(val) => handleAnswerChange(currentQuestion.id, val)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{answerSheetOpen && (
|
||||
<Modal
|
||||
title="答题卡"
|
||||
open={answerSheetOpen}
|
||||
onCancel={() => setAnswerSheetOpen(false)}
|
||||
footer={null}
|
||||
centered
|
||||
width={560}
|
||||
destroyOnClose
|
||||
>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="text-sm text-gray-700">
|
||||
已答 {answeredCount} / 共 {questions.length}
|
||||
</div>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => setAnswerSheetOpen(false)}
|
||||
className="h-10 px-4 bg-mars-600 hover:bg-mars-700 border-none"
|
||||
>
|
||||
回到当前题
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-5 sm:grid-cols-6 gap-2">
|
||||
{questions.map((q, idx) => {
|
||||
const isCurrent = idx === currentQuestionIndex;
|
||||
const isAnswered = !!answers[q.id];
|
||||
|
||||
const baseClassName = 'h-12 w-12 rounded-lg border focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-mars-500';
|
||||
const className = isCurrent
|
||||
? `${baseClassName} border-mars-500 text-mars-700 bg-mars-50`
|
||||
: isAnswered
|
||||
? `${baseClassName} border-green-200 text-green-700 bg-green-50 hover:border-green-300`
|
||||
: `${baseClassName} border-gray-200 text-gray-800 bg-white hover:border-mars-200`;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={q.id}
|
||||
type="button"
|
||||
className={className}
|
||||
onClick={() => {
|
||||
setAnswerSheetOpen(false);
|
||||
handleJumpTo(idx);
|
||||
}}
|
||||
aria-label={`跳转到第 ${idx + 1} 题`}
|
||||
>
|
||||
{idx + 1}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
</div>
|
||||
</UserLayout>
|
||||
|
||||
<QuizFooter
|
||||
current={currentQuestionIndex}
|
||||
total={questions.length}
|
||||
onPrev={handlePrevious}
|
||||
onNext={handleNext}
|
||||
onSubmit={() => handleSubmit()}
|
||||
onOpenSheet={() => setAnswerSheetOpen(true)}
|
||||
answeredCount={answeredCount}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
title="答题卡"
|
||||
open={answerSheetOpen}
|
||||
onCancel={() => setAnswerSheetOpen(false)}
|
||||
footer={null}
|
||||
centered
|
||||
width={560}
|
||||
destroyOnClose
|
||||
>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="text-sm text-gray-700">
|
||||
已答 <span className="text-[#00897B] font-medium">{answeredCount}</span> / {questions.length}
|
||||
</div>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => setAnswerSheetOpen(false)}
|
||||
className="bg-[#00897B] hover:bg-[#00796B]"
|
||||
>
|
||||
回到当前题
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-5 sm:grid-cols-6 gap-3">
|
||||
{questions.map((q, idx) => {
|
||||
const isCurrent = idx === currentQuestionIndex;
|
||||
const isAnswered = !!answers[q.id];
|
||||
|
||||
let className = 'h-10 w-10 rounded-full flex items-center justify-center text-sm font-medium border transition-colors ';
|
||||
if (isCurrent) {
|
||||
className += 'border-[#00897B] bg-[#E0F2F1] text-[#00695C]';
|
||||
} else if (isAnswered) {
|
||||
className += 'border-[#00897B] bg-[#00897B] text-white';
|
||||
} else {
|
||||
className += 'border-gray-200 text-gray-600 bg-white hover:border-[#00897B]';
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
key={q.id}
|
||||
type="button"
|
||||
className={className}
|
||||
onClick={() => {
|
||||
setAnswerSheetOpen(false);
|
||||
handleJumpTo(idx);
|
||||
}}
|
||||
aria-label={`跳转到第 ${idx + 1} 题`}
|
||||
>
|
||||
{idx + 1}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
93
src/pages/quiz/components/OptionList.tsx
Normal file
93
src/pages/quiz/components/OptionList.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import { Input } from 'antd';
|
||||
|
||||
const { TextArea } = Input;
|
||||
|
||||
interface OptionListProps {
|
||||
type: 'single' | 'multiple' | 'judgment' | 'text';
|
||||
options?: string[];
|
||||
value: string | string[];
|
||||
onChange: (val: string | string[]) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const OptionList = ({ type, options, value, onChange, disabled }: OptionListProps) => {
|
||||
const getOptionLabel = (index: number) => String.fromCharCode(65 + index);
|
||||
|
||||
const isSelected = (val: string) => {
|
||||
if (Array.isArray(value)) {
|
||||
return value.includes(val);
|
||||
}
|
||||
return value === val;
|
||||
};
|
||||
|
||||
const handleSelect = (val: string) => {
|
||||
if (disabled) return;
|
||||
|
||||
if (type === 'multiple') {
|
||||
const current = Array.isArray(value) ? value : [];
|
||||
const next = current.includes(val)
|
||||
? current.filter(v => v !== val)
|
||||
: [...current, val];
|
||||
onChange(next);
|
||||
} else {
|
||||
onChange(val);
|
||||
}
|
||||
};
|
||||
|
||||
if (type === 'text') {
|
||||
return (
|
||||
<div className="mt-4">
|
||||
<TextArea
|
||||
rows={6}
|
||||
value={value as string || ''}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
placeholder="请输入您的答案..."
|
||||
disabled={disabled}
|
||||
className="rounded-lg border-gray-300 focus:border-[#00897B] focus:ring-[#00897B] text-base p-4"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const renderOptions = () => {
|
||||
if (type === 'judgment') {
|
||||
return ['正确', '错误'].map((opt, idx) => ({ label: opt, value: opt, key: idx }));
|
||||
}
|
||||
return (options || []).map((opt, idx) => ({ label: opt, value: opt, key: idx }));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-3 mt-2">
|
||||
{renderOptions().map((opt, index) => {
|
||||
const selected = isSelected(opt.value);
|
||||
return (
|
||||
<div
|
||||
key={opt.key}
|
||||
onClick={() => handleSelect(opt.value)}
|
||||
className={`
|
||||
relative flex items-start p-4 rounded-xl border-2 transition-all duration-200 active:scale-[0.99] cursor-pointer
|
||||
${selected
|
||||
? 'border-[#00897B] bg-[#E0F2F1]'
|
||||
: 'border-transparent bg-white shadow-sm hover:border-gray-200'}
|
||||
`}
|
||||
>
|
||||
{/* 选项圆圈 */}
|
||||
<div className={`
|
||||
flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium border mr-3 mt-0.5 transition-colors
|
||||
${selected
|
||||
? 'bg-[#00897B] border-[#00897B] text-white'
|
||||
: 'bg-white border-gray-300 text-gray-500'}
|
||||
`}>
|
||||
{type === 'judgment' ? (index === 0 ? 'T' : 'F') : getOptionLabel(index)}
|
||||
</div>
|
||||
|
||||
{/* 选项内容 */}
|
||||
<div className={`text-base leading-snug select-none ${selected ? 'text-[#00695C] font-medium' : 'text-gray-700'}`}>
|
||||
{opt.label}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
69
src/pages/quiz/components/QuizFooter.tsx
Normal file
69
src/pages/quiz/components/QuizFooter.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import { Button, Modal } from 'antd';
|
||||
import { AppstoreOutlined, LeftOutlined, RightOutlined } from '@ant-design/icons';
|
||||
|
||||
interface QuizFooterProps {
|
||||
current: number;
|
||||
total: number;
|
||||
onPrev: () => void;
|
||||
onNext: () => void;
|
||||
onSubmit: () => void;
|
||||
onOpenSheet: () => void;
|
||||
answeredCount: number;
|
||||
}
|
||||
|
||||
export const QuizFooter = ({
|
||||
current,
|
||||
total,
|
||||
onPrev,
|
||||
onNext,
|
||||
onSubmit,
|
||||
onOpenSheet,
|
||||
answeredCount
|
||||
}: QuizFooterProps) => {
|
||||
const isFirst = current === 0;
|
||||
const isLast = current === total - 1;
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-0 left-0 right-0 bg-white border-t border-gray-100 px-4 py-3 safe-area-bottom z-30 shadow-[0_-2px_10px_rgba(0,0,0,0.05)]">
|
||||
<div className="max-w-4xl mx-auto flex items-center justify-between">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<LeftOutlined />}
|
||||
onClick={onPrev}
|
||||
disabled={isFirst}
|
||||
className={`flex items-center text-gray-600 hover:text-[#00897B] ${isFirst ? 'opacity-30' : ''}`}
|
||||
>
|
||||
上一题
|
||||
</Button>
|
||||
|
||||
<div
|
||||
onClick={onOpenSheet}
|
||||
className="flex flex-col items-center justify-center -mt-8 bg-white rounded-full h-16 w-16 shadow-lg border border-gray-100 cursor-pointer active:scale-95 transition-transform"
|
||||
>
|
||||
<AppstoreOutlined className="text-xl text-[#00897B] mb-1" />
|
||||
<span className="text-[10px] text-gray-500 scale-90">
|
||||
{answeredCount}/{total}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{isLast ? (
|
||||
<Button
|
||||
type="text"
|
||||
onClick={onSubmit}
|
||||
className="flex items-center text-[#00897B] font-medium hover:bg-teal-50"
|
||||
>
|
||||
提交 <RightOutlined />
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
type="text"
|
||||
onClick={onNext}
|
||||
className="flex items-center text-gray-600 hover:text-[#00897B]"
|
||||
>
|
||||
下一题 <RightOutlined />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
39
src/pages/quiz/components/QuizHeader.tsx
Normal file
39
src/pages/quiz/components/QuizHeader.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import { Button, Modal } from 'antd';
|
||||
import { LeftOutlined, EyeOutlined, ClockCircleOutlined } from '@ant-design/icons';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
interface QuizHeaderProps {
|
||||
current: number;
|
||||
total: number;
|
||||
timeLeft: number | null;
|
||||
onGiveUp: () => void;
|
||||
}
|
||||
|
||||
export const QuizHeader = ({ current, total, timeLeft, onGiveUp }: QuizHeaderProps) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const formatTime = (seconds: number) => {
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const remainingSeconds = seconds % 60;
|
||||
return `${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-[#00897B] text-white px-4 h-14 flex items-center justify-between shadow-md sticky top-0 z-30">
|
||||
<div className="flex items-center gap-1" onClick={onGiveUp}>
|
||||
<LeftOutlined className="text-lg" />
|
||||
<span className="text-base">返回</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 bg-[#00796B] px-3 py-1 rounded-full text-sm">
|
||||
<EyeOutlined />
|
||||
<span>监控中</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 text-base font-medium tabular-nums">
|
||||
<ClockCircleOutlined />
|
||||
<span>{timeLeft !== null ? formatTime(timeLeft) : '--:--'}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
28
src/pages/quiz/components/QuizProgress.tsx
Normal file
28
src/pages/quiz/components/QuizProgress.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { Progress } from 'antd';
|
||||
|
||||
interface QuizProgressProps {
|
||||
current: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export const QuizProgress = ({ current, total }: QuizProgressProps) => {
|
||||
const percent = Math.round((current / total) * 100);
|
||||
|
||||
return (
|
||||
<div className="bg-white px-4 py-3 border-b border-gray-100">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<span className="text-gray-500 text-sm">
|
||||
<span className="text-gray-900 text-lg font-medium">{current}</span>/{total}
|
||||
</span>
|
||||
<Progress
|
||||
percent={percent}
|
||||
showInfo={false}
|
||||
strokeColor="#00897B"
|
||||
trailColor="#E0F2F1"
|
||||
size="small"
|
||||
className="m-0 flex-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user