import { detectCircularDependencies, getAllOperators, parseFieldConditions, type FieldConditions, } from './field-conditions' import { parseValidationRules, validateValidationRules, getSupportedValidations, type ValidationRule, } from './field-validation' const ALLOWED_TEMPLATE_STATUSES = new Set(['draft', 'active', 'archived']) export const UPLOAD_TEMPLATE_FIELD_LIMITS = { maxFields: 80, maxKeyLength: 64, maxLabelLength: 80, maxOptions: 200, maxOptionLength: 80, } as const const ALLOWED_FIELD_TYPES = new Set([ 'text', 'textarea', 'number', 'select', 'checkbox', 'radio', 'date', 'uppercase', ]) const OPTION_FIELD_TYPES = new Set(['select', 'radio']) const CONDITION_OPERATORS = new Set(getAllOperators().map((operator) => operator.value)) const CONDITION_OPERATORS_REQUIRING_VALUE = new Set( getAllOperators() .filter((operator) => operator.needsValue) .map((operator) => operator.value) ) export type FormattedUploadTemplateField = { id?: string | null key: string label: string type: string required: boolean orderIndex: number layoutWidth: string validations: string | null optionsSource: string | null optionsStatic: string | null allowedExtensions: string | null visibleOnHomeFilter: boolean isSearchable: boolean isReadOnly: boolean addToCategory: boolean conditions: string | null } export type UploadTemplateSaveValidationResult = | { valid: true; fields: FormattedUploadTemplateField[] } | { valid: false error: string field: string details?: Record } export function isValidUploadTemplateStatus(status: unknown): status is string { return typeof status === 'string' && ALLOWED_TEMPLATE_STATUSES.has(status) } function normalizeStaticOptions(value: unknown): { value: string | null; invalid: boolean } { if (value === null || value === undefined) { return { value: null, invalid: false } } if (Array.isArray(value)) { const options = value .map((option) => String(option ?? '').trim()) .filter(Boolean) return { value: options.length > 0 ? JSON.stringify(Array.from(new Set(options))) : null, invalid: false, } } if (typeof value === 'string') { const trimmed = value.trim() if (!trimmed) { return { value: null, invalid: false } } try { const parsed = JSON.parse(trimmed) return normalizeStaticOptions(parsed) } catch { const options = trimmed .split('\n') .map((option) => option.trim()) .filter(Boolean) return { value: options.length > 0 ? JSON.stringify(Array.from(new Set(options))) : null, invalid: false, } } } return { value: null, invalid: true } } function parseNormalizedOptions(value: string | null): string[] { if (!value) { return [] } try { const parsed = JSON.parse(value) return Array.isArray(parsed) ? parsed.map((option) => String(option ?? '').trim()).filter(Boolean) : [] } catch { return [] } } function normalizeValidations( value: unknown, fieldType: string, ): { value: string | null; invalid?: string; rules?: ValidationRule } { if (value === null || value === undefined) { return { value: null } } if (typeof value === 'string' && value.trim() === '') { return { value: null } } let raw: unknown = value if (typeof value === 'string') { try { raw = JSON.parse(value) } catch { return { value: null, invalid: '验证规则必须是合法 JSON 对象' } } } if (!raw || typeof raw !== 'object' || Array.isArray(raw)) { return { value: null, invalid: '验证规则必须是对象' } } const parsedRules = parseValidationRules(JSON.stringify(raw)) if (!parsedRules) { return { value: null, invalid: '验证规则必须是对象' } } const validation = validateValidationRules(parsedRules) if (!validation.valid) { return { value: null, invalid: validation.error || '验证规则配置无效' } } const supportedValidations = new Set(getSupportedValidations(fieldType)) const unsupportedRules = [ ['minLength', parsedRules.minLength], ['maxLength', parsedRules.maxLength], ['pattern', parsedRules.pattern], ['min', parsedRules.min], ['max', parsedRules.max], ] .filter(([ruleKey, ruleValue]) => ruleValue !== undefined && !supportedValidations.has(String(ruleKey))) .map(([ruleKey]) => String(ruleKey)) if (unsupportedRules.length > 0) { return { value: null, invalid: `字段类型 ${fieldType} 不支持验证规则:${unsupportedRules.join(', ')}`, } } return { value: JSON.stringify(parsedRules), rules: parsedRules, } } function normalizeConditions( value: unknown, ): { value: string | null; invalid: boolean; conditions?: FieldConditions } { if (value === null || value === undefined) { return { value: null, invalid: false } } if (typeof value === 'string' && value.trim() === '') { return { value: null, invalid: false } } let raw: any try { raw = typeof value === 'string' ? JSON.parse(value) : value } catch { return { value: null, invalid: true } } if (!raw || typeof raw !== 'object') { return { value: null, invalid: true } } if (raw.logic !== undefined && raw.logic !== 'AND' && raw.logic !== 'OR') { return { value: null, invalid: true } } if (!Array.isArray(raw.conditions)) { return { value: null, invalid: true } } if (raw.conditions.length === 0) { return { value: null, invalid: false } } for (const condition of raw.conditions) { if (!condition || typeof condition !== 'object') { return { value: null, invalid: true } } if (!condition.fieldKey || !condition.operator) { return { value: null, invalid: true } } } const parsed = parseFieldConditions(raw as FieldConditions) if (!parsed || parsed.conditions.length !== raw.conditions.length) { return { value: null, invalid: true } } return { value: JSON.stringify(parsed), invalid: false, conditions: parsed } } export function validateAndFormatUploadTemplateFields( fields: unknown, ): UploadTemplateSaveValidationResult { if (!Array.isArray(fields)) { return { valid: false, error: '字段列表必须是数组', field: 'fields', } } if (fields.length > UPLOAD_TEMPLATE_FIELD_LIMITS.maxFields) { return { valid: false, error: `单个模板最多支持 ${UPLOAD_TEMPLATE_FIELD_LIMITS.maxFields} 个字段,请拆分模板或减少字段数量`, field: 'fields', details: { maxFields: UPLOAD_TEMPLATE_FIELD_LIMITS.maxFields, currentFields: fields.length, }, } } const fieldKeys = new Set() const duplicateKeys: string[] = [] const formattedFields: FormattedUploadTemplateField[] = [] for (let index = 0; index < fields.length; index += 1) { const rawField = fields[index] const field = (rawField || {}) as Record const key = String(field.key || '').trim() if (!key) { return { valid: false, error: '字段key不能为空', field: 'fields', } } if (fieldKeys.has(key)) { duplicateKeys.push(key) } else { fieldKeys.add(key) } const label = String(field.label || '').trim() || `字段${index + 1}` const type = String(field.type || 'text').trim() || 'text' const addToCategory = !!field.addToCategory const normalizedOptions = normalizeStaticOptions(field.optionsStatic) const optionsStatic = normalizedOptions.value const normalizedValidations = normalizeValidations(field.validations, type) const normalizedConditions = normalizeConditions(field.conditions) const conditions = normalizedConditions.value if (!/^[a-zA-Z][a-zA-Z0-9_]*$/.test(key)) { return { valid: false, error: `字段“${label}”的 key 必须以字母开头,并且只能包含字母、数字和下划线`, field: 'fields', details: { fieldKey: key }, } } if (key.length > UPLOAD_TEMPLATE_FIELD_LIMITS.maxKeyLength) { return { valid: false, error: `字段“${label}”的 key 不能超过 ${UPLOAD_TEMPLATE_FIELD_LIMITS.maxKeyLength} 个字符`, field: 'fields', details: { fieldKey: key, maxKeyLength: UPLOAD_TEMPLATE_FIELD_LIMITS.maxKeyLength, }, } } if (label.length > UPLOAD_TEMPLATE_FIELD_LIMITS.maxLabelLength) { return { valid: false, error: `字段“${label.slice(0, 20)}...”的名称不能超过 ${UPLOAD_TEMPLATE_FIELD_LIMITS.maxLabelLength} 个字符`, field: 'fields', details: { fieldKey: key, maxLabelLength: UPLOAD_TEMPLATE_FIELD_LIMITS.maxLabelLength, }, } } if (!ALLOWED_FIELD_TYPES.has(type)) { return { valid: false, error: `字段“${label}”的类型不支持:${type}`, field: 'fields', details: { fieldKey: key, fieldType: type, allowedTypes: Array.from(ALLOWED_FIELD_TYPES), }, } } if (OPTION_FIELD_TYPES.has(type)) { if (normalizedOptions.invalid) { return { valid: false, error: `字段“${label}”的选项必须是数组或每行一个选项`, field: 'fields', details: { fieldKey: key }, } } const options = parseNormalizedOptions(optionsStatic) if (options.length > UPLOAD_TEMPLATE_FIELD_LIMITS.maxOptions) { return { valid: false, error: `字段“${label}”的选项不能超过 ${UPLOAD_TEMPLATE_FIELD_LIMITS.maxOptions} 个`, field: 'fields', details: { fieldKey: key, maxOptions: UPLOAD_TEMPLATE_FIELD_LIMITS.maxOptions, currentOptions: options.length, }, } } const tooLongOption = options.find((option) => option.length > UPLOAD_TEMPLATE_FIELD_LIMITS.maxOptionLength) if (tooLongOption) { return { valid: false, error: `字段“${label}”的单个选项不能超过 ${UPLOAD_TEMPLATE_FIELD_LIMITS.maxOptionLength} 个字符`, field: 'fields', details: { fieldKey: key, maxOptionLength: UPLOAD_TEMPLATE_FIELD_LIMITS.maxOptionLength, sample: tooLongOption.slice(0, 20), }, } } if (field.required && !addToCategory && options.length === 0) { return { valid: false, error: `字段“${label}”是必填${type === 'radio' ? '单选' : '下拉'}字段,至少需要配置一个选项`, field: 'fields', details: { fieldKey: key }, } } } if (normalizedValidations.invalid) { return { valid: false, error: `字段“${label}”的验证规则不正确:${normalizedValidations.invalid}`, field: 'fields', details: { fieldKey: key }, } } if (normalizedConditions.invalid) { return { valid: false, error: `字段“${label}”的显示条件格式不正确`, field: 'fields', details: { fieldKey: key }, } } formattedFields.push({ id: field.id ?? null, key, label, type, required: !!field.required, orderIndex: field.orderIndex ?? index, layoutWidth: field.layoutWidth || 'full', validations: normalizedValidations.value, optionsSource: field.optionsSource === null || field.optionsSource === undefined ? null : String(field.optionsSource), optionsStatic, allowedExtensions: field.allowedExtensions === null || field.allowedExtensions === undefined ? null : String(field.allowedExtensions), visibleOnHomeFilter: !!field.visibleOnHomeFilter, isSearchable: !!field.isSearchable, isReadOnly: !!field.isReadOnly, addToCategory, conditions, }) } if (duplicateKeys.length > 0) { return { valid: false, error: `字段key重复:${duplicateKeys.join(', ')}`, field: 'fields', details: { duplicateKeys }, } } const availableKeys = new Set(formattedFields.map((field) => field.key)) for (const field of formattedFields) { const conditions = parseFieldConditions(field.conditions) if (!conditions) { continue } for (const condition of conditions.conditions) { if (!CONDITION_OPERATORS.has(condition.operator)) { return { valid: false, error: `字段“${field.label}”的显示条件包含不支持的判断方式`, field: 'fields', details: { fieldKey: field.key, operator: condition.operator }, } } if (!availableKeys.has(condition.fieldKey)) { return { valid: false, error: `字段“${field.label}”的显示条件引用了不存在的字段:${condition.fieldKey}`, field: 'fields', details: { fieldKey: field.key, dependencyKey: condition.fieldKey }, } } if (condition.fieldKey === field.key) { return { valid: false, error: `字段“${field.label}”的显示条件不能引用自己`, field: 'fields', details: { fieldKey: field.key }, } } if ( CONDITION_OPERATORS_REQUIRING_VALUE.has(condition.operator) && String(condition.value ?? '').trim() === '' ) { return { valid: false, error: `字段“${field.label}”的显示条件缺少比较值`, field: 'fields', details: { fieldKey: field.key, operator: condition.operator }, } } } } const circularFields = detectCircularDependencies(formattedFields) if (circularFields.length > 0) { return { valid: false, error: `字段显示条件存在循环依赖:${circularFields.join(', ')}`, field: 'fields', details: { circularFields }, } } return { valid: true, fields: formattedFields, } }