feat: add performance challenge mode

This commit is contained in:
ittoview
2026-05-16 12:19:01 +01:00
parent 3f90031185
commit 35c0146a96

View File

@@ -49,6 +49,7 @@ interface AnswerState {
const STORAGE_KEY = 'performance-domain-practice-progress-v3' const STORAGE_KEY = 'performance-domain-practice-progress-v3'
const CORRECT_AUTO_NEXT_DELAY = 1000 const CORRECT_AUTO_NEXT_DELAY = 1000
const CHALLENGE_RESTART_DELAY = 3000
const scopeOptions: Array<{ value: PracticeScope; label: string }> = [ const scopeOptions: Array<{ value: PracticeScope; label: string }> = [
{ value: 'all', label: '全部' }, { value: 'all', label: '全部' },
@@ -190,7 +191,9 @@ export default function PerformanceDomainPracticePage() {
) )
const [answerState, setAnswerState] = useState<AnswerState | null>(null) const [answerState, setAnswerState] = useState<AnswerState | null>(null)
const [showCelebration, setShowCelebration] = useState(false) const [showCelebration, setShowCelebration] = useState(false)
const [challengeMode, setChallengeMode] = useState(false)
const autoNextTimerRef = useRef<number | null>(null) const autoNextTimerRef = useRef<number | null>(null)
const challengeRestartTimerRef = useRef<number | null>(null)
const currentQuestionId = progress.queue[0] ?? null const currentQuestionId = progress.queue[0] ?? null
const currentQuestion = currentQuestionId const currentQuestion = currentQuestionId
@@ -246,6 +249,10 @@ export default function PerformanceDomainPracticePage() {
window.clearTimeout(autoNextTimerRef.current) window.clearTimeout(autoNextTimerRef.current)
autoNextTimerRef.current = null autoNextTimerRef.current = null
} }
if (challengeRestartTimerRef.current) {
window.clearTimeout(challengeRestartTimerRef.current)
challengeRestartTimerRef.current = null
}
setProgress(createProgress(scope, questionBank)) setProgress(createProgress(scope, questionBank))
setAnswerState(null) setAnswerState(null)
setShowCelebration(false) setShowCelebration(false)
@@ -256,6 +263,25 @@ export default function PerformanceDomainPracticePage() {
restartPractice(scope) restartPractice(scope)
} }
useEffect(() => {
if (!challengeMode || !answerState || answerState.isCorrect) return
if (challengeRestartTimerRef.current) {
window.clearTimeout(challengeRestartTimerRef.current)
}
challengeRestartTimerRef.current = window.setTimeout(() => {
restartPractice(progress.scope)
}, CHALLENGE_RESTART_DELAY)
return () => {
if (challengeRestartTimerRef.current) {
window.clearTimeout(challengeRestartTimerRef.current)
challengeRestartTimerRef.current = null
}
}
}, [answerState, challengeMode, progress.scope])
const advanceToNext = (isCorrect: boolean) => { const advanceToNext = (isCorrect: boolean) => {
if (!currentQuestionId) return if (!currentQuestionId) return
@@ -263,6 +289,10 @@ export default function PerformanceDomainPracticePage() {
window.clearTimeout(autoNextTimerRef.current) window.clearTimeout(autoNextTimerRef.current)
autoNextTimerRef.current = null autoNextTimerRef.current = null
} }
if (challengeRestartTimerRef.current) {
window.clearTimeout(challengeRestartTimerRef.current)
challengeRestartTimerRef.current = null
}
setAnswerState(null) setAnswerState(null)
setProgress((prev) => { setProgress((prev) => {
if (prev.queue[0] !== currentQuestionId) return prev if (prev.queue[0] !== currentQuestionId) return prev
@@ -478,14 +508,29 @@ export default function PerformanceDomainPracticePage() {
</div> </div>
</div> </div>
<button <div className="flex items-center gap-2">
type="button" <button
onClick={() => restartPractice()} type="button"
className="inline-flex items-center gap-2 rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700" aria-pressed={challengeMode}
> onClick={() => setChallengeMode((value) => !value)}
<RefreshCw size={16} /> className={clsx(
'rounded-lg border px-3 py-2 text-sm font-medium transition-colors',
</button> challengeMode
? 'border-indigo-500 bg-indigo-600 text-white'
: 'border-gray-200 bg-white text-gray-700 hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700'
)}
>
</button>
<button
type="button"
onClick={() => restartPractice()}
className="inline-flex items-center gap-2 rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700"
>
<RefreshCw size={16} />
</button>
</div>
</div> </div>
<div className="rounded-2xl bg-white p-4 shadow-sm ring-1 ring-gray-100 dark:bg-gray-800 dark:ring-gray-700"> <div className="rounded-2xl bg-white p-4 shadow-sm ring-1 ring-gray-100 dark:bg-gray-800 dark:ring-gray-700">