feat(过程详情): 新增ITTO熟练模式并优化练习交互

This commit is contained in:
ittoview
2026-03-23 15:24:16 +00:00
parent b09e203a90
commit d9bbd28da5
6 changed files with 742 additions and 381 deletions

View File

@@ -0,0 +1,88 @@
import { useEffect, useRef } from 'react'
import clsx from 'clsx'
import { AnimatePresence, motion } from 'framer-motion'
interface ProficientInputAreaProps {
value: string
isComposing: boolean
inputLocked: boolean
hasError: boolean
statusText?: string | null
onChange: (value: string) => void
onCompositionStart: () => void
onCompositionEnd: (value: string) => void
}
export function ProficientInputArea({
value,
isComposing,
inputLocked,
hasError,
statusText,
onChange,
onCompositionStart,
onCompositionEnd,
}: ProficientInputAreaProps) {
const inputRef = useRef<HTMLInputElement | null>(null)
useEffect(() => {
if (inputLocked) return
inputRef.current?.focus()
}, [inputLocked, value])
return (
<div className="flex w-full max-w-3xl flex-col gap-3 practice-input-area">
<input
ref={inputRef}
type="text"
value={value}
onChange={(e) => onChange(e.target.value)}
onCompositionStart={onCompositionStart}
onCompositionEnd={(e) => onCompositionEnd(e.currentTarget.value)}
disabled={inputLocked}
placeholder="输入当前分组中的一个完整条目,停顿 800ms 自动核对"
className={clsx(
'w-full rounded-xl border px-4 py-3 text-base md:text-lg',
'bg-white dark:bg-gray-800/80 text-gray-900 dark:text-gray-100',
'transition-all duration-200 focus:outline-none focus:ring-2',
isComposing && 'border-gray-300 dark:border-gray-600 opacity-80',
!isComposing && !hasError && 'border-gray-300 dark:border-gray-600 focus:ring-indigo-400 focus:border-indigo-400',
!isComposing && hasError && 'border-red-400 focus:ring-red-400 focus:border-red-400',
inputLocked && 'cursor-not-allowed opacity-60'
)}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
/>
<div className="min-h-6 text-sm">
<AnimatePresence mode="wait">
{inputLocked ? (
<motion.div
key="locked"
initial={{ opacity: 0, y: -6 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -6 }}
className="text-gray-500 dark:text-gray-400"
>
</motion.div>
) : statusText ? (
<motion.div
key={statusText}
initial={{ opacity: 0, y: -6 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -6 }}
className={clsx(
hasError ? 'text-red-500 dark:text-red-400' : 'text-gray-500 dark:text-gray-400'
)}
>
{statusText}
</motion.div>
) : null}
</AnimatePresence>
</div>
</div>
)
}