feat: show domain option progress
This commit is contained in:
@@ -208,6 +208,7 @@ export default function PerformanceDomainPracticePage() {
|
||||
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 {
|
||||
@@ -311,37 +312,87 @@ export default function PerformanceDomainPracticePage() {
|
||||
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 currentKindProgress = currentQuestion?.kind === 'expectedGoal'
|
||||
? expectedGoalProgress
|
||||
: keyPointProgress
|
||||
const currentKindDone = currentKindProgress.total > 0 && currentKindProgress.completed >= currentKindProgress.total
|
||||
const optionDisabled = isAnswerShown || currentKindDone
|
||||
|
||||
return (
|
||||
<motion.button
|
||||
key={domain.id}
|
||||
type="button"
|
||||
whileTap={!isAnswerShown ? { scale: 0.98 } : undefined}
|
||||
whileTap={!optionDisabled ? { scale: 0.98 } : undefined}
|
||||
onClick={() => handleSelect(domain.id)}
|
||||
disabled={isAnswerShown}
|
||||
disabled={optionDisabled}
|
||||
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',
|
||||
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 &&
|
||||
'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 &&
|
||||
'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
|
||||
className="flex h-11 w-11 shrink-0 items-center justify-center rounded-lg text-white"
|
||||
style={{ backgroundColor: domain.color }}
|
||||
>
|
||||
<Icon size={20} />
|
||||
<div className="flex items-center gap-3">
|
||||
<div
|
||||
className={clsx(
|
||||
'flex h-11 w-11 shrink-0 items-center justify-center rounded-lg text-white',
|
||||
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 className="min-w-0 flex-1">
|
||||
<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 className="grid grid-cols-2 gap-2">
|
||||
{[
|
||||
{ label: '目标', data: expectedGoalProgress },
|
||||
{ label: '要点', data: keyPointProgress },
|
||||
].map((item) => {
|
||||
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>
|
||||
|
||||
{isCorrectSelected && (
|
||||
|
||||
Reference in New Issue
Block a user