表单设计器

This commit is contained in:
dengqichen 2025-10-23 23:53:43 +08:00
parent 6c8ded573b
commit 8c9813ed55
6 changed files with 755 additions and 9 deletions

View File

@ -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}

View 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;

View File

@ -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>
);
};

View File

@ -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;

View File

@ -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>

View File

@ -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;