feat: show domain option progress

This commit is contained in:
ittoview
2026-05-13 19:08:55 +01:00
parent 6b180576b4
commit 77183cb340

View File

@@ -208,6 +208,7 @@ export default function PerformanceDomainPracticePage() {
const accuracyBase = progress.correctCount + progress.wrongCount const accuracyBase = progress.correctCount + progress.wrongCount
const accuracy = accuracyBase > 0 ? Math.round((progress.correctCount / accuracyBase) * 100) : 0 const accuracy = accuracyBase > 0 ? Math.round((progress.correctCount / accuracyBase) * 100) : 0
const progressPercent = progress.totalCount > 0 ? (completedCount / progress.totalCount) * 100 : 0 const progressPercent = progress.totalCount > 0 ? (completedCount / progress.totalCount) * 100 : 0
const completedIdSet = useMemo(() => new Set(progress.completedIds), [progress.completedIds])
useEffect(() => { useEffect(() => {
try { try {
@@ -311,37 +312,87 @@ export default function PerformanceDomainPracticePage() {
const isCorrectSelected = isAnswerShown && isSelected && answerState?.isCorrect const isCorrectSelected = isAnswerShown && isSelected && answerState?.isCorrect
const isWrongSelected = isAnswerShown && isSelected && !answerState?.isCorrect const isWrongSelected = isAnswerShown && isSelected && !answerState?.isCorrect
const shouldHighlightCorrect = isAnswerShown && isCorrectDomain 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 currentKindProgress = currentQuestion?.kind === 'expectedGoal'
? expectedGoalProgress
: keyPointProgress
const currentKindDone = currentKindProgress.total > 0 && currentKindProgress.completed >= currentKindProgress.total
const optionDisabled = isAnswerShown || currentKindDone
return ( return (
<motion.button <motion.button
key={domain.id} key={domain.id}
type="button" type="button"
whileTap={!isAnswerShown ? { scale: 0.98 } : undefined} whileTap={!optionDisabled ? { scale: 0.98 } : undefined}
onClick={() => handleSelect(domain.id)} onClick={() => handleSelect(domain.id)}
disabled={isAnswerShown} disabled={optionDisabled}
className={clsx( className={clsx(
'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', 'relative flex min-h-[128px] flex-col gap-3 rounded-xl border bg-white p-3 text-left shadow-sm transition-colors dark:bg-gray-800',
'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', '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 hover:bg-white dark:hover:bg-gray-800', optionDisabled && 'cursor-default hover:bg-white dark:hover:bg-gray-800',
currentKindDone && !isAnswerShown && 'bg-gray-50 opacity-55 dark:bg-gray-800/70',
shouldHighlightCorrect && shouldHighlightCorrect &&
'border-emerald-400 bg-emerald-50 shadow-[inset_0_0_0_2px_rgba(52,211,153,0.65)] dark:border-emerald-500 dark:bg-emerald-950/30 dark:shadow-[inset_0_0_0_2px_rgba(16,185,129,0.45)]', 'border-emerald-400 bg-emerald-50 shadow-[inset_0_0_0_2px_rgba(52,211,153,0.65)] dark:border-emerald-500 dark:bg-emerald-950/30 dark:shadow-[inset_0_0_0_2px_rgba(16,185,129,0.45)]',
isWrongSelected && 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)]' '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)]'
)} )}
> >
<div <div className="flex items-center gap-3">
className="flex h-11 w-11 shrink-0 items-center justify-center rounded-lg text-white" <div
style={{ backgroundColor: domain.color }} className={clsx(
> 'flex h-11 w-11 shrink-0 items-center justify-center rounded-lg text-white',
<Icon size={20} /> currentKindDone && !isAnswerShown && 'grayscale'
)}
style={{ backgroundColor: domain.color }}
>
<Icon size={20} />
</div>
<div className="min-w-0 flex-1 pr-8">
<div className="text-sm font-semibold text-gray-900 dark:text-white">
{domain.name}
</div>
<div className="mt-0.5 truncate text-xs text-gray-500 dark:text-gray-400">
{domain.nameEn}
</div>
</div>
</div> </div>
<div className="min-w-0 flex-1">
<div className="text-sm font-semibold text-gray-900 dark:text-white"> <div className="grid grid-cols-2 gap-2">
{domain.name} {[
</div> { label: '目标', data: expectedGoalProgress },
<div className="mt-0.5 truncate text-xs text-gray-500 dark:text-gray-400"> { label: '要点', data: keyPointProgress },
{domain.nameEn} ].map((item) => {
</div> const isFull = item.data.total > 0 && item.data.completed >= item.data.total
return (
<div key={item.label} className="min-w-0">
<div className="mb-1 flex items-center justify-between gap-1 text-[11px] text-gray-500 dark:text-gray-400">
<span>{item.label}</span>
<span>{item.data.completed}/{item.data.total}</span>
</div>
<div className="h-1.5 overflow-hidden rounded-full bg-gray-100 dark:bg-gray-700">
<div
className={clsx(
'h-full rounded-full transition-all duration-300',
isFull ? 'bg-gray-300 dark:bg-gray-500' : 'bg-indigo-500'
)}
style={{ width: `${item.data.percent}%` }}
/>
</div>
</div>
)
})}
</div> </div>
{isCorrectSelected && ( {isCorrectSelected && (