VetNet-LCPS / vetcopilot-app /src /utils /validation.ts
thomasm291678
VetCopilot-LCPS: LCPS 2.0 本地推理引擎替换 DeepSeek API
793e953
Raw History Blame Contribute Delete
7.25 kB
/**
* 通用表单验证工具
*/
/** 验证结果:通过返回 null,失败返回错误消息 */
export type ValidationResult = string | null
/** 字段级错误集合 */
export type FieldErrors = Record<string, string>
/** 中国手机号格式 */
const PHONE_REGEX = /^1[3-9]\d{9}$/
/** 邮箱格式 */
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
/** 动物名称:2-20字符,中英文数字 */
const ANIMAL_NAME_REGEX = /^[\u4e00-\u9fa5a-zA-Z0-9]{2,20}$/
/** 主人姓名:2-20字符,中文(允许中间有 · ) */
const OWNER_NAME_REGEX = /^[\u4e00-\u9fa5·]{2,20}$/
/** 中文年龄格式:数字 + 岁/月/周/天/年 */
const AGE_REGEX = /^(\d+(\.\d+)?)\s*(岁|月|周|天|年)$/
// ============================================================
// 基础验证
// ============================================================
/** 验证手机号 */
export function validatePhone(value: string): ValidationResult {
if (!value || !value.trim()) return '请输入手机号'
if (!PHONE_REGEX.test(value.trim())) return '手机号格式不正确(11位数字)'
return null
}
/** 验证必填字段 */
export function validateRequired(value: string, label: string): ValidationResult {
if (!value || !value.trim()) return `请输入${label}`
return null
}
/** 验证正数(体重等) */
export function validatePositiveNumber(value: string, label: string): ValidationResult {
if (!value || !value.trim()) return null // 可选字段
const num = Number(value)
if (isNaN(num) || num <= 0) return `${label}必须为正数`
return null
}
/** 验证日期不早于今天(用于下次回访日期等未来日期) */
export function validateFutureDate(value: string, label: string): ValidationResult {
if (!value) return null // 可选字段
const date = new Date(value)
const today = new Date()
today.setHours(0, 0, 0, 0)
if (date < today) return `${label}不能早于今天`
return null
}
/** 验证结束日期不早于开始日期 */
export function validateDateRange(startDate: string, endDate: string, startLabel: string, endLabel: string): ValidationResult {
if (!startDate || !endDate) return null
if (new Date(endDate) < new Date(startDate)) return `${endLabel}不能早于${startLabel}`
return null
}
// ============================================================
// 新增验证函数
// ============================================================
/** 验证邮箱格式 */
export function validateEmail(value: string): ValidationResult {
if (!value || !value.trim()) return null // 可选字段
if (!EMAIL_REGEX.test(value.trim())) return '邮箱格式不正确'
return null
}
/**
* 验证年龄
* 支持格式:
* - 纯数字(视为"岁"):3 → 3岁
* - 带单位:3岁 / 6个月 / 2周 / 14天 / 1年
* 范围:0-50岁(月/周/天会自动换算校验)
*/
export function validateAge(value: string): ValidationResult {
if (!value || !value.trim()) return null // 可选字段
const trimmed = value.trim()
// 尝试匹配中文年龄格式
const ageMatch = trimmed.match(AGE_REGEX)
if (ageMatch) {
const num = parseFloat(ageMatch[1])
const unit = ageMatch[3]
switch (unit) {
case '岁':
case '年':
if (num < 0 || num > 50) return '年龄应在 0-50 岁之间'
break
case '月':
if (num < 0 || num > 600) return '年龄应在 0-50 岁之间(600个月)'
break
case '周':
if (num < 0 || num > 2600) return '年龄应在 0-50 岁之间(2600周)'
break
case '天':
if (num < 0 || num > 18250) return '年龄应在 0-50 岁之间(18250天)'
break
}
return null
}
// 纯数字视为"岁"
const pureNum = Number(trimmed)
if (!isNaN(pureNum) && pureNum >= 0 && pureNum <= 50) return null
return '年龄格式不正确(如:3岁 / 6个月 / 2周)'
}
/** 验证体重(正数,0.01-200kg) */
export function validateWeight(value: string): ValidationResult {
if (!value || !value.trim()) return null // 可选字段
const num = Number(value)
if (isNaN(num)) return '体重必须为数字'
if (num <= 0) return '体重必须为正数'
if (num < 0.01) return '体重最小为 0.01 kg'
if (num > 200) return '体重不能超过 200 kg'
return null
}
/**
* 验证密码强度
* - 至少 6 位
* - 建议包含字母和数字(仅警告,不阻断)
*/
export function validatePassword(value: string): ValidationResult {
if (!value) return '请输入密码'
if (value.length < 6) return '密码至少 6 位'
// 建议包含字母和数字(弱密码仅提示,不阻断提交)
const hasLetter = /[a-zA-Z]/.test(value)
const hasDigit = /\d/.test(value)
if (!hasLetter || !hasDigit) {
return '建议包含字母和数字以增强安全性'
}
return null
}
/** 验证密码强度(严格模式,用于注册等场景)
* - 至少 6 位
* - 必须包含字母和数字
*/
export function validatePasswordStrict(value: string): ValidationResult {
if (!value) return '请输入密码'
if (value.length < 6) return '密码至少 6 位'
const hasLetter = /[a-zA-Z]/.test(value)
const hasDigit = /\d/.test(value)
if (!hasLetter || !hasDigit) return '密码必须包含字母和数字'
return null
}
/** 注册/登录密码校验:仅校验最小长度,不阻断弱密码 */
export function validatePasswordBasic(value: string): ValidationResult {
if (!value) return '请输入密码'
if (value.length < 6) return '密码至少 6 位'
return null
}
/** 验证动物名称(2-20字符,中英文数字) */
export function validateAnimalName(value: string): ValidationResult {
if (!value || !value.trim()) return '请输入动物名称'
const trimmed = value.trim()
if (trimmed.length < 2) return '动物名称至少 2 个字符'
if (trimmed.length > 20) return '动物名称最多 20 个字符'
if (!ANIMAL_NAME_REGEX.test(trimmed)) return '动物名称仅支持中英文和数字'
return null
}
/** 验证主人姓名(2-20字符,中文,允许中间有 ·) */
export function validateOwnerName(value: string): ValidationResult {
if (!value || !value.trim()) return '请输入宠主姓名'
const trimmed = value.trim()
if (trimmed.length < 2) return '宠主姓名至少 2 个字符'
if (trimmed.length > 20) return '宠主姓名最多 20 个字符'
if (!OWNER_NAME_REGEX.test(trimmed)) return '宠主姓名仅支持中文'
return null
}
/** 验证最大长度 */
export function validateMaxLength(value: string, max: number, label: string): ValidationResult {
if (!value) return null
if (value.length > max) return `${label}不能超过 ${max} 个字符`
return null
}
// ============================================================
// 批量验证
// ============================================================
/** 批量验证字段,返回第一个错误或 null */
export function validateFields(validations: Array<{ field: string; result: ValidationResult }>): FieldErrors | null {
const errors: FieldErrors = {}
for (const { field, result } of validations) {
if (result) errors[field] = result
}
return Object.keys(errors).length > 0 ? errors : null
}