后台页面尝试用UI UX优化,有点意思~
This commit is contained in:
@@ -2,6 +2,7 @@ import { useState, useEffect, useMemo, useRef, type TouchEventHandler } from 're
|
||||
import { Card, Button, Modal, App } from 'antd';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { useUser, useQuiz } from '../contexts';
|
||||
import type { QuizQuestion } from '../contexts';
|
||||
import { quizAPI } from '../services/api';
|
||||
import { questionTypeMap } from '../utils/validation';
|
||||
import { detectHorizontalSwipe } from '../utils/swipe';
|
||||
@@ -11,19 +12,10 @@ import { QuizProgress } from './quiz/components/QuizProgress';
|
||||
import { OptionList } from './quiz/components/OptionList';
|
||||
import { QuizFooter } from './quiz/components/QuizFooter';
|
||||
|
||||
interface Question {
|
||||
id: string;
|
||||
content: string;
|
||||
type: 'single' | 'multiple' | 'judgment' | 'text';
|
||||
options?: string[];
|
||||
answer: string | string[];
|
||||
analysis?: string;
|
||||
score: number;
|
||||
category: string;
|
||||
}
|
||||
type Question = QuizQuestion;
|
||||
|
||||
interface LocationState {
|
||||
questions?: Question[];
|
||||
questions?: QuizQuestion[];
|
||||
totalScore?: number;
|
||||
timeLimit?: number;
|
||||
subjectId?: string;
|
||||
@@ -456,7 +448,7 @@ const QuizPage = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex flex-col">
|
||||
<div className="min-h-screen bg-gray-50 flex flex-col font-sans">
|
||||
<QuizHeader
|
||||
current={currentQuestionIndex + 1}
|
||||
total={questions.length}
|
||||
@@ -465,67 +457,194 @@ const QuizPage = () => {
|
||||
taskName={taskName}
|
||||
/>
|
||||
|
||||
<QuizProgress
|
||||
current={currentQuestionIndex + 1}
|
||||
total={questions.length}
|
||||
/>
|
||||
{/* Mobile Progress Bar */}
|
||||
<div className="lg:hidden">
|
||||
<QuizProgress
|
||||
current={currentQuestionIndex + 1}
|
||||
total={questions.length}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto pb-24 safe-area-bottom">
|
||||
<div className="max-w-md mx-auto px-4">
|
||||
<div
|
||||
onTouchStart={handleTouchStart}
|
||||
onTouchEnd={handleTouchEnd}
|
||||
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="bg-white rounded-xl shadow-sm border border-gray-100 p-2 min-h-[280px]">
|
||||
<div className="mb-2">
|
||||
<span className={`inline-block px-1 py-0.5 rounded-md text-xs font-medium border ${getTagColor(currentQuestion.type)}`}>
|
||||
<main className="flex-1 w-full max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6 lg:py-8">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 h-full items-start">
|
||||
{/* Left Column: Question Area */}
|
||||
<div className="lg:col-span-9 flex flex-col h-full">
|
||||
<div
|
||||
onTouchStart={handleTouchStart}
|
||||
onTouchEnd={handleTouchEnd}
|
||||
className={`
|
||||
bg-white rounded-2xl shadow-sm border border-gray-200 p-6 sm:p-8 lg:p-10 flex-1 flex flex-col min-h-[60vh] transition-all duration-300 ease-out
|
||||
${enterAnimationOn
|
||||
? 'opacity-100 translate-x-0'
|
||||
: navDirection === 'next'
|
||||
? 'opacity-0 translate-x-4'
|
||||
: 'opacity-0 -translate-x-4'
|
||||
}
|
||||
`}
|
||||
>
|
||||
{/* Question Meta */}
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<span className="text-3xl font-bold text-gray-200 select-none">
|
||||
{(currentQuestionIndex + 1).toString().padStart(2, '0')}
|
||||
</span>
|
||||
<span className={`px-3 py-1 rounded-full text-sm font-medium border ${getTagColor(currentQuestion.type)}`}>
|
||||
{questionTypeMap[currentQuestion.type]}
|
||||
</span>
|
||||
<span className="text-gray-400 text-sm">
|
||||
/ 共 {questions.length} 题
|
||||
</span>
|
||||
{currentQuestion.category && (
|
||||
<span className="ml-2 inline-block px-1 py-0.5 bg-gray-50 text-gray-600 text-xs rounded border border-gray-100">
|
||||
<span className="ml-auto px-2 py-1 bg-gray-50 text-gray-500 text-xs rounded border border-gray-100">
|
||||
{currentQuestion.category}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Question Content */}
|
||||
<h2
|
||||
ref={questionHeadingRef}
|
||||
tabIndex={-1}
|
||||
className="text-sm font-medium text-gray-900 leading-tight mb-3 outline-none"
|
||||
className="text-xl sm:text-2xl 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)}
|
||||
/>
|
||||
{/* Options */}
|
||||
<div className="flex-1">
|
||||
<OptionList
|
||||
type={currentQuestion.type}
|
||||
options={currentQuestion.options}
|
||||
value={answers[currentQuestion.id]}
|
||||
onChange={(val) => handleAnswerChange(currentQuestion.id, val)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Desktop Navigation Buttons */}
|
||||
<div className="hidden lg:flex items-center justify-between mt-12 pt-8 border-t border-gray-100">
|
||||
<Button
|
||||
size="large"
|
||||
onClick={handlePrevious}
|
||||
disabled={currentQuestionIndex === 0}
|
||||
className="px-8 h-12 text-base rounded-xl"
|
||||
>
|
||||
上一题
|
||||
</Button>
|
||||
|
||||
{currentQuestionIndex === questions.length - 1 ? (
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
onClick={() => handleSubmit()}
|
||||
loading={submitting}
|
||||
className="px-8 h-12 text-base rounded-xl bg-[#008C8C] hover:bg-[#00796B]"
|
||||
>
|
||||
提交试卷
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
onClick={handleNext}
|
||||
className="px-8 h-12 text-base rounded-xl bg-[#008C8C] hover:bg-[#00796B]"
|
||||
>
|
||||
下一题
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Column: Sidebar (Desktop Only) */}
|
||||
<div className="hidden lg:flex lg:col-span-3 flex-col gap-6 sticky top-24">
|
||||
{/* User Info Card */}
|
||||
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 p-6">
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
<div className="w-12 h-12 rounded-full bg-[#E0F7FA] flex items-center justify-center text-[#008C8C] text-xl font-bold">
|
||||
{user?.name?.[0] || 'U'}
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-bold text-gray-900">{user?.name}</div>
|
||||
<div className="text-xs text-gray-500">考生</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between items-center text-sm text-gray-600 bg-gray-50 px-4 py-2 rounded-lg">
|
||||
<span>已完成</span>
|
||||
<span className="font-bold text-[#008C8C]">{answeredCount} / {questions.length}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Answer Sheet Card */}
|
||||
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 p-6 flex-1">
|
||||
<h3 className="font-bold text-gray-900 mb-4">答题卡</h3>
|
||||
<div className="grid grid-cols-5 gap-2 max-h-[400px] overflow-y-auto pr-2 custom-scrollbar">
|
||||
{questions.map((q, idx) => {
|
||||
const isCurrent = idx === currentQuestionIndex;
|
||||
const isAnswered = !!answers[q.id];
|
||||
let className = 'h-10 w-10 rounded-lg flex items-center justify-center text-sm font-medium border transition-all duration-200 ';
|
||||
|
||||
if (isCurrent) {
|
||||
className += 'border-[#008C8C] bg-[#E0F7FA] text-[#006064] ring-2 ring-[#008C8C] ring-offset-2';
|
||||
} else if (isAnswered) {
|
||||
className += 'border-[#008C8C] bg-[#008C8C] text-white';
|
||||
} else {
|
||||
className += 'border-gray-200 text-gray-600 bg-gray-50 hover:border-[#008C8C] hover:bg-white';
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
key={q.id}
|
||||
onClick={() => handleJumpTo(idx)}
|
||||
className={className}
|
||||
>
|
||||
{idx + 1}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="mt-6 pt-6 border-t border-gray-100 grid grid-cols-2 gap-3 text-xs text-gray-500">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded bg-[#008C8C]"></div>
|
||||
<span>已作答</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded bg-[#E0F7FA] border border-[#008C8C]"></div>
|
||||
<span>当前</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded bg-gray-50 border border-gray-200"></div>
|
||||
<span>未作答</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="primary"
|
||||
block
|
||||
size="large"
|
||||
className="mt-6 bg-[#008C8C] hover:bg-[#00796B] h-12 rounded-xl font-medium"
|
||||
onClick={() => handleSubmit()}
|
||||
>
|
||||
交卷
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* Mobile Footer */}
|
||||
<div className="lg:hidden">
|
||||
<QuizFooter
|
||||
current={currentQuestionIndex}
|
||||
total={questions.length}
|
||||
onPrev={handlePrevious}
|
||||
onNext={handleNext}
|
||||
onSubmit={() => handleSubmit()}
|
||||
onOpenSheet={() => setAnswerSheetOpen(true)}
|
||||
answeredCount={answeredCount}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<QuizFooter
|
||||
current={currentQuestionIndex}
|
||||
total={questions.length}
|
||||
onPrev={handlePrevious}
|
||||
onNext={handleNext}
|
||||
onSubmit={() => handleSubmit()}
|
||||
onOpenSheet={() => setAnswerSheetOpen(true)}
|
||||
answeredCount={answeredCount}
|
||||
/>
|
||||
|
||||
{/* Mobile Answer Sheet Modal */}
|
||||
<Modal
|
||||
title="答题卡"
|
||||
open={answerSheetOpen}
|
||||
@@ -534,32 +653,33 @@ const QuizPage = () => {
|
||||
centered
|
||||
width={340}
|
||||
destroyOnClose
|
||||
className="mobile-sheet-modal"
|
||||
>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="text-xs text-gray-700">
|
||||
已答 <span className="text-[#00897B] font-medium">{answeredCount}</span> / {questions.length}
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="text-sm text-gray-700">
|
||||
进度:<span className="text-[#008C8C] font-bold">{answeredCount}</span> / {questions.length}
|
||||
</div>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleJumpToFirstUnanswered}
|
||||
className="bg-[#00897B] hover:bg-[#00796B] text-xs h-7 px-3"
|
||||
className="bg-[#008C8C] hover:bg-[#00796B] text-xs h-8 px-4 rounded-lg"
|
||||
>
|
||||
回到当前题
|
||||
跳转未答
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-5 gap-2">
|
||||
<div className="grid grid-cols-5 gap-3">
|
||||
{questions.map((q, idx) => {
|
||||
const isCurrent = idx === currentQuestionIndex;
|
||||
const isAnswered = !!answers[q.id];
|
||||
|
||||
let className = 'h-9 w-9 rounded-full flex items-center justify-center text-xs font-medium border transition-colors ';
|
||||
let className = 'h-10 w-10 rounded-lg flex items-center justify-center text-sm font-medium border transition-colors ';
|
||||
if (isCurrent) {
|
||||
className += 'border-[#00897B] bg-[#E0F2F1] text-[#00695C]';
|
||||
className += 'border-[#008C8C] bg-[#E0F7FA] text-[#006064]';
|
||||
} else if (isAnswered) {
|
||||
className += 'border-[#00897B] bg-[#00897B] text-white';
|
||||
className += 'border-[#008C8C] bg-[#008C8C] text-white';
|
||||
} else {
|
||||
className += 'border-gray-200 text-gray-600 bg-white hover:border-[#00897B]';
|
||||
className += 'border-gray-200 text-gray-600 bg-white hover:border-[#008C8C]';
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user