表单设计器

This commit is contained in:
dengqichen 2025-10-24 01:01:21 +08:00
parent b4836b1c91
commit ef5415f412
20 changed files with 1556 additions and 558 deletions

View File

@ -0,0 +1,697 @@
/**
*
*
*
* -
* -
* -
* - / JSON Schema
*
* 使
* ```tsx
* <FormDesigner
* value={formSchema}
* onChange={setFormSchema}
* onSave={handleSave}
* readonly={false}
* />
* ```
*/
import React, { useState, useCallback, useEffect } from 'react';
import { Button, Space, message, Modal } from 'antd';
import {
SaveOutlined,
DownloadOutlined,
UploadOutlined,
EyeOutlined,
ClearOutlined,
} from '@ant-design/icons';
import {
DndContext,
DragEndEvent,
DragStartEvent,
DragOverlay,
PointerSensor,
useSensor,
useSensors,
closestCenter
} from '@dnd-kit/core';
import { arrayMove } from '@dnd-kit/sortable';
import ComponentPanel from './components/ComponentPanel';
import DesignCanvas from './components/DesignCanvas';
import PropertyPanel from './components/PropertyPanel';
import FormPreview from './components/FormPreview';
import { COMPONENT_LIST } from './config';
import type { FieldConfig, FormConfig, FormSchema } from './types';
import './styles.css';
export interface FormDesignerProps {
value?: FormSchema; // 受控模式:表单 Schema
onChange?: (schema: FormSchema) => void; // 受控模式Schema 变化回调
onSave?: (schema: FormSchema) => void; // 保存回调
readonly?: boolean; // 只读模式
showToolbar?: boolean; // 是否显示工具栏
extraActions?: React.ReactNode; // 额外的操作按钮
}
const FormDesigner: React.FC<FormDesignerProps> = ({
value,
onChange,
onSave,
readonly = false,
showToolbar = true,
extraActions
}) => {
// 内部状态(非受控模式使用)
const [internalFields, setInternalFields] = useState<FieldConfig[]>([]);
const [internalFormConfig, setInternalFormConfig] = useState<FormConfig>({
labelAlign: 'right',
size: 'middle',
formWidth: 600,
});
// 使用受控值或内部值
const fields = value?.fields ?? internalFields;
const formConfig = value?.formConfig ?? internalFormConfig;
const setFields = useCallback((updater: React.SetStateAction<FieldConfig[]>) => {
const newFields = typeof updater === 'function' ? updater(fields) : updater;
if (onChange) {
// 受控模式:通知父组件
onChange({
version: '1.0',
formConfig,
fields: newFields,
});
} else {
// 非受控模式:更新内部状态
setInternalFields(newFields);
}
}, [fields, formConfig, onChange]);
const setFormConfig = useCallback((updater: React.SetStateAction<FormConfig>) => {
const newConfig = typeof updater === 'function' ? updater(formConfig) : updater;
if (onChange) {
onChange({
version: '1.0',
formConfig: newConfig,
fields,
});
} else {
setInternalFormConfig(newConfig);
}
}, [fields, formConfig, onChange]);
// 选中的字段
const [selectedFieldId, setSelectedFieldId] = useState<string>();
// 剪贴板(用于复制粘贴)
const [clipboard, setClipboard] = useState<FieldConfig | null>(null);
// 预览模式
const [previewVisible, setPreviewVisible] = useState(false);
// 正在拖拽的元素ID
const [activeId, setActiveId] = useState<string | null>(null);
// 拖拽传感器 - 添加激活约束,减少误触和卡顿
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
distance: 8, // 移动8px后才激活拖拽
},
})
);
// 生成唯一 ID
const generateId = () => `field_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
// 处理拖拽开始
const handleDragStart = useCallback((event: DragStartEvent) => {
setActiveId(event.active.id.toString());
}, []);
// 处理拖拽结束
const handleDragEnd = useCallback((event: DragEndEvent) => {
const { active, over } = event;
// 清除拖拽状态
setActiveId(null);
if (!over) return;
const activeData = active.data.current;
const overData = over.data.current;
// 从组件面板拖拽新组件到栅格列
if (activeData?.isNew && overData?.gridId && overData?.colIndex !== undefined) {
const componentType = active.id.toString().replace('new-', '');
const component = COMPONENT_LIST.find((c) => c.type === componentType);
if (component) {
const newField: FieldConfig = {
id: generateId(),
type: component.type,
label: component.label,
name: `${component.type}_${Date.now()}`,
...component.defaultConfig,
};
setFields((prev) => {
const updateGrid = (fieldList: FieldConfig[]): FieldConfig[] => {
return fieldList.map(field => {
if (field.id === overData.gridId && field.type === 'grid' && field.children) {
const newChildren = [...field.children];
newChildren[overData.colIndex] = [...newChildren[overData.colIndex], newField];
return { ...field, children: newChildren };
}
if (field.type === 'grid' && field.children) {
return {
...field,
children: field.children.map(col => updateGrid(col)),
};
}
return field;
});
};
return updateGrid(prev);
});
setSelectedFieldId(newField.id);
message.success(`已添加${component.label}到栅格列`);
}
return;
}
// 从组件面板拖拽新组件到画布或字段
if (activeData?.isNew && overData?.accept === 'field') {
const componentType = active.id.toString().replace('new-', '');
const component = COMPONENT_LIST.find((c) => c.type === componentType);
if (component) {
const newField: FieldConfig = {
id: generateId(),
type: component.type,
label: component.label,
name: `${component.type}_${Date.now()}`,
...component.defaultConfig,
};
setFields((prev) => {
const newFields = [...prev];
// 如果拖到画布空白处,添加到末尾
if (over.id === 'canvas') {
newFields.push(newField);
} else {
// 如果拖到某个字段上,插入到该字段位置
const overIndex = newFields.findIndex((f) => f.id === over.id);
if (overIndex >= 0) {
newFields.splice(overIndex, 0, newField);
} else {
newFields.push(newField);
}
}
return newFields;
});
setSelectedFieldId(newField.id);
message.success(`已添加${component.label}`);
}
}
// 画布内字段排序
if (!activeData?.isNew && overData?.accept === 'field') {
setFields((prev) => {
const oldIndex = prev.findIndex((f) => f.id === active.id);
const newIndex = prev.findIndex((f) => f.id === over.id);
if (oldIndex !== -1 && newIndex !== -1) {
return arrayMove(prev, oldIndex, newIndex);
}
return prev;
});
}
}, [setFields]);
// 选择字段
const handleSelectField = useCallback((fieldId: string) => {
if (readonly) return;
setSelectedFieldId(fieldId);
}, [readonly]);
// 深拷贝字段(包括嵌套的栅格子字段)
const deepCopyField = useCallback((field: FieldConfig): FieldConfig => {
const newField: FieldConfig = {
...field,
id: generateId(),
name: field.name + '_copy_' + Date.now(),
};
// 如果是栅格布局,递归复制子字段
if (field.type === 'grid' && field.children) {
newField.children = field.children.map(columnFields =>
columnFields.map(childField => deepCopyField(childField))
);
}
return newField;
}, []);
// 复制字段到剪贴板
const handleCopyField = useCallback((fieldId: string) => {
if (readonly) return;
// 递归查找字段
const findField = (fieldList: FieldConfig[], id: string): FieldConfig | null => {
for (const field of fieldList) {
if (field.id === id) {
return field;
}
// 如果是栅格布局,递归查找子字段
if (field.type === 'grid' && field.children) {
for (const columnFields of field.children) {
const found = findField(columnFields, id);
if (found) return found;
}
}
}
return null;
};
const field = findField(fields, fieldId);
if (field) {
setClipboard(field);
message.success('已复制字段到剪贴板');
}
}, [fields, readonly]);
// 粘贴字段到主列表末尾
const handlePasteToCanvas = useCallback(() => {
if (readonly || !clipboard) {
message.warning('剪贴板为空');
return;
}
const newField = deepCopyField(clipboard);
setFields(prev => [...prev, newField]);
setSelectedFieldId(newField.id);
message.success('已粘贴字段');
}, [clipboard, deepCopyField, readonly, setFields]);
// 粘贴字段到栅格列
const handlePasteToGrid = useCallback((gridId: string, colIndex: number) => {
if (readonly || !clipboard) {
message.warning('剪贴板为空');
return;
}
const newField = deepCopyField(clipboard);
setFields(prev => prev.map(field => {
if (field.id === gridId && field.type === 'grid') {
const newChildren = [...(field.children || [])];
if (!newChildren[colIndex]) {
newChildren[colIndex] = [];
}
newChildren[colIndex] = [...newChildren[colIndex], newField];
return { ...field, children: newChildren };
}
return field;
}));
setSelectedFieldId(newField.id);
message.success('已粘贴字段到栅格');
}, [clipboard, deepCopyField, readonly, setFields]);
// 删除字段(支持删除栅格内的字段)
const handleDeleteField = useCallback((fieldId: string) => {
if (readonly) return;
let deleted = false;
// 递归删除函数
const deleteFieldRecursive = (fieldList: FieldConfig[]): FieldConfig[] => {
return fieldList
.filter(f => {
if (f.id === fieldId) {
deleted = true;
return false;
}
return true;
})
.map(f => {
// 如果是栅格布局,递归处理其子字段
if (f.type === 'grid' && f.children) {
return {
...f,
children: f.children.map(columnFields =>
deleteFieldRecursive(columnFields)
),
};
}
return f;
});
};
setFields(prev => deleteFieldRecursive(prev));
if (deleted) {
if (selectedFieldId === fieldId) {
setSelectedFieldId(undefined);
}
message.success('已删除字段');
}
}, [selectedFieldId, readonly, setFields]);
// 更新字段属性(支持更新栅格内的字段)
const handleFieldChange = useCallback((updatedField: FieldConfig) => {
if (readonly) return;
// 递归更新函数
const updateFieldRecursive = (fieldList: FieldConfig[]): FieldConfig[] => {
return fieldList.map(f => {
if (f.id === updatedField.id) {
return updatedField;
}
// 如果是栅格布局,递归处理其子字段
if (f.type === 'grid' && f.children) {
return {
...f,
children: f.children.map(columnFields =>
updateFieldRecursive(columnFields)
),
};
}
return f;
});
};
setFields(prev => updateFieldRecursive(prev));
}, [readonly, setFields]);
// 更新表单配置
const handleFormConfigChange = useCallback((newConfig: Partial<FormConfig>) => {
if (readonly) return;
setFormConfig(prev => ({ ...prev, ...newConfig }));
}, [readonly, setFormConfig]);
// 保存表单
const handleSave = useCallback(() => {
if (fields.length === 0) {
message.warning('表单为空,请先添加字段');
return;
}
const schema: FormSchema = {
version: '1.0',
formConfig,
fields,
};
if (onSave) {
onSave(schema);
} else {
console.log('保存的表单 Schema:', JSON.stringify(schema, null, 2));
message.success('表单已保存!请查看控制台');
}
}, [fields, formConfig, onSave]);
// 导出 JSON
const handleExport = useCallback(() => {
if (fields.length === 0) {
message.warning('表单为空,无法导出');
return;
}
const schema: FormSchema = {
version: '1.0',
formConfig,
fields,
};
const dataStr = JSON.stringify(schema, null, 2);
const dataUri = 'data:application/json;charset=utf-8,' + encodeURIComponent(dataStr);
const exportFileDefaultName = `form-schema-${Date.now()}.json`;
const linkElement = document.createElement('a');
linkElement.setAttribute('href', dataUri);
linkElement.setAttribute('download', exportFileDefaultName);
linkElement.click();
message.success('JSON Schema 已导出');
}, [fields, formConfig]);
// 导入 JSON
const handleImport = useCallback(() => {
if (readonly) return;
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
input.onchange = (e: any) => {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (event: any) => {
try {
const schema: FormSchema = JSON.parse(event.target.result);
if (!schema.fields || !Array.isArray(schema.fields)) {
message.error('无效的表单 Schema 格式');
return;
}
setFields(schema.fields);
setFormConfig(schema.formConfig || formConfig);
setSelectedFieldId(undefined);
message.success('JSON Schema 导入成功');
} catch (error) {
message.error('JSON 格式错误');
console.error('导入错误:', error);
}
};
reader.readAsText(file);
}
};
input.click();
}, [formConfig, readonly, setFields, setFormConfig]);
// 清空表单
const handleClear = useCallback(() => {
if (readonly) return;
Modal.confirm({
title: '确认清空',
content: '确定要清空所有字段吗?此操作不可恢复。',
onOk: () => {
setFields([]);
setSelectedFieldId(undefined);
message.info('表单已清空');
},
});
}, [readonly, setFields]);
// 递归查找选中的字段(包括栅格内的字段)
const findFieldById = (fieldList: FieldConfig[], id?: string): FieldConfig | undefined => {
if (!id) return undefined;
for (const field of fieldList) {
if (field.id === id) {
return field;
}
// 如果是栅格布局,递归查找子字段
if (field.type === 'grid' && field.children) {
for (const columnFields of field.children) {
const found = findFieldById(columnFields, id);
if (found) return found;
}
}
}
return undefined;
};
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(() => {
if (readonly) return;
const handleKeyDown = (e: KeyboardEvent) => {
// 检查是否在输入框、文本域等可编辑元素中
const target = e.target as HTMLElement;
const isEditable =
target.tagName === 'INPUT' ||
target.tagName === 'TEXTAREA' ||
target.isContentEditable;
// 如果在可编辑元素中,不触发快捷键
if (isEditable) return;
const isMac = /Mac|iPod|iPhone|iPad/.test(navigator.platform);
const ctrlKey = isMac ? e.metaKey : e.ctrlKey;
// Ctrl+C / Cmd+C: 复制选中的字段
if (ctrlKey && e.key === 'c' && selectedFieldId) {
e.preventDefault();
handleCopyField(selectedFieldId);
}
// Ctrl+V / Cmd+V: 粘贴到画布
if (ctrlKey && e.key === 'v' && clipboard) {
e.preventDefault();
handlePasteToCanvas();
}
// Delete: 删除选中的字段
if (e.key === 'Delete' && selectedFieldId) {
e.preventDefault();
handleDeleteField(selectedFieldId);
}
};
window.addEventListener('keydown', handleKeyDown);
return () => {
window.removeEventListener('keydown', handleKeyDown);
};
}, [selectedFieldId, clipboard, readonly, handleCopyField, handlePasteToCanvas, handleDeleteField]);
return (
<div className="form-designer">
{showToolbar && (
<div className="form-designer-header">
<div className="form-designer-title"></div>
<Space>
<Button
icon={<UploadOutlined />}
onClick={handleImport}
disabled={readonly}
>
JSON
</Button>
<Button icon={<DownloadOutlined />} onClick={handleExport}>
JSON
</Button>
<Button icon={<EyeOutlined />} onClick={() => setPreviewVisible(true)}>
</Button>
<Button
icon={<ClearOutlined />}
danger
onClick={handleClear}
disabled={readonly}
>
</Button>
<Button
type="primary"
icon={<SaveOutlined />}
onClick={handleSave}
disabled={readonly}
>
</Button>
{extraActions}
</Space>
</div>
)}
<DndContext
sensors={sensors}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
collisionDetection={closestCenter}
>
<div className="form-designer-body">
{/* 左侧组件面板 */}
<div style={{ width: 280 }}>
<ComponentPanel />
</div>
{/* 中间设计画布 */}
<DesignCanvas
fields={fields}
selectedFieldId={selectedFieldId}
onSelectField={handleSelectField}
onDeleteField={handleDeleteField}
onCopyField={handleCopyField}
onPasteToCanvas={handlePasteToCanvas}
onPasteToGrid={handlePasteToGrid}
labelAlign={formConfig.labelAlign}
hasClipboard={!!clipboard}
/>
{/* 右侧属性面板 */}
<PropertyPanel
selectedField={selectedField}
formConfig={formConfig}
onFieldChange={handleFieldChange}
onFormConfigChange={handleFormConfigChange}
allFields={allFields}
/>
</div>
{/* 拖拽覆盖层 - 显示正在拖拽的元素 */}
<DragOverlay dropAnimation={null}>
{activeId && activeId.startsWith('new-') ? (
<div style={{
padding: '10px 12px',
background: '#fff',
border: '2px solid #1890ff',
borderRadius: 6,
boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
cursor: 'grabbing',
}}>
{COMPONENT_LIST.find(c => `new-${c.type}` === activeId)?.label || '组件'}
</div>
) : null}
</DragOverlay>
</DndContext>
{/* 预览模态框 */}
<Modal
title="表单预览"
open={previewVisible}
onCancel={() => setPreviewVisible(false)}
width={formConfig.formWidth || 600}
footer={null}
>
<FormPreview fields={fields} formConfig={formConfig} />
</Modal>
</div>
);
};
export default FormDesigner;

View File

@ -0,0 +1,394 @@
/**
*
* Schema
*/
import React, { useState, useEffect } from 'react';
import { Form, Button, message } from 'antd';
import type { Rule } from 'antd/es/form';
import type { FieldConfig, FormSchema, ValidationRule, LinkageRule } from './types';
import FieldRenderer from './components/FieldRenderer';
import GridFieldPreview from './components/GridFieldPreview';
import './styles.css';
export interface FormRendererProps {
schema: FormSchema; // 表单 Schema
value?: Record<string, any>; // 表单值(受控)
onChange?: (values: Record<string, any>) => void; // 值变化回调
onSubmit?: (values: Record<string, any>) => void; // 提交回调
onCancel?: () => void; // 取消回调
readonly?: boolean; // 只读模式
showSubmit?: boolean; // 是否显示提交按钮
showCancel?: boolean; // 是否显示取消按钮
submitText?: string; // 提交按钮文本
cancelText?: string; // 取消按钮文本
}
const FormRenderer: React.FC<FormRendererProps> = ({
schema,
value,
onChange,
onSubmit,
onCancel,
readonly = false,
showSubmit = true,
showCancel = false,
submitText = '提交',
cancelText = '取消'
}) => {
const { fields, formConfig } = schema;
const [form] = Form.useForm();
const [formData, setFormData] = useState<Record<string, any>>(value || {});
const [fieldStates, setFieldStates] = useState<Record<string, {
visible?: boolean;
disabled?: boolean;
required?: boolean;
}>>({});
// 同步外部值到表单
useEffect(() => {
if (value) {
form.setFieldsValue(value);
setFormData(value);
}
}, [value, form]);
const handleSubmit = async () => {
if (readonly) return;
try {
const values = await form.validateFields();
setFormData(values);
if (onSubmit) {
onSubmit(values);
} else {
console.log('表单提交数据:', values);
message.success('表单提交成功!');
}
} catch (error) {
message.error('请填写必填项');
}
};
const handleReset = () => {
if (readonly) return;
form.resetFields();
const resetValues = {};
setFormData(resetValues);
if (onChange) {
onChange(resetValues);
}
message.info('表单已重置');
};
const handleValuesChange = (_: any, allValues: Record<string, any>) => {
setFormData(allValues);
if (onChange) {
onChange(allValues);
}
};
// 将验证规则转换为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) => {
// 布局组件:文本、分割线
if (field.type === 'text' || field.type === 'divider') {
return (
<div key={field.id}>
<FieldRenderer field={field} isPreview={true} />
</div>
);
}
// 栅格布局:使用专门的预览组件,自动处理内部字段的 Form.Item
if (field.type === 'grid') {
return (
<GridFieldPreview
key={field.id}
field={field}
formData={formData}
onFieldChange={(name, value) => setFormData(prev => ({ ...prev, [name]: value }))}
formConfig={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);
// 🐛 调试:打印验证规则
if (field.validationRules && field.validationRules.length > 0) {
console.log(`📋 字段 "${field.label}" 的验证规则:`, field.validationRules);
console.log(`✅ 转换后的规则:`, customRules);
}
// 检查自定义验证规则中是否已经包含必填验证
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];
// 🐛 调试:打印最终规则
if (allRules.length > 0) {
console.log(`🎯 字段 "${field.label}" 最终验证规则:`, allRules);
}
// 普通表单组件使用 Form.Item 包裹
return (
<Form.Item
key={field.id}
label={field.label}
name={field.name}
colon={true}
rules={allRules}
>
<FieldRenderer
field={{ ...field, disabled: isDisabled }}
value={formData[field.name]}
onChange={(value) => setFormData(prev => ({ ...prev, [field.name]: value }))}
isPreview={true}
/>
</Form.Item>
);
});
};
if (fields.length === 0) {
return (
<div style={{ padding: 40, textAlign: 'center', color: '#8c8c8c' }}>
</div>
);
}
const formLayout = formConfig.labelAlign === 'top' ? 'vertical' : 'horizontal';
const labelColSpan = formConfig.labelAlign === 'left' ? 6 : formConfig.labelAlign === 'right' ? 6 : undefined;
const wrapperColSpan = formConfig.labelAlign === 'left' || formConfig.labelAlign === 'right' ? 18 : undefined;
return (
<div style={{ padding: '24px 40px' }}>
<Form
form={form}
layout={formLayout}
size={formConfig.size}
colon={true}
labelAlign={formConfig.labelAlign === 'top' ? undefined : formConfig.labelAlign}
labelCol={labelColSpan ? { span: labelColSpan } : undefined}
wrapperCol={wrapperColSpan ? { span: wrapperColSpan } : undefined}
onValuesChange={handleValuesChange}
disabled={readonly}
>
{formConfig.title && (
<h3 style={{ marginBottom: 24 }}>{formConfig.title}</h3>
)}
{renderFields(fields)}
{!readonly && (showSubmit || showCancel) && (
<Form.Item
style={{ marginTop: 32, marginBottom: 0 }}
labelCol={{ span: 0 }}
wrapperCol={{ span: 24 }}
>
<div style={{ display: 'flex', justifyContent: 'center', gap: 8 }}>
{showSubmit && (
<Button type="primary" onClick={handleSubmit}>
{submitText}
</Button>
)}
{showCancel && (
<Button onClick={onCancel || handleReset}>
{cancelText}
</Button>
)}
{!showCancel && (
<Button onClick={handleReset}>
</Button>
)}
</div>
</Form.Item>
)}
</Form>
</div>
);
};
export default FormRenderer;

View File

@ -39,6 +39,9 @@ const DesignCanvas: React.FC<DesignCanvasProps> = ({
}) => { }) => {
const { setNodeRef } = useDroppable({ const { setNodeRef } = useDroppable({
id: 'canvas', id: 'canvas',
data: {
accept: 'field',
},
}); });
return ( return (

View File

@ -50,6 +50,9 @@ const FieldItem: React.FC<FieldItemProps> = ({
isDragging, isDragging,
} = useSortable({ } = useSortable({
id: field.id, id: field.id,
data: {
accept: 'field',
},
}); });
const style = { const style = {

View File

@ -74,8 +74,16 @@ const FieldRenderer: React.FC<FieldRendererProps> = ({
if (field.type === 'text') { if (field.type === 'text') {
return ( return (
<div style={{ padding: '8px 0' }}> <div style={{
<Text>{field.content || '这是一段文字'}</Text> padding: '8px 0',
textAlign: field.textAlign || 'left',
}}>
<Text style={{
fontSize: field.fontSize ? `${field.fontSize}px` : '14px',
color: field.textColor || '#000000',
}}>
{field.content || '这是一段文字'}
</Text>
</div> </div>
); );
} }
@ -161,6 +169,11 @@ const FieldRenderer: React.FC<FieldRendererProps> = ({
disabled={field.disabled} disabled={field.disabled}
value={value} value={value}
onChange={onChange} onChange={onChange}
style={{
display: 'flex',
flexDirection: field.checkboxLayout === 'vertical' ? 'column' : 'row',
gap: field.checkboxLayout === 'vertical' ? '8px' : '16px',
}}
/> />
); );

View File

@ -109,6 +109,11 @@ const GridColumn: React.FC<GridColumnProps> = ({
}) => { }) => {
const { setNodeRef, isOver } = useDroppable({ const { setNodeRef, isOver } = useDroppable({
id: dropId, id: dropId,
data: {
accept: 'field',
gridId,
colIndex,
},
}); });
// 预览模式下的样式 // 预览模式下的样式

View File

@ -274,9 +274,30 @@ const PropertyPanel: React.FC<PropertyPanelProps> = ({
)} )}
{selectedField.type === 'text' && ( {selectedField.type === 'text' && (
<Form.Item label="文本内容" name="content"> <>
<Input.TextArea rows={3} placeholder="请输入文本内容" /> <Form.Item label="文本内容" name="content">
</Form.Item> <Input.TextArea rows={3} placeholder="请输入文本内容" />
</Form.Item>
<Form.Item label="文字对齐" name="textAlign">
<Radio.Group>
<Radio.Button value="left"></Radio.Button>
<Radio.Button value="center"></Radio.Button>
<Radio.Button value="right"></Radio.Button>
</Radio.Group>
</Form.Item>
<Form.Item label="字体大小" name="fontSize">
<InputNumber
min={12}
max={72}
placeholder="字体大小px"
style={{ width: '100%' }}
addonAfter="px"
/>
</Form.Item>
<Form.Item label="字体颜色" name="textColor">
<Input type="color" style={{ width: '100%' }} />
</Form.Item>
</>
)} )}
{selectedField.type === 'grid' && ( {selectedField.type === 'grid' && (
@ -383,6 +404,15 @@ const PropertyPanel: React.FC<PropertyPanelProps> = ({
</> </>
)} )}
{selectedField.type === 'checkbox' && (
<Form.Item label="布局方式" name="checkboxLayout">
<Radio.Group>
<Radio.Button value="horizontal"></Radio.Button>
<Radio.Button value="vertical"></Radio.Button>
</Radio.Group>
</Form.Item>
)}
{hasOptions && ( {hasOptions && (
<div> <div>
<Divider style={{ margin: '16px 0' }} /> <Divider style={{ margin: '16px 0' }} />

View File

@ -119,6 +119,7 @@ export const COMPONENT_LIST: ComponentMeta[] = [
category: '基础字段', category: '基础字段',
defaultConfig: { defaultConfig: {
dataSourceType: 'static', dataSourceType: 'static',
checkboxLayout: 'horizontal', // 默认行内布局
options: [ options: [
{ label: '选项1', value: '1' }, { label: '选项1', value: '1' },
{ label: '选项2', value: '2' }, { label: '选项2', value: '2' },
@ -161,6 +162,9 @@ export const COMPONENT_LIST: ComponentMeta[] = [
category: '基础字段', category: '基础字段',
defaultConfig: { defaultConfig: {
content: '这是一段文字', content: '这是一段文字',
textAlign: 'left', // 默认左对齐
fontSize: 14, // 默认字体大小 14px
textColor: '#000000', // 默认黑色
}, },
}, },
// 高级字段 // 高级字段

View File

@ -0,0 +1,37 @@
/**
*
*
*
* 1. FormDesigner -
* 2. FormRenderer -
*/
export { default as FormDesigner } from './Designer';
export type { FormDesignerProps } from './Designer';
export { default as FormRenderer } from './Renderer';
export type { FormRendererProps } from './Renderer';
// 导出类型定义
export type {
FieldType,
FieldOption,
CascadeFieldOption,
DataSourceType,
ValidationType,
ValidationRule,
LinkageType,
LinkageCondition,
LinkageRule,
ApiDataSource,
PredefinedDataSource,
PredefinedCascadeDataSource,
FieldConfig,
FormConfig,
FormSchema,
} from './types';
// 导出组件配置
export { COMPONENT_LIST, getComponentsByCategory } from './config';
export type { ComponentMeta } from './config';

View File

@ -2,6 +2,35 @@
* 表单设计器样式 * 表单设计器样式
*/ */
/* 设计器容器 */
.form-designer {
height: calc(100vh - 150px); /* 默认高度,适应带导航的页面 */
min-height: 600px;
display: flex;
flex-direction: column;
}
.form-designer-header {
padding: 12px 16px;
background: #fff;
border-bottom: 1px solid #e8e8e8;
display: flex;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
}
.form-designer-title {
font-size: 16px;
font-weight: 600;
}
.form-designer-body {
flex: 1;
display: flex;
overflow: hidden;
}
/* 组件面板 */ /* 组件面板 */
.form-designer-component-panel { .form-designer-component-panel {
height: 100%; height: 100%;

View File

@ -131,6 +131,10 @@ export interface FieldConfig {
maxCount?: number; maxCount?: number;
span?: number; // 栅格占位格数(该字段在栅格中占几列) span?: number; // 栅格占位格数(该字段在栅格中占几列)
content?: string; // 文本内容(用于 text 组件) content?: string; // 文本内容(用于 text 组件)
textAlign?: 'left' | 'center' | 'right'; // 文字对齐方式(用于 text 组件)
fontSize?: number; // 文字大小(用于 text 组件)
textColor?: string; // 文字颜色(用于 text 组件)
checkboxLayout?: 'horizontal' | 'vertical'; // 多选框布局方式horizontal:行内vertical:块级)
columns?: number; // 栅格列数(用于 grid 组件) columns?: number; // 栅格列数(用于 grid 组件)
columnSpans?: number[]; // 栅格每列宽度(用于 grid 组件,如 [2, 18, 2]总和不超过24 columnSpans?: number[]; // 栅格每列宽度(用于 grid 组件,如 [2, 18, 2]总和不超过24
gutter?: number; // 栅格间距(用于 grid 组件) gutter?: number; // 栅格间距(用于 grid 组件)
@ -145,9 +149,15 @@ export interface FormConfig {
size?: 'small' | 'middle' | 'large'; size?: 'small' | 'middle' | 'large';
} }
// 表单 Schema // 表单 Schema(标准化数据格式)
export interface FormSchema { export interface FormSchema {
version: string; // Schema 版本号
formConfig: FormConfig; formConfig: FormConfig;
fields: FieldConfig[]; fields: FieldConfig[];
metadata?: { // 元数据(可选)
createdAt?: string;
updatedAt?: string;
author?: string;
};
} }

View File

@ -1,576 +1,349 @@
/** /**
* * FormDesigner 使
* *
* * 使 @/components/FormDesigner
* - * - FormDesigner: 表单设计器
* - * - FormRenderer: 表单渲染器
* -
* - / JSON Schema
*/ */
import React, { useState, useCallback, useEffect } from 'react'; import React, { useState } from 'react';
import { Button, Space, message, Modal } from 'antd'; import { message, Tabs, Card, Button, Modal, Space, Divider } from 'antd';
import { import { FormDesigner, FormRenderer, type FormSchema } from '@/components/FormDesigner';
SaveOutlined,
DownloadOutlined,
UploadOutlined,
EyeOutlined,
ClearOutlined,
} from '@ant-design/icons';
import { DndContext, DragEndEvent, DragOverlay, PointerSensor, useSensor, useSensors } from '@dnd-kit/core';
import { arrayMove } from '@dnd-kit/sortable';
import ComponentPanel from './components/ComponentPanel';
import DesignCanvas from './components/DesignCanvas';
import PropertyPanel from './components/PropertyPanel';
import FormPreview from './components/FormPreview';
import { COMPONENT_LIST } from './config';
import type { FieldConfig, FormConfig, FormSchema } from './types';
const FormDesigner: React.FC = () => { const FormDesignerExamplesPage: React.FC = () => {
// 表单字段列表 // ==================== 示例 1: 表单设计器 ====================
const [fields, setFields] = useState<FieldConfig[]>([]); const [designerSchema, setDesignerSchema] = useState<FormSchema>();
// 选中的字段
const [selectedFieldId, setSelectedFieldId] = useState<string>();
// 剪贴板(用于复制粘贴)
const [clipboard, setClipboard] = useState<FieldConfig | null>(null);
// 表单配置
const [formConfig, setFormConfig] = useState<FormConfig>({
labelAlign: 'right',
size: 'middle',
formWidth: 600, // 表单弹窗宽度(像素)
});
// 预览模式 const handleDesignerSave = async (schema: FormSchema) => {
const [previewVisible, setPreviewVisible] = useState(false); console.log('💾 保存的表单 Schema:', JSON.stringify(schema, null, 2));
message.success('表单设计已保存!请查看控制台');
// 拖拽传感器 setPreviewSchema(schema); // 保存后更新预览
const sensors = useSensors( // 实际项目中这里会调用后端 API 保存
useSensor(PointerSensor, { // await formSchemaService.save(schema);
activationConstraint: {
distance: 8,
},
})
);
// 生成唯一 ID
const generateId = () => `field_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
// 处理拖拽结束
const handleDragEnd = useCallback((event: DragEndEvent) => {
const { active, over } = event;
if (!over) return;
const overId = over.id.toString();
// 从组件面板拖入新组件
if (active.data.current?.isNew) {
const componentType = active.id.toString().replace('new-', '');
const componentMeta = COMPONENT_LIST.find(c => c.type === componentType);
if (componentMeta) {
const newField: FieldConfig = {
id: generateId(),
type: componentMeta.type,
label: componentMeta.label,
name: componentType + '_' + Date.now(),
...componentMeta.defaultConfig,
};
// 检测是否拖入栅格列
if (overId.startsWith('grid-') && overId.includes('-col-')) {
const match = overId.match(/grid-(.+)-col-(\d+)/);
if (match) {
const [, gridId, colIndexStr] = match;
const colIndex = parseInt(colIndexStr, 10);
setFields(prev => prev.map(field => {
if (field.id === gridId && field.type === 'grid') {
const newChildren = [...(field.children || [])];
if (!newChildren[colIndex]) {
newChildren[colIndex] = [];
}
newChildren[colIndex] = [...newChildren[colIndex], newField];
return { ...field, children: newChildren };
}
return field;
}));
setSelectedFieldId(newField.id);
message.success(`已添加${componentMeta.label}到栅格列`);
return;
}
}
// 拖入主画布
setFields(prev => [...prev, newField]);
setSelectedFieldId(newField.id);
message.success(`已添加${componentMeta.label}`);
}
}
// 画布内拖拽排序
else if (over.id === 'canvas' || fields.some(f => f.id === over.id)) {
const oldIndex = fields.findIndex(f => f.id === active.id);
const newIndex = fields.findIndex(f => f.id === over.id);
if (oldIndex !== -1 && newIndex !== -1 && oldIndex !== newIndex) {
setFields(arrayMove(fields, oldIndex, newIndex));
}
}
}, [fields]);
// 选中字段
const handleSelectField = useCallback((fieldId: string) => {
setSelectedFieldId(fieldId);
}, []);
// 深拷贝字段(包括嵌套的栅格子字段)
const deepCopyField = useCallback((field: FieldConfig): FieldConfig => {
const newField: FieldConfig = {
...field,
id: generateId(),
name: field.name + '_copy_' + Date.now(),
};
// 如果是栅格布局,递归复制子字段
if (field.type === 'grid' && field.children) {
newField.children = field.children.map(columnFields =>
columnFields.map(childField => deepCopyField(childField))
);
}
return newField;
}, []);
// 复制字段到剪贴板
const handleCopyField = useCallback((fieldId: string) => {
// 递归查找字段
const findField = (fieldList: FieldConfig[], id: string): FieldConfig | null => {
for (const field of fieldList) {
if (field.id === id) {
return field;
}
// 如果是栅格布局,递归查找子字段
if (field.type === 'grid' && field.children) {
for (const columnFields of field.children) {
const found = findField(columnFields, id);
if (found) return found;
}
}
}
return null;
};
const field = findField(fields, fieldId);
if (field) {
setClipboard(field);
message.success('已复制字段到剪贴板');
}
}, [fields]);
// 粘贴字段到主列表末尾
const handlePasteToCanvas = useCallback(() => {
if (!clipboard) {
message.warning('剪贴板为空');
return;
}
const newField = deepCopyField(clipboard);
setFields(prev => [...prev, newField]);
setSelectedFieldId(newField.id);
message.success('已粘贴字段');
}, [clipboard, deepCopyField]);
// 粘贴字段到栅格列
const handlePasteToGrid = useCallback((gridId: string, colIndex: number) => {
if (!clipboard) {
message.warning('剪贴板为空');
return;
}
const newField = deepCopyField(clipboard);
setFields(prev => prev.map(field => {
if (field.id === gridId && field.type === 'grid') {
const newChildren = [...(field.children || [])];
if (!newChildren[colIndex]) {
newChildren[colIndex] = [];
}
newChildren[colIndex] = [...newChildren[colIndex], newField];
return { ...field, children: newChildren };
}
return field;
}));
setSelectedFieldId(newField.id);
message.success('已粘贴字段到栅格');
}, [clipboard, deepCopyField]);
// 删除字段(支持删除栅格内的字段)
const handleDeleteField = useCallback((fieldId: string) => {
let deleted = false;
// 递归删除函数
const deleteFieldRecursive = (fieldList: FieldConfig[]): FieldConfig[] => {
return fieldList
.filter(f => {
if (f.id === fieldId) {
deleted = true;
return false;
}
return true;
})
.map(f => {
// 如果是栅格布局,递归处理其子字段
if (f.type === 'grid' && f.children) {
return {
...f,
children: f.children.map(columnFields =>
deleteFieldRecursive(columnFields)
),
};
}
return f;
});
};
setFields(prev => deleteFieldRecursive(prev));
if (deleted) {
if (selectedFieldId === fieldId) {
setSelectedFieldId(undefined);
}
message.success('已删除字段');
}
}, [selectedFieldId]);
// 更新字段属性(支持更新栅格内的字段)
const handleFieldChange = useCallback((updatedField: FieldConfig) => {
// 🐛 调试:打印字段更新
console.log('🔄 [FormDesigner] 字段更新:', updatedField);
if (updatedField.validationRules && updatedField.validationRules.length > 0) {
console.log('✅ [FormDesigner] 字段包含验证规则:', updatedField.validationRules);
}
// 递归更新函数
const updateFieldRecursive = (fieldList: FieldConfig[]): FieldConfig[] => {
return fieldList.map(f => {
if (f.id === updatedField.id) {
return updatedField;
}
// 如果是栅格布局,递归处理其子字段
if (f.type === 'grid' && f.children) {
return {
...f,
children: f.children.map(columnFields =>
updateFieldRecursive(columnFields)
),
};
}
return f;
});
};
setFields(prev => {
const newFields = updateFieldRecursive(prev);
console.log('💾 [FormDesigner] 字段列表已更新');
return newFields;
});
}, []);
// 更新表单配置
const handleFormConfigChange = useCallback((newConfig: Partial<FormConfig>) => {
setFormConfig(prev => ({ ...prev, ...newConfig }));
}, []);
// 保存表单
const handleSave = useCallback(() => {
if (fields.length === 0) {
message.warning('表单为空,请先添加字段');
return;
}
const schema: FormSchema = {
formConfig,
fields,
};
console.log('保存的表单 Schema:', JSON.stringify(schema, null, 2));
message.success('表单已保存!请查看控制台');
// TODO: 调用后端 API 保存
}, [fields, formConfig]);
// 导出 JSON
const handleExport = useCallback(() => {
if (fields.length === 0) {
message.warning('表单为空,无法导出');
return;
}
const schema: FormSchema = {
formConfig,
fields,
};
const dataStr = JSON.stringify(schema, null, 2);
const dataUri = 'data:application/json;charset=utf-8,' + encodeURIComponent(dataStr);
const exportFileDefaultName = `form-schema-${Date.now()}.json`;
const linkElement = document.createElement('a');
linkElement.setAttribute('href', dataUri);
linkElement.setAttribute('download', exportFileDefaultName);
linkElement.click();
message.success('JSON Schema 已导出');
}, [fields, formConfig]);
// 导入 JSON
const handleImport = useCallback(() => {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
input.onchange = (e: any) => {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (event: any) => {
try {
const schema: FormSchema = JSON.parse(event.target.result);
if (!schema.fields || !Array.isArray(schema.fields)) {
message.error('无效的表单 Schema 格式');
return;
}
setFields(schema.fields);
setFormConfig(schema.formConfig || formConfig);
setSelectedFieldId(undefined);
message.success('JSON Schema 导入成功');
} catch (error) {
message.error('JSON 格式错误');
console.error('导入错误:', error);
}
};
reader.readAsText(file);
}
};
input.click();
}, [formConfig]);
// 清空表单
const handleClear = useCallback(() => {
Modal.confirm({
title: '确认清空',
content: '确定要清空所有字段吗?此操作不可恢复。',
onOk: () => {
setFields([]);
setSelectedFieldId(undefined);
message.info('表单已清空');
},
});
}, []);
// 递归查找选中的字段(包括栅格内的字段)
const findFieldById = (fieldList: FieldConfig[], id?: string): FieldConfig | undefined => {
if (!id) return undefined;
for (const field of fieldList) {
if (field.id === id) {
return field;
}
// 如果是栅格布局,递归查找子字段
if (field.type === 'grid' && field.children) {
for (const columnFields of field.children) {
const found = findFieldById(columnFields, id);
if (found) return found;
}
}
}
return undefined;
}; };
const selectedField = findFieldById(fields, selectedFieldId) || null; // ==================== 示例 2: 表单渲染器(静态) ====================
const [previewSchema, setPreviewSchema] = useState<FormSchema>();
const [staticFormData, setStaticFormData] = useState<Record<string, any>>({});
// 获取所有字段扁平化包括grid中的嵌套字段 const handleStaticFormSubmit = async (data: Record<string, any>) => {
const getAllFields = useCallback((fieldList: FieldConfig[]): FieldConfig[] => { console.log('📤 静态表单提交数据:', data);
const result: FieldConfig[] = []; message.success('表单提交成功!请查看控制台');
};
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); // ==================== 示例 3: 工作流表单(弹窗) ====================
const [workflowModalVisible, setWorkflowModalVisible] = useState(false);
// 快捷键支持 const workflowFormSchema: FormSchema = {
useEffect(() => { version: '1.0',
const handleKeyDown = (e: KeyboardEvent) => { formConfig: {
// 检查是否在输入框、文本域等可编辑元素中 labelAlign: 'right',
const target = e.target as HTMLElement; size: 'middle',
const isEditable = formWidth: 600
target.tagName === 'INPUT' || },
target.tagName === 'TEXTAREA' || fields: [
target.isContentEditable; {
id: 'wf_field_1',
type: 'input',
label: '工作流名称',
name: 'workflowName',
placeholder: '请输入工作流名称',
required: true
},
{
id: 'wf_field_2',
type: 'select',
label: '目标环境',
name: 'environment',
placeholder: '请选择环境',
required: true,
dataSourceType: 'static',
options: [
{ label: '开发环境', value: 'dev' },
{ label: '测试环境', value: 'test' },
{ label: '生产环境', value: 'prod' }
]
},
{
id: 'wf_field_3',
type: 'textarea',
label: '执行说明',
name: 'description',
placeholder: '请输入执行说明',
rows: 4
},
],
};
// 如果在可编辑元素中,不触发快捷键 const handleWorkflowSubmit = async (data: Record<string, any>) => {
if (isEditable) return; console.log('🚀 工作流启动参数:', data);
message.success('工作流已启动!');
setWorkflowModalVisible(false);
};
const isMac = /Mac|iPod|iPhone|iPad/.test(navigator.platform); // ==================== 示例 4: 只读模式 ====================
const ctrlKey = isMac ? e.metaKey : e.ctrlKey; const readonlySchema: FormSchema = {
version: '1.0',
formConfig: {
labelAlign: 'right',
size: 'middle',
formWidth: 600
},
fields: [
{
id: 'ro_field_1',
type: 'input',
label: '申请人',
name: 'applicant'
},
{
id: 'ro_field_2',
type: 'date',
label: '申请日期',
name: 'applyDate'
},
{
id: 'ro_field_3',
type: 'select',
label: '审批状态',
name: 'approvalStatus',
dataSourceType: 'static',
options: [
{ label: '待审批', value: 'pending' },
{ label: '已通过', value: 'approved' },
{ label: '已拒绝', value: 'rejected' }
]
},
{
id: 'ro_field_4',
type: 'textarea',
label: '审批意见',
name: 'approvalComment'
},
],
};
// Ctrl+C / Cmd+C: 复制选中的字段 const readonlyData = {
if (ctrlKey && e.key === 'c' && selectedFieldId) { applicant: '张三',
e.preventDefault(); applyDate: '2024-07-20',
handleCopyField(selectedFieldId); approvalStatus: 'approved',
} approvalComment: '同意申请,准予执行。',
};
// Ctrl+V / Cmd+V: 粘贴到画布 // ==================== 示例 5: 复杂表单(带栅格布局) ====================
if (ctrlKey && e.key === 'v' && clipboard) { const [complexFormData, setComplexFormData] = useState<Record<string, any>>({});
e.preventDefault();
handlePasteToCanvas(); const complexFormSchema: FormSchema = {
} version: '1.0',
formConfig: {
labelAlign: 'right',
size: 'middle',
formWidth: 800,
title: '用户信息登记表'
},
fields: [
{
id: 'grid_1',
type: 'grid',
label: '基本信息',
name: 'grid_basic',
columns: 2,
columnSpans: [12, 12],
gutter: 16,
children: [
[
{ id: 'f1', type: 'input', label: '姓名', name: 'name', placeholder: '请输入姓名', required: true },
{ id: 'f2', type: 'number', label: '年龄', name: 'age', placeholder: '请输入年龄', min: 1, max: 150 },
],
[
{ id: 'f3', type: 'radio', label: '性别', name: 'gender', dataSourceType: 'static', options: [{ label: '男', value: 'male' }, { label: '女', value: 'female' }] },
{ id: 'f4', type: 'date', label: '出生日期', name: 'birthday', placeholder: '请选择日期' },
],
],
},
{
id: 'grid_2',
type: 'grid',
label: '联系方式',
name: 'grid_contact',
columns: 2,
columnSpans: [12, 12],
gutter: 16,
children: [
[
{ id: 'f5', type: 'input', label: '手机号', name: 'phone', placeholder: '请输入手机号' },
{ id: 'f6', type: 'input', label: '邮箱', name: 'email', placeholder: '请输入邮箱' },
],
[
{ id: 'f7', type: 'input', label: '微信', name: 'wechat', placeholder: '请输入微信号' },
{ id: 'f8', type: 'input', label: 'QQ', name: 'qq', placeholder: '请输入QQ号' },
],
],
},
{
id: 'f9',
type: 'textarea',
label: '个人简介',
name: 'bio',
placeholder: '请输入个人简介',
rows: 4,
},
],
};
// Delete: 删除选中的字段 const handleComplexFormSubmit = async (data: Record<string, any>) => {
if (e.key === 'Delete' && selectedFieldId) { console.log('📋 复杂表单提交数据:', data);
e.preventDefault(); message.success('表单提交成功!');
handleDeleteField(selectedFieldId); };
}
};
window.addEventListener('keydown', handleKeyDown); // ==================== Tab 配置 ====================
return () => { const tabItems = [
window.removeEventListener('keydown', handleKeyDown); {
}; key: '1',
}, [selectedFieldId, clipboard, handleCopyField, handlePasteToCanvas, handleDeleteField]); label: '📝 示例 1: 表单设计器',
children: (
<Card>
<h2>FormDesigner</h2>
<p style={{ marginBottom: 16, color: '#666' }}>
/ JSON Schema
</p>
<Divider />
<FormDesigner
value={designerSchema}
onChange={setDesignerSchema}
onSave={handleDesignerSave}
/>
</Card>
),
},
{
key: '2',
label: '🎨 示例 2: 静态表单渲染',
children: (
<Card>
<h2></h2>
<p style={{ marginBottom: 16, color: '#666' }}>
Schema
</p>
<Divider />
{previewSchema ? (
<FormRenderer
schema={previewSchema}
value={staticFormData}
onChange={setStaticFormData}
onSubmit={handleStaticFormSubmit}
showCancel
/>
) : (
<div style={{ padding: 40, textAlign: 'center', color: '#999' }}>
"示例 1"
</div>
)}
</Card>
),
},
{
key: '3',
label: '🚀 示例 3: 工作流表单',
children: (
<Card>
<h2></h2>
<p style={{ marginBottom: 16, color: '#666' }}>
Modal 使
</p>
<Divider />
<Space direction="vertical" size="large" style={{ width: '100%' }}>
<div>
<h3>使</h3>
<ul>
<li></li>
<li></li>
<li></li>
</ul>
</div>
<Button type="primary" size="large" onClick={() => setWorkflowModalVisible(true)}>
</Button>
</Space>
<Modal
title="启动工作流"
open={workflowModalVisible}
onCancel={() => setWorkflowModalVisible(false)}
footer={null}
width={workflowFormSchema.formConfig.formWidth || 600}
>
<FormRenderer
schema={workflowFormSchema}
onSubmit={handleWorkflowSubmit}
onCancel={() => setWorkflowModalVisible(false)}
showCancel
submitText="启动"
cancelText="取消"
/>
</Modal>
</Card>
),
},
{
key: '4',
label: '👁️ 示例 4: 只读模式',
children: (
<Card>
<h2></h2>
<p style={{ marginBottom: 16, color: '#666' }}>
</p>
<Divider />
<div>
<h3>使</h3>
<ul>
<li></li>
<li></li>
<li></li>
</ul>
</div>
<Divider />
<FormRenderer
schema={readonlySchema}
value={readonlyData}
readonly={true}
/>
</Card>
),
},
{
key: '5',
label: '📋 示例 5: 复杂表单',
children: (
<Card>
<h2></h2>
<p style={{ marginBottom: 16, color: '#666' }}>
</p>
<Divider />
<FormRenderer
schema={complexFormSchema}
value={complexFormData}
onChange={setComplexFormData}
onSubmit={handleComplexFormSubmit}
showCancel
/>
</Card>
),
},
];
return ( return (
<div style={{ height: '100vh', width: '100%', display: 'flex', flexDirection: 'column' }}> <div style={{ padding: 24 }}>
{/* 顶部工具栏 */} <div style={{ marginBottom: 24 }}>
<div <h1>FormDesigner 使</h1>
style={{ <p style={{ color: '#666', fontSize: 14 }}>
padding: '12px 24px', 使 <code>@/components/FormDesigner</code> <strong>FormDesigner</strong> <strong>FormRenderer</strong>
background: '#fff', </p>
borderBottom: '1px solid #e8e8e8',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
zIndex: 1000,
}}
>
<h2 style={{ margin: 0, fontSize: '18px', fontWeight: 500 }}>
</h2>
<Space>
<Button icon={<UploadOutlined />} onClick={handleImport}>
JSON
</Button>
<Button icon={<DownloadOutlined />} onClick={handleExport}>
JSON
</Button>
<Button icon={<EyeOutlined />} onClick={() => setPreviewVisible(true)}>
</Button>
<Button icon={<ClearOutlined />} danger onClick={handleClear}>
</Button>
<Button type="primary" icon={<SaveOutlined />} onClick={handleSave}>
</Button>
</Space>
</div> </div>
<Tabs defaultActiveKey="1" items={tabItems} />
{/* 主体区域 */}
<DndContext sensors={sensors} onDragEnd={handleDragEnd}>
<div style={{ flex: 1, display: 'flex', overflow: 'hidden' }}>
{/* 左侧组件面板 */}
<div style={{ width: 280 }}>
<ComponentPanel />
</div>
{/* 中间设计画布 */}
<DesignCanvas
fields={fields}
selectedFieldId={selectedFieldId}
onSelectField={handleSelectField}
onCopyField={handleCopyField}
onPasteToCanvas={handlePasteToCanvas}
onPasteToGrid={handlePasteToGrid}
onDeleteField={handleDeleteField}
labelAlign={formConfig.labelAlign}
hasClipboard={!!clipboard}
/>
{/* 右侧属性面板 */}
<PropertyPanel
selectedField={selectedField}
formConfig={formConfig}
onFieldChange={handleFieldChange}
onFormConfigChange={handleFormConfigChange}
allFields={allFields}
/>
</div>
<DragOverlay>{/* 拖拽时的视觉反馈 */}</DragOverlay>
</DndContext>
{/* 预览模态框 */}
<Modal
title="表单预览"
open={previewVisible}
onCancel={() => setPreviewVisible(false)}
width={formConfig.formWidth || 600}
footer={null}
afterOpenChange={(open) => {
if (open) {
console.log('🎬 [Modal] 打开预览,当前字段列表:', fields);
// 递归检查所有字段包括grid内的嵌套字段
const checkFieldsRecursive = (fieldList: FieldConfig[], level = 0) => {
fieldList.forEach(field => {
const indent = ' '.repeat(level);
console.log(`${indent}📦 [Modal] 字段: "${field.label}" (${field.type})`, field);
if (field.validationRules && field.validationRules.length > 0) {
console.log(`${indent}📋 [Modal] ✅ 包含验证规则:`, field.validationRules);
} else {
console.log(`${indent}📋 [Modal] ❌ 没有验证规则`);
}
// 递归检查grid的子字段
if (field.type === 'grid' && field.children) {
field.children.forEach((columnFields, colIndex) => {
console.log(`${indent} 🔸 Grid列 ${colIndex + 1}:`);
checkFieldsRecursive(columnFields, level + 2);
});
}
});
};
checkFieldsRecursive(fields);
}
}}
>
<FormPreview fields={fields} formConfig={formConfig} />
</Modal>
</div> </div>
); );
}; };
export default FormDesigner; export default FormDesignerExamplesPage;