Browse Source

表单设计添加布局组件

wyn 5 months ago
parent
commit
47b40e7a46
1 changed files with 495 additions and 81 deletions
  1. 495 81
      src/components/FormDesigner.tsx

+ 495 - 81
src/components/FormDesigner.tsx

@@ -32,7 +32,8 @@ import {
   Cascader,
   Upload,
   Dropdown,
-  Alert
+  Alert,
+  Tag
 } from 'antd';
 import type { MenuProps } from 'antd';
 import { UploadOutlined, DownSquareOutlined, CheckSquareOutlined, ClockCircleOutlined } from '@ant-design/icons';
@@ -166,6 +167,10 @@ interface FormField {
   alertShowIcon?: boolean;
   alertCentered?: boolean;
   alertTheme?: 'light' | 'dark';
+  // 容器相关属性
+  children?: FormField[]; // 子字段(用于grid、card、tabs等容器组件)
+  gridColumns?: 1 | 2 | 3; // 栅格布局的列数
+  cardTitle?: string; // 卡片标题
 }
 
 // 表单配置接口
@@ -180,6 +185,8 @@ interface FormConfig {
   inlineValidation?: boolean; // 以行内形式展示校验信息
   showSubmitButton?: boolean; // 是否显示表单提交按钮
   showResetButton?: boolean; // 是否显示表单重置按钮
+  layoutColumns?: 1 | 2 | 3; // 表单布局列数
+  useCardLayout?: boolean; // 是否使用外层卡片包装
 }
 
 // 组件库配置
@@ -188,6 +195,7 @@ type ComponentItem = {
   label: string;
   icon: any;
   uploadType?: 'single-image' | 'multiple-image' | 'file';
+  layoutColumns?: number; // 针对布局组件的列数
 };
 
 const componentLibrary: Record<string, ComponentItem[]> = {
@@ -213,12 +221,12 @@ const componentLibrary: Record<string, ComponentItem[]> = {
     // { type: 'rate', label: '评分器', icon: Hash },
     // { type: 'treeselect', label: '树选择', icon: DownSquareOutlined },
   ],
-  // '布局组件': [
-  //   { type: 'card', label: '卡片', icon: Layout },
-  //   { type: 'grid', label: '网格布局', icon: Grid },
-  //   { type: 'tabs', label: '选项卡', icon: FileText },
-  //   { type: 'collapse', label: '折叠面板', icon: FileText },
-  // ],
+  '布局组件': [
+    { type: 'card', label: '卡片容器', icon: Layout },
+    { type: 'grid', label: '双栏布局', icon: Grid, layoutColumns: 2 },
+    { type: 'grid', label: '三栏布局', icon: Grid, layoutColumns: 3 },
+    { type: 'tabs', label: '选项卡', icon: FileText },
+  ],
 };
 
 // 可拖拽的组件项
