fix: 修复虚拟列表拖拽时显示空白问题

- 添加 DragOverlay 独立渲染拖拽中的项目
- 拖拽时扩展预加载范围 (OVERSCAN 2→10)
- 添加 activeId 状态跟踪拖拽项目
- 拖拽中的卡片以半透明效果独立显示
This commit is contained in:
雨霖铃
2026-06-06 10:17:16 +08:00
parent 707f17ac84
commit aa8b84af50
@@ -16,6 +16,8 @@ import {
useSensor, useSensor,
useSensors, useSensors,
type DragEndEvent, type DragEndEvent,
type DragStartEvent,
DragOverlay,
} from '@dnd-kit/core'; } from '@dnd-kit/core';
import { import {
SortableContext, SortableContext,
@@ -35,6 +37,7 @@ const VISIBLE_COUNT = 5;
const ROW_HEIGHT = 60; // 卡片高度 52 + 间距 8 const ROW_HEIGHT = 60; // 卡片高度 52 + 间距 8
const ROW_GAP = 8; const ROW_GAP = 8;
const OVERSCAN = 2; const OVERSCAN = 2;
const DRAG_OVERSCAN = 10; // 拖拽时增加的预加载范围
interface FieldListProps { interface FieldListProps {
fields: FieldConfig[]; fields: FieldConfig[];
@@ -120,6 +123,7 @@ export default function FieldList({
}: FieldListProps) { }: FieldListProps) {
const { t } = useI18n('testDataGenerator'); const { t } = useI18n('testDataGenerator');
const [scrollTop, setScrollTop] = useState(0); const [scrollTop, setScrollTop] = useState(0);
const [activeId, setActiveId] = useState<string | null>(null);
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const sensors = useSensors( const sensors = useSensors(
@@ -139,8 +143,13 @@ export default function FieldList({
[selectedIndex, onSelect], [selectedIndex, onSelect],
); );
const handleDragStart = (event: DragStartEvent) => {
setActiveId(String(event.active.id));
};
const handleDragEnd = (event: DragEndEvent) => { const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event; const { active, over } = event;
setActiveId(null);
if (!over || active.id === over.id) return; if (!over || active.id === over.id) return;
const oldIndex = fields.findIndex((f) => f.id === active.id); const oldIndex = fields.findIndex((f) => f.id === active.id);
@@ -165,14 +174,18 @@ export default function FieldList({
? { height: 'auto' as const, overflowY: 'visible' as const } ? { height: 'auto' as const, overflowY: 'visible' as const }
: { height: fixedContainerHeight, overflowY: 'auto' as const }; : { height: fixedContainerHeight, overflowY: 'auto' as const };
// 虚拟列表:计算可见范围 // 虚拟列表:计算可见范围(拖拽时扩展预加载范围)
const visibleStart = Math.max(0, Math.floor(scrollTop / ROW_HEIGHT) - OVERSCAN); const currentOverscan = activeId ? DRAG_OVERSCAN : OVERSCAN;
const visibleStart = Math.max(0, Math.floor(scrollTop / ROW_HEIGHT) - currentOverscan);
const visibleEnd = Math.min( const visibleEnd = Math.min(
fields.length, fields.length,
Math.ceil((scrollTop + fixedContainerHeight) / ROW_HEIGHT) + OVERSCAN, Math.ceil((scrollTop + fixedContainerHeight) / ROW_HEIGHT) + currentOverscan,
); );
const visibleFields = isVirtual ? fields.slice(visibleStart, visibleEnd) : fields; const visibleFields = isVirtual ? fields.slice(visibleStart, visibleEnd) : fields;
// 查找拖拽中的项目(用于 DragOverlay
const activeField = activeId ? fields.find((f) => f.id === activeId) : null;
const isMaxReached = fields.length >= MAX_FIELDS; const isMaxReached = fields.length >= MAX_FIELDS;
return ( return (
@@ -206,6 +219,7 @@ export default function FieldList({
<DndContext <DndContext
sensors={sensors} sensors={sensors}
collisionDetection={closestCenter} collisionDetection={closestCenter}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd} onDragEnd={handleDragEnd}
> >
<SortableContext items={fields.map((f) => f.id)} strategy={verticalListSortingStrategy}> <SortableContext items={fields.map((f) => f.id)} strategy={verticalListSortingStrategy}>
@@ -264,6 +278,21 @@ export default function FieldList({
)} )}
</div> </div>
</SortableContext> </SortableContext>
<DragOverlay>
{activeField ? (
<div className="rounded-lg border border-primary bg-background shadow-lg opacity-90">
<div className="flex items-center gap-2 h-full px-3 py-2">
<div className="p-1">
<GripVertical className="h-4 w-4 text-muted-foreground" />
</div>
<div className="flex-1 min-w-0">
<FieldItem field={activeField} isSelected={false} onClick={() => {}} />
</div>
<div className="h-8 w-8 shrink-0" />
</div>
</div>
) : null}
</DragOverlay>
</DndContext> </DndContext>
)} )}
</div> </div>