-
-
- {questionTypeMap[currentQuestion.type]}
-
-
- {currentQuestion.category && (
-
-
- {currentQuestion.category}
+
+
+
+ {questionTypeMap[currentQuestion.type]}
-
- )}
-
- {currentQuestion.content}
-
-
-
-
- {renderQuestion(currentQuestion)}
-
-
-
-
-
-
-
-
-
-
-
- {currentQuestionIndex === questions.length - 1 ? (
-
- ) : (
-
+ {currentQuestion.category && (
+
+ {currentQuestion.category}
+
)}
+
+
+ {currentQuestion.content}
+
+
+
handleAnswerChange(currentQuestion.id, val)}
+ />
-
- {answerSheetOpen && (
- setAnswerSheetOpen(false)}
- footer={null}
- centered
- width={560}
- destroyOnClose
- >
-
-
- 已答 {answeredCount} / 共 {questions.length}
-
-
-
-
-
- {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 (
-
- );
- })}
-
-
- )}
-
+
+
handleSubmit()}
+ onOpenSheet={() => setAnswerSheetOpen(true)}
+ answeredCount={answeredCount}
+ />
+
+ setAnswerSheetOpen(false)}
+ footer={null}
+ centered
+ width={560}
+ destroyOnClose
+ >
+
+
+ 已答 {answeredCount} / {questions.length}
+
+
+
+
+
+ {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 (
+
+ );
+ })}
+
+
+
);
};
diff --git a/src/pages/quiz/components/OptionList.tsx b/src/pages/quiz/components/OptionList.tsx
new file mode 100644
index 0000000..2871822
--- /dev/null
+++ b/src/pages/quiz/components/OptionList.tsx
@@ -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 (
+
+
+ );
+ }
+
+ 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 (
+
+ {renderOptions().map((opt, index) => {
+ const selected = isSelected(opt.value);
+ return (
+
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'}
+ `}
+ >
+ {/* 选项圆圈 */}
+
+ {type === 'judgment' ? (index === 0 ? 'T' : 'F') : getOptionLabel(index)}
+
+
+ {/* 选项内容 */}
+
+ {opt.label}
+
+
+ );
+ })}
+
+ );
+};
diff --git a/src/pages/quiz/components/QuizFooter.tsx b/src/pages/quiz/components/QuizFooter.tsx
new file mode 100644
index 0000000..12c33ed
--- /dev/null
+++ b/src/pages/quiz/components/QuizFooter.tsx
@@ -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 (
+
+
+
}
+ onClick={onPrev}
+ disabled={isFirst}
+ className={`flex items-center text-gray-600 hover:text-[#00897B] ${isFirst ? 'opacity-30' : ''}`}
+ >
+ 上一题
+
+
+
+
+
+ {answeredCount}/{total}
+
+
+
+ {isLast ? (
+
+ ) : (
+
+ )}
+
+
+ );
+};
diff --git a/src/pages/quiz/components/QuizHeader.tsx b/src/pages/quiz/components/QuizHeader.tsx
new file mode 100644
index 0000000..da4392a
--- /dev/null
+++ b/src/pages/quiz/components/QuizHeader.tsx
@@ -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 (
+
+
+
+ 返回
+
+
+
+
+ 监控中
+
+
+
+
+ {timeLeft !== null ? formatTime(timeLeft) : '--:--'}
+
+
+ );
+};
diff --git a/src/pages/quiz/components/QuizProgress.tsx b/src/pages/quiz/components/QuizProgress.tsx
new file mode 100644
index 0000000..a1b6534
--- /dev/null
+++ b/src/pages/quiz/components/QuizProgress.tsx
@@ -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 (
+
+
+
+ {current}/{total}
+
+
+
+
+ );
+};