表单设计器
This commit is contained in:
parent
6c8ded573b
commit
8c9813ed55
@ -3,9 +3,10 @@
|
||||
* 预览设计好的表单效果
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Form, Button, message } from 'antd';
|
||||
import type { FieldConfig, FormConfig } from '../types';
|
||||
import type { Rule } from 'antd/es/form';
|
||||
import type { FieldConfig, FormConfig, ValidationRule, LinkageRule } from '../types';
|
||||
import FieldRenderer from './FieldRenderer';
|
||||
import GridFieldPreview from './GridFieldPreview';
|
||||
import '../styles.css';
|
||||
@ -18,6 +19,11 @@ interface FormPreviewProps {
|
||||
const FormPreview: React.FC<FormPreviewProps> = ({ fields, formConfig }) => {
|
||||
const [form] = Form.useForm();
|
||||
const [formData, setFormData] = useState<Record<string, any>>({});
|
||||
const [fieldStates, setFieldStates] = useState<Record<string, {
|
||||
visible?: boolean;
|
||||
disabled?: boolean;
|
||||
required?: boolean;
|
||||
}>>({});
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
@ -36,6 +42,159 @@ const FormPreview: React.FC<FormPreviewProps> = ({ fields, formConfig }) => {
|
||||
message.info('表单已重置');
|
||||
};
|
||||
|
||||
// 将验证规则转换为Ant Design的Rule数组
|
||||
const convertValidationRules = (validationRules?: ValidationRule[]): Rule[] => {
|
||||
if (!validationRules || validationRules.length === 0) return [];
|
||||
|
||||
return validationRules.map(rule => {
|
||||
const antdRule: Rule = {
|
||||
type: rule.type === 'email' ? 'email' : rule.type === 'url' ? 'url' : undefined,
|
||||
message: rule.message || `请输入正确的${rule.type}`,
|
||||
// 注意:不设置 trigger,或者不仅仅设置为 blur,这样提交时也会触发验证
|
||||
// 如果用户指定了 trigger,我们也在提交时触发
|
||||
};
|
||||
|
||||
switch (rule.type) {
|
||||
case 'required':
|
||||
antdRule.required = true;
|
||||
// 必填验证始终在提交时触发,不管用户选择了什么 trigger
|
||||
break;
|
||||
case 'pattern':
|
||||
if (rule.value) {
|
||||
antdRule.pattern = new RegExp(rule.value);
|
||||
}
|
||||
break;
|
||||
case 'min':
|
||||
antdRule.type = 'number';
|
||||
antdRule.min = rule.value;
|
||||
break;
|
||||
case 'max':
|
||||
antdRule.type = 'number';
|
||||
antdRule.max = rule.value;
|
||||
break;
|
||||
case 'minLength':
|
||||
antdRule.min = rule.value;
|
||||
break;
|
||||
case 'maxLength':
|
||||
antdRule.max = rule.value;
|
||||
break;
|
||||
case 'phone':
|
||||
antdRule.pattern = /^1[3-9]\d{9}$/;
|
||||
break;
|
||||
case 'idCard':
|
||||
antdRule.pattern = /^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dX]$/;
|
||||
break;
|
||||
}
|
||||
|
||||
return antdRule;
|
||||
});
|
||||
};
|
||||
|
||||
// 评估联动条件
|
||||
const evaluateCondition = (condition: any, formValues: Record<string, any>): boolean => {
|
||||
const fieldValue = formValues[condition.field];
|
||||
const compareValue = condition.value;
|
||||
|
||||
switch (condition.operator) {
|
||||
case '==':
|
||||
return fieldValue == compareValue;
|
||||
case '!=':
|
||||
return fieldValue != compareValue;
|
||||
case '>':
|
||||
return Number(fieldValue) > Number(compareValue);
|
||||
case '<':
|
||||
return Number(fieldValue) < Number(compareValue);
|
||||
case '>=':
|
||||
return Number(fieldValue) >= Number(compareValue);
|
||||
case '<=':
|
||||
return Number(fieldValue) <= Number(compareValue);
|
||||
case 'includes':
|
||||
return Array.isArray(fieldValue) ? fieldValue.includes(compareValue) : String(fieldValue).includes(compareValue);
|
||||
case 'notIncludes':
|
||||
return Array.isArray(fieldValue) ? !fieldValue.includes(compareValue) : !String(fieldValue).includes(compareValue);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// 处理联动规则
|
||||
useEffect(() => {
|
||||
const flattenFields = (fieldList: FieldConfig[]): FieldConfig[] => {
|
||||
const result: FieldConfig[] = [];
|
||||
fieldList.forEach(field => {
|
||||
if (field.type !== 'divider' && field.type !== 'text') {
|
||||
result.push(field);
|
||||
}
|
||||
if (field.type === 'grid' && field.children) {
|
||||
field.children.forEach(columnFields => {
|
||||
result.push(...flattenFields(columnFields));
|
||||
});
|
||||
}
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
const allFields = flattenFields(fields);
|
||||
const newFieldStates: Record<string, any> = {};
|
||||
|
||||
allFields.forEach(field => {
|
||||
if (field.linkageRules && field.linkageRules.length > 0) {
|
||||
field.linkageRules.forEach((rule: LinkageRule) => {
|
||||
// 检查所有条件是否都满足
|
||||
const allConditionsMet = rule.conditions.every(condition =>
|
||||
evaluateCondition(condition, formData)
|
||||
);
|
||||
|
||||
if (allConditionsMet) {
|
||||
if (!newFieldStates[field.name]) {
|
||||
newFieldStates[field.name] = {};
|
||||
}
|
||||
|
||||
switch (rule.type) {
|
||||
case 'visible':
|
||||
newFieldStates[field.name].visible = rule.action;
|
||||
break;
|
||||
case 'disabled':
|
||||
newFieldStates[field.name].disabled = rule.action;
|
||||
break;
|
||||
case 'required':
|
||||
newFieldStates[field.name].required = rule.action;
|
||||
break;
|
||||
case 'value':
|
||||
// 值联动需要直接设置表单值
|
||||
form.setFieldValue(field.name, rule.action);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
setFieldStates(newFieldStates);
|
||||
}, [formData, fields, form]);
|
||||
|
||||
// 设置默认值
|
||||
useEffect(() => {
|
||||
const defaultValues: Record<string, any> = {};
|
||||
|
||||
const collectDefaultValues = (fieldList: FieldConfig[]) => {
|
||||
fieldList.forEach(field => {
|
||||
if (field.defaultValue !== undefined && field.name) {
|
||||
defaultValues[field.name] = field.defaultValue;
|
||||
}
|
||||
if (field.type === 'grid' && field.children) {
|
||||
field.children.forEach(columnFields => {
|
||||
collectDefaultValues(columnFields);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
collectDefaultValues(fields);
|
||||
form.setFieldsValue(defaultValues);
|
||||
setFormData(prev => ({ ...prev, ...defaultValues }));
|
||||
}, [fields, form]);
|
||||
|
||||
// 递归渲染字段(包括栅格布局内的字段)
|
||||
const renderFields = (fieldList: FieldConfig[]): React.ReactNode => {
|
||||
return fieldList.map((field) => {
|
||||
@ -61,6 +220,34 @@ const FormPreview: React.FC<FormPreviewProps> = ({ fields, formConfig }) => {
|
||||
);
|
||||
}
|
||||
|
||||
// 获取字段状态(联动规则影响)
|
||||
const fieldState = fieldStates[field.name] || {};
|
||||
const isVisible = fieldState.visible !== false; // 默认显示
|
||||
const isDisabled = fieldState.disabled || field.disabled || false;
|
||||
const isRequired = fieldState.required !== undefined ? fieldState.required : field.required;
|
||||
|
||||
// 如果字段被隐藏,不渲染
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 合并基础验证规则和自定义验证规则
|
||||
const customRules = convertValidationRules(field.validationRules);
|
||||
|
||||
// 检查自定义验证规则中是否已经包含必填验证
|
||||
const hasRequiredRule = field.validationRules?.some(rule => rule.type === 'required');
|
||||
|
||||
// 基础规则:只有在没有自定义必填验证时,才使用字段属性中的"是否必填"
|
||||
const baseRules: Rule[] = [];
|
||||
if (isRequired && !hasRequiredRule) {
|
||||
baseRules.push({
|
||||
required: true,
|
||||
message: `请输入${field.label}`,
|
||||
});
|
||||
}
|
||||
|
||||
const allRules = [...baseRules, ...customRules];
|
||||
|
||||
// 普通表单组件使用 Form.Item 包裹
|
||||
return (
|
||||
<Form.Item
|
||||
@ -68,15 +255,10 @@ const FormPreview: React.FC<FormPreviewProps> = ({ fields, formConfig }) => {
|
||||
label={field.label}
|
||||
name={field.name}
|
||||
colon={true}
|
||||
rules={[
|
||||
{
|
||||
required: field.required,
|
||||
message: `请输入${field.label}`,
|
||||
},
|
||||
]}
|
||||
rules={allRules}
|
||||
>
|
||||
<FieldRenderer
|
||||
field={field}
|
||||
field={{ ...field, disabled: isDisabled }}
|
||||
value={formData[field.name]}
|
||||
onChange={(value) => setFormData(prev => ({ ...prev, [field.name]: value }))}
|
||||
isPreview={true}
|
||||
|
||||
268
frontend/src/pages/FormDesigner/components/LinkageRuleEditor.tsx
Normal file
268
frontend/src/pages/FormDesigner/components/LinkageRuleEditor.tsx
Normal file
@ -0,0 +1,268 @@
|
||||
/**
|
||||
* 联动规则编辑器
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Select, Input, Button, Space, Typography } from 'antd';
|
||||
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||
import type { LinkageRule, LinkageCondition, FieldConfig } from '../types';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
interface LinkageRuleEditorProps {
|
||||
value?: LinkageRule[];
|
||||
onChange?: (value: LinkageRule[]) => void;
|
||||
allFields?: FieldConfig[]; // 所有字段,用于联动字段选择
|
||||
}
|
||||
|
||||
const LinkageRuleEditor: React.FC<LinkageRuleEditorProps> = ({
|
||||
value = [],
|
||||
onChange,
|
||||
allFields = []
|
||||
}) => {
|
||||
const handleAddRule = () => {
|
||||
const newRule: LinkageRule = {
|
||||
id: `rule_${Date.now()}`,
|
||||
type: 'visible',
|
||||
conditions: [],
|
||||
action: false,
|
||||
};
|
||||
onChange?.([...value, newRule]);
|
||||
};
|
||||
|
||||
const handleDeleteRule = (index: number) => {
|
||||
const newRules = value.filter((_, i) => i !== index);
|
||||
onChange?.(newRules);
|
||||
};
|
||||
|
||||
const handleRuleChange = (index: number, field: keyof LinkageRule, fieldValue: any) => {
|
||||
const newRules = [...value];
|
||||
newRules[index] = { ...newRules[index], [field]: fieldValue };
|
||||
onChange?.(newRules);
|
||||
};
|
||||
|
||||
const handleAddCondition = (ruleIndex: number) => {
|
||||
const newCondition: LinkageCondition = {
|
||||
field: '',
|
||||
operator: '==',
|
||||
value: '',
|
||||
};
|
||||
const newRules = [...value];
|
||||
newRules[ruleIndex] = {
|
||||
...newRules[ruleIndex],
|
||||
conditions: [...newRules[ruleIndex].conditions, newCondition],
|
||||
};
|
||||
onChange?.(newRules);
|
||||
};
|
||||
|
||||
const handleDeleteCondition = (ruleIndex: number, conditionIndex: number) => {
|
||||
const newRules = [...value];
|
||||
newRules[ruleIndex] = {
|
||||
...newRules[ruleIndex],
|
||||
conditions: newRules[ruleIndex].conditions.filter((_, i) => i !== conditionIndex),
|
||||
};
|
||||
onChange?.(newRules);
|
||||
};
|
||||
|
||||
const handleConditionChange = (
|
||||
ruleIndex: number,
|
||||
conditionIndex: number,
|
||||
field: keyof LinkageCondition,
|
||||
fieldValue: any
|
||||
) => {
|
||||
const newRules = [...value];
|
||||
const newConditions = [...newRules[ruleIndex].conditions];
|
||||
newConditions[conditionIndex] = { ...newConditions[conditionIndex], [field]: fieldValue };
|
||||
newRules[ruleIndex] = { ...newRules[ruleIndex], conditions: newConditions };
|
||||
onChange?.(newRules);
|
||||
};
|
||||
|
||||
const renderActionInput = (rule: LinkageRule, index: number) => {
|
||||
switch (rule.type) {
|
||||
case 'visible':
|
||||
case 'disabled':
|
||||
case 'required':
|
||||
return (
|
||||
<div>
|
||||
<Text style={{ fontSize: 12, marginBottom: 4, display: 'block' }}>
|
||||
{rule.type === 'visible' ? '显示/隐藏' : rule.type === 'disabled' ? '禁用/启用' : '必填/非必填'}
|
||||
</Text>
|
||||
<Select
|
||||
style={{ width: '100%' }}
|
||||
value={rule.action}
|
||||
onChange={(val) => handleRuleChange(index, 'action', val)}
|
||||
>
|
||||
<Select.Option value={true}>
|
||||
{rule.type === 'visible' ? '显示' : rule.type === 'disabled' ? '禁用' : '必填'}
|
||||
</Select.Option>
|
||||
<Select.Option value={false}>
|
||||
{rule.type === 'visible' ? '隐藏' : rule.type === 'disabled' ? '启用' : '非必填'}
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</div>
|
||||
);
|
||||
case 'value':
|
||||
return (
|
||||
<div>
|
||||
<Text style={{ fontSize: 12, marginBottom: 4, display: 'block' }}>赋值</Text>
|
||||
<Input
|
||||
placeholder="输入要设置的值"
|
||||
value={rule.action}
|
||||
onChange={(e) => handleRuleChange(index, 'action', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
|
||||
<Text strong>联动规则</Text>
|
||||
<Button type="dashed" size="small" icon={<PlusOutlined />} onClick={handleAddRule}>
|
||||
添加规则
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{value.length === 0 ? (
|
||||
<div style={{ padding: '24px 0', textAlign: 'center', color: '#8c8c8c', fontSize: 12 }}>
|
||||
暂无联动规则,点击上方按钮添加
|
||||
</div>
|
||||
) : (
|
||||
<Space direction="vertical" style={{ width: '100%' }} size="middle">
|
||||
{value.map((rule, ruleIndex) => (
|
||||
<div
|
||||
key={rule.id}
|
||||
style={{
|
||||
padding: 12,
|
||||
border: '1px solid #e8e8e8',
|
||||
borderRadius: 4,
|
||||
background: '#fafafa',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 8 }}>
|
||||
<Text style={{ fontSize: 12, color: '#666' }}>规则 {ruleIndex + 1}</Text>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => handleDeleteRule(ruleIndex)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Space direction="vertical" style={{ width: '100%' }} size="small">
|
||||
<div>
|
||||
<Text style={{ fontSize: 12, marginBottom: 4, display: 'block' }}>联动类型</Text>
|
||||
<Select
|
||||
style={{ width: '100%' }}
|
||||
value={rule.type}
|
||||
onChange={(val) => handleRuleChange(ruleIndex, 'type', val)}
|
||||
>
|
||||
<Select.Option value="visible">显示/隐藏</Select.Option>
|
||||
<Select.Option value="disabled">禁用/启用</Select.Option>
|
||||
<Select.Option value="required">必填/非必填</Select.Option>
|
||||
<Select.Option value="value">值联动</Select.Option>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 4 }}>
|
||||
<Text style={{ fontSize: 12 }}>触发条件(满足所有条件时生效)</Text>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => handleAddCondition(ruleIndex)}
|
||||
>
|
||||
添加条件
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{rule.conditions.length === 0 ? (
|
||||
<div style={{ padding: 8, textAlign: 'center', color: '#999', fontSize: 11, border: '1px dashed #d9d9d9', borderRadius: 4 }}>
|
||||
暂无条件
|
||||
</div>
|
||||
) : (
|
||||
<Space direction="vertical" style={{ width: '100%' }} size="small">
|
||||
{rule.conditions.map((condition, conditionIndex) => (
|
||||
<div
|
||||
key={conditionIndex}
|
||||
style={{
|
||||
padding: 8,
|
||||
background: '#fff',
|
||||
border: '1px solid #e8e8e8',
|
||||
borderRadius: 4,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', gap: 4, alignItems: 'center' }}>
|
||||
<Select
|
||||
style={{ flex: 1 }}
|
||||
placeholder={allFields.length === 0 ? "暂无可选字段" : "选择字段"}
|
||||
value={condition.field}
|
||||
onChange={(val) =>
|
||||
handleConditionChange(ruleIndex, conditionIndex, 'field', val)
|
||||
}
|
||||
notFoundContent={allFields.length === 0 ? "请先添加其他字段" : "暂无匹配字段"}
|
||||
>
|
||||
{allFields.map((field) => (
|
||||
<Select.Option key={field.name} value={field.name}>
|
||||
{field.label} ({field.name})
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
<Select
|
||||
style={{ width: 80 }}
|
||||
value={condition.operator}
|
||||
onChange={(val) =>
|
||||
handleConditionChange(ruleIndex, conditionIndex, 'operator', val)
|
||||
}
|
||||
>
|
||||
<Select.Option value="==">等于</Select.Option>
|
||||
<Select.Option value="!=">不等于</Select.Option>
|
||||
<Select.Option value=">">大于</Select.Option>
|
||||
<Select.Option value="<">小于</Select.Option>
|
||||
<Select.Option value=">=">大于等于</Select.Option>
|
||||
<Select.Option value="<=">小于等于</Select.Option>
|
||||
<Select.Option value="includes">包含</Select.Option>
|
||||
<Select.Option value="notIncludes">不包含</Select.Option>
|
||||
</Select>
|
||||
|
||||
<Input
|
||||
style={{ flex: 1 }}
|
||||
placeholder="值"
|
||||
value={condition.value}
|
||||
onChange={(e) =>
|
||||
handleConditionChange(ruleIndex, conditionIndex, 'value', e.target.value)
|
||||
}
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => handleDeleteCondition(ruleIndex, conditionIndex)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</Space>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{renderActionInput(rule, ruleIndex)}
|
||||
</Space>
|
||||
</div>
|
||||
))}
|
||||
</Space>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LinkageRuleEditor;
|
||||
|
||||
@ -21,6 +21,8 @@ import { PlusOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||
import type { FieldConfig, FormConfig } from '../types';
|
||||
import { DataSourceType, CascadeDataSourceType } from '@/domain/dataSource';
|
||||
import CascadeOptionEditor from './CascadeOptionEditor';
|
||||
import ValidationRuleEditor from './ValidationRuleEditor';
|
||||
import LinkageRuleEditor from './LinkageRuleEditor';
|
||||
|
||||
const { Text } = Typography;
|
||||
const { TabPane } = Tabs;
|
||||
@ -30,6 +32,7 @@ interface PropertyPanelProps {
|
||||
formConfig: FormConfig;
|
||||
onFieldChange: (field: FieldConfig) => void;
|
||||
onFormConfigChange: (newConfig: Partial<FormConfig>) => void;
|
||||
allFields?: FieldConfig[]; // 所有字段,用于联动规则配置
|
||||
}
|
||||
|
||||
const PropertyPanel: React.FC<PropertyPanelProps> = ({
|
||||
@ -37,6 +40,7 @@ const PropertyPanel: React.FC<PropertyPanelProps> = ({
|
||||
formConfig,
|
||||
onFieldChange,
|
||||
onFormConfigChange,
|
||||
allFields = [],
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
|
||||
@ -211,6 +215,28 @@ const PropertyPanel: React.FC<PropertyPanelProps> = ({
|
||||
<Form.Item label="是否禁用" name="disabled" valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="默认值" name="defaultValue">
|
||||
{selectedField.type === 'number' ? (
|
||||
<InputNumber style={{ width: '100%' }} placeholder="请输入默认值" />
|
||||
) : selectedField.type === 'switch' ? (
|
||||
<Switch />
|
||||
) : selectedField.type === 'checkbox' || selectedField.type === 'select' && selectedField.multiple ? (
|
||||
<Select mode="tags" style={{ width: '100%' }} placeholder="请选择默认值" options={selectedField.options} />
|
||||
) : selectedField.type === 'select' || selectedField.type === 'radio' ? (
|
||||
<Select style={{ width: '100%' }} placeholder="请选择默认值" options={selectedField.options} />
|
||||
) : selectedField.type === 'date' || selectedField.type === 'datetime' || selectedField.type === 'time' ? (
|
||||
<Input placeholder="请输入默认值(如:2024-01-01)" />
|
||||
) : selectedField.type === 'slider' ? (
|
||||
<InputNumber style={{ width: '100%' }} placeholder="请输入默认值" />
|
||||
) : selectedField.type === 'rate' ? (
|
||||
<InputNumber style={{ width: '100%' }} min={0} max={5} placeholder="请输入默认值(0-5)" />
|
||||
) : selectedField.type === 'textarea' ? (
|
||||
<Input.TextArea rows={3} placeholder="请输入默认值" />
|
||||
) : (
|
||||
<Input placeholder="请输入默认值" />
|
||||
)}
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
|
||||
@ -551,6 +577,30 @@ const PropertyPanel: React.FC<PropertyPanelProps> = ({
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 验证规则配置 - 仅表单字段显示 */}
|
||||
{selectedField.type !== 'divider' &&
|
||||
selectedField.type !== 'grid' &&
|
||||
selectedField.type !== 'text' && (
|
||||
<div>
|
||||
<Divider style={{ margin: '16px 0' }} />
|
||||
<Form.Item name="validationRules">
|
||||
<ValidationRuleEditor />
|
||||
</Form.Item>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 联动规则配置 - 仅表单字段显示 */}
|
||||
{selectedField.type !== 'divider' &&
|
||||
selectedField.type !== 'grid' &&
|
||||
selectedField.type !== 'text' && (
|
||||
<div>
|
||||
<Divider style={{ margin: '16px 0' }} />
|
||||
<Form.Item name="linkageRules">
|
||||
<LinkageRuleEditor allFields={allFields.filter(f => f.id !== selectedField.id)} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
)}
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
@ -0,0 +1,175 @@
|
||||
/**
|
||||
* 验证规则编辑器
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Select, Input, InputNumber, Button, Space, Typography } from 'antd';
|
||||
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||
import type { ValidationRule } from '../types';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
interface ValidationRuleEditorProps {
|
||||
value?: ValidationRule[];
|
||||
onChange?: (value: ValidationRule[]) => void;
|
||||
}
|
||||
|
||||
const ValidationRuleEditor: React.FC<ValidationRuleEditorProps> = ({ value = [], onChange }) => {
|
||||
const handleAddRule = () => {
|
||||
const newRule: ValidationRule = {
|
||||
type: 'required',
|
||||
message: '',
|
||||
trigger: 'blur',
|
||||
};
|
||||
onChange?.([...value, newRule]);
|
||||
};
|
||||
|
||||
const handleDeleteRule = (index: number) => {
|
||||
const newRules = value.filter((_, i) => i !== index);
|
||||
onChange?.(newRules);
|
||||
};
|
||||
|
||||
const handleRuleChange = (index: number, field: keyof ValidationRule, fieldValue: any) => {
|
||||
const newRules = [...value];
|
||||
newRules[index] = { ...newRules[index], [field]: fieldValue };
|
||||
onChange?.(newRules);
|
||||
};
|
||||
|
||||
const getRuleValueInput = (rule: ValidationRule, index: number) => {
|
||||
switch (rule.type) {
|
||||
case 'required':
|
||||
return null;
|
||||
case 'pattern':
|
||||
return (
|
||||
<Input
|
||||
placeholder="输入正则表达式,如:^[a-zA-Z0-9]+$"
|
||||
value={rule.value}
|
||||
onChange={(e) => handleRuleChange(index, 'value', e.target.value)}
|
||||
/>
|
||||
);
|
||||
case 'min':
|
||||
case 'max':
|
||||
case 'minLength':
|
||||
case 'maxLength':
|
||||
return (
|
||||
<InputNumber
|
||||
style={{ width: '100%' }}
|
||||
placeholder={`请输入${rule.type === 'min' || rule.type === 'minLength' ? '最小' : '最大'}值`}
|
||||
value={rule.value}
|
||||
onChange={(val) => handleRuleChange(index, 'value', val)}
|
||||
/>
|
||||
);
|
||||
case 'email':
|
||||
case 'url':
|
||||
case 'phone':
|
||||
case 'idCard':
|
||||
return null;
|
||||
case 'custom':
|
||||
return (
|
||||
<Input
|
||||
placeholder="输入自定义验证函数名"
|
||||
value={rule.value}
|
||||
onChange={(e) => handleRuleChange(index, 'value', e.target.value)}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
|
||||
<Text strong>验证规则</Text>
|
||||
<Button type="dashed" size="small" icon={<PlusOutlined />} onClick={handleAddRule}>
|
||||
添加规则
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{value.length === 0 ? (
|
||||
<div style={{ padding: '24px 0', textAlign: 'center', color: '#8c8c8c', fontSize: 12 }}>
|
||||
暂无验证规则,点击上方按钮添加
|
||||
</div>
|
||||
) : (
|
||||
<Space direction="vertical" style={{ width: '100%' }} size="middle">
|
||||
{value.map((rule, index) => (
|
||||
<div
|
||||
key={index}
|
||||
style={{
|
||||
padding: 12,
|
||||
border: '1px solid #e8e8e8',
|
||||
borderRadius: 4,
|
||||
background: '#fafafa',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 8 }}>
|
||||
<Text style={{ fontSize: 12, color: '#666' }}>规则 {index + 1}</Text>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => handleDeleteRule(index)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Space direction="vertical" style={{ width: '100%' }} size="small">
|
||||
<div>
|
||||
<Text style={{ fontSize: 12, marginBottom: 4, display: 'block' }}>验证类型</Text>
|
||||
<Select
|
||||
style={{ width: '100%' }}
|
||||
value={rule.type}
|
||||
onChange={(val) => handleRuleChange(index, 'type', val)}
|
||||
>
|
||||
<Select.Option value="required">必填</Select.Option>
|
||||
<Select.Option value="pattern">正则表达式</Select.Option>
|
||||
<Select.Option value="min">最小值</Select.Option>
|
||||
<Select.Option value="max">最大值</Select.Option>
|
||||
<Select.Option value="minLength">最小长度</Select.Option>
|
||||
<Select.Option value="maxLength">最大长度</Select.Option>
|
||||
<Select.Option value="email">邮箱格式</Select.Option>
|
||||
<Select.Option value="url">URL格式</Select.Option>
|
||||
<Select.Option value="phone">手机号格式</Select.Option>
|
||||
<Select.Option value="idCard">身份证格式</Select.Option>
|
||||
<Select.Option value="custom">自定义验证</Select.Option>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{getRuleValueInput(rule, index) && (
|
||||
<div>
|
||||
<Text style={{ fontSize: 12, marginBottom: 4, display: 'block' }}>规则值</Text>
|
||||
{getRuleValueInput(rule, index)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<Text style={{ fontSize: 12, marginBottom: 4, display: 'block' }}>错误提示</Text>
|
||||
<Input
|
||||
placeholder="请输入验证失败时的提示信息"
|
||||
value={rule.message}
|
||||
onChange={(e) => handleRuleChange(index, 'message', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text style={{ fontSize: 12, marginBottom: 4, display: 'block' }}>触发时机</Text>
|
||||
<Select
|
||||
style={{ width: '100%' }}
|
||||
value={rule.trigger}
|
||||
onChange={(val) => handleRuleChange(index, 'trigger', val)}
|
||||
>
|
||||
<Select.Option value="blur">失去焦点</Select.Option>
|
||||
<Select.Option value="change">值改变</Select.Option>
|
||||
</Select>
|
||||
</div>
|
||||
</Space>
|
||||
</div>
|
||||
))}
|
||||
</Space>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ValidationRuleEditor;
|
||||
|
||||
@ -386,6 +386,29 @@ const FormDesigner: React.FC = () => {
|
||||
|
||||
const selectedField = findFieldById(fields, selectedFieldId) || null;
|
||||
|
||||
// 获取所有字段(扁平化,包括grid中的嵌套字段)
|
||||
const getAllFields = useCallback((fieldList: FieldConfig[]): FieldConfig[] => {
|
||||
const result: FieldConfig[] = [];
|
||||
|
||||
fieldList.forEach(field => {
|
||||
// 只添加实际的表单字段,不包括布局组件
|
||||
if (field.type !== 'divider' && field.type !== 'grid' && field.type !== 'text') {
|
||||
result.push(field);
|
||||
}
|
||||
|
||||
// 如果是grid,递归获取其子字段
|
||||
if (field.type === 'grid' && field.children) {
|
||||
field.children.forEach(columnFields => {
|
||||
result.push(...getAllFields(columnFields));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}, []);
|
||||
|
||||
const allFields = getAllFields(fields);
|
||||
|
||||
// 快捷键支持
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
@ -490,6 +513,7 @@ const FormDesigner: React.FC = () => {
|
||||
formConfig={formConfig}
|
||||
onFieldChange={handleFieldChange}
|
||||
onFormConfigChange={handleFormConfigChange}
|
||||
allFields={allFields}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@ -39,6 +39,51 @@ export interface CascadeFieldOption {
|
||||
// 数据源类型
|
||||
export type DataSourceType = 'static' | 'api' | 'predefined';
|
||||
|
||||
// 验证规则类型
|
||||
export type ValidationType =
|
||||
| 'required' // 必填
|
||||
| 'pattern' // 正则表达式
|
||||
| 'min' // 最小值/最小长度
|
||||
| 'max' // 最大值/最大长度
|
||||
| 'minLength' // 最小长度
|
||||
| 'maxLength' // 最大长度
|
||||
| 'email' // 邮箱
|
||||
| 'url' // URL
|
||||
| 'phone' // 手机号
|
||||
| 'idCard' // 身份证
|
||||
| 'custom'; // 自定义验证
|
||||
|
||||
// 验证规则
|
||||
export interface ValidationRule {
|
||||
type: ValidationType;
|
||||
value?: any; // 规则值(如正则表达式、最小值等)
|
||||
message?: string; // 错误提示信息
|
||||
trigger?: 'blur' | 'change'; // 触发时机
|
||||
}
|
||||
|
||||
// 联动类型
|
||||
export type LinkageType =
|
||||
| 'visible' // 显示/隐藏
|
||||
| 'disabled' // 禁用/启用
|
||||
| 'required' // 必填/非必填
|
||||
| 'options' // 选项联动(如省市区)
|
||||
| 'value'; // 值联动
|
||||
|
||||
// 联动条件
|
||||
export interface LinkageCondition {
|
||||
field: string; // 关联字段名
|
||||
operator: '==' | '!=' | '>' | '<' | '>=' | '<=' | 'includes' | 'notIncludes';
|
||||
value: any; // 比较值
|
||||
}
|
||||
|
||||
// 联动规则
|
||||
export interface LinkageRule {
|
||||
id: string;
|
||||
type: LinkageType;
|
||||
conditions: LinkageCondition[]; // 多个条件(且关系)
|
||||
action: any; // 执行的动作(根据type不同而不同)
|
||||
}
|
||||
|
||||
// API 数据源配置
|
||||
export interface ApiDataSource {
|
||||
url: string; // 接口地址
|
||||
@ -75,6 +120,8 @@ export interface FieldConfig {
|
||||
apiDataSource?: ApiDataSource; // API 数据源配置(当 dataSourceType 为 'api' 时使用)
|
||||
predefinedDataSource?: PredefinedDataSource; // 预定义数据源配置(当 dataSourceType 为 'predefined' 时使用)
|
||||
predefinedCascadeDataSource?: PredefinedCascadeDataSource; // 预定义级联数据源配置(cascader 使用)
|
||||
validationRules?: ValidationRule[]; // 验证规则列表
|
||||
linkageRules?: LinkageRule[]; // 联动规则列表
|
||||
min?: number;
|
||||
max?: number;
|
||||
rows?: number;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user