表单设计器

This commit is contained in:
dengqichen 2025-10-24 09:43:14 +08:00
parent 03d4d21424
commit 1058487821
2 changed files with 78 additions and 27 deletions

View File

@ -37,7 +37,6 @@ import {
useSensor, useSensor,
useSensors, useSensors,
pointerWithin, pointerWithin,
rectIntersection,
closestCenter closestCenter
} from '@dnd-kit/core'; } from '@dnd-kit/core';
import { arrayMove } from '@dnd-kit/sortable'; import { arrayMove } from '@dnd-kit/sortable';
@ -135,35 +134,84 @@ const FormDesigner: React.FC<FormDesignerProps> = ({
}) })
); );
// 自定义碰撞检测策略:优先使用指针检测,避免栅格内部区域干扰排序 // 自定义碰撞检测策略:基于边界判断,区分"插入"和"拖入"
const customCollisionDetection = useCallback((args: any) => { const customCollisionDetection = useCallback((args: any) => {
// 首先尝试使用 pointerWithin - 只有指针真正进入区域时才触发
const pointerCollisions = pointerWithin(args); const pointerCollisions = pointerWithin(args);
if (pointerCollisions.length > 0) { if (pointerCollisions.length === 0) {
// 过滤掉栅格列grid-xxx-col-xxx优先选择画布或字段 return closestCenter(args);
const nonGridCollisions = pointerCollisions.filter((collision: any) => { }
const id = collision.id.toString();
return !id.startsWith('grid-') || id === 'canvas' || id === 'canvas-bottom'; // 查找栅格字段的碰撞
}); const gridFieldCollisions = pointerCollisions.filter((collision: any) => {
const fieldId = collision.id.toString();
if (nonGridCollisions.length > 0) { return fields.some(f => f.id === fieldId && f.type === 'grid');
return nonGridCollisions; });
// 如果拖到栅格字段上,使用边界检测
if (gridFieldCollisions.length > 0 && args.pointerCoordinates) {
const gridCollision = gridFieldCollisions[0];
const droppableContainer = args.droppableContainers.find(
(container: any) => container.id === gridCollision.id
);
if (droppableContainer?.rect?.current) {
const rect = droppableContainer.rect.current;
const pointer = args.pointerCoordinates;
// 计算鼠标在栅格字段中的相对垂直位置0-1
const relativeY = (pointer.y - rect.top) / rect.height;
// 边缘阈值:上下各 20%
const edgeThreshold = 0.2;
if (relativeY < edgeThreshold || relativeY > (1 - edgeThreshold)) {
// 在边缘区域 → 返回栅格字段本身(插入到栅格前后)
return [gridCollision];
} else {
// 在中心区域 → 查找栅格列(拖入栅格内部)
const gridColumnCollisions = pointerCollisions.filter((collision: any) => {
const id = collision.id.toString();
return id.startsWith('grid-') && id.includes('-col-');
});
if (gridColumnCollisions.length > 0) {
return gridColumnCollisions;
}
}
} }
// 如果只有栅格列,说明确实是要拖入栅格
return pointerCollisions;
} }
// 如果 pointerWithin 没有结果,使用 rectIntersection 作为后备 // 非栅格字段:优先级排序
const rectCollisions = rectIntersection(args); // 1. 优先选择非栅格字段
if (rectCollisions.length > 0) { const nonGridFields = pointerCollisions.filter((collision: any) => {
return rectCollisions; const id = collision.id.toString();
return id.startsWith('field_') && !fields.some(f => f.id === id && f.type === 'grid');
});
if (nonGridFields.length > 0) {
return nonGridFields;
} }
// 最后使用 closestCenter 作为兜底 // 2. 画布或底部区域
return closestCenter(args); const canvasCollisions = pointerCollisions.filter((collision: any) => {
}, []); const id = collision.id.toString();
return id === 'canvas' || id === 'canvas-bottom';
});
if (canvasCollisions.length > 0) {
return canvasCollisions;
}
// 3. 栅格列
const gridColumns = pointerCollisions.filter((collision: any) => {
const id = collision.id.toString();
return id.startsWith('grid-') && id.includes('-col-');
});
if (gridColumns.length > 0) {
return gridColumns;
}
return pointerCollisions;
}, [fields]);
// 生成唯一 ID // 生成唯一 ID
const generateId = () => `field_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; const generateId = () => `field_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;

View File

@ -96,9 +96,12 @@ const DesignCanvas: React.FC<DesignCanvasProps> = ({
<div className="form-designer-field-list"> <div className="form-designer-field-list">
{fields.map((field) => ( {fields.map((field) => (
<React.Fragment key={field.id}> <React.Fragment key={field.id}>
{/* 插入指示器 - 显示在拖拽悬停的字段上方 */} {/* 插入指示器 - 显示在字段上方(栅格字段会根据边界自动判断) */}
{activeId && overId === field.id && activeId !== field.id && ( {activeId &&
<DropIndicator text="释放以插入到此处" /> overId === field.id &&
activeId !== field.id &&
!overId?.toString().includes('-col-') && (
<DropIndicator text={field.type === 'grid' ? "释放以插入到栅格前/后" : "释放以插入到此处"} />
)} )}
<FieldItem <FieldItem