VetNet-LCPS / vetcopilot-app /src /hooks /useFormValidation.ts
thomasm291678
VetCopilot-LCPS: LCPS 2.0 本地推理引擎替换 DeepSeek API
793e953
Raw History Blame Contribute Delete
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,
}
}