import { detectCircularDependencies, getAllOperators, parseFieldConditions, type FieldConditions, } from './field-conditions' const ALLOWED_TEMPLATE_STATUSES = new Set(['draft', 'active', 'archived']) 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 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', } } 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') const addToCategory = !!field.addToCategory const normalizedOptions = normalizeStaticOptions(field.optionsStatic) const optionsStatic = normalizedOptions.value const normalizedConditions = normalizeConditions(field.conditions) const conditions = normalizedConditions.value if (OPTION_FIELD_TYPES.has(type)) { if (normalizedOptions.invalid) { return { valid: false, error: `字段“${label}”的选项必须是数组或每行一个选项`, field: 'fields', details: { fieldKey: key }, } } const options = parseNormalizedOptions(optionsStatic) if (field.required && !addToCategory && options.length === 0) { return { valid: false, error: `字段“${label}”是必填${type === 'radio' ? '单选' : '下拉'}字段,至少需要配置一个选项`, 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: field.validations === null || field.validations === undefined ? null : String(field.validations), 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, } }