@@ -227,17 +235,18 @@ const DraggableComponent: React.FC<{
   label: string; 
   icon: any;
   uploadType?: 'single-image' | 'multiple-image' | 'file';
-  onDrag: (type: FieldType, uploadType?: 'single-image' | 'multiple-image' | 'file') => void;
-}> = ({ type, label, icon: Icon, uploadType, onDrag }) => {
+  layoutColumns?: number;
+  onDrag: (type: FieldType, uploadType?: 'single-image' | 'multiple-image' | 'file', layoutColumns?: number, parentId?: string) => void;
+}> = ({ type, label, icon: Icon, uploadType, layoutColumns, onDrag }) => {
   const [{ isDragging }, drag] = useDrag({
     type: 'component',
-    item: { type, uploadType },
+    item: { type, uploadType, layoutColumns },
     collect: (monitor) => ({
       isDragging: monitor.isDragging(),
     }),
     end: (item, monitor) => {
       if (monitor.didDrop()) {
-        onDrag(item.type, item.uploadType);
+        onDrag(item.type, item.uploadType, item.layoutColumns);
       }
     },
   });
@@ -260,12 +269,15 @@ const DraggableComponent: React.FC<{
 // 可放置的画布区域
 const CanvasDropZone: React.FC<{
   children: React.ReactNode;
-  onDrop: (type: FieldType, uploadType?: 'single-image' | 'multiple-image' | 'file') => void;
-}> = ({ children, onDrop }) => {
+  onDrop: (type: FieldType, uploadType?: 'single-image' | 'multiple-image' | 'file', layoutColumns?: number, parentId?: string) => void;
+  parentId?: string;
+  className?: string;
+  style?: React.CSSProperties;
+}> = ({ children, onDrop, parentId, className = '', style }) => {
   const [{ isOver }, drop] = useDrop({
     accept: 'component',
-    drop: (item: { type: FieldType; uploadType?: 'single-image' | 'multiple-image' | 'file' }) => {
-      onDrop(item.type, item.uploadType);
+    drop: (item: { type: FieldType; uploadType?: 'single-image' | 'multiple-image' | 'file'; layoutColumns?: number }) => {
+      onDrop(item.type, item.uploadType, item.layoutColumns, parentId);
     },
     collect: (monitor) => ({
       isOver: monitor.isOver(),
@@ -275,23 +287,42 @@ const CanvasDropZone: React.FC<{
   return (
     <div
       ref={drop}
-      className={`h-full p-8 transition-colors ${
-        isOver ? 'bg-blue-50 border-2 border-blue-300 border-dashed' : 'bg-white'
+      className={`min-h-[80px] transition-colors ${className} ${
+        isOver ? 'bg-blue-50 border-2 border-blue-300 border-dashed rounded' : ''
       }`}
+      style={style}
     >
       {children}
     </div>
   );
 };
 
-// 字段渲染组件
+// 字段渲染组件(支持容器和嵌套)
 const FieldRenderer: React.FC<{
   field: FormField;
   isSelected: boolean;
   onSelect: () => void;
   onDelete: () => void;
+  onAddFieldToContainer?: (parentId: string, type: FieldType, uploadType?: 'single-image' | 'multiple-image' | 'file', layoutColumns?: number) => void;
+  onUpdateField?: (fieldId: string, updates: Partial<FormField>) => void;
+  onDeleteField?: (fieldId: string) => void;
+  onSelectField?: (fieldId: string) => void;
   formConfig?: FormConfig;
-}> = ({ field, isSelected, onSelect, onDelete, formConfig }) => {
+  selectedFieldId?: string | null;
+  allFields?: FormField[];
+}> = ({ 
+  field, 
+  isSelected, 
+  onSelect, 
+  onDelete,
+  onAddFieldToContainer,
+  onUpdateField,
+  onDeleteField,
+  onSelectField,
+  formConfig,
+  selectedFieldId,
+  allFields = []
+}) => {
   const renderField = () => {
     const baseProps = {
       disabled: field.disabled,
@@ -443,15 +474,6 @@ const FieldRenderer: React.FC<{
     }
   };
 
-  // 计算标签样式
-  const labelWidth = field.labelWidth || formConfig?.labelWidth || 100;
-  const labelStyle: React.CSSProperties = {
-    width: `${labelWidth}px`,
-    textAlign: formConfig?.labelPosition === 'left' ? 'left' : formConfig?.labelPosition === 'right' ? 'right' : 'left',
-    display: 'inline-block',
-    marginRight: formConfig?.labelPosition === 'top' ? 0 : '8px',
-    verticalAlign: formConfig?.labelPosition === 'top' ? 'top' : 'middle',
-  };
 
   const fieldContainerStyle: React.CSSProperties = {
     marginTop: field.margin?.top,
@@ -461,6 +483,175 @@ const FieldRenderer: React.FC<{
     padding: field.padding ? `${field.padding.top || 0}px ${field.padding.right || 0}px ${field.padding.bottom || 0}px ${field.padding.left || 0}px` : undefined,
   };
 
+  // 如果是容器组件(grid或card),渲染容器及其子字段
+  if (field.type === 'grid') {
+    const gridColumns = field.gridColumns || 2;
+    const children = field.children || [];
+    
+    return (
+      <div
+        className={`relative border-2 rounded-lg transition-all ${
+          isSelected 
+            ? 'border-blue-500 bg-blue-50 shadow-md' 
+            : 'border-dashed border-gray-300 hover:border-blue-300 hover:bg-gray-50'
+        }`}
+        onClick={(e) => {
+          e.stopPropagation();
+          onSelect();
+        }}
+        style={fieldContainerStyle}
+      >
+        <div className="p-2 border-b border-gray-200 bg-gray-50 flex items-center justify-between">
+          <span className="text-xs text-gray-600">{field.label || `${gridColumns}栏布局`}</span>
+          {isSelected && (
+            <Button
+              type="text"
+              size="small"
+              icon={<Trash2 className="w-3 h-3" />}
+              onClick={(e) => {
+                e.stopPropagation();
+                onDelete();
+              }}
+              danger
+            />
+          )}
+        </div>
+        <div className="p-4">
+          <CanvasDropZone 
+            onDrop={(type, uploadType, layoutCols) => {
+              if (onAddFieldToContainer) {
+                onAddFieldToContainer(field.id, type, uploadType, layoutCols);
+              }
+            }}
+            parentId={field.id}
+            className="min-h-[100px]"
+            style={{ minHeight: '100px', width: '100%' }}
+          >
+            <div
+              style={{
+                display: 'grid',
+                gridTemplateColumns: `repeat(${gridColumns}, minmax(0, 1fr))`,
+                gap: '12px',
+              }}
+            >
+              {children.length === 0 ? (
+                Array.from({ length: gridColumns }).map((_, idx) => (
+                  <div
+                    key={idx}
+                    className="border-2 border-dashed border-gray-300 rounded p-4 min-h-[80px] flex items-center justify-center text-gray-400 text-xs"
+                  >
+                    拖拽组件到第 {idx + 1} 列
+                  </div>
+                ))
+              ) : (
+                children.map((childField) => (
+                  <div
+                    key={childField.id}
+                    style={
+                      gridColumns > 1
+                        ? { gridColumn: `span ${Math.min(gridColumns, childField.span || 1)}` }
+                        : undefined
+                    }
+                  >
+                    <FieldRenderer
+                      field={childField}
+                      isSelected={selectedFieldId === childField.id}
+                      onSelect={() => onSelectField?.(childField.id)}
+                      onDelete={() => onDeleteField?.(childField.id)}
+                      onAddFieldToContainer={onAddFieldToContainer}
+                      onUpdateField={onUpdateField}
+                      onDeleteField={onDeleteField}
+                      onSelectField={onSelectField}
+                      formConfig={formConfig}
+                      selectedFieldId={selectedFieldId}
+                      allFields={allFields}
+                    />
+                  </div>
+                ))
+              )}
+            </div>
+          </CanvasDropZone>
+        </div>
+      </div>
+    );
+  }
+
+  if (field.type === 'card') {
+    const children = field.children || [];
+    const cardTitle = field.cardTitle || field.label || '卡片容器';
+    
+    return (
+      <div
+        className={`relative border-2 rounded-lg transition-all ${
+          isSelected 
+            ? 'border-blue-500 bg-blue-50 shadow-md' 
+            : 'border-dashed border-gray-300 hover:border-blue-300 hover:bg-gray-50'
+        }`}
+        onClick={(e) => {
+          e.stopPropagation();
+          onSelect();
+        }}
+        style={fieldContainerStyle}
+      >
+        <div className="p-2 border-b border-gray-200 bg-gray-50 flex items-center justify-between">
+          <span className="text-xs text-gray-600 font-medium">{cardTitle}</span>
+          {isSelected && (
+            <Button
+              type="text"
+              size="small"
+              icon={<Trash2 className="w-3 h-3" />}
+              onClick={(e) => {
+                e.stopPropagation();
+                onDelete();
+              }}
+              danger
+            />
+          )}
+        </div>
+        <div className="p-4">
+          <CanvasDropZone 
+            onDrop={(type, uploadType, layoutCols) => {
+              if (onAddFieldToContainer) {
+                onAddFieldToContainer(field.id, type, uploadType, layoutCols);
+              }
+            }}
+            parentId={field.id}
+            className="min-h-[100px]"
+            style={{ minHeight: '100px', width: '100%' }}
+          >
+            {children.length === 0 ? (
+              <div className="border-2 border-dashed border-gray-300 rounded p-8 min-h-[100px] flex items-center justify-center text-gray-400 text-xs">
+                拖拽组件到此卡片容器内
+              </div>
+            ) : (
+              <div className="space-y-4">
+                {children.map((childField) => (
+                  <FieldRenderer
+                    key={childField.id}
+                    field={childField}
+                    isSelected={selectedFieldId === childField.id}
+                    onSelect={() => onSelectField?.(childField.id)}
+                    onDelete={() => onDeleteField?.(childField.id)}
+                    onAddFieldToContainer={onAddFieldToContainer}
+                    onUpdateField={onUpdateField}
+                    onDeleteField={onDeleteField}
+                    onSelectField={onSelectField}
+                    formConfig={formConfig}
+                    selectedFieldId={selectedFieldId}
+                    allFields={allFields}
+                  />
+                ))}
+              </div>
+            )}
+          </CanvasDropZone>
+        </div>
+      </div>
+    );
+  }
+
+  // 普通字段的渲染
+  const labelWidth = field.labelWidth || formConfig?.labelWidth || 100;
+  
   return (
     <div
       className={`relative p-4 border-2 rounded-lg transition-all cursor-pointer ${
@@ -468,7 +659,10 @@ const FieldRenderer: React.FC<{
           ? 'border-blue-500 bg-blue-50 shadow-md' 
           : 'border-transparent hover:border-gray-300 hover:bg-gray-50'
       }`}
-      onClick={onSelect}
+      onClick={(e) => {
+        e.stopPropagation();
+        onSelect();
+      }}
       style={fieldContainerStyle}
     >
       {field.type === 'alert' ? (
@@ -673,6 +867,8 @@ export default function FormDesigner() {
     inlineValidation: false,
     showSubmitButton: true, // 预览默认显示提交按钮
     showResetButton: false,
+    layoutColumns: 1,
+    useCardLayout: true,
   };
 
   const [formConfig, setFormConfig] = useState<FormConfig>(defaultFormConfig);
@@ -708,6 +904,17 @@ export default function FormDesigner() {
     try {
       const data = await FormApi.getForm(id);
       
+      // ========== 打印回显使用的原始数据 ==========
+      console.log('========== 表单回显 - 原始接口数据 ==========');
+      console.log('接口返回的完整数据 (data):', JSON.stringify(data, null, 2));
+      console.log('表单ID (data.id):', data.id);
+      console.log('表单名称 (data.name):', data.name);
+      console.log('表单备注 (data.remark):', data.remark);
+      console.log('表单状态 (data.status):', data.status);
+      console.log('表单配置字符串 (data.conf):', data.conf);
+      console.log('字段列表字符串数组 (data.fields):', data.fields);
+      console.log('==========================================');
+      
       // 如果是复制表单,清除 id 并修改表单名称
       if (formType === 'copy') {
         setFormName((data.name || '') + '_copy');
@@ -727,18 +934,25 @@ export default function FormDesigner() {
             ? JSON.parse(data.fields) 
             : data.fields;
           
+          console.log('========== 表单回显 - 字段数据解析 ==========');
+          console.log('解析后的字段数据 (fieldsData):', JSON.stringify(fieldsData, null, 2));
+          
           if (Array.isArray(fieldsData)) {
             parsedFields = fieldsData.map((field: any, index: number) => {
               let parsedField = field;
               if (typeof field === 'string') {
                 try {
                   parsedField = JSON.parse(field);
+                  console.log(`字段 ${index} - 从字符串解析:`, parsedField);
                 } catch (err) {
                   console.error('单个字段解析失败:', err);
                   parsedField = {};
                 }
+              } else {
+                console.log(`字段 ${index} - 直接使用对象:`, parsedField);
               }
-              return {
+              
+              const mappedField = {
                 id: parsedField.id || `field_${Date.now()}_${index}`, // 复制时生成新的 id
                 type: parsedField.type || 'input',
                 label: parsedField.label || parsedField.title || '',
@@ -780,8 +994,16 @@ export default function FormDesigner() {
                 alertCentered: parsedField.alertCentered,
                 alertTheme: parsedField.alertTheme,
               };
+              
+              console.log(`字段 ${index} - 映射后的字段对象:`, mappedField);
+              return mappedField;
             });
           }
+          
+          console.log('========== 表单回显 - 最终字段列表 ==========');
+          console.log('解析后的所有字段 (parsedFields):', JSON.stringify(parsedFields, null, 2));
+          console.log('字段总数:', parsedFields.length);
+          console.log('==========================================');
         } catch (e) {
           console.error('解析字段数据失败:', e);
         }
@@ -793,8 +1015,16 @@ export default function FormDesigner() {
       if (data.conf) {
         try {
           const confData = typeof data.conf === 'string' ? JSON.parse(data.conf) : data.conf;
+          
+          console.log('========== 表单回显 - 表单配置解析 ==========');
+          console.log('解析后的配置数据 (confData):', JSON.stringify(confData, null, 2));
+          
           if (confData.formConfig) {
-            setFormConfig({ ...defaultFormConfig, ...confData.formConfig });
+            const mergedConfig = { ...defaultFormConfig, ...confData.formConfig };
+            console.log('默认表单配置 (defaultFormConfig):', JSON.stringify(defaultFormConfig, null, 2));
+            console.log('合并后的表单配置 (mergedConfig):', JSON.stringify(mergedConfig, null, 2));
+            setFormConfig(mergedConfig);
+            
             // 如果表单配置中有名称且当前表单名称为空,则使用配置中的名称
             if (confData.formConfig.name && !formName) {
               if (formType === 'copy') {
@@ -804,17 +1034,77 @@ export default function FormDesigner() {
               }
             }
           }
+          console.log('==========================================');
         } catch (e) {
           console.error('解析表单配置失败:', e);
         }
       }
+      
+      console.log('========== 表单回显 - 回显完成 ==========');
+      console.log('最终表单名称 (formName):', formName);
+      console.log('最终表单备注 (formRemark):', formRemark);
+      console.log('最终表单状态 (formStatus):', formStatus);
+      console.log('==========================================');
     } catch (error: any) {
       toast.error(error.message || '加载表单数据失败');
     }
   };
 
+  // 递归查找字段
+  const findField = (fields: FormField[], fieldId: string): FormField | null => {
+    for (const field of fields) {
+      if (field.id === fieldId) {
+        return field;
+      }
+      if (field.children && field.children.length > 0) {
+        const found = findField(field.children, fieldId);
+        if (found) return found;
+      }
+    }
+    return null;
+  };
+
+  // 递归更新字段
+  const updateFieldRecursive = (fields: FormField[], fieldId: string, updates: Partial<FormField>): FormField[] => {
+    return fields.map(field => {
+      if (field.id === fieldId) {
+        return { ...field, ...updates };
+      }
+      if (field.children && field.children.length > 0) {
+        return { ...field, children: updateFieldRecursive(field.children, fieldId, updates) };
+      }
+      return field;
+    });
+  };
+
+  // 递归删除字段
+  const deleteFieldRecursive = (fields: FormField[], fieldId: string): FormField[] => {
+    return fields.filter(field => {
+      if (field.id === fieldId) {
+        return false;
+      }
+      if (field.children && field.children.length > 0) {
+        field.children = deleteFieldRecursive(field.children, fieldId);
+      }
+      return true;
+    });
+  };
+
+  // 递归添加字段到容器
+  const addFieldToContainer = (fields: FormField[], parentId: string, newField: FormField): FormField[] => {
+    return fields.map(field => {
+      if (field.id === parentId) {
+        return { ...field, children: [...(field.children || []), newField] };
+      }
+      if (field.children && field.children.length > 0) {
+        return { ...field, children: addFieldToContainer(field.children, parentId, newField) };
+      }
+      return field;
+    });
+  };
+
   // 添加字段
-  const addField = (type: FieldType, uploadType?: 'single-image' | 'multiple-image' | 'file') => {
+  const addField = (type: FieldType, uploadType?: 'single-image' | 'multiple-image' | 'file', layoutColumns?: number, parentId?: string) => {
     const fieldConfig: Record<FieldType, Partial<FormField>> = {
       input: { placeholder: '请输入' },
       textarea: { placeholder: '请输入内容' },
@@ -872,8 +1162,15 @@ export default function FormDesigner() {
       },
       treeselect: {},
       timepicker: { placeholder: '请选择时间' },
-      card: {},
-      grid: {},
+      card: { 
+        children: [],
+        cardTitle: '卡片容器'
+      },
+      grid: { 
+        children: [],
+        gridColumns: (layoutColumns || 2) as 1 | 2 | 3,
+        label: layoutColumns === 3 ? '三栏布局' : '双栏布局'
+      },
       tabs: {},
       collapse: {},
       alert: {
@@ -890,31 +1187,36 @@ export default function FormDesigner() {
     const newField: FormField = {
       id: `field_${Date.now()}`,
       type,
-      label: `字段${fields.length + 1}`,
-      name: `field${fields.length + 1}`,
+      label: type === 'grid' ? (layoutColumns === 3 ? '三栏布局' : '双栏布局') : type === 'card' ? '卡片容器' : `字段${fields.length + 1}`,
+      name: `field${Date.now()}`,
       required: false,
       ...fieldConfig[type],
     };
     
-    setFields([...fields, newField]);
+    // 如果指定了父容器ID,添加到容器中
+    if (parentId) {
+      setFields(addFieldToContainer(fields, parentId, newField));
+    } else {
+      setFields([...fields, newField]);
+    }
     setSelectedFieldId(newField.id);
     // 添加字段后,自动切换到组件配置标签页
     setConfigTab('component');
     markDirty();
   };
 
-  // 删除字段
+  // 删除字段(支持递归删除)
   const deleteField = (id: string) => {
-    setFields(fields.filter(f => f.id !== id));
+    setFields(deleteFieldRecursive(fields, id));
     if (selectedFieldId === id) {
       setSelectedFieldId(null);
     }
     markDirty();
   };
 
-  // 更新字段
+  // 更新字段(支持递归更新)
   const updateField = (id: string, updates: Partial<FormField>) => {
-    setFields(fields.map(f => f.id === id ? { ...f, ...updates } : f));
+    setFields(updateFieldRecursive(fields, id, updates));
     markDirty();
   };
 
@@ -1013,6 +1315,13 @@ export default function FormDesigner() {
 
   // 使用 useMemo 确保 backend 实例在组件生命周期内保持一致
   const backend = useMemo(() => getHTML5Backend(), []);
+  const layoutColumns = formConfig.layoutColumns || 1;
+  const cardEnabled = formConfig.useCardLayout !== false;
+  const gridStyle = layoutColumns > 1 ? {
+    display: 'grid',
+    gridTemplateColumns: `repeat(${layoutColumns}, minmax(0, 1fr))`,
+    gap: '16px',
+  } : undefined;
 
   return (
     <DndProvider backend={backend}>
@@ -1050,7 +1359,7 @@ export default function FormDesigner() {
           </Button>
         </div>
 
-        <div className="flex-1 flex overflow-hidden">
+        <div className="flex-1 flex overflow-hidden" style={{ height: 'calc(100vh - 64px)' }}>
           {/* 左侧:组件库 */}
           <div className="w-80 bg-white border-r border-gray-200 overflow-y-auto">
             <div className="p-4 border-b border-gray-200">
@@ -1068,6 +1377,7 @@ export default function FormDesigner() {
                         label={comp.label}
                         icon={comp.icon}
                         uploadType={comp.uploadType}
+                        layoutColumns={comp.layoutColumns}
                         onDrag={addField}
                       />
                     ))}
@@ -1078,45 +1388,113 @@ export default function FormDesigner() {
           </div>
 
           {/* 中间:设计画布 */}
-          <div className="flex-1 overflow-y-auto h-full">
-            <CanvasDropZone onDrop={addField}>
-              <div className="max-w-4xl mx-auto h-full">
-                <Card 
-                  title={formConfig.name || "表单设计"} 
-                  className="h-full flex flex-col"
-                  bodyStyle={{ flex: 1, overflow: 'auto' }}
-                >
+          <div className="flex-1 overflow-hidden">
+            <CanvasDropZone onDrop={addField} style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
+              <div className="w-full p-8" style={{ minHeight: '100%', overflow: 'auto' }}>
+                {formConfig.useCardLayout !== false ? (
+                  <Card 
+                    title={formConfig.name || "表单设计"} 
+                    className="flex flex-col flex-1"
+                    style={{ height: '100%', display: 'flex', flexDirection: 'column' }}
+                    bodyStyle={{ flex: 1, overflow: 'auto', height: 0 }}
+                  >
                   {fields.length === 0 ? (
-                    <div className="text-center text-gray-400 py-20">
-                      <p className="text-sm mb-2">从左侧拖拽组件到此处开始设计</p>
+                    <div className="pt-8 w-full text-center">
+                      <p className="text-sm text-gray-400">从左侧拖拽组件到此处开始设计</p>
                     </div>
                   ) : (
-                    <AntdForm
-                      layout={formConfig.labelPosition === 'top' ? 'vertical' : formConfig.labelPosition === 'left' ? 'horizontal' : 'horizontal'}
-                      size={formConfig.formSize === 'default' ? 'middle' : formConfig.formSize}
-                      requiredMark={formConfig.hideRequiredMark ? false : undefined}
-                      labelCol={formConfig.labelWidth ? { 
-                        style: { 
-                          width: `${formConfig.labelWidth}px`,
-                          textAlign: formConfig.labelPosition === 'left' ? 'left' : formConfig.labelPosition === 'right' ? 'right' : 'left'
-                        } 
-                      } : undefined}
-                    >
-                    <div className="space-y-4">
-                      {fields.map((field, index) => (
-                        <FieldRenderer
-                          key={field.id}
-                          field={field}
-                          isSelected={selectedFieldId === field.id}
-                          onSelect={() => setSelectedFieldId(field.id)}
-                          onDelete={() => deleteField(field.id)}
-                            formConfig={formConfig}
-                        />
-                      ))}
-                    </div>
-                    </AntdForm>
-                  )}
-                </Card>
+                      <AntdForm
+                        layout={formConfig.labelPosition === 'top' ? 'vertical' : formConfig.labelPosition === 'left' ? 'horizontal' : 'horizontal'}
+                        size={formConfig.formSize === 'default' ? 'middle' : formConfig.formSize}
+                        requiredMark={formConfig.hideRequiredMark ? false : undefined}
+                        labelCol={formConfig.labelWidth ? { 
+                          style: { 
+                            width: `${formConfig.labelWidth}px`,
+                            textAlign: formConfig.labelPosition === 'left' ? 'left' : formConfig.labelPosition === 'right' ? 'right' : 'left'
+                          } 
+                        } : undefined}
+                      >
+                        <div style={gridStyle} className={layoutColumns === 1 ? 'space-y-4' : ''}>
+                          {fields.map((field) => (
+                            <div
+                              key={field.id}
+                              style={
+                                layoutColumns > 1
+                                  ? { gridColumn: `span ${Math.min(layoutColumns, field.span || 1)}` }
+                                  : undefined
+                              }
+                            >
+                              <FieldRenderer
+                                field={field}
+                                isSelected={selectedFieldId === field.id}
+                                onSelect={() => setSelectedFieldId(field.id)}
+                                onDelete={() => deleteField(field.id)}
+                                onAddFieldToContainer={(parentId, type, uploadType, layoutCols) => {
+                                  addField(type, uploadType, layoutCols, parentId);
+                                }}
+                                onUpdateField={updateField}
+                                onDeleteField={deleteField}
+                                onSelectField={setSelectedFieldId}
+                                formConfig={formConfig}
+                                selectedFieldId={selectedFieldId}
+                                allFields={fields}
+                              />
+                            </div>
+                          ))}
+                        </div>
+                      </AntdForm>
+                    )}
+                  </Card>
+                ) : (
+                  <div className="flex-1 flex flex-col" style={{ height: '100%', overflow: 'auto' }}>
+                    {fields.length === 0 ? (
+                      <div className="pt-8 w-full text-center">
+                        <p className="text-sm text-gray-400">从左侧拖拽组件到此处开始设计</p>
+                      </div>
+                    ) : (
+                      <AntdForm
+                        layout={formConfig.labelPosition === 'top' ? 'vertical' : formConfig.labelPosition === 'left' ? 'horizontal' : 'horizontal'}
+                        size={formConfig.formSize === 'default' ? 'middle' : formConfig.formSize}
+                        requiredMark={formConfig.hideRequiredMark ? false : undefined}
+                        labelCol={formConfig.labelWidth ? { 
+                          style: { 
+                            width: `${formConfig.labelWidth}px`,
+                            textAlign: formConfig.labelPosition === 'left' ? 'left' : formConfig.labelPosition === 'right' ? 'right' : 'left'
+                          } 
+                        } : undefined}
+                      >
+                        <div style={gridStyle} className={layoutColumns === 1 ? 'space-y-4' : ''}>
+                          {fields.map((field) => (
+                            <div
+                              key={field.id}
+                              style={
+                                layoutColumns > 1
+                                  ? { gridColumn: `span ${Math.min(layoutColumns, field.span || 1)}` }
+                                  : undefined
+                              }
+                            >
+                              <FieldRenderer
+                                field={field}
+                                isSelected={selectedFieldId === field.id}
+                                onSelect={() => setSelectedFieldId(field.id)}
+                                onDelete={() => deleteField(field.id)}
+                                formConfig={formConfig}
+                                onAddFieldToContainer={(parentId, type, uploadType, layoutCols) => {
+                                  addField(type, uploadType, layoutCols, parentId);
+                                }}
+                                onUpdateField={updateField}
+                                onDeleteField={deleteField}
+                                onSelectField={setSelectedFieldId}
+                                selectedFieldId={selectedFieldId}
+                                allFields={fields}
+                              />
+                            </div>
+                          ))}
+                        </div>
+                      </AntdForm>
+                    )}
+                  </div>
+                )}
               </div>
             </CanvasDropZone>
           </div>
@@ -1420,6 +1798,19 @@ export default function FormDesigner() {
                     <Select.Option value="large">大</Select.Option>
                   </Select>
                 </div>
+                        {layoutColumns > 1 && (
+                          <div>
+                            <label className="text-xs text-gray-600 mb-1 block">跨列数</label>
+                            <InputNumber
+                              value={field.span || 1}
+                              min={1}
+                              max={layoutColumns}
+                              onChange={(value) => updateField(field.id, { span: value || 1 })}
+                              size="small"
+                              className="w-full"
+                            />
+                          </div>
+                        )}
                       </div>
                     </div>
 
@@ -1940,6 +2331,26 @@ export default function FormDesigner() {
                     addonAfter="px"
                   />
                 </div>
+                <div>
+                  <label className="text-xs text-gray-600 mb-1 block">表单布局</label>
+                  <Radio.Group
+                    value={layoutColumns}
+                    onChange={(e) => updateFormConfig({ layoutColumns: e.target.value as 1 | 2 | 3 })}
+                    size="small"
+                  >
+                    <Radio value={1}>单栏</Radio>
+                    <Radio value={2}>双栏</Radio>
+                    <Radio value={3}>三栏</Radio>
+                  </Radio.Group>
+                </div>
+                <div className="flex items-center justify-between">
+                  <label className="text-xs text-gray-600">使用卡片容器</label>
+                  <Switch
+                    checked={formConfig.useCardLayout !== false}
+                    onChange={(checked) => updateFormConfig({ useCardLayout: checked })}
+                    size="small"
+                  />
+                </div>
                 <Divider className="my-2" />
                 <div className="space-y-3">
                   <div className="flex items-center justify-between">
@@ -2038,7 +2449,6 @@ export default function FormDesigner() {
             form={previewForm} 
             layout={formConfig.labelPosition === 'top' ? 'vertical' : formConfig.labelPosition === 'left' ? 'horizontal' : 'horizontal'}
             size={formConfig.formSize === 'default' ? 'middle' : formConfig.formSize}
-            className="space-y-4"
             requiredMark={formConfig.hideRequiredMark ? false : undefined}
             labelCol={formConfig.labelWidth ? { 
               style: { 
@@ -2048,14 +2458,16 @@ export default function FormDesigner() {
             } : undefined}
             validateMessages={formConfig.inlineValidation ? undefined : { required: '' }}
           >
+            <div style={gridStyle} className={layoutColumns === 1 ? 'space-y-4' : ''}>
             {fields.map((field) => {
+              const spanStyle = layoutColumns > 1 ? { gridColumn: `span ${Math.min(layoutColumns, field.span || 1)}` } : undefined;
               if (field.type === 'alert') {
                 const themeClass =
                   field.alertTheme === 'dark'
                     ? 'bg-gray-800 text-white border-gray-700'
                     : '';
                 return (
-                  <div key={field.id} className="mb-4">
+                  <div key={field.id} className="mb-4" style={spanStyle}>
                     <Alert
                       message={field.alertTitle || field.label}
                       description={field.alertDescription}
@@ -2073,8 +2485,8 @@ export default function FormDesigner() {
               }
 
               return (
+                <div key={field.id} style={spanStyle}>
                 <AntdForm.Item
-                  key={field.id}
                   label={field.label + (formConfig.labelSuffix || '')}
                   name={field.name}
                   required={field.required && !formConfig.hideRequiredMark}
@@ -2303,8 +2715,10 @@ export default function FormDesigner() {
                     </Upload>
                   )}
                 </AntdForm.Item>
+                </div>
               );
             })}
+            </div>
           </AntdForm>
         </Modal>