Download vetcopilot-app/src/hooks/useFormValidation.ts from WWsCa/VetNet-LCPS: direct link, hf CLI and curl.
- Browser
- Download file 5.64 kB
-
https://huggingface.co/WWsCa/VetNet-LCPS/resolve/main/vetcopilot-app/src/hooks/useFormValidation.ts
- Command line
-
hf download hf://WWsCa/VetNet-LCPS/vetcopilot-app/src/hooks/useFormValidation.ts
-
curl -L -o useFormValidation.ts https://huggingface.co/WWsCa/VetNet-LCPS/resolve/main/vetcopilot-app/src/hooks/useFormValidation.ts
5.64 kB
| /** | |
| * 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'), | |
| * }, | |
| * }) | |
| * | |
| * <input {...getFieldProps('phone', phone, setPhone)} /> | |
| * | |
| * @template Fields - 表单字段名联合类型 | |
| */ | |
| import { useState, useCallback, useRef } from 'react' | |
| /** 单个字段的验证函数:返回错误消息(空字符串表示通过) */ | |
| export type ValidationRule<T = string> = (value: T) => string | |
| /** 验证规则配置 */ | |
| export interface ValidationRules { | |
| [field: string]: ValidationRule | |
| } | |
| /** Hook 配置选项 */ | |
| export interface UseFormValidationOptions { | |
| /** 验证规则映射:字段名 → 验证函数 */ | |
| rules: ValidationRules | |
| /** 初始触屏状态 */ | |
| initialTouched?: Record<string, boolean> | |
| } | |
| /** Hook 返回值 */ | |
| export interface UseFormValidationReturn<T extends Record<string, string>> { | |
| /** 当前错误:field → error message,无错误时字段不存在 */ | |
| errors: Partial<Record<keyof T & string, string>> | |
| /** 触屏状态:field → has been interacted with */ | |
| touched: Record<string, boolean> | |
| /** | |
| * 获取某个字段的通用 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<Record<keyof T & string, string>> } | |
| /** 重置全部状态 */ | |
| reset: () => void | |
| } | |
| export function useFormValidation<T extends Record<string, string>>( | |
| options: UseFormValidationOptions | |
| ): UseFormValidationReturn<T> { | |
| const { rules, initialTouched = {} } = options | |
| const [errors, setErrors] = useState<Partial<Record<string, string>>>({}) | |
| const [touched, setTouched] = useState<Record<string, boolean>>(initialTouched) | |
| // 用于控制验证图标动画:空字符 = 未验证, 'error' = 错误, 'valid' = 通过 | |
| const [fieldStatus, setFieldStatus] = useState<Record<string, '' | 'error' | 'valid'>>({}) | |
| /** 验证单个字段 */ | |
| 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<string, string> = {} | |
| const newTouched: Record<string, boolean> = {} | |
| const newStatus: Record<string, 'error' | 'valid'> = {} | |
| 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<Record<keyof T & string, string>> } | |
| }, | |
| [rules, validateField] | |
| ) | |
| /** 重置 */ | |
| const reset = useCallback(() => { | |
| setErrors({}) | |
| setTouched({}) | |
| setFieldStatus({}) | |
| }, []) | |
| return { | |
| errors, | |
| touched, | |
| getFieldStatus, | |
| handleChange, | |
| handleBlur, | |
| validateAll, | |
| reset, | |
| } | |
| } | |