feat: add performance challenge mode
This commit is contained in:
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user