'use client'; import { motion, AnimatePresence } from 'framer-motion'; import { ReactNode, useState, useRef, useEffect, forwardRef } from 'react'; import { EyeIcon, EyeSlashIcon, ExclamationCircleIcon, CheckCircleIcon } from '@heroicons/react/24/outline'; interface EnhancedInputProps { type?: 'text' | 'password' | 'email' | 'number' | 'tel' | 'url' | 'search'; placeholder?: string; value?: string; onChange?: (e: React.ChangeEvent) => void; onFocus?: (e: React.FocusEvent) => void; onBlur?: (e: React.FocusEvent) => void; label?: string; error?: string; success?: string; hint?: string; disabled?: boolean; required?: boolean; leftIcon?: ReactNode; rightIcon?: ReactNode; className?: string; containerClassName?: string; autoFocus?: boolean; autoComplete?: string; name?: string; id?: string; showPasswordToggle?: boolean; showStrengthIndicator?: boolean; showCharCount?: boolean; maxLength?: number; minLength?: number; pattern?: string; validate?: (value: string) => string | null; onValidationChange?: (isValid: boolean) => void; } const EnhancedInput = forwardRef(({ type = 'text', placeholder, value = '', onChange, onFocus, onBlur, label, error, success, hint, disabled = false, required = false, leftIcon, rightIcon, className = '', containerClassName = '', autoFocus = false, autoComplete, name, id, showPasswordToggle = false, showStrengthIndicator = false, showCharCount = false, maxLength, minLength, pattern, validate, onValidationChange, ...props }, ref) => { const [isFocused, setIsFocused] = useState(false); const [isPasswordVisible, setIsPasswordVisible] = useState(false); const [internalValue, setInternalValue] = useState(value); const [validationMessage, setValidationMessage] = useState(null); const [isValidating, setIsValidating] = useState(false); const inputRef = useRef(null); const containerRef = useRef(null); // 同步外部值 useEffect(() => { setInternalValue(value); }, [value]); // 验证逻辑 useEffect(() => { if (validate && internalValue) { setIsValidating(true); const message = validate(internalValue); setValidationMessage(message); setIsValidating(false); if (onValidationChange) { onValidationChange(!message); } } }, [internalValue, validate, onValidationChange]); // 密码强度计算 const getPasswordStrength = (password: string) => { let strength = 0; if (password.length >= 8) strength++; if (/[a-z]/.test(password)) strength++; if (/[A-Z]/.test(password)) strength++; if (/[0-9]/.test(password)) strength++; if (/[^A-Za-z0-9]/.test(password)) strength++; return strength; }; const passwordStrength = type === 'password' && showStrengthIndicator ? getPasswordStrength(internalValue) : 0; const handleFocus = (e: React.FocusEvent) => { setIsFocused(true); onFocus?.(e); }; const handleBlur = (e: React.FocusEvent) => { setIsFocused(false); onBlur?.(e); }; const handleChange = (e: React.ChangeEvent) => { const newValue = e.target.value; setInternalValue(newValue); onChange?.(e); }; const togglePasswordVisibility = () => { setIsPasswordVisible(!isPasswordVisible); }; const getInputType = () => { if (type === 'password' && showPasswordToggle) { return isPasswordVisible ? 'text' : 'password'; } return type; }; const getBorderColor = () => { if (error || validationMessage) return 'border-red-500 focus:border-red-500 focus:ring-red-500'; if (success || (validationMessage === null && internalValue && validate)) return 'border-green-500 focus:border-green-500 focus:ring-green-500'; if (isFocused) return 'border-orange-500 focus:border-orange-500 focus:ring-orange-500'; return 'border-gray-300 dark:border-gray-600 focus:border-orange-500 focus:ring-orange-500'; }; const getLabelColor = () => { if (error || validationMessage) return 'text-red-600 dark:text-red-400'; if (success || (validationMessage === null && internalValue && validate)) return 'text-green-600 dark:text-green-400'; if (isFocused) return 'text-orange-600 dark:text-orange-400'; return 'text-gray-700 dark:text-gray-300'; }; const inputVariants = { initial: { scale: 1 }, focus: { scale: 1.02, transition: { duration: 0.2, ease: "easeOut" } }, blur: { scale: 1, transition: { duration: 0.2, ease: "easeOut" } } }; const labelVariants = { initial: { y: 0, scale: 1 }, focus: { y: -20, scale: 0.85, transition: { duration: 0.2, ease: "easeOut" } }, blur: { y: internalValue ? -20 : 0, scale: internalValue ? 0.85 : 1, transition: { duration: 0.2, ease: "easeOut" } } }; return ( {/* 标签 */} {label && ( {label} {required && *} )} {/* 输入框容器 */} {/* 左侧图标 */} {leftIcon && ( {leftIcon} )} {/* 输入框 */} {/* 验证状态图标 */} {isValidating && ( )} {!isValidating && validationMessage === null && internalValue && validate && ( )} {(error || validationMessage) && ( )} {/* 右侧图标和密码切换 */}
{type === 'password' && showPasswordToggle && ( {isPasswordVisible ? : } )} {rightIcon && ( {rightIcon} )}
{/* 密码强度指示器 */} {type === 'password' && showStrengthIndicator && internalValue && (
{[1, 2, 3, 4, 5].map((level) => ( = level ? level <= 2 ? 'bg-red-500' : level <= 3 ? 'bg-yellow-500' : 'bg-green-500' : 'bg-gray-200 dark:bg-gray-700' }`} initial={{ scaleX: 0 }} animate={{ scaleX: passwordStrength >= level ? 1 : 0.3 }} transition={{ delay: level * 0.1 }} /> ))}
{passwordStrength <= 2 && '密码强度:弱'} {passwordStrength === 3 && '密码强度:中等'} {passwordStrength >= 4 && '密码强度:强'}
)} {/* 字符计数 */} {showCharCount && maxLength && (
maxLength ? 'text-red-500' : internalValue.length > maxLength * 0.8 ? 'text-yellow-500' : 'text-gray-500' }`} animate={{ scale: internalValue.length > maxLength ? 1.1 : 1, color: internalValue.length > maxLength ? '#ef4444' : internalValue.length > maxLength * 0.8 ? '#f59e0b' : '#6b7280' }} > {internalValue.length}/{maxLength} )} {/* 错误信息 */} {(error || validationMessage) && ( {error || validationMessage} )} {/* 成功信息 */} {success && ( {success} )} {/* 提示信息 */} {hint && !error && !validationMessage && !success && ( {hint} )} ); }); EnhancedInput.displayName = 'EnhancedInput'; export default EnhancedInput;