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

React DnD:可放置容器内拖拽排序避免复制问题的实现方案

问题描述

我已实现将可拖拽组件拖入可放置容器的功能,但在尝试实现可放置容器内部的拖拽排序时,操作会生成副本。我需要实现容器内原有项的拖拽排序,而非生成副本,请问该如何实现?

原代码
import React, { useState } from "react";
import { useDrag, useDrop } from "react-dnd";
import "../Styles/DroppableComponent.css";

const DragDropItem = ({ item, onDrop, index }) => {
  const [{ isDragging }, drag] = useDrag({
    type: "DRAGGABLE_ITEM_TYPE",
    item: { id: item.id, index },
    collect: (monitor) => ({
      isDragging: monitor.isDragging(),
    }),
    dragPreviewType: "none",
  });

  const itemStyles = {
    backgroundColor: "#f0f0f0",
    border: isDragging ? "5px dashed  #ccc" : "1px solid transparent",
    padding: "8px",
  };

  return (
    <div ref={drag} style={itemStyles} onClick={() => onDrop(item.id, index)}>
      <h3>{item.title}</h3>
      <ul>
        {item.names?.map((name, nameIndex) => (
          <li key={nameIndex}>{name}</li>
        ))}
      </ul>
    </div>
  );
};

const DroppableComponent = ({ data }) => {
  const [droppedItemIds, setDroppedItemIds] = useState([]);

  const handleDrop = (itemId, index) => {
    const item = data.find((item) => item.id === itemId);

    if (item) {
      const sameCategoryItems = droppedItemIds.filter(
        (id) => data.find((dataItem) => dataItem.id === id)?.category === item.category
      );

      const hoverIndex = index;
      const insertIndex = sameCategoryItems.findIndex((id) =>
        data.find((dataItem) => dataItem.id === id)?.category === item.category
          ? hoverIndex < sameCategoryItems.indexOf(id)
          : false
      );

      const newDroppedItemIds = [...droppedItemIds];
      newDroppedItemIds.splice(
        insertIndex === -1 ? sameCategoryItems.length : insertIndex,
        0,
        itemId
      );

      setDroppedItemIds(newDroppedItemIds);
    }
  };

  const [{ isOver, canDrop }, drop] = useDrop({
    accept: "DRAGGABLE_ITEM_TYPE",
    drop: (item) => {
      handleDrop(item.id, droppedItemIds.length);
    },
    collect: (monitor) => ({
      isOver: monitor.isOver(),
      canDrop: monitor.canDrop(),
    }),
  });

  const gridStyles = {
    display: "grid",
    gridTemplateColumns: "repeat(2, 1fr)",
    gap: "8px",
    border: isOver && canDrop ? "2px dashed #ccc" : "2px dashed transparent",
    padding: "8px",
  };

  return (
    <div ref={drop} style={{ border: "2px dashed #ccc", padding: "16px" }}>
      <h2>Draggable and Droppable Container</h2>
      <div className="grid" style={gridStyles}>
        {droppedItemIds.map((itemId, index) => {
          const item = data.find((dataItem) => dataItem.id === itemId);
          return item ? (
            <DragDropItem key={itemId} item={item} onDrop={handleDrop} index={index} />
          ) : null;
        })}
      </div>
    </div>
  );
};
export default DroppableComponent;
解决方案

问题核心是handleDrop未区分外部拖入和内部排序场景:内部拖拽时直接插入新ID,但原位置ID未被移除,导致生成副本。修改思路如下:

  1. 拆分逻辑,分别处理内部排序和外部拖入
  2. 为内部拖拽传递原位置索引,用于移动时的位置计算
  3. 给容器内每个item添加接收拖拽的能力,实现精确位置排序

修改后的完整代码:

import React, { useState } from "react";
import { useDrag, useDrop } from "react-dnd";
import "../Styles/DroppableComponent.css";

