feat(过程详情): 新增ITTO熟练模式并优化练习交互
This commit is contained in:
88
src/components/practice/ProficientInputArea.tsx
Normal file
88
src/components/practice/ProficientInputArea.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user