331 lines
12 KiB
TypeScript
331 lines
12 KiB
TypeScript
'use client'
|
||
|
||
import { useState, useEffect, useMemo } from 'react'
|
||
import { cn } from '@/lib/ui-styles'
|
||
import {
|
||
ValidationRule,
|
||
parseValidationRules,
|
||
serializeValidationRules,
|
||
validateValidationRules,
|
||
getSupportedValidations,
|
||
DEFAULT_VALIDATION_RULES,
|
||
} from '@/lib/field-validation'
|
||
|
||
interface FieldValidationConfigProps {
|
||
fieldType: string
|
||
validations: string
|
||
onChange: (validations: string) => void
|
||
className?: string
|
||
}
|
||
|
||
export default function FieldValidationConfig({
|
||
fieldType,
|
||
validations,
|
||
onChange,
|
||
className,
|
||
}: FieldValidationConfigProps) {
|
||
// 解析当前验证规则
|
||
const currentRules = useMemo(() => {
|
||
return parseValidationRules(validations) || { ...DEFAULT_VALIDATION_RULES }
|
||
}, [validations])
|
||
|
||
// 本地状态用于编辑
|
||
const [rules, setRules] = useState<ValidationRule>(currentRules)
|
||
const [error, setError] = useState<string | null>(null)
|
||
const [isExpanded, setIsExpanded] = useState(false)
|
||
|
||
// 当外部 validations 变化时同步
|
||
useEffect(() => {
|
||
setRules(currentRules)
|
||
}, [currentRules])
|
||
|
||
// 获取当前字段类型支持的验证规则
|
||
const supportedValidations = useMemo(() => {
|
||
return getSupportedValidations(fieldType)
|
||
}, [fieldType])
|
||
|
||
// 更新规则并通知父组件
|
||
const updateRules = (patch: Partial<ValidationRule>) => {
|
||
const newRules = { ...rules, ...patch }
|
||
setRules(newRules)
|
||
|
||
// 验证规则是否有效
|
||
const validation = validateValidationRules(newRules)
|
||
if (!validation.valid) {
|
||
setError(validation.error || '验证规则配置无效')
|
||
return
|
||
}
|
||
setError(null)
|
||
|
||
// 序列化并通知父组件
|
||
const serialized = serializeValidationRules(newRules)
|
||
onChange(serialized)
|
||
}
|
||
|
||
// 检查是否有任何验证规则配置
|
||
const hasAnyRules = useMemo(() => {
|
||
return (
|
||
rules.required ||
|
||
(typeof rules.minLength === 'number' && rules.minLength > 0) ||
|
||
(typeof rules.maxLength === 'number' && rules.maxLength > 0) ||
|
||
typeof rules.min === 'number' ||
|
||
typeof rules.max === 'number' ||
|
||
!!rules.pattern
|
||
)
|
||
}, [rules])
|
||
|
||
const inputClass =
|
||
'w-full px-3 py-2 text-sm border border-slate-300 dark:border-slate-600 dark:bg-slate-900 dark:text-white rounded-lg focus:ring-2 focus:ring-slate-200 dark:focus:ring-slate-700 focus:border-slate-500 dark:focus:border-slate-500 outline-none transition-colors duration-150'
|
||
const labelClass = 'block text-xs font-medium text-slate-700 dark:text-slate-300 mb-1'
|
||
const hintClass = 'text-[10px] text-slate-500 dark:text-slate-400 mt-1'
|
||
|
||
return (
|
||
<div className={cn('mt-3', className)}>
|
||
{/* 折叠标题 */}
|
||
<button
|
||
type="button"
|
||
onClick={() => setIsExpanded(!isExpanded)}
|
||
className="flex items-center gap-2 text-xs font-medium text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-200 transition-colors"
|
||
>
|
||
<svg
|
||
className={cn('w-4 h-4 transition-transform', isExpanded && 'rotate-90')}
|
||
fill="none"
|
||
stroke="currentColor"
|
||
viewBox="0 0 24 24"
|
||
>
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||
</svg>
|
||
<span>验证规则配置</span>
|
||
{hasAnyRules && (
|
||
<span className="px-1.5 py-0.5 text-[10px] bg-slate-100 dark:bg-slate-700 text-slate-700 dark:text-slate-300 rounded">
|
||
已配置
|
||
</span>
|
||
)}
|
||
</button>
|
||
|
||
{/* 展开的配置面板 */}
|
||
{isExpanded && (
|
||
<div className="mt-3 p-4 bg-slate-50 dark:bg-slate-800/50 rounded-lg border border-slate-200 dark:border-slate-700 space-y-4">
|
||
{/* 错误提示 */}
|
||
{error && (
|
||
<div className="p-2 bg-red-100 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg text-xs text-red-700 dark:text-red-300">
|
||
{error}
|
||
</div>
|
||
)}
|
||
|
||
{/* 必填验证 - 所有类型都支持 */}
|
||
{supportedValidations.includes('required') && (
|
||
<div className="space-y-2">
|
||
<label className="flex items-center gap-2 cursor-pointer">
|
||
<input
|
||
type="checkbox"
|
||
checked={rules.required || false}
|
||
onChange={(e) => updateRules({ required: e.target.checked })}
|
||
className="w-4 h-4 rounded border-slate-300 dark:border-slate-600 text-slate-700 focus:ring-2 focus:ring-slate-300 dark:focus:ring-slate-600"
|
||
/>
|
||
<span className="text-sm text-slate-700 dark:text-slate-300">必填字段</span>
|
||
</label>
|
||
{rules.required && (
|
||
<div>
|
||
<label className={labelClass}>自定义必填提示(可选)</label>
|
||
<input
|
||
type="text"
|
||
value={rules.requiredMessage || ''}
|
||
onChange={(e) => updateRules({ requiredMessage: e.target.value || undefined })}
|
||
placeholder="例如:请填写商品型号"
|
||
className={inputClass}
|
||
/>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* 长度验证 - text/textarea */}
|
||
{supportedValidations.includes('minLength') && (
|
||
<div className="grid grid-cols-2 gap-3">
|
||
<div>
|
||
<label className={labelClass}>最小长度</label>
|
||
<input
|
||
type="number"
|
||
min="0"
|
||
value={rules.minLength ?? ''}
|
||
onChange={(e) =>
|
||
updateRules({
|
||
minLength: e.target.value ? parseInt(e.target.value, 10) : undefined,
|
||
})
|
||
}
|
||
placeholder="不限制"
|
||
className={inputClass}
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className={labelClass}>最大长度</label>
|
||
<input
|
||
type="number"
|
||
min="0"
|
||
value={rules.maxLength ?? ''}
|
||
onChange={(e) =>
|
||
updateRules({
|
||
maxLength: e.target.value ? parseInt(e.target.value, 10) : undefined,
|
||
})
|
||
}
|
||
placeholder="不限制"
|
||
className={inputClass}
|
||
/>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 自定义长度错误提示 */}
|
||
{supportedValidations.includes('minLength') &&
|
||
(typeof rules.minLength === 'number' || typeof rules.maxLength === 'number') && (
|
||
<div className="grid grid-cols-2 gap-3">
|
||
{typeof rules.minLength === 'number' && (
|
||
<div>
|
||
<label className={labelClass}>最小长度提示(可选)</label>
|
||
<input
|
||
type="text"
|
||
value={rules.minLengthMessage || ''}
|
||
onChange={(e) =>
|
||
updateRules({ minLengthMessage: e.target.value || undefined })
|
||
}
|
||
placeholder={`至少${rules.minLength}个字符`}
|
||
className={inputClass}
|
||
/>
|
||
</div>
|
||
)}
|
||
{typeof rules.maxLength === 'number' && (
|
||
<div>
|
||
<label className={labelClass}>最大长度提示(可选)</label>
|
||
<input
|
||
type="text"
|
||
value={rules.maxLengthMessage || ''}
|
||
onChange={(e) =>
|
||
updateRules({ maxLengthMessage: e.target.value || undefined })
|
||
}
|
||
placeholder={`最多${rules.maxLength}个字符`}
|
||
className={inputClass}
|
||
/>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* 数值范围验证 - number */}
|
||
{supportedValidations.includes('min') && (
|
||
<div className="grid grid-cols-2 gap-3">
|
||
<div>
|
||
<label className={labelClass}>最小值</label>
|
||
<input
|
||
type="number"
|
||
value={rules.min ?? ''}
|
||
onChange={(e) =>
|
||
updateRules({
|
||
min: e.target.value ? parseFloat(e.target.value) : undefined,
|
||
})
|
||
}
|
||
placeholder="不限制"
|
||
className={inputClass}
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className={labelClass}>最大值</label>
|
||
<input
|
||
type="number"
|
||
value={rules.max ?? ''}
|
||
onChange={(e) =>
|
||
updateRules({
|
||
max: e.target.value ? parseFloat(e.target.value) : undefined,
|
||
})
|
||
}
|
||
placeholder="不限制"
|
||
className={inputClass}
|
||
/>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 自定义数值范围错误提示 */}
|
||
{supportedValidations.includes('min') &&
|
||
(typeof rules.min === 'number' || typeof rules.max === 'number') && (
|
||
<div className="grid grid-cols-2 gap-3">
|
||
{typeof rules.min === 'number' && (
|
||
<div>
|
||
<label className={labelClass}>最小值提示(可选)</label>
|
||
<input
|
||
type="text"
|
||
value={rules.minMessage || ''}
|
||
onChange={(e) => updateRules({ minMessage: e.target.value || undefined })}
|
||
placeholder={`不能小于${rules.min}`}
|
||
className={inputClass}
|
||
/>
|
||
</div>
|
||
)}
|
||
{typeof rules.max === 'number' && (
|
||
<div>
|
||
<label className={labelClass}>最大值提示(可选)</label>
|
||
<input
|
||
type="text"
|
||
value={rules.maxMessage || ''}
|
||
onChange={(e) => updateRules({ maxMessage: e.target.value || undefined })}
|
||
placeholder={`不能大于${rules.max}`}
|
||
className={inputClass}
|
||
/>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* 正则表达式验证 - text/textarea */}
|
||
{supportedValidations.includes('pattern') && (
|
||
<div className="space-y-2">
|
||
<div>
|
||
<label className={labelClass}>正则表达式验证</label>
|
||
<input
|
||
type="text"
|
||
value={rules.pattern || ''}
|
||
onChange={(e) => updateRules({ pattern: e.target.value || undefined })}
|
||
placeholder="例如:^[A-Z]{2}\d{4}$"
|
||
className={inputClass}
|
||
/>
|
||
<p className={hintClass}>
|
||
输入正则表达式来验证输入格式,例如:^[A-Z]{2}\d{4}$ 匹配两个大写字母加四位数字
|
||
</p>
|
||
</div>
|
||
{rules.pattern && (
|
||
<div>
|
||
<label className={labelClass}>格式错误提示(可选)</label>
|
||
<input
|
||
type="text"
|
||
value={rules.patternMessage || ''}
|
||
onChange={(e) => updateRules({ patternMessage: e.target.value || undefined })}
|
||
placeholder="例如:请输入正确的编码格式(如:AB1234)"
|
||
className={inputClass}
|
||
/>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* 清除所有规则按钮 */}
|
||
{hasAnyRules && (
|
||
<div className="pt-2 border-t border-slate-200 dark:border-slate-700">
|
||
<button
|
||
type="button"
|
||
onClick={() => {
|
||
setRules({ ...DEFAULT_VALIDATION_RULES })
|
||
onChange('')
|
||
setError(null)
|
||
}}
|
||
className="text-xs text-red-600 dark:text-red-400 hover:text-red-700 dark:hover:text-red-300 transition-colors"
|
||
>
|
||
清除所有验证规则
|
||
</button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|