You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Next.js中使用dnd-kit拖拽排序时,占位符提前消失导致UI视觉断层问题求助

Next.js中使用dnd-kit拖拽排序时,占位符提前消失导致UI视觉断层问题求助

大家好,我在Next.js项目里用dnd-kit实现侧边栏项的拖拽排序功能,拖拽本身能正常工作,但松开鼠标完成放置的时候,出现了很影响体验的视觉问题,想请教下有没有解决办法。

具体场景是这样的:拖拽过程中,我会在列表里渲染一个占位符元素(通过isPlaceholder状态判断),同时用DragOverlay组件来显示被拖拽的项。

问题出在释放拖拽的瞬间:

  • 刚松开鼠标,占位符就立刻消失了
  • 与此同时,DragOverlay里的元素会执行动画回到它的目标位置
  • 因为占位符瞬间消失,UI会出现明显的生硬断层,体验很不流畅

我想要的理想效果是:

  • 占位符应该保持可见,直到Overlay的过渡动画完全结束
  • 换句话说,就是Overlay里的元素要视觉上“落到”占位符的位置之后,占位符再消失

当前的行为流程

  1. 拖拽某一项 → 列表中出现占位符
  2. 松开鼠标 → 占位符立刻消失
  3. Overlay元素动画回到目标位置 → UI出现跳变

预期的行为流程

  1. 拖拽某一项 → 占位符出现
  2. 松开鼠标 → Overlay元素开始动画返回
  3. 占位符在Overlay动画结束后才消失

下面是负责渲染可排序项的组件代码,拖拽手柄是通过相关props传给子组件的:

"use client";
import React, { useState } from "react";
import {
    DndContext,
    closestCenter,
    PointerSensor,
    useSensor,
    useSensors,
    DragOverlay,
} from "@dnd-kit/core";
import {
    SortableContext,
    useSortable,
    arrayMove,
    verticalListSortingStrategy,
} from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";

type Shelf = {
    id: number;
    name: string;
};

const initialShelves: Shelf[] = [
    { id: 1, name: "Shelf A" },
    { id: 2, name: "Shelf B" },
    { id: 3, name: "Shelf C" },
];

function SortableItem({
    shelf,
    selectedShelfs,
    setSelectedShelfs,
    selectedShelfId,
    setSelectedShelfId,
    isPlaceholder,
}: any) {
    const { attributes, listeners, setNodeRef, transform, transition } =
        useSortable({ id: shelf.id });

    const style = {
        transform: CSS.Transform.toString(transform),
        transition,
    };

    return (
        <li ref={setNodeRef} style={style}>
            {isPlaceholder ? (
                <div className="p-3 mb-2 rounded-md bg-red-600 border border-red-700 h-[44px]" />
            ) : (
                <div
                    className={`
          flex items-center gap-2 p-3 mb-2 rounded-md border text-black bg-white
          ${selectedShelfId === shelf.id ? "border-blue-500 border-2" : "border-gray-300"}
          `}
                >
                    <span
                        {...attributes}
                        {...listeners}
                        className="cursor-grab select-none px-2 py-1 rounded bg-gray-200"
                    >
                        ☰
                    </span>

                    <input
                        type="checkbox"
                        checked={selectedShelfs.includes(shelf.id)}
                        onChange={() =>
                            setSelectedShelfs((prev: number[]) =>
                                prev.includes(shelf.id)
                                    ? prev.filter((id) => id !== shelf.id)
                                    : [...prev, shelf.id],
                            )
                        }
                    />

                    <div
                        className="flex-1 cursor-pointer"
                        onClick={() => setSelectedShelfId(shelf.id)}
                    >
                        {shelf.name}
                    </div>
                </div>
            )}
        </li>
    );
}

function OverlayItem({ shelf }: { shelf: Shelf }) {
    return (
        <div className="flex items-center gap-2 p-3 rounded-md border border-blue-400 bg-blue-50 shadow-lg w-80">
            <span className="px-2 py-1 rounded bg-gray-200">☰</span>
            <span>{shelf.name}</span>
        </div>
    );
}

export default function Home() {
    const [shelves, setShelves] = useState(initialShelves);
    const [selectedShelfs, setSelectedShelfs] = useState<number[]>([]);
    const [selectedShelfId, setSelectedShelfId] = useState<number | null>(null);
    const [activeId, setActiveId] = useState<number | null>(null);

    const sensors = useSensors(useSensor(PointerSensor));

    function handleDragEnd(event: any) {
        const { active, over } = event;

        if (over && active.id !== over.id) {
            const oldIndex = shelves.findIndex((s) => s.id === active.id);
            const newIndex = shelves.findIndex((s) => s.id === over.id);

            setShelves((items) => arrayMove(items, oldIndex, newIndex));
        }

        setActiveId(null);
    }

    const activeShelf = shelves.find((s) => s.id === activeId);

    return (
        <div className="max-w-md mx-auto mt-10 font-sans">
            <h3 className="mb-4 text-lg font-semibold">Shelves</h3>

            <DndContext
                sensors={sensors}
                collisionDetection={closestCenter}
                onDragStart={(e) => setActiveId(e.active.id as number)}
                onDragEnd={handleDragEnd}
                onDragCancel={() => setActiveId(null)}
            >
                <SortableContext
                    items={shelves.map((s) => s.id)}
                    strategy={verticalListSortingStrategy}
                >
                    <ul className="list-none p-0">
                        {shelves.map((shelf) => (
                            <SortableItem
                                key={shelf.id}
                                shelf={shelf}
                                selectedShelfs={selectedShelfs}
                                setSelectedShelfs={setSelectedShelfs}
                                selectedShelfId={selectedShelfId}
                                setSelectedShelfId={setSelectedShelfId}
                                isPlaceholder={activeId === shelf.id}
                            />
                        ))}
                    </ul>
                </SortableContext>

                <DragOverlay>
                    {activeShelf ? <OverlayItem shelf={activeShelf} /> : null}
                </DragOverlay>
            </DndContext>
        </div>
    );
}

问题的直观示例:
拖拽时UI断层的示例

备注:内容来源于stack exchange,提问作者user32470124

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.13 18:39:29