const DragDropItem = ({ item, index }) => {
  const [{ isDragging }, drag] = useDrag({
    type: "DRAGGABLE_ITEM_TYPE",
    item: { id: item.id, originalIndex: index },
    collect: (monitor) => ({
      isDragging: monitor.isDragging(),
    }),
    dragPreviewType: "none",
  });

  const itemStyles = {
    backgroundColor: "#f0f0f0",
    opacity: isDragging ? 0.5 : 1,
    border: isDragging ? "5px dashed #ccc" : "1px solid transparent",
    padding: "8px",
    cursor: "move",
  };

  return (
    <div ref={drag} style={itemStyles}>
      <h3>{item.title}</h3>
      <ul>
        {item.names?.map((name, nameIndex) => (
          <li key={nameIndex}>{name}</li>
        ))}
      </ul>
    </div>
  );
};

const DroppableComponent = ({ data }) => {
  const [droppedItemIds, setDroppedItemIds] = useState([]);

  // 处理容器内item的位置移动
  const handleMoveItem = (originalIndex, targetIndex) => {
    const newIds = [...droppedItemIds];
    const [movedItemId] = newIds.splice(originalIndex, 1);
    newIds.splice(targetIndex, 0, movedItemId);
    setDroppedItemIds(newIds);
  };

  // 处理外部item拖入容器
  const handleAddItem = (itemId) => {
    if (droppedItemIds.includes(itemId)) return;

    const item = data.find((dataItem) => dataItem.id === itemId);
    if (!item) return;

    const sameCategoryItems = droppedItemIds.filter(
      (id) => data.find((dataItem) => dataItem.id === id)?.category === item.category
    );

    const insertIndex = sameCategoryItems.length;
    const newIds = [...droppedItemIds];
    newIds.splice(insertIndex, 0, itemId);
    setDroppedItemIds(newIds);
  };

  // 容器级别的drop处理(接收外部拖入)
  const [{ isOver, canDrop }, containerDrop] = useDrop({
    accept: "DRAGGABLE_ITEM_TYPE",
    drop: (draggedItem) => {
      if (!droppedItemIds.includes(draggedItem.id)) {
        handleAddItem(draggedItem.id);
      }
    },
    collect: (monitor) => ({
      isOver: monitor.isOver(),
      canDrop: monitor.canDrop(),
    }),
  });

  // 每个item的drop处理(接收内部拖拽)
  const ItemDropWrapper = ({ index, children }) => {
    const [{ isOver: isItemOver }, itemDrop] = useDrop({
      accept: "DRAGGABLE_ITEM_TYPE",
      drop: (draggedItem) => {
        if (droppedItemIds.includes(draggedItem.id)) {
          handleMoveItem(draggedItem.originalIndex, index);
        }
      },
      collect: (monitor) => ({
        isOver: monitor.isOver(),
      }),
    });

    const wrapperStyles = {
      padding: "4px",
      backgroundColor: isItemOver ? "#e0e0e0" : "transparent",
    };

    return <div ref={itemDrop} style={wrapperStyles}>{children}</div>;
  };

  const gridStyles = {
    display: "grid",
    gridTemplateColumns: "repeat(2, 1fr)",
    gap: "8px",
    border: isOver && canDrop ? "2px dashed #ccc" : "2px dashed transparent",
    padding: "8px",
  };

  return (
    <div ref={containerDrop} style={{ border: "2px dashed #ccc", padding: "16px" }}>
      <h2>Draggable and Droppable Container</h2>
      <div className="grid" style={gridStyles}>
        {droppedItemIds.map((itemId, index) => {
          const item = data.find((dataItem) => dataItem.id === itemId);
          return item ? (
            <ItemDropWrapper key={itemId} index={index}>
              <DragDropItem item={item} index={index} />
            </ItemDropWrapper>
          ) : null;
        })}
      </div>
    </div>
  );
};
export default DroppableComponent;
关键修改说明
  • 拆分逻辑:将原handleDrop拆分为handleMoveItem(内部排序)和handleAddItem(外部拖入),避免逻辑混淆
  • 传递原索引:在useDrag的item中携带originalIndex,用于内部移动时定位原位置
  • Item级Drop:新增ItemDropWrapper组件,让每个item都能接收拖拽,实现精确位置排序
  • 防重复添加:外部拖入时先检查item是否已存在,避免重复
  • 视觉优化:拖拽时添加透明效果,hover目标位置时高亮背景,提升操作反馈

内容的提问来源于stack exchange,提问作者Josna Reddy

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.10 06:42:07