|
|
@@ -285,17 +285,13 @@ const CustomDragLayer: React.FC<{ onDragEnd?: () => void }> = ({ onDragEnd }) =>
|
|
|
}));
|
|
|
|
|
|
// 当拖拽结束时,清理预览状态
|
|
|
+ const prevIsDraggingRef = React.useRef(isDragging);
|
|
|
React.useEffect(() => {
|
|
|
- if (!isDragging && onDragEnd) {
|
|
|
- onDragEnd();
|
|
|
- }
|
|
|
- }, [isDragging, onDragEnd]);
|
|
|
-
|
|
|
- // 当拖拽结束时,清理预览状态
|
|
|
- React.useEffect(() => {
|
|
|
- if (!isDragging && onDragEnd) {
|
|
|
+ // 只在从拖拽状态变为非拖拽状态时调用
|
|
|
+ if (prevIsDraggingRef.current && !isDragging && onDragEnd) {
|
|
|
onDragEnd();
|
|
|
}
|
|
|
+ prevIsDraggingRef.current = isDragging;
|
|
|
}, [isDragging, onDragEnd]);
|
|
|
|
|
|
// 不显示预览标签,只处理拖拽结束回调
|
|
|
@@ -1161,6 +1157,69 @@ export default function FormDesigner() {
|
|
|
console.log(`字段 ${index} - 直接使用对象:`, parsedField);
|
|
|
}
|
|
|
|
|
|
+ // 递归解析 children(支持嵌套结构)
|
|
|
+ const parseChildren = (children: any[]): FormField[] => {
|
|
|
+ if (!Array.isArray(children)) return [];
|
|
|
+ return children.map((child: any) => {
|
|
|
+ let parsedChild = child;
|
|
|
+ if (typeof child === 'string') {
|
|
|
+ try {
|
|
|
+ parsedChild = JSON.parse(child);
|
|
|
+ } catch (err) {
|
|
|
+ console.error('解析子字段失败:', err);
|
|
|
+ parsedChild = {};
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return {
|
|
|
+ id: parsedChild.id || `field_${Date.now()}_${Math.random()}`,
|
|
|
+ type: parsedChild.type || 'input',
|
|
|
+ label: parsedChild.label || parsedChild.title || '',
|
|
|
+ name: parsedChild.name || parsedChild.field || '',
|
|
|
+ required: parsedChild.required || false,
|
|
|
+ placeholder: parsedChild.placeholder || '',
|
|
|
+ options: parsedChild.options || [],
|
|
|
+ defaultValue: parsedChild.defaultValue,
|
|
|
+ span: parsedChild.span,
|
|
|
+ rules: parsedChild.rules,
|
|
|
+ disabled: parsedChild.disabled,
|
|
|
+ hidden: parsedChild.hidden,
|
|
|
+ width: parsedChild.width,
|
|
|
+ size: parsedChild.size,
|
|
|
+ uploadType: parsedChild.uploadType,
|
|
|
+ cascaderOptions: parsedChild.cascaderOptions,
|
|
|
+ maxCount: parsedChild.maxCount,
|
|
|
+ accept: parsedChild.accept,
|
|
|
+ readOnly: parsedChild.readOnly,
|
|
|
+ maxLength: parsedChild.maxLength,
|
|
|
+ showClear: parsedChild.showClear,
|
|
|
+ hint: parsedChild.hint,
|
|
|
+ labelWidth: parsedChild.labelWidth,
|
|
|
+ fieldId: parsedChild.fieldId,
|
|
|
+ inputType: parsedChild.inputType,
|
|
|
+ className: parsedChild.className,
|
|
|
+ style: parsedChild.style,
|
|
|
+ margin: parsedChild.margin,
|
|
|
+ padding: parsedChild.padding,
|
|
|
+ validationRules: parsedChild.validationRules,
|
|
|
+ events: parsedChild.events,
|
|
|
+ requiredMessage: parsedChild.requiredMessage,
|
|
|
+ alertTitle: parsedChild.alertTitle,
|
|
|
+ alertDescription: parsedChild.alertDescription,
|
|
|
+ alertType: parsedChild.alertType,
|
|
|
+ alertClosable: parsedChild.alertClosable,
|
|
|
+ alertCloseText: parsedChild.alertCloseText,
|
|
|
+ alertShowIcon: parsedChild.alertShowIcon,
|
|
|
+ alertCentered: parsedChild.alertCentered,
|
|
|
+ alertTheme: parsedChild.alertTheme,
|
|
|
+ // 容器类型特有字段
|
|
|
+ gridColumns: parsedChild.gridColumns,
|
|
|
+ cardTitle: parsedChild.cardTitle,
|
|
|
+ // 递归解析子字段
|
|
|
+ children: parsedChild.children ? parseChildren(parsedChild.children) : undefined,
|
|
|
+ };
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
const mappedField = {
|
|
|
id: parsedField.id || `field_${Date.now()}_${index}`, // 复制时生成新的 id
|
|
|
type: parsedField.type || 'input',
|
|
|
@@ -1202,6 +1261,11 @@ export default function FormDesigner() {
|
|
|
alertShowIcon: parsedField.alertShowIcon,
|
|
|
alertCentered: parsedField.alertCentered,
|
|
|
alertTheme: parsedField.alertTheme,
|
|
|
+ // 容器类型特有字段
|
|
|
+ gridColumns: parsedField.gridColumns,
|
|
|
+ cardTitle: parsedField.cardTitle,
|
|
|
+ // 递归解析子字段
|
|
|
+ children: parsedField.children ? parseChildren(parsedField.children) : undefined,
|
|
|
};
|
|
|
|
|
|
console.log(`字段 ${index} - 映射后的字段对象:`, mappedField);
|
|
|
@@ -1741,9 +1805,14 @@ export default function FormDesigner() {
|
|
|
gap: '16px',
|
|
|
} : undefined;
|
|
|
|
|
|
+ // 使用 useCallback 稳定 onDragEnd 函数引用,避免无限循环
|
|
|
+ const handleDragEnd = useCallback(() => {
|
|
|
+ setDragPreview({ show: false });
|
|
|
+ }, []);
|
|
|
+
|
|
|
return (
|
|
|
<DndProvider backend={backend}>
|
|
|
- <CustomDragLayer onDragEnd={() => setDragPreview({ show: false })} />
|
|
|
+ <CustomDragLayer onDragEnd={handleDragEnd} />
|
|
|
<div className="h-screen flex flex-col bg-gray-50">
|
|
|
{/* 顶部工具栏 */}
|
|
|
|
|
|
@@ -2919,33 +2988,80 @@ export default function FormDesigner() {
|
|
|
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" style={spanStyle}>
|
|
|
- <Alert
|
|
|
- message={field.alertTitle || field.label}
|
|
|
- description={field.alertDescription}
|
|
|
- type={field.alertType || 'info'}
|
|
|
- showIcon={field.alertShowIcon !== false}
|
|
|
- closable={field.alertClosable}
|
|
|
- closeText={field.alertCloseText}
|
|
|
- className={`${field.alertCentered ? 'text-center' : ''} ${themeClass}`}
|
|
|
- banner
|
|
|
- style={field.style}
|
|
|
- />
|
|
|
- {field.hint && <div className="text-xs text-gray-400 mt-1">{field.hint}</div>}
|
|
|
- </div>
|
|
|
- );
|
|
|
- }
|
|
|
+ {/* 递归渲染字段(支持嵌套) */}
|
|
|
+ {(() => {
|
|
|
+ const renderPreviewField = (field: FormField, parentSpanStyle?: React.CSSProperties): React.ReactNode => {
|
|
|
+ const spanStyle = parentSpanStyle || (layoutColumns > 1 ? { gridColumn: `span ${Math.min(layoutColumns, field.span || 1)}` } : undefined);
|
|
|
+
|
|
|
+ // 处理容器类型(card 和 grid)
|
|
|
+ if (field.type === 'card') {
|
|
|
+ const children = field.children || [];
|
|
|
+ const cardTitle = field.cardTitle || field.label || '卡片容器';
|
|
|
+ return (
|
|
|
+ <div key={field.id} style={spanStyle} className="mb-4">
|
|
|
+ <Card title={cardTitle} className="w-full">
|
|
|
+ <div className="space-y-4">
|
|
|
+ {children.map(child => renderPreviewField(child))}
|
|
|
+ </div>
|
|
|
+ </Card>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ if (field.type === 'grid') {
|
|
|
+ const gridColumns = field.gridColumns || 2;
|
|
|
+ const children = field.children || [];
|
|
|
+ return (
|
|
|
+ <div key={field.id} style={spanStyle} className="mb-4">
|
|
|
+ <div
|
|
|
+ style={{
|
|
|
+ display: 'grid',
|
|
|
+ gridTemplateColumns: `repeat(${gridColumns}, minmax(0, 1fr))`,
|
|
|
+ gap: '16px',
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ {children.map((child) => {
|
|
|
+ const childSpanStyle = gridColumns > 1
|
|
|
+ ? { gridColumn: `span ${Math.min(gridColumns, child.span || 1)}` }
|
|
|
+ : undefined;
|
|
|
+ return (
|
|
|
+ <div key={child.id} style={childSpanStyle}>
|
|
|
+ {renderPreviewField(child)}
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ })}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理 alert 类型
|
|
|
+ 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" style={spanStyle}>
|
|
|
+ <Alert
|
|
|
+ message={field.alertTitle || field.label}
|
|
|
+ description={field.alertDescription}
|
|
|
+ type={field.alertType || 'info'}
|
|
|
+ showIcon={field.alertShowIcon !== false}
|
|
|
+ closable={field.alertClosable}
|
|
|
+ closeText={field.alertCloseText}
|
|
|
+ className={`${field.alertCentered ? 'text-center' : ''} ${themeClass}`}
|
|
|
+ banner
|
|
|
+ style={field.style}
|
|
|
+ />
|
|
|
+ {field.hint && <div className="text-xs text-gray-400 mt-1">{field.hint}</div>}
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ }
|
|
|
|
|
|
- return (
|
|
|
- <div key={field.id} style={spanStyle}>
|
|
|
+ // 处理普通字段
|
|
|
+ return (
|
|
|
+ <div key={field.id} style={spanStyle}>
|
|
|
<AntdForm.Item
|
|
|
label={field.label + (formConfig.labelSuffix || '')}
|
|
|
name={field.name}
|
|
|
@@ -3176,8 +3292,12 @@ export default function FormDesigner() {
|
|
|
)}
|
|
|
</AntdForm.Item>
|
|
|
</div>
|
|
|
- );
|
|
|
- })}
|
|
|
+ );
|
|
|
+ };
|
|
|
+
|
|
|
+ // 渲染所有根级别字段
|
|
|
+ return fields.map(field => renderPreviewField(field));
|
|
|
+ })()}
|
|
|
</div>
|
|
|
</AntdForm>
|
|
|
</Modal>
|