74 lines
2.3 KiB
TypeScript
74 lines
2.3 KiB
TypeScript
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
|
||
onChange: (value: string) => void
|
||
onCompositionStart: () => void
|
||
onCompositionEnd: (value: string) => void
|
||
showLockedHint?: boolean
|
||
}
|
||
|
||
export function ProficientInputArea({
|
||
value,
|
||
isComposing,
|
||
inputLocked,
|
||
hasError,
|
||
onChange,
|
||
onCompositionStart,
|
||
onCompositionEnd,
|
||
showLockedHint = true,
|
||
}: 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="输入当前分组中的一个完整条目"
|
||
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"
|
||
/>
|
||
|
||
<AnimatePresence>
|
||
{showLockedHint && inputLocked && (
|
||
<motion.div
|
||
initial={{ opacity: 0, y: -6 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
exit={{ opacity: 0, y: -6 }}
|
||
className="text-sm text-gray-500 dark:text-gray-400"
|
||
>
|
||
答案显示中,输入已锁定
|
||
</motion.div>
|
||
)}
|
||
</AnimatePresence>
|
||
</div>
|
||
)
|
||
}
|