flnxi-Wu/app/admin/_components/template-manager/editor/FieldValidationConfig.tsx

331 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'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-1 gap-3 sm:grid-cols-2">
<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-1 gap-3 sm:grid-cols-2">
{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-1 gap-3 sm:grid-cols-2">
<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-1 gap-3 sm:grid-cols-2">
{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]&#123;2&#125;\d&#123;4&#125;$ 匹配两个大写字母加四位数字
</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>
)
}