import { useEffect, useMemo, useRef, useState } from 'react' import { motion } from 'framer-motion' import { Link } from 'react-router-dom' import clsx from 'clsx' import { AlertTriangle, BarChart3, CheckCircle2, GitBranch, GraduationCap, Handshake, RefreshCw, Rocket, Target, Users, Workflow, XCircle, } from 'lucide-react' import { CelebrationAnimation } from '@/components/practice/CelebrationAnimation' import { performanceDomains, performanceDomainMap, } from '@/data/performance-domains' type QuestionKind = 'expectedGoal' | 'keyPoint' type PracticeScope = 'all' | QuestionKind interface PracticeQuestion { id: string domainId: string kind: QuestionKind text: string } interface PracticeProgress { scope: PracticeScope queue: string[] completedIds: string[] totalCount: number correctCount: number wrongCount: number } interface AnswerState { selectedDomainId: string correctDomainId: string isCorrect: boolean } const STORAGE_KEY = 'performance-domain-practice-progress-v3' const CORRECT_AUTO_NEXT_DELAY = 1000 const CHALLENGE_RESTART_DELAY = 3000 const scopeOptions: Array<{ value: PracticeScope; label: string }> = [ { value: 'all', label: '全部' }, { value: 'expectedGoal', label: '预期目标' }, { value: 'keyPoint', label: '绩效要点' }, ] const kindLabelMap: Record = { expectedGoal: '预期目标', keyPoint: '绩效要点', } const iconMap = { PD01: Handshake, PD02: Users, PD03: GitBranch, PD04: Target, PD05: Workflow, PD06: Rocket, PD07: BarChart3, PD08: AlertTriangle, } as const function shuffleArray(items: T[]): T[] { const result = [...items] for (let i = result.length - 1; i > 0; i -= 1) { const j = Math.floor(Math.random() * (i + 1)) ;[result[i], result[j]] = [result[j], result[i]] } return result } function buildQuestionBank(): PracticeQuestion[] { return performanceDomains.flatMap((domain) => { const detail = domain.detail if (!detail) return [] const expectedGoalQuestions = detail.expectedGoals.map((text, index) => ({ id: `${domain.id}-goal-${index}`, domainId: domain.id, kind: 'expectedGoal' as const, text, })) const keyPointQuestions = detail.keyPoints.map((text, index) => ({ id: `${domain.id}-point-${index}`, domainId: domain.id, kind: 'keyPoint' as const, text, })) return [...expectedGoalQuestions, ...keyPointQuestions] }) } function createProgress( scope: PracticeScope, questions: PracticeQuestion[] ): PracticeProgress { const filteredIds = questions .filter((question) => (scope === 'all' ? true : question.kind === scope)) .map((question) => question.id) return { scope, queue: shuffleArray(filteredIds), completedIds: [], totalCount: filteredIds.length, correctCount: 0, wrongCount: 0, } } function getStoredProgress(questions: PracticeQuestion[]): PracticeProgress { const questionIdSet = new Set(questions.map((question) => question.id)) try { const saved = localStorage.getItem(STORAGE_KEY) if (!saved) return createProgress('all', questions) const parsed = JSON.parse(saved) as Partial const scope = parsed.scope === 'all' || parsed.scope === 'expectedGoal' || parsed.scope === 'keyPoint' ? parsed.scope : 'all' const filteredIds = questions .filter((question) => (scope === 'all' ? true : question.kind === scope)) .map((question) => question.id) const validIdSet = new Set(filteredIds) const completedIds = Array.isArray(parsed.completedIds) ? parsed.completedIds.filter( (id, index, array): id is string => questionIdSet.has(String(id)) && validIdSet.has(String(id)) && array.indexOf(id) === index ) : [] const queue = Array.isArray(parsed.queue) ? parsed.queue.filter( (id): id is string => questionIdSet.has(String(id)) && validIdSet.has(String(id)) && !completedIds.includes(String(id)) ) : [] const missingIds = filteredIds.filter( (id) => !completedIds.includes(id) && !queue.includes(id) ) return { scope, queue: [...queue, ...shuffleArray(missingIds)], completedIds, totalCount: filteredIds.length, correctCount: Math.max(Number(parsed.correctCount) || 0, 0), wrongCount: Math.max(Number(parsed.wrongCount) || 0, 0), } } catch (error) { console.error('加载绩效域练习进度失败:', error) return createProgress('all', questions) } } export default function PerformanceDomainPracticePage() { const questionBank = useMemo(() => buildQuestionBank(), []) const questionMap = useMemo( () => new Map(questionBank.map((question) => [question.id, question])), [questionBank] ) const [progress, setProgress] = useState(() => getStoredProgress(questionBank) ) const [answerState, setAnswerState] = useState(null) const [showCelebration, setShowCelebration] = useState(false) const [challengeMode, setChallengeMode] = useState(false) const autoNextTimerRef = useRef(null) const challengeRestartTimerRef = useRef(null) const currentQuestionId = progress.queue[0] ?? null const currentQuestion = currentQuestionId ? questionMap.get(currentQuestionId) ?? null : null const currentDomain = currentQuestion ? performanceDomainMap.get(currentQuestion.domainId) ?? null : null const selectedDomain = answerState ? performanceDomainMap.get(answerState.selectedDomainId) ?? null : null const isFinished = progress.totalCount > 0 && progress.queue.length === 0 const completedCount = progress.completedIds.length + (answerState?.isCorrect ? 1 : 0) const remainingCount = Math.max(progress.totalCount - completedCount, 0) const accuracyBase = progress.correctCount + progress.wrongCount const accuracy = accuracyBase > 0 ? Math.round((progress.correctCount / accuracyBase) * 100) : 0 const progressPercent = progress.totalCount > 0 ? (completedCount / progress.totalCount) * 100 : 0 const completedIdSet = useMemo(() => new Set(progress.completedIds), [progress.completedIds]) useEffect(() => { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(progress)) } catch (error) { console.error('保存绩效域练习进度失败:', error) } }, [progress]) useEffect(() => { if (isFinished) setShowCelebration(true) }, [isFinished]) useEffect(() => { if (!answerState?.isCorrect) return if (autoNextTimerRef.current) { window.clearTimeout(autoNextTimerRef.current) } autoNextTimerRef.current = window.setTimeout(() => { advanceToNext(true) }, CORRECT_AUTO_NEXT_DELAY) return () => { if (autoNextTimerRef.current) { window.clearTimeout(autoNextTimerRef.current) autoNextTimerRef.current = null } } }, [answerState]) const restartPractice = (scope = progress.scope) => { if (autoNextTimerRef.current) { window.clearTimeout(autoNextTimerRef.current) autoNextTimerRef.current = null } if (challengeRestartTimerRef.current) { window.clearTimeout(challengeRestartTimerRef.current) challengeRestartTimerRef.current = null } setProgress(createProgress(scope, questionBank)) setAnswerState(null) setShowCelebration(false) } const switchScope = (scope: PracticeScope) => { if (scope === progress.scope) return 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) => { if (!currentQuestionId) return if (autoNextTimerRef.current) { window.clearTimeout(autoNextTimerRef.current) autoNextTimerRef.current = null } if (challengeRestartTimerRef.current) { window.clearTimeout(challengeRestartTimerRef.current) challengeRestartTimerRef.current = null } setAnswerState(null) setProgress((prev) => { if (prev.queue[0] !== currentQuestionId) return prev const [, ...restQueue] = prev.queue if (isCorrect) { return { ...prev, queue: restQueue, completedIds: prev.completedIds.includes(currentQuestionId) ? prev.completedIds : [...prev.completedIds, currentQuestionId], } } return { ...prev, queue: [...restQueue, currentQuestionId], } }) } const handleSelect = (domainId: string) => { if (!currentQuestion || answerState) return const isCorrect = domainId === currentQuestion.domainId setAnswerState({ selectedDomainId: domainId, correctDomainId: currentQuestion.domainId, isCorrect, }) setProgress((prev) => ({ ...prev, correctCount: prev.correctCount + (isCorrect ? 1 : 0), wrongCount: prev.wrongCount + (isCorrect ? 0 : 1), })) } const renderOptionButton = (domainId: string) => { const domain = performanceDomainMap.get(domainId) if (!domain) return null const Icon = iconMap[domain.id as keyof typeof iconMap] const isAnswerShown = Boolean(answerState) const isSelected = answerState?.selectedDomainId === domain.id const isCorrectDomain = answerState?.correctDomainId === domain.id const isCorrectSelected = isAnswerShown && isSelected && answerState?.isCorrect const isWrongSelected = isAnswerShown && isSelected && !answerState?.isCorrect const shouldHighlightCorrect = isAnswerShown && isCorrectDomain const getKindProgress = (kind: QuestionKind) => { const ids = questionBank .filter((question) => question.domainId === domain.id && question.kind === kind) .map((question) => question.id) const completed = ids.filter((id) => completedIdSet.has(id)).length return { completed, total: ids.length, percent: ids.length > 0 ? (completed / ids.length) * 100 : 0, } } const expectedGoalProgress = getKindProgress('expectedGoal') const keyPointProgress = getKindProgress('keyPoint') const expectedGoalDone = expectedGoalProgress.total > 0 && expectedGoalProgress.completed >= expectedGoalProgress.total const keyPointDone = keyPointProgress.total > 0 && keyPointProgress.completed >= keyPointProgress.total const domainDone = expectedGoalDone && keyPointDone const scopedDone = progress.scope === 'expectedGoal' ? expectedGoalDone : progress.scope === 'keyPoint' ? keyPointDone : domainDone const shouldKeepDisabled = scopedDone && !shouldHighlightCorrect && !isWrongSelected const optionDisabled = isAnswerShown || scopedDone return ( handleSelect(domain.id)} disabled={optionDisabled} className={clsx( 'relative flex h-16 items-center justify-center overflow-hidden rounded-xl border bg-white p-1 text-center shadow-sm transition-colors dark:bg-gray-800 sm:h-[112px] sm:flex-col sm:items-stretch sm:justify-start sm:gap-2 sm:p-3 sm:text-left', 'border-gray-100 dark:border-gray-700 focus:outline-none', optionDisabled && 'cursor-default', shouldKeepDisabled && 'bg-gray-50 opacity-55 dark:bg-gray-800/70', shouldHighlightCorrect && 'border-emerald-500 bg-emerald-100 text-emerald-900 shadow-[inset_0_0_0_3px_rgba(16,185,129,0.78),0_0_0_1px_rgba(16,185,129,0.55)] dark:border-emerald-400 dark:bg-emerald-900/50 dark:text-emerald-50 dark:shadow-[inset_0_0_0_3px_rgba(52,211,153,0.65),0_0_0_1px_rgba(52,211,153,0.45)] sm:bg-emerald-50 sm:shadow-[inset_0_0_0_2px_rgba(52,211,153,0.65)] sm:dark:bg-emerald-950/30 sm:dark:shadow-[inset_0_0_0_2px_rgba(16,185,129,0.45)]', isWrongSelected && 'border-rose-300 bg-rose-50 shadow-[inset_0_0_0_2px_rgba(251,113,133,0.65)] dark:border-rose-600 dark:bg-rose-950/30 dark:shadow-[inset_0_0_0_2px_rgba(244,63,94,0.45)]' )} >
{domain.name}
{domain.nameEn}
{domain.name.charAt(0)}
{(() => { const activeProgress = progress.scope === 'expectedGoal' ? expectedGoalProgress : keyPointProgress const activeLabel = progress.scope === 'expectedGoal' ? '目标' : '要点' const isFull = activeProgress.total > 0 && activeProgress.completed >= activeProgress.total if (progress.scope === 'all') { return (
{[ { label: '目标', data: expectedGoalProgress }, { label: '要点', data: keyPointProgress }, ].map((item) => { const itemFull = item.data.total > 0 && item.data.completed >= item.data.total return (
{item.label} {item.data.completed}/{item.data.total}
) })}
) } return (
{activeLabel} {activeProgress.completed}/{activeProgress.total}
) })()}
{isCorrectSelected && ( )} {isWrongSelected && ( )} {shouldHighlightCorrect && !isCorrectSelected && ( 正确 )}
) } return (
{showCelebration && ( setShowCelebration(false)} /> )}

八大绩效域练习

根据预期目标和绩效要点,判断对应的绩效域。

{scopeOptions.map((option) => { const isActive = option.value === progress.scope return ( ) })}
已完成 {completedCount}/{progress.totalCount} 剩余 {remainingCount} 正确率 {accuracy}% 错误 {progress.wrongCount}
{isFinished ? (
本轮完成

八大绩效域练习已完成

共完成 {progress.totalCount} 题,错误 {progress.wrongCount} 次。

总题数
{progress.totalCount}
正确率
{accuracy}%
当前范围
{scopeOptions.find((option) => option.value === progress.scope)?.label ?? '全部'}
返回绩效域
) : currentQuestion ? (
{kindLabelMap[currentQuestion.kind]} {completedCount + 1} / {progress.totalCount}

{currentQuestion.text}

{answerState && currentDomain ? (
{answerState.isCorrect ? : } {answerState.isCorrect ? `正确:${currentDomain.name}` : `错误:你选了 ${selectedDomain?.name ?? ''}`}
{!answerState.isCorrect && (

正确答案:{currentDomain.name}

)}
) : null}
{performanceDomains.map((domain) => renderOptionButton(domain.id))}
) : null}
) }