fix: simplify performance domain practice layout

This commit is contained in:
ittoview
2026-05-13 18:44:57 +01:00
parent 67436d20fc
commit 49dcbc4e59

View File

@@ -1,10 +1,11 @@
import { useEffect, useMemo, useState, type ReactNode } from 'react' import { useEffect, useMemo, useState } from 'react'
import { motion } from 'framer-motion' import { motion } from 'framer-motion'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import clsx from 'clsx' import clsx from 'clsx'
import { import {
AlertTriangle, AlertTriangle,
BarChart3, BarChart3,
CheckCircle2,
GitBranch, GitBranch,
GraduationCap, GraduationCap,
Handshake, Handshake,
@@ -13,6 +14,7 @@ import {
Target, Target,
Users, Users,
Workflow, Workflow,
XCircle,
} from 'lucide-react' } from 'lucide-react'
import { CelebrationAnimation } from '@/components/practice/CelebrationAnimation' import { CelebrationAnimation } from '@/components/practice/CelebrationAnimation'
import { import {
@@ -45,16 +47,7 @@ interface AnswerState {
isCorrect: boolean isCorrect: boolean
} }
interface CircularProgressProps { const STORAGE_KEY = 'performance-domain-practice-progress-v3'
value: number
total: number
size: number
strokeWidth: number
className?: string
children?: ReactNode
}
const STORAGE_KEY = 'performance-domain-practice-progress-v2'
const scopeOptions: Array<{ value: PracticeScope; label: string }> = [ const scopeOptions: Array<{ value: PracticeScope; label: string }> = [
{ value: 'all', label: '全部' }, { value: 'all', label: '全部' },
@@ -78,17 +71,6 @@ const iconMap = {
PD08: AlertTriangle, PD08: AlertTriangle,
} as const } as const
const desktopPositions = [
'left-6 top-10',
'left-1/2 top-1 -translate-x-1/2',
'right-6 top-10',
'right-1 top-1/2 -translate-y-[118%]',
'right-6 bottom-10',
'left-1/2 bottom-1 -translate-x-1/2',
'left-6 bottom-10',
'left-1 top-1/2 translate-y-[18%]',
] as const
function shuffleArray<T>(items: T[]): T[] { function shuffleArray<T>(items: T[]): T[] {
const result = [...items] const result = [...items]
for (let i = result.length - 1; i > 0; i -= 1) { for (let i = result.length - 1; i > 0; i -= 1) {
@@ -180,11 +162,10 @@ function getStoredProgress(questions: PracticeQuestion[]): PracticeProgress {
const missingIds = filteredIds.filter( const missingIds = filteredIds.filter(
(id) => !completedIds.includes(id) && !queue.includes(id) (id) => !completedIds.includes(id) && !queue.includes(id)
) )
const mergedQueue = [...queue, ...shuffleArray(missingIds)]
return { return {
scope, scope,
queue: mergedQueue, queue: [...queue, ...shuffleArray(missingIds)],
completedIds, completedIds,
totalCount: filteredIds.length, totalCount: filteredIds.length,
correctCount: Math.max(Number(parsed.correctCount) || 0, 0), correctCount: Math.max(Number(parsed.correctCount) || 0, 0),
@@ -196,66 +177,6 @@ function getStoredProgress(questions: PracticeQuestion[]): PracticeProgress {
} }
} }
function CircularProgress({
value,
total,
size,
strokeWidth,
className,
children,
}: CircularProgressProps) {
const clampedTotal = total > 0 ? total : 1
const ratio = Math.min(Math.max(value / clampedTotal, 0), 1)
const radius = (size - strokeWidth) / 2
const circumference = 2 * Math.PI * radius
const dashOffset = circumference * (1 - ratio)
return (
<div
className={clsx(
'relative flex items-center justify-center',
className
)}
style={{ width: size, height: size }}
>
<svg
width={size}
height={size}
viewBox={`0 0 ${size} ${size}`}
className="-rotate-90"
>
<circle
cx={size / 2}
cy={size / 2}
r={radius}
stroke="currentColor"
strokeWidth={strokeWidth}
fill="none"
className="text-white/70 dark:text-gray-700"
/>
<motion.circle
cx={size / 2}
cy={size / 2}
r={radius}
stroke="currentColor"
strokeWidth={strokeWidth}
strokeLinecap="round"
fill="none"
strokeDasharray={circumference}
animate={{ strokeDashoffset: dashOffset }}
transition={{ duration: 0.35, ease: 'easeOut' }}
className="text-indigo-500 dark:text-indigo-400"
/>
</svg>
{children && (
<div className="absolute inset-0 flex items-center justify-center">
{children}
</div>
)}
</div>
)
}
export default function PerformanceDomainPracticePage() { export default function PerformanceDomainPracticePage() {
const questionBank = useMemo(() => buildQuestionBank(), []) const questionBank = useMemo(() => buildQuestionBank(), [])
const questionMap = useMemo( const questionMap = useMemo(
@@ -280,12 +201,11 @@ export default function PerformanceDomainPracticePage() {
? performanceDomainMap.get(answerState.selectedDomainId) ?? null ? performanceDomainMap.get(answerState.selectedDomainId) ?? null
: null : null
const isFinished = progress.totalCount > 0 && progress.queue.length === 0 const isFinished = progress.totalCount > 0 && progress.queue.length === 0
const displayCompletedCount = const completedCount = progress.completedIds.length + (answerState?.isCorrect ? 1 : 0)
progress.completedIds.length + (answerState?.isCorrect ? 1 : 0) const remainingCount = Math.max(progress.totalCount - completedCount, 0)
const remainingCount = Math.max(progress.totalCount - displayCompletedCount, 0)
const accuracyBase = progress.correctCount + progress.wrongCount const accuracyBase = progress.correctCount + progress.wrongCount
const accuracy = const accuracy = accuracyBase > 0 ? Math.round((progress.correctCount / accuracyBase) * 100) : 0
accuracyBase > 0 ? Math.round((progress.correctCount / accuracyBase) * 100) : 0 const progressPercent = progress.totalCount > 0 ? (completedCount / progress.totalCount) * 100 : 0
useEffect(() => { useEffect(() => {
try { try {
@@ -296,9 +216,7 @@ export default function PerformanceDomainPracticePage() {
}, [progress]) }, [progress])
useEffect(() => { useEffect(() => {
if (isFinished) { if (isFinished) setShowCelebration(true)
setShowCelebration(true)
}
}, [isFinished]) }, [isFinished])
const restartPractice = (scope = progress.scope) => { const restartPractice = (scope = progress.scope) => {
@@ -316,12 +234,10 @@ export default function PerformanceDomainPracticePage() {
if (!currentQuestionId) return if (!currentQuestionId) return
setAnswerState(null) setAnswerState(null)
setProgress((prev) => { setProgress((prev) => {
if (prev.queue[0] !== currentQuestionId) return prev if (prev.queue[0] !== currentQuestionId) return prev
const [, ...restQueue] = prev.queue const [, ...restQueue] = prev.queue
if (isCorrect) { if (isCorrect) {
return { return {
...prev, ...prev,
@@ -355,7 +271,7 @@ export default function PerformanceDomainPracticePage() {
})) }))
} }
const renderOptionButton = (domainId: string, className?: string) => { const renderOptionButton = (domainId: string) => {
const domain = performanceDomainMap.get(domainId) const domain = performanceDomainMap.get(domainId)
if (!domain) return null if (!domain) return null
@@ -371,64 +287,51 @@ export default function PerformanceDomainPracticePage() {
<motion.button <motion.button
key={domain.id} key={domain.id}
type="button" type="button"
whileHover={!isAnswerShown ? { y: -2 } : undefined}
whileTap={!isAnswerShown ? { scale: 0.98 } : undefined} whileTap={!isAnswerShown ? { scale: 0.98 } : undefined}
onClick={() => handleSelect(domain.id)} onClick={() => handleSelect(domain.id)}
disabled={isAnswerShown} disabled={isAnswerShown}
className={clsx( className={clsx(
'w-full rounded-2xl border bg-white/95 px-4 py-3 text-left shadow-sm transition-all backdrop-blur-sm dark:bg-gray-800/95', 'relative flex min-h-[84px] items-center gap-3 rounded-xl border bg-white p-3 text-left shadow-sm transition-colors dark:bg-gray-800',
'border-gray-200 hover:border-gray-300 dark:border-gray-700 dark:hover:border-gray-600', 'border-gray-100 hover:border-indigo-200 hover:bg-indigo-50/40 dark:border-gray-700 dark:hover:border-indigo-700 dark:hover:bg-indigo-950/20',
isAnswerShown && 'cursor-default', isAnswerShown && 'cursor-default hover:bg-white dark:hover:bg-gray-800',
!isAnswerShown && 'hover:shadow-md',
shouldHighlightCorrect && shouldHighlightCorrect &&
'border-emerald-300 bg-emerald-50 dark:border-emerald-700 dark:bg-emerald-950/40', 'border-emerald-300 bg-emerald-50 dark:border-emerald-700 dark:bg-emerald-950/30',
isWrongSelected && isWrongSelected &&
'border-rose-300 bg-rose-50 dark:border-rose-700 dark:bg-rose-950/40', 'border-rose-300 bg-rose-50 dark:border-rose-700 dark:bg-rose-950/30'
className
)} )}
> >
<div className="flex items-center gap-3"> <div
<div className="flex h-11 w-11 shrink-0 items-center justify-center rounded-lg text-white"
className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl text-white shadow-sm" style={{ backgroundColor: domain.color }}
style={{ backgroundColor: domain.color }} >
> <Icon size={20} />
<Icon size={18} /> </div>
<div className="min-w-0 flex-1">
<div className="text-sm font-semibold text-gray-900 dark:text-white">
{domain.name}
</div> </div>
<div className="min-w-0 flex-1"> <div className="mt-0.5 truncate text-xs text-gray-500 dark:text-gray-400">
<div className="text-sm font-semibold text-gray-900 dark:text-white"> {domain.nameEn}
{domain.name}
</div>
<div className="mt-0.5 text-xs text-gray-500 dark:text-gray-400">
{domain.nameEn}
</div>
</div> </div>
</div> </div>
{isAnswerShown && ( {isCorrectSelected && (
<div className="mt-3 flex flex-wrap gap-2"> <CheckCircle2 className="absolute right-3 top-3 text-emerald-500" size={18} />
{isCorrectSelected && ( )}
<span className="rounded-full bg-emerald-100 px-2.5 py-1 text-xs font-medium text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-300"> {isWrongSelected && (
<XCircle className="absolute right-3 top-3 text-rose-500" size={18} />
</span> )}
)} {shouldHighlightCorrect && !isCorrectSelected && (
{isWrongSelected && ( <span className="absolute right-3 top-3 rounded-full bg-emerald-100 px-2 py-0.5 text-xs font-medium text-emerald-700 dark:bg-emerald-900/50 dark:text-emerald-300">
<span className="rounded-full bg-rose-100 px-2.5 py-1 text-xs font-medium text-rose-700 dark:bg-rose-900/40 dark:text-rose-300">
</span>
</span>
)}
{shouldHighlightCorrect && !isCorrectSelected && (
<span className="rounded-full bg-emerald-100 px-2.5 py-1 text-xs font-medium text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-300">
</span>
)}
</div>
)} )}
</motion.button> </motion.button>
) )
} }
return ( return (
<div className="space-y-5"> <div className="mx-auto max-w-6xl space-y-4">
{showCelebration && ( {showCelebration && (
<CelebrationAnimation onComplete={() => setShowCelebration(false)} /> <CelebrationAnimation onComplete={() => setShowCelebration(false)} />
)} )}
@@ -436,19 +339,14 @@ export default function PerformanceDomainPracticePage() {
<div className="flex flex-wrap items-center justify-between gap-3"> <div className="flex flex-wrap items-center justify-between gap-3">
<div className="space-y-1"> <div className="space-y-1">
<nav className="flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400"> <nav className="flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400">
<Link <Link to="/performance-domains" className="hover:text-indigo-600 dark:hover:text-indigo-400">
to="/performance-domains"
className="hover:text-indigo-600 dark:hover:text-indigo-400"
>
</Link> </Link>
<span>/</span> <span>/</span>
<span className="text-gray-900 dark:text-white"></span> <span className="text-gray-900 dark:text-white"></span>
</nav> </nav>
<div> <div>
<h1 className="text-xl font-bold text-gray-900 dark:text-white"> <h1 className="text-xl font-bold text-gray-900 dark:text-white"></h1>
</h1>
<p className="text-sm text-gray-500 dark:text-gray-400"> <p className="text-sm text-gray-500 dark:text-gray-400">
</p> </p>
@@ -465,89 +363,75 @@ export default function PerformanceDomainPracticePage() {
</button> </button>
</div> </div>
<div className="flex flex-wrap gap-2"> <div className="rounded-2xl bg-white p-4 shadow-sm ring-1 ring-gray-100 dark:bg-gray-800 dark:ring-gray-700">
{scopeOptions.map((option) => { <div className="flex flex-wrap items-center justify-between gap-3">
const isActive = option.value === progress.scope <div className="flex flex-wrap gap-2">
return ( {scopeOptions.map((option) => {
<button const isActive = option.value === progress.scope
key={option.value} return (
type="button" <button
onClick={() => switchScope(option.value)} key={option.value}
className={clsx( type="button"
'rounded-full px-3 py-1.5 text-sm font-medium transition-colors', onClick={() => switchScope(option.value)}
isActive className={clsx(
? 'bg-indigo-600 text-white' 'rounded-full px-3 py-1.5 text-sm font-medium transition-colors',
: 'bg-white text-gray-600 shadow-sm ring-1 ring-gray-200 hover:bg-gray-50 dark:bg-gray-800 dark:text-gray-300 dark:ring-gray-700 dark:hover:bg-gray-700' isActive
)} ? 'bg-indigo-600 text-white'
> : 'bg-gray-100 text-gray-600 hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600'
{option.label} )}
</button> >
) {option.label}
})} </button>
</div> )
})}
</div>
<div className="flex flex-wrap items-center gap-x-6 gap-y-2 rounded-2xl bg-white px-4 py-3 shadow-sm ring-1 ring-gray-100 dark:bg-gray-800 dark:ring-gray-700"> <div className="flex flex-wrap gap-x-5 gap-y-1 text-sm text-gray-600 dark:text-gray-300">
<div className="text-sm text-gray-600 dark:text-gray-300"> <span> <b className="text-gray-900 dark:text-white">{completedCount}/{progress.totalCount}</b></span>
<span> <b className="text-gray-900 dark:text-white">{remainingCount}</b></span>
<span className="ml-2 font-semibold text-gray-900 dark:text-white"> <span> <b className="text-gray-900 dark:text-white">{accuracy}%</b></span>
{displayCompletedCount} / {progress.totalCount} <span> <b className="text-gray-900 dark:text-white">{progress.wrongCount}</b></span>
</span> </div>
</div> </div>
<div className="text-sm text-gray-600 dark:text-gray-300">
<div className="mt-4 h-2 overflow-hidden rounded-full bg-gray-100 dark:bg-gray-700">
<span className="ml-2 font-semibold text-gray-900 dark:text-white"> <div
{remainingCount} className="h-full rounded-full bg-indigo-600 transition-all duration-300"
</span> style={{ width: `${progressPercent}%` }}
</div> />
<div className="text-sm text-gray-600 dark:text-gray-300">
<span className="ml-2 font-semibold text-gray-900 dark:text-white">
{accuracy}%
</span>
</div>
<div className="text-sm text-gray-600 dark:text-gray-300">
<span className="ml-2 font-semibold text-gray-900 dark:text-white">
{progress.wrongCount}
</span>
</div> </div>
</div> </div>
{isFinished ? ( {isFinished ? (
<motion.div <motion.div
initial={{ opacity: 0, y: 12 }} initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
className="overflow-hidden rounded-3xl bg-white shadow-sm ring-1 ring-gray-100 dark:bg-gray-800 dark:ring-gray-700" className="overflow-hidden rounded-2xl bg-white shadow-sm ring-1 ring-gray-100 dark:bg-gray-800 dark:ring-gray-700"
> >
<div className="bg-gradient-to-r from-indigo-500 via-violet-500 to-cyan-500 px-6 py-8 text-white"> <div className="bg-indigo-600 px-6 py-7 text-white">
<div className="inline-flex items-center gap-2 rounded-full bg-white/15 px-3 py-1 text-sm"> <div className="inline-flex items-center gap-2 rounded-full bg-white/15 px-3 py-1 text-sm">
<GraduationCap size={16} /> <GraduationCap size={16} />
</div> </div>
<h2 className="mt-4 text-3xl font-bold"></h2> <h2 className="mt-4 text-2xl font-bold"></h2>
<p className="mt-2 text-sm text-white/80"> <p className="mt-2 text-sm text-white/80">
{progress.totalCount} {progress.wrongCount} {progress.totalCount} {progress.wrongCount}
</p> </p>
</div> </div>
<div className="grid gap-4 px-6 py-6 md:grid-cols-3"> <div className="grid gap-4 px-6 py-6 md:grid-cols-3">
<div className="rounded-2xl bg-gray-50 px-4 py-4 dark:bg-gray-900/40"> <div className="rounded-xl bg-gray-50 px-4 py-4 dark:bg-gray-900/40">
<div className="text-sm text-gray-500 dark:text-gray-400"></div> <div className="text-sm text-gray-500 dark:text-gray-400"></div>
<div className="mt-2 text-3xl font-bold text-gray-900 dark:text-white"> <div className="mt-2 text-3xl font-bold text-gray-900 dark:text-white">{progress.totalCount}</div>
{progress.totalCount}
</div>
</div> </div>
<div className="rounded-2xl bg-gray-50 px-4 py-4 dark:bg-gray-900/40"> <div className="rounded-xl bg-gray-50 px-4 py-4 dark:bg-gray-900/40">
<div className="text-sm text-gray-500 dark:text-gray-400"></div> <div className="text-sm text-gray-500 dark:text-gray-400"></div>
<div className="mt-2 text-3xl font-bold text-gray-900 dark:text-white"> <div className="mt-2 text-3xl font-bold text-gray-900 dark:text-white">{accuracy}%</div>
{accuracy}%
</div>
</div> </div>
<div className="rounded-2xl bg-gray-50 px-4 py-4 dark:bg-gray-900/40"> <div className="rounded-xl bg-gray-50 px-4 py-4 dark:bg-gray-900/40">
<div className="text-sm text-gray-500 dark:text-gray-400"></div> <div className="text-sm text-gray-500 dark:text-gray-400"></div>
<div className="mt-2 text-3xl font-bold text-gray-900 dark:text-white"> <div className="mt-2 text-3xl font-bold text-gray-900 dark:text-white">
{scopeOptions.find((option) => option.value === progress.scope)?.label ?? {scopeOptions.find((option) => option.value === progress.scope)?.label ?? '全部'}
'全部'}
</div> </div>
</div> </div>
</div> </div>
@@ -570,178 +454,71 @@ export default function PerformanceDomainPracticePage() {
</div> </div>
</motion.div> </motion.div>
) : currentQuestion ? ( ) : currentQuestion ? (
<div className="overflow-hidden rounded-3xl bg-gradient-to-br from-indigo-50 via-white to-cyan-50 p-4 shadow-sm ring-1 ring-indigo-100 dark:from-gray-900 dark:via-gray-800 dark:to-gray-900 dark:ring-gray-700 lg:p-6"> <div className="grid gap-4 xl:grid-cols-[1fr_1.45fr]">
<div className="lg:hidden"> <section className="rounded-2xl bg-white p-5 shadow-sm ring-1 ring-gray-100 dark:bg-gray-800 dark:ring-gray-700">
<div className="mb-4 flex items-center gap-4 rounded-3xl bg-white/85 px-4 py-4 shadow-sm ring-1 ring-gray-100 backdrop-blur-sm dark:bg-gray-800/85 dark:ring-gray-700"> <div className="flex items-center justify-between gap-3">
<CircularProgress value={displayCompletedCount} total={progress.totalCount} size={82} strokeWidth={8}> <span className="rounded-full bg-indigo-50 px-3 py-1 text-sm font-medium text-indigo-600 dark:bg-indigo-900/40 dark:text-indigo-300">
<div className="text-center"> {kindLabelMap[currentQuestion.kind]}
<div className="text-lg font-bold text-gray-900 dark:text-white"> </span>
{displayCompletedCount} <span className="text-sm text-gray-500 dark:text-gray-400">
</div> {completedCount + 1} / {progress.totalCount}
<div className="text-[11px] text-gray-500 dark:text-gray-400"> </span>
/ {progress.totalCount}
</div>
</div>
</CircularProgress>
<div className="space-y-2">
<div className="inline-flex rounded-full bg-indigo-600 px-3 py-1 text-sm font-medium text-white">
{kindLabelMap[currentQuestion.kind]}
</div>
<div className="text-sm text-gray-500 dark:text-gray-400">
{remainingCount}
</div>
</div>
</div> </div>
<div className="rounded-3xl bg-white px-5 py-6 shadow-sm ring-1 ring-gray-100 dark:bg-gray-800 dark:ring-gray-700"> <div className="flex min-h-[180px] items-center py-6 md:min-h-[220px] xl:min-h-[300px]">
<div className="flex min-h-[160px] items-center"> <p className="text-2xl font-semibold leading-relaxed text-gray-900 dark:text-white md:text-3xl">
<p className="text-xl font-semibold leading-9 text-gray-900 dark:text-white"> {currentQuestion.text}
{currentQuestion.text} </p>
</p>
</div>
</div> </div>
<motion.div <div
initial={false}
animate={{ opacity: answerState ? 1 : 0.96 }}
className={clsx( className={clsx(
'mt-4 min-h-[124px] rounded-2xl border px-4 py-4 shadow-sm', 'min-h-[104px] rounded-xl border px-4 py-3 transition-colors',
answerState?.isCorrect answerState?.isCorrect
? 'border-emerald-200 bg-emerald-50 dark:border-emerald-800 dark:bg-emerald-950/30' ? 'border-emerald-200 bg-emerald-50 dark:border-emerald-800 dark:bg-emerald-950/30'
: answerState : answerState
? 'border-amber-200 bg-amber-50 dark:border-amber-800 dark:bg-amber-950/30' ? 'border-rose-200 bg-rose-50 dark:border-rose-800 dark:bg-rose-950/30'
: 'border-white/70 bg-white/80 dark:border-gray-700 dark:bg-gray-800/80' : 'border-gray-100 bg-gray-50 dark:border-gray-700 dark:bg-gray-900/30'
)} )}
> >
{answerState && currentDomain ? ( {answerState && currentDomain ? (
<div className="flex h-full flex-col justify-between gap-3"> <div className="flex h-full flex-col justify-between gap-3">
<div className="space-y-1"> <div>
<div <div
className={clsx( className={clsx(
'text-sm font-semibold', 'flex items-center gap-2 text-sm font-semibold',
answerState.isCorrect answerState.isCorrect
? 'text-emerald-700 dark:text-emerald-300' ? 'text-emerald-700 dark:text-emerald-300'
: 'text-amber-700 dark:text-amber-300' : 'text-rose-700 dark:text-rose-300'
)} )}
> >
{answerState.isCorrect ? <CheckCircle2 size={17} /> : <XCircle size={17} />}
{answerState.isCorrect {answerState.isCorrect
? '回答正确' ? `正确:${currentDomain.name}`
: `你选了 ${selectedDomain?.name ?? ''},正确答案是 ${currentDomain.name}`} : `错误:你选了 ${selectedDomain?.name ?? ''}`}
</div> </div>
<p className="text-sm leading-6 text-gray-600 dark:text-gray-300"> {!answerState.isCorrect && (
{answerState.isCorrect <p className="mt-1 text-sm text-gray-700 dark:text-gray-300">
? '这题已答对。' {currentDomain.name}
: '这题已放到后面,稍后会再出现。'} </p>
</p> )}
</div> </div>
<div className="flex justify-center">
<button
type="button"
onClick={() => advanceToNext(answerState.isCorrect)}
className="rounded-xl bg-indigo-600 px-4 py-2 text-sm font-semibold text-white transition-colors hover:bg-indigo-500"
>
</button>
</div>
</div>
) : (
<div className="h-full" />
)}
</motion.div>
<div className="mt-4 grid gap-3 sm:grid-cols-2"> <button
{performanceDomains.map((domain) => renderOptionButton(domain.id))} type="button"
onClick={() => advanceToNext(answerState.isCorrect)}
className="self-start rounded-lg bg-indigo-600 px-4 py-2 text-sm font-semibold text-white transition-colors hover:bg-indigo-500"
>
</button>
</div>
) : null}
</div> </div>
</div> </section>
<div className="hidden lg:block"> <section className="grid gap-3 sm:grid-cols-2">
<div className="relative mx-auto h-[38rem] max-w-6xl overflow-hidden rounded-[2rem]"> {performanceDomains.map((domain) => renderOptionButton(domain.id))}
<CircularProgress </section>
value={displayCompletedCount}
total={progress.totalCount}
size={430}
strokeWidth={16}
className="pointer-events-none absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 text-indigo-500"
/>
<div className="absolute left-1/2 top-1/2 w-full max-w-md -translate-x-1/2 -translate-y-1/2 px-4">
<div className="mb-4 flex items-center justify-center gap-3">
<span className="rounded-full bg-white/90 px-3 py-1 text-sm font-medium text-gray-700 shadow-sm ring-1 ring-gray-200 dark:bg-gray-800 dark:text-gray-200 dark:ring-gray-700">
{displayCompletedCount} / {progress.totalCount}
</span>
<span className="rounded-full bg-indigo-600 px-3 py-1 text-sm font-medium text-white">
{kindLabelMap[currentQuestion.kind]}
</span>
</div>
<div className="rounded-[2rem] bg-white/95 px-6 py-7 text-center shadow-xl ring-1 ring-gray-100 backdrop-blur-sm dark:bg-gray-800/95 dark:ring-gray-700">
<div className="flex min-h-[220px] items-center justify-center">
<p className="text-2xl font-semibold leading-10 text-gray-900 dark:text-white">
{currentQuestion.text}
</p>
</div>
</div>
<motion.div
initial={false}
animate={{ opacity: answerState ? 1 : 0.96 }}
className={clsx(
'mt-4 min-h-[132px] rounded-2xl border px-4 py-4 shadow-sm',
answerState?.isCorrect
? 'border-emerald-200 bg-emerald-50/95 dark:border-emerald-800 dark:bg-emerald-950/40'
: answerState
? 'border-amber-200 bg-amber-50/95 dark:border-amber-800 dark:bg-amber-950/40'
: 'border-white/70 bg-white/85 dark:border-gray-700 dark:bg-gray-800/85'
)}
>
{answerState && currentDomain ? (
<div className="flex h-full flex-col justify-between gap-3">
<div className="space-y-1">
<div
className={clsx(
'text-sm font-semibold',
answerState.isCorrect
? 'text-emerald-700 dark:text-emerald-300'
: 'text-amber-700 dark:text-amber-300'
)}
>
{answerState.isCorrect
? '回答正确'
: `你选了 ${selectedDomain?.name ?? ''},正确答案是 ${currentDomain.name}`}
</div>
<p className="text-sm leading-6 text-gray-600 dark:text-gray-300">
{answerState.isCorrect
? '这题已答对。'
: '这题已放到后面,稍后会再出现。'}
</p>
</div>
<div className="flex justify-center">
<button
type="button"
onClick={() => advanceToNext(answerState.isCorrect)}
className="rounded-xl bg-indigo-600 px-4 py-2 text-sm font-semibold text-white transition-colors hover:bg-indigo-500"
>
</button>
</div>
</div>
) : (
<div className="h-full" />
)}
</motion.div>
</div>
{performanceDomains.map((domain, index) => (
<div
key={domain.id}
className={clsx('absolute w-56', desktopPositions[index])}
>
{renderOptionButton(domain.id)}
</div>
))}
</div>
</div>
</div> </div>
) : null} ) : null}
</div> </div>