diff --git a/frontend/src/components/FormDesigner/Designer.tsx b/frontend/src/components/FormDesigner/Designer.tsx index b2eb9636..38c37117 100644 --- a/frontend/src/components/FormDesigner/Designer.tsx +++ b/frontend/src/components/FormDesigner/Designer.tsx @@ -31,10 +31,13 @@ import { DndContext, DragEndEvent, DragStartEvent, + DragOverEvent, DragOverlay, PointerSensor, useSensor, useSensors, + pointerWithin, + rectIntersection, closestCenter } from '@dnd-kit/core'; import { arrayMove } from '@dnd-kit/sortable'; @@ -119,6 +122,9 @@ const FormDesigner: React.FC = ({ // 正在拖拽的元素ID const [activeId, setActiveId] = useState(null); + + // 拖拽悬停的目标ID(用于显示插入指示器) + const [overId, setOverId] = useState(null); // 拖拽传感器 - 添加激活约束,减少误触和卡顿 const sensors = useSensors( @@ -129,12 +135,49 @@ const FormDesigner: React.FC = ({ }) ); + // 自定义碰撞检测策略:优先使用指针检测,避免栅格内部区域干扰排序 + const customCollisionDetection = useCallback((args: any) => { + // 首先尝试使用 pointerWithin - 只有指针真正进入区域时才触发 + const pointerCollisions = pointerWithin(args); + + if (pointerCollisions.length > 0) { + // 过滤掉栅格列(grid-xxx-col-xxx),优先选择画布或字段 + const nonGridCollisions = pointerCollisions.filter((collision: any) => { + const id = collision.id.toString(); + return !id.startsWith('grid-') || id === 'canvas' || id === 'canvas-bottom'; + }); + + if (nonGridCollisions.length > 0) { + return nonGridCollisions; + } + + // 如果只有栅格列,说明确实是要拖入栅格 + return pointerCollisions; + } + + // 如果 pointerWithin 没有结果,使用 rectIntersection 作为后备 + const rectCollisions = rectIntersection(args); + if (rectCollisions.length > 0) { + return rectCollisions; + } + + // 最后使用 closestCenter 作为兜底 + return closestCenter(args); + }, []); + // 生成唯一 ID const generateId = () => `field_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; // 处理拖拽开始 const handleDragStart = useCallback((event: DragStartEvent) => { setActiveId(event.active.id.toString()); + setOverId(null); + }, []); + + // 处理拖拽悬停 + const handleDragOver = useCallback((event: DragOverEvent) => { + const { over } = event; + setOverId(over ? over.id.toString() : null); }, []); // 处理拖拽结束 @@ -143,6 +186,7 @@ const FormDesigner: React.FC = ({ // 清除拖拽状态 setActiveId(null); + setOverId(null); if (!over) return; @@ -207,8 +251,8 @@ const FormDesigner: React.FC = ({ setFields((prev) => { const newFields = [...prev]; - // 如果拖到画布空白处,添加到末尾 - if (over.id === 'canvas') { + // 如果拖到画布空白处或底部区域,添加到末尾 + if (over.id === 'canvas' || over.id === 'canvas-bottom' || overData?.isBottom) { newFields.push(newField); } else { // 如果拖到某个字段上,插入到该字段之后 @@ -633,8 +677,9 @@ const FormDesigner: React.FC = ({
{/* 左侧组件面板 */} @@ -653,6 +698,8 @@ const FormDesigner: React.FC = ({ onPasteToGrid={handlePasteToGrid} labelAlign={formConfig.labelAlign} hasClipboard={!!clipboard} + activeId={activeId} + overId={overId} /> {/* 右侧属性面板 */} diff --git a/frontend/src/components/FormDesigner/components/DesignCanvas.tsx b/frontend/src/components/FormDesigner/components/DesignCanvas.tsx index 2ab4808b..c1970d3f 100644 --- a/frontend/src/components/FormDesigner/components/DesignCanvas.tsx +++ b/frontend/src/components/FormDesigner/components/DesignCanvas.tsx @@ -24,6 +24,8 @@ interface DesignCanvasProps { onDeleteField: (fieldId: string) => void; labelAlign?: 'left' | 'right' | 'top'; hasClipboard?: boolean; + activeId?: string | null; // 正在拖拽的元素ID + overId?: string | null; // 拖拽悬停的目标ID } const DesignCanvas: React.FC = ({ @@ -36,6 +38,8 @@ const DesignCanvas: React.FC = ({ onDeleteField, labelAlign = 'right', hasClipboard = false, + activeId, + overId, }) => { const { setNodeRef } = useDroppable({ id: 'canvas', @@ -91,22 +95,29 @@ const DesignCanvas: React.FC = ({ >
{fields.map((field) => ( - onSelectField(field.id)} - onCopy={() => onCopyField(field.id)} - onDelete={() => onDeleteField(field.id)} - selectedFieldId={selectedFieldId} - onSelectField={onSelectField} - onCopyField={onCopyField} - onPasteToGrid={onPasteToGrid} - onDeleteField={onDeleteField} - labelAlign={labelAlign} - hasClipboard={hasClipboard} - /> + + {/* 插入指示器 - 显示在拖拽悬停的字段上方 */} + {activeId && overId === field.id && activeId !== field.id && ( + + )} + + onSelectField(field.id)} + onCopy={() => onCopyField(field.id)} + onDelete={() => onDeleteField(field.id)} + selectedFieldId={selectedFieldId} + onSelectField={onSelectField} + onCopyField={onCopyField} + onPasteToGrid={onPasteToGrid} + onDeleteField={onDeleteField} + labelAlign={labelAlign} + hasClipboard={hasClipboard} + /> + ))} +
)} @@ -115,5 +126,66 @@ const DesignCanvas: React.FC = ({ ); }; +// 插入指示器组件 +interface DropIndicatorProps { + text: string; +} + +const DropIndicator: React.FC = ({ text }) => { + return ( +
+ {text} +
+ ); +}; + +// 画布底部拖放区域组件 +const CanvasBottomDropZone: React.FC = () => { + const { setNodeRef, isOver } = useDroppable({ + id: 'canvas-bottom', + data: { + accept: 'field', + isBottom: true, // 标识这是底部区域 + }, + }); + + return ( +
+ {isOver ? '释放以添加到底部' : '拖拽到此处添加到底部'} +
+ ); +}; + export default DesignCanvas; diff --git a/frontend/src/components/FormDesigner/components/GridField.tsx b/frontend/src/components/FormDesigner/components/GridField.tsx index 3fb74d2a..cf9f941b 100644 --- a/frontend/src/components/FormDesigner/components/GridField.tsx +++ b/frontend/src/components/FormDesigner/components/GridField.tsx @@ -141,7 +141,7 @@ const GridColumn: React.FC = ({