/** * useFormValidation Hook * * 通用的表单验证 Hook,提供: * - 字段级别的 touched / error 状态管理 * - onBlur 触发单字段验证 * - onChange 实时验证(仅在已触碰后) * - validateAll 全量验证 * - 验证图标动画支持(success / error 状态) * * 用法: * const { errors, touched, getFieldProps, validateAll, reset } = useFormValidation({ * rules: { * phone: (v) => validatePhone(v), * password: (v) => v.length >= 6 ? '' : t('auth.passwordMinLength'), * }, * }) * * * * @template Fields - 表单字段名联合类型 */ import { useState, useCallback, useRef } from 'react' /** 单个字段的验证函数:返回错误消息(空字符串表示通过) */ export type ValidationRule = (value: T) => string /** 验证规则配置 */ export interface ValidationRules { [field: string]: ValidationRule } /** Hook 配置选项 */ export interface UseFormValidationOptions { /** 验证规则映射:字段名 → 验证函数 */ rules: ValidationRules /** 初始触屏状态 */ initialTouched?: Record } /** Hook 返回值 */ export interface UseFormValidationReturn> { /** 当前错误:field → error message,无错误时字段不存在 */ errors: Partial> /** 触屏状态:field → has been interacted with */ touched: Record /** * 获取某个字段的通用 props(onChange / onBlur / statusClass) * 适用于浮度标签 input */ getFieldStatus: (field: keyof T & string) => { hasError: boolean isTouched: boolean error: string | undefined isValid: boolean } /** 处理 onChange:如果字段已触碰,实时验证 */ handleChange: (field: keyof T & string, value: string) => void /** 处理 onBlur:标记触碰 + 验证 */ handleBlur: (field: keyof T & string, value: string) => void /** 验证所有字段,返回是否通过 */ validateAll: (values: T) => { valid: boolean; errors: Partial> } /** 重置全部状态 */ reset: () => void } export function useFormValidation>( options: UseFormValidationOptions ): UseFormValidationReturn { const { rules, initialTouched = {} } = options const [errors, setErrors] = useState>>({}) const [touched, setTouched] = useState>(initialTouched) // 用于控制验证图标动画:空字符 = 未验证, 'error' = 错误, 'valid' = 通过 const [fieldStatus, setFieldStatus] = useState>({}) /** 验证单个字段 */ const validateField = useCallback( (field: string, value: string): string => { const rule = rules[field] if (!rule) return '' return rule(value) || '' }, [rules] ) /** 获取字段状态 */ const getFieldStatus = useCallback( (field: keyof T & string) => { const hasError = !!errors[field] const isTouched = !!touched[field] const error = errors[field] const isValid = isTouched && !hasError && fieldStatus[field] === 'valid' return { hasError, isTouched, error, isValid } }, [errors, touched, fieldStatus] ) /** 处理 onChange */ const handleChange = useCallback( (field: keyof T & string, value: string) => { // 如果已触碰,实时验证 if (touched[field]) { const err = validateField(field, value) setErrors((prev) => { const next = { ...prev } if (err) { next[field] = err setFieldStatus((s) => ({ ...s, [field]: 'error' })) } else { delete next[field] setFieldStatus((s) => ({ ...s, [field]: 'valid' })) } return next }) } }, [touched, validateField] ) /** 处理 onBlur */ const handleBlur = useCallback( (field: keyof T & string, value: string) => { setTouched((prev) => ({ ...prev, [field]: true })) const err = validateField(field, value) setErrors((prev) => { const next = { ...prev } if (err) { next[field] = err setFieldStatus((s) => ({ ...s, [field]: 'error' })) } else { delete next[field] setFieldStatus((s) => ({ ...s, [field]: 'valid' })) } return next }) }, [validateField] ) /** 全量验证 */ const validateAll = useCallback( (values: T) => { const newErrors: Record = {} const newTouched: Record = {} const newStatus: Record = {} Object.keys(rules).forEach((field) => { newTouched[field] = true const value = values[field] ?? '' const err = validateField(field, value) if (err) { newErrors[field] = err newStatus[field] = 'error' } else { newStatus[field] = 'valid' } }) setTouched(newTouched) setErrors(newErrors) setFieldStatus(newStatus) return { valid: Object.keys(newErrors).length === 0, errors: newErrors as Partial> } }, [rules, validateField] ) /** 重置 */ const reset = useCallback(() => { setErrors({}) setTouched({}) setFieldStatus({}) }, []) return { errors, touched, getFieldStatus, handleChange, handleBlur, validateAll, reset, } }