diff --git a/frontend/src/components/FormDesigner/Designer.tsx b/frontend/src/components/FormDesigner/Designer.tsx new file mode 100644 index 00000000..c34bb1fe --- /dev/null +++ b/frontend/src/components/FormDesigner/Designer.tsx @@ -0,0 +1,697 @@ +/** + * 表单设计器组件(设计时) + * + * 功能: + * - 可视化拖拽设计表单 + * - 支持字段属性配置 + * - 支持验证规则和联动规则 + * - 导入/导出 JSON Schema + * + * 使用方式: + * ```tsx + * + * ``` + */ + +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 = ({ + value, + onChange, + onSave, + readonly = false, + showToolbar = true, + extraActions +}) => { + // 内部状态(非受控模式使用) + const [internalFields, setInternalFields] = useState([]); + const [internalFormConfig, setInternalFormConfig] = useState({ + labelAlign: 'right', + size: 'middle', + formWidth: 600, + }); + + // 使用受控值或内部值 + const fields = value?.fields ?? internalFields; + const formConfig = value?.formConfig ?? internalFormConfig; + + const setFields = useCallback((updater: React.SetStateAction) => { + 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) => { + 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(); + + // 剪贴板(用于复制粘贴) + const [clipboard, setClipboard] = useState(null); + + // 预览模式 + const [previewVisible, setPreviewVisible] = useState(false); + + // 正在拖拽的元素ID + const [activeId, setActiveId] = useState(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) => { + 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 ( +
+ {showToolbar && ( +
+
表单设计
+ + + + + + + {extraActions} + +
+ )} + + +
+ {/* 左侧组件面板 */} +
+ +
+ + {/* 中间设计画布 */} + + + {/* 右侧属性面板 */} + +
+ + {/* 拖拽覆盖层 - 显示正在拖拽的元素 */} + + {activeId && activeId.startsWith('new-') ? ( +
+ {COMPONENT_LIST.find(c => `new-${c.type}` === activeId)?.label || '组件'} +
+ ) : null} +
+
+ + {/* 预览模态框 */} + setPreviewVisible(false)} + width={formConfig.formWidth || 600} + footer={null} + > + + +
+ ); +}; + +export default FormDesigner; + diff --git a/frontend/src/components/FormDesigner/Renderer.tsx b/frontend/src/components/FormDesigner/Renderer.tsx new file mode 100644 index 00000000..9fb8a28a --- /dev/null +++ b/frontend/src/components/FormDesigner/Renderer.tsx @@ -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; // 表单值(受控) + onChange?: (values: Record) => void; // 值变化回调 + onSubmit?: (values: Record) => void; // 提交回调 + onCancel?: () => void; // 取消回调 + readonly?: boolean; // 只读模式 + showSubmit?: boolean; // 是否显示提交按钮 + showCancel?: boolean; // 是否显示取消按钮 + submitText?: string; // 提交按钮文本 + cancelText?: string; // 取消按钮文本 +} + +const FormRenderer: React.FC = ({ + 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>(value || {}); + const [fieldStates, setFieldStates] = useState>({}); + + // 同步外部值到表单 + 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) => { + 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): 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 = {}; + + 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 = {}; + + 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 ( +
+ +
+ ); + } + + // 栅格布局:使用专门的预览组件,自动处理内部字段的 Form.Item + if (field.type === 'grid') { + return ( + 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 ( + + setFormData(prev => ({ ...prev, [field.name]: value }))} + isPreview={true} + /> + + ); + }); + }; + + if (fields.length === 0) { + return ( +
+ 表单为空 +
+ ); + } + + 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 ( +
+
+ {formConfig.title && ( +

{formConfig.title}

+ )} + + {renderFields(fields)} + + {!readonly && (showSubmit || showCancel) && ( + +
+ {showSubmit && ( + + )} + {showCancel && ( + + )} + {!showCancel && ( + + )} +
+
+ )} +
+
+ ); +}; + +export default FormRenderer; + diff --git a/frontend/src/pages/FormDesigner/components/CascadeOptionEditor.tsx b/frontend/src/components/FormDesigner/components/CascadeOptionEditor.tsx similarity index 100% rename from frontend/src/pages/FormDesigner/components/CascadeOptionEditor.tsx rename to frontend/src/components/FormDesigner/components/CascadeOptionEditor.tsx diff --git a/frontend/src/pages/FormDesigner/components/ComponentPanel.tsx b/frontend/src/components/FormDesigner/components/ComponentPanel.tsx similarity index 100% rename from frontend/src/pages/FormDesigner/components/ComponentPanel.tsx rename to frontend/src/components/FormDesigner/components/ComponentPanel.tsx diff --git a/frontend/src/pages/FormDesigner/components/DesignCanvas.tsx b/frontend/src/components/FormDesigner/components/DesignCanvas.tsx similarity index 98% rename from frontend/src/pages/FormDesigner/components/DesignCanvas.tsx rename to frontend/src/components/FormDesigner/components/DesignCanvas.tsx index f133bcdb..2ab4808b 100644 --- a/frontend/src/pages/FormDesigner/components/DesignCanvas.tsx +++ b/frontend/src/components/FormDesigner/components/DesignCanvas.tsx @@ -39,6 +39,9 @@ const DesignCanvas: React.FC = ({ }) => { const { setNodeRef } = useDroppable({ id: 'canvas', + data: { + accept: 'field', + }, }); return ( diff --git a/frontend/src/pages/FormDesigner/components/FieldItem.tsx b/frontend/src/components/FormDesigner/components/FieldItem.tsx similarity index 98% rename from frontend/src/pages/FormDesigner/components/FieldItem.tsx rename to frontend/src/components/FormDesigner/components/FieldItem.tsx index a73deccf..a5969722 100644 --- a/frontend/src/pages/FormDesigner/components/FieldItem.tsx +++ b/frontend/src/components/FormDesigner/components/FieldItem.tsx @@ -50,6 +50,9 @@ const FieldItem: React.FC = ({ isDragging, } = useSortable({ id: field.id, + data: { + accept: 'field', + }, }); const style = { diff --git a/frontend/src/pages/FormDesigner/components/FieldRenderer.tsx b/frontend/src/components/FormDesigner/components/FieldRenderer.tsx similarity index 92% rename from frontend/src/pages/FormDesigner/components/FieldRenderer.tsx rename to frontend/src/components/FormDesigner/components/FieldRenderer.tsx index 4ffa2bda..f4c381e5 100644 --- a/frontend/src/pages/FormDesigner/components/FieldRenderer.tsx +++ b/frontend/src/components/FormDesigner/components/FieldRenderer.tsx @@ -74,8 +74,16 @@ const FieldRenderer: React.FC = ({ if (field.type === 'text') { return ( -
- {field.content || '这是一段文字'} +
+ + {field.content || '这是一段文字'} +
); } @@ -161,6 +169,11 @@ const FieldRenderer: React.FC = ({ disabled={field.disabled} value={value} onChange={onChange} + style={{ + display: 'flex', + flexDirection: field.checkboxLayout === 'vertical' ? 'column' : 'row', + gap: field.checkboxLayout === 'vertical' ? '8px' : '16px', + }} /> ); diff --git a/frontend/src/pages/FormDesigner/components/FormPreview.tsx b/frontend/src/components/FormDesigner/components/FormPreview.tsx similarity index 100% rename from frontend/src/pages/FormDesigner/components/FormPreview.tsx rename to frontend/src/components/FormDesigner/components/FormPreview.tsx diff --git a/frontend/src/pages/FormDesigner/components/GridField.tsx b/frontend/src/components/FormDesigner/components/GridField.tsx similarity index 99% rename from frontend/src/pages/FormDesigner/components/GridField.tsx rename to frontend/src/components/FormDesigner/components/GridField.tsx index 119f19ca..3fb74d2a 100644 --- a/frontend/src/pages/FormDesigner/components/GridField.tsx +++ b/frontend/src/components/FormDesigner/components/GridField.tsx @@ -109,6 +109,11 @@ const GridColumn: React.FC = ({ }) => { const { setNodeRef, isOver } = useDroppable({ id: dropId, + data: { + accept: 'field', + gridId, + colIndex, + }, }); // 预览模式下的样式 diff --git a/frontend/src/pages/FormDesigner/components/GridFieldPreview.tsx b/frontend/src/components/FormDesigner/components/GridFieldPreview.tsx similarity index 100% rename from frontend/src/pages/FormDesigner/components/GridFieldPreview.tsx rename to frontend/src/components/FormDesigner/components/GridFieldPreview.tsx diff --git a/frontend/src/pages/FormDesigner/components/LinkageRuleEditor.tsx b/frontend/src/components/FormDesigner/components/LinkageRuleEditor.tsx similarity index 100% rename from frontend/src/pages/FormDesigner/components/LinkageRuleEditor.tsx rename to frontend/src/components/FormDesigner/components/LinkageRuleEditor.tsx diff --git a/frontend/src/pages/FormDesigner/components/PropertyPanel.tsx b/frontend/src/components/FormDesigner/components/PropertyPanel.tsx similarity index 95% rename from frontend/src/pages/FormDesigner/components/PropertyPanel.tsx rename to frontend/src/components/FormDesigner/components/PropertyPanel.tsx index 41d14bdc..35355fcf 100644 --- a/frontend/src/pages/FormDesigner/components/PropertyPanel.tsx +++ b/frontend/src/components/FormDesigner/components/PropertyPanel.tsx @@ -274,9 +274,30 @@ const PropertyPanel: React.FC = ({ )} {selectedField.type === 'text' && ( - - - + <> + + + + + + 靠左 + 居中 + 靠右 + + + + + + + + + )} {selectedField.type === 'grid' && ( @@ -383,6 +404,15 @@ const PropertyPanel: React.FC = ({ )} + {selectedField.type === 'checkbox' && ( + + + 行内 + 块级 + + + )} + {hasOptions && (
diff --git a/frontend/src/pages/FormDesigner/components/ValidationRuleEditor.tsx b/frontend/src/components/FormDesigner/components/ValidationRuleEditor.tsx similarity index 100% rename from frontend/src/pages/FormDesigner/components/ValidationRuleEditor.tsx rename to frontend/src/components/FormDesigner/components/ValidationRuleEditor.tsx diff --git a/frontend/src/pages/FormDesigner/config.ts b/frontend/src/components/FormDesigner/config.ts similarity index 96% rename from frontend/src/pages/FormDesigner/config.ts rename to frontend/src/components/FormDesigner/config.ts index 682cf4e9..e66bafb3 100644 --- a/frontend/src/pages/FormDesigner/config.ts +++ b/frontend/src/components/FormDesigner/config.ts @@ -119,6 +119,7 @@ export const COMPONENT_LIST: ComponentMeta[] = [ category: '基础字段', defaultConfig: { dataSourceType: 'static', + checkboxLayout: 'horizontal', // 默认行内布局 options: [ { label: '选项1', value: '1' }, { label: '选项2', value: '2' }, @@ -161,6 +162,9 @@ export const COMPONENT_LIST: ComponentMeta[] = [ category: '基础字段', defaultConfig: { content: '这是一段文字', + textAlign: 'left', // 默认左对齐 + fontSize: 14, // 默认字体大小 14px + textColor: '#000000', // 默认黑色 }, }, // 高级字段 diff --git a/frontend/src/pages/FormDesigner/hooks/useCascaderOptions.ts b/frontend/src/components/FormDesigner/hooks/useCascaderOptions.ts similarity index 100% rename from frontend/src/pages/FormDesigner/hooks/useCascaderOptions.ts rename to frontend/src/components/FormDesigner/hooks/useCascaderOptions.ts diff --git a/frontend/src/pages/FormDesigner/hooks/useFieldOptions.ts b/frontend/src/components/FormDesigner/hooks/useFieldOptions.ts similarity index 100% rename from frontend/src/pages/FormDesigner/hooks/useFieldOptions.ts rename to frontend/src/components/FormDesigner/hooks/useFieldOptions.ts diff --git a/frontend/src/components/FormDesigner/index.tsx b/frontend/src/components/FormDesigner/index.tsx new file mode 100644 index 00000000..5964b885 --- /dev/null +++ b/frontend/src/components/FormDesigner/index.tsx @@ -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'; + diff --git a/frontend/src/pages/FormDesigner/styles.css b/frontend/src/components/FormDesigner/styles.css similarity index 88% rename from frontend/src/pages/FormDesigner/styles.css rename to frontend/src/components/FormDesigner/styles.css index 5ae68566..5418bb4d 100644 --- a/frontend/src/pages/FormDesigner/styles.css +++ b/frontend/src/components/FormDesigner/styles.css @@ -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 { height: 100%; diff --git a/frontend/src/pages/FormDesigner/types.ts b/frontend/src/components/FormDesigner/types.ts similarity index 91% rename from frontend/src/pages/FormDesigner/types.ts rename to frontend/src/components/FormDesigner/types.ts index 66ddd72d..4fe40957 100644 --- a/frontend/src/pages/FormDesigner/types.ts +++ b/frontend/src/components/FormDesigner/types.ts @@ -131,6 +131,10 @@ export interface FieldConfig { maxCount?: number; span?: number; // 栅格占位格数(该字段在栅格中占几列) content?: string; // 文本内容(用于 text 组件) + textAlign?: 'left' | 'center' | 'right'; // 文字对齐方式(用于 text 组件) + fontSize?: number; // 文字大小(用于 text 组件) + textColor?: string; // 文字颜色(用于 text 组件) + checkboxLayout?: 'horizontal' | 'vertical'; // 多选框布局方式(horizontal:行内,vertical:块级) columns?: number; // 栅格列数(用于 grid 组件) columnSpans?: number[]; // 栅格每列宽度(用于 grid 组件,如 [2, 18, 2],总和不超过24) gutter?: number; // 栅格间距(用于 grid 组件) @@ -145,9 +149,15 @@ export interface FormConfig { size?: 'small' | 'middle' | 'large'; } -// 表单 Schema +// 表单 Schema(标准化数据格式) export interface FormSchema { + version: string; // Schema 版本号 formConfig: FormConfig; fields: FieldConfig[]; + metadata?: { // 元数据(可选) + createdAt?: string; + updatedAt?: string; + author?: string; + }; } diff --git a/frontend/src/pages/FormDesigner/index.tsx b/frontend/src/pages/FormDesigner/index.tsx index eaf6ffc7..c8b7c1c6 100644 --- a/frontend/src/pages/FormDesigner/index.tsx +++ b/frontend/src/pages/FormDesigner/index.tsx @@ -1,576 +1,349 @@ /** - * 表单设计器主入口 + * FormDesigner 组件使用示例页面 * - * 功能: - * - 左侧:组件面板 - * - 中间:设计画布 - * - 右侧:属性配置 - * - 导入/导出 JSON Schema + * 展示如何使用 @/components/FormDesigner 中的组件: + * - FormDesigner: 表单设计器(设计时) + * - FormRenderer: 表单渲染器(运行时) */ -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, 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'; +import React, { useState } from 'react'; +import { message, Tabs, Card, Button, Modal, Space, Divider } from 'antd'; +import { FormDesigner, FormRenderer, type FormSchema } from '@/components/FormDesigner'; -const FormDesigner: React.FC = () => { - // 表单字段列表 - const [fields, setFields] = useState([]); - - // 选中的字段 - const [selectedFieldId, setSelectedFieldId] = useState(); - - // 剪贴板(用于复制粘贴) - const [clipboard, setClipboard] = useState(null); - - // 表单配置 - const [formConfig, setFormConfig] = useState({ - labelAlign: 'right', - size: 'middle', - formWidth: 600, // 表单弹窗宽度(像素) - }); +const FormDesignerExamplesPage: React.FC = () => { + // ==================== 示例 1: 表单设计器 ==================== + const [designerSchema, setDesignerSchema] = useState(); - // 预览模式 - 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 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) => { - 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 handleDesignerSave = async (schema: FormSchema) => { + console.log('💾 保存的表单 Schema:', JSON.stringify(schema, null, 2)); + message.success('表单设计已保存!请查看控制台'); + setPreviewSchema(schema); // 保存后更新预览 + // 实际项目中这里会调用后端 API 保存 + // await formSchemaService.save(schema); }; - const selectedField = findFieldById(fields, selectedFieldId) || null; + // ==================== 示例 2: 表单渲染器(静态) ==================== + const [previewSchema, setPreviewSchema] = useState(); + const [staticFormData, setStaticFormData] = useState>({}); - // 获取所有字段(扁平化,包括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 handleStaticFormSubmit = async (data: Record) => { + console.log('📤 静态表单提交数据:', data); + message.success('表单提交成功!请查看控制台'); + }; - const allFields = getAllFields(fields); + // ==================== 示例 3: 工作流表单(弹窗) ==================== + const [workflowModalVisible, setWorkflowModalVisible] = useState(false); - // 快捷键支持 - useEffect(() => { - const handleKeyDown = (e: KeyboardEvent) => { - // 检查是否在输入框、文本域等可编辑元素中 - const target = e.target as HTMLElement; - const isEditable = - target.tagName === 'INPUT' || - target.tagName === 'TEXTAREA' || - target.isContentEditable; + const workflowFormSchema: FormSchema = { + version: '1.0', + formConfig: { + labelAlign: 'right', + size: 'middle', + formWidth: 600 + }, + fields: [ + { + 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 + }, + ], + }; - // 如果在可编辑元素中,不触发快捷键 - if (isEditable) return; + const handleWorkflowSubmit = async (data: Record) => { + console.log('🚀 工作流启动参数:', data); + message.success('工作流已启动!'); + setWorkflowModalVisible(false); + }; - const isMac = /Mac|iPod|iPhone|iPad/.test(navigator.platform); - const ctrlKey = isMac ? e.metaKey : e.ctrlKey; + // ==================== 示例 4: 只读模式 ==================== + 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: 复制选中的字段 - if (ctrlKey && e.key === 'c' && selectedFieldId) { - e.preventDefault(); - handleCopyField(selectedFieldId); - } + const readonlyData = { + applicant: '张三', + applyDate: '2024-07-20', + approvalStatus: 'approved', + approvalComment: '同意申请,准予执行。', + }; - // Ctrl+V / Cmd+V: 粘贴到画布 - if (ctrlKey && e.key === 'v' && clipboard) { - e.preventDefault(); - handlePasteToCanvas(); - } + // ==================== 示例 5: 复杂表单(带栅格布局) ==================== + const [complexFormData, setComplexFormData] = useState>({}); + + 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: 删除选中的字段 - if (e.key === 'Delete' && selectedFieldId) { - e.preventDefault(); - handleDeleteField(selectedFieldId); - } - }; + const handleComplexFormSubmit = async (data: Record) => { + console.log('📋 复杂表单提交数据:', data); + message.success('表单提交成功!'); + }; - window.addEventListener('keydown', handleKeyDown); - return () => { - window.removeEventListener('keydown', handleKeyDown); - }; - }, [selectedFieldId, clipboard, handleCopyField, handlePasteToCanvas, handleDeleteField]); + // ==================== Tab 配置 ==================== + const tabItems = [ + { + key: '1', + label: '📝 示例 1: 表单设计器', + children: ( + +

表单设计器(FormDesigner)

+

+ 可视化拖拽设计表单,支持导入/导出 JSON Schema,支持字段属性配置、验证规则、联动规则等。 +

+ + +
+ ), + }, + { + key: '2', + label: '🎨 示例 2: 静态表单渲染', + children: ( + +

表单渲染器(静态模式)

+

+ 根据设计器保存的 Schema 渲染表单,支持数据收集、验证和提交。 +

+ + {previewSchema ? ( + + ) : ( +
+ 请先在"示例 1"中设计表单并保存 +
+ )} +
+ ), + }, + { + key: '3', + label: '🚀 示例 3: 工作流表单', + children: ( + +

工作流表单(弹窗模式)

+

+ 模拟工作流启动场景,在 Modal 中使用表单渲染器收集参数。 +

+ + +
+

使用场景

+
    +
  • 工作流启动参数收集
  • +
  • 任务审批表单
  • +
  • 快捷操作弹窗
  • +
+
+ +
+ setWorkflowModalVisible(false)} + footer={null} + width={workflowFormSchema.formConfig.formWidth || 600} + > + setWorkflowModalVisible(false)} + showCancel + submitText="启动" + cancelText="取消" + /> + +
+ ), + }, + { + key: '4', + label: '👁️ 示例 4: 只读模式', + children: ( + +

只读模式

+

+ 查看已提交的表单数据,所有字段禁用编辑,不显示提交按钮。 +

+ +
+

使用场景

+
    +
  • 审批流程详情查看
  • +
  • 历史数据展示
  • +
  • 表单归档查看
  • +
+
+ + +
+ ), + }, + { + key: '5', + label: '📋 示例 5: 复杂表单', + children: ( + +

复杂表单(带栅格布局)

+

+ 展示包含栅格布局的复杂表单,支持多列排版。 +

+ + +
+ ), + }, + ]; return ( -
- {/* 顶部工具栏 */} -
-

- 表单设计器 -

- - - - - - - +
+
+

FormDesigner 组件使用示例

+

+ 本页面展示如何使用 @/components/FormDesigner 中的 FormDesignerFormRenderer 组件。 +

- - {/* 主体区域 */} - -
- {/* 左侧组件面板 */} -
- -
- - {/* 中间设计画布 */} - - - {/* 右侧属性面板 */} - -
- - {/* 拖拽时的视觉反馈 */} -
- - {/* 预览模态框 */} - 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); - } - }} - > - - +
); }; -export default FormDesigner; +export default FormDesignerExamplesPage;