/** * 表单设计器主入口 * * 功能: * - 左侧:组件面板 * - 中间:设计画布 * - 右侧:属性配置 * - 导入/导出 JSON Schema */ import React, { useState, useCallback } from 'react'; import { Button, Space, message, Modal } from 'antd'; import { 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 [fields, setFields] = useState([]); // 选中的字段 const [selectedFieldId, setSelectedFieldId] = useState(); // 表单配置 const [formConfig, setFormConfig] = useState({ labelAlign: 'right', size: 'middle', formWidth: 600, // 表单弹窗宽度(像素) }); // 预览模式 const [previewVisible, setPreviewVisible] = useState(false); // 拖拽传感器 const sensors = useSensors( useSensor(PointerSensor, { 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 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) => { // 递归更新函数 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)); }, []); // 保存表单 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); return (
{/* 顶部工具栏 */}

表单设计器

{/* 主体区域 */}
{/* 左侧组件面板 */}
{/* 中间设计画布 */} {/* 右侧属性面板 */}
{/* 拖拽时的视觉反馈 */}
{/* 预览模态框 */} setPreviewVisible(false)} width={formConfig.formWidth || 600} footer={null} >
); }; export default FormDesigner;