所有分类
  • 所有分类
  • Html5资源
  • React资源
  • Vue资源
  • Php资源
  • ‌小程序资源
  • Python资源

2025年最火的React拖拽库!AI开发者都在用的@hello-pangea/dnd完整指南

简介

@hello-pangea/dnd 是一个专为 React 应用设计的高性能拖拽排序库,提供了流畅的拖拽体验和丰富的自定义选项。它基于现代 Web API 构建,支持触摸设备和键盘操作,是构建拖拽界面的理想选择。

主要特性

  • 🚀 高性能: 基于现代 Web API,流畅的 60fps 动画
  • 📱 触摸支持: 完美支持移动设备触摸操作
  • 🎯 React 原生: 专为 React 设计,完美集成
  • 🔧 高度可配置: 丰富的配置选项和事件回调
  • 🌐 跨浏览器: 支持所有现代浏览器
  • 可访问性: 支持键盘操作和屏幕阅读器
  • 🎨 美观动画: 内置流畅的拖拽动画效果

安装

# npm
npm install @hello-pangea/dnd

# yarn
yarn add @hello-pangea/dnd

# pnpm
pnpm add @hello-pangea/dnd

React 基础使用示例

基本拖拽排序

import React, { useState } from "react";
import { DragDropContext, Droppable, Draggable } from "@hello-pangea/dnd";

const BasicDragDrop = () => {
  const [items, setItems] = useState([
    { id: "1", content: "项目 1" },
    { id: "2", content: "项目 2" },
    { id: "3", content: "项目 3" },
    { id: "4", content: "项目 4" },
  ]);

  const handleDragEnd = (result) => {
    if (!result.destination) return;

    const newItems = Array.from(items);
    const [reorderedItem] = newItems.splice(result.source.index, 1);
    newItems.splice(result.destination.index, 0, reorderedItem);

    setItems(newItems);
  };

  return (
    <div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 py-12 px-4">
      <div className="max-w-md mx-auto">
        <h3 className="text-2xl font-bold text-gray-800 text-center mb-8">
          基本拖拽排序
        </h3>
        <DragDropContext onDragEnd={handleDragEnd}>
          <Droppable droppableId="basic-list">
            {(provided, snapshot) => (
              <ul
                {...provided.droppableProps}
                ref={provided.innerRef}
                className={`space-y-3 rounded-lg p-4 ${
                  snapshot.isDraggingOver ? "bg-blue-50" : "bg-white"
                }`}
              >
                {items.map((item, index) => (
                  <Draggable key={item.id} draggableId={item.id} index={index}>
                    {(provided, snapshot) => (
                      <li
                        ref={provided.innerRef}
                        {...provided.draggableProps}
                        {...provided.dragHandleProps}
                        className={`bg-white rounded-lg shadow-md hover:shadow-lg p-4 cursor-move border ${
                          snapshot.isDragging
                            ? "shadow-2xl transform rotate-2 transition-none"
                            : "border-gray-200 hover:border-blue-300 transition-all duration-200"
                        }`}
                      >
                        <div className="flex items-center">
                          <span className="text-gray-700 font-medium">
                            {item.content}
                          </span>
                        </div>
                      </li>
                    )}
                  </Draggable>
                ))}
                {provided.placeholder}
              </ul>
            )}
          </Droppable>
        </DragDropContext>
      </div>
    </div>
  );
};

export default BasicDragDrop;

多列表拖拽看板

import React, { useState } from "react";
import { DragDropContext, Droppable, Draggable } from "@hello-pangea/dnd";

const KanbanBoard = () => {
  const [columns, setColumns] = useState({
    todo: {
      id: "todo",
      title: "📝 待办事项",
      items: [
        { id: "1", content: "学习 React" },
        { id: "2", content: "完成项目文档" },
      ],
    },
    doing: {
      id: "doing",
      title: "🚀 进行中",
      items: [{ id: "3", content: "开发新功能" }],
    },
    done: {
      id: "done",
      title: "✅ 已完成",
      items: [
        { id: "4", content: "修复 Bug" },
        { id: "5", content: "代码审查" },
      ],
    },
  });

  const handleDragEnd = (result) => {
    const { destination, source, draggableId } = result;

    if (!destination) return;

    if (
      destination.droppableId === source.droppableId &&
      destination.index === source.index
    ) {
      return;
    }

    const sourceColumn = columns[source.droppableId];
    const destColumn = columns[destination.droppableId];

    if (sourceColumn === destColumn) {
      // 同一列内移动
      const newItems = Array.from(sourceColumn.items);
      const [removed] = newItems.splice(source.index, 1);
      newItems.splice(destination.index, 0, removed);

      setColumns({
        ...columns,
        [sourceColumn.id]: {
          ...sourceColumn,
          items: newItems,
        },
      });
    } else {
      // 跨列移动
      const sourceItems = Array.from(sourceColumn.items);
      const destItems = Array.from(destColumn.items);
      const [removed] = sourceItems.splice(source.index, 1);
      destItems.splice(destination.index, 0, removed);

      setColumns({
        ...columns,
        [sourceColumn.id]: {
          ...sourceColumn,
          items: sourceItems,
        },
        [destColumn.id]: {
          ...destColumn,
          items: destItems,
        },
      });
    }
  };

  return (
    <div className="min-h-screen bg-gradient-to-br from-slate-50 to-blue-50 p-6">
      <div className="max-w-6xl mx-auto">
        <h3 className="text-3xl font-bold text-gray-800 text-center mb-8">
          多列表拖拽看板
        </h3>
        <DragDropContext onDragEnd={handleDragEnd}>
          <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
            {Object.values(columns).map((column) => (
              <div
                key={column.id}
                className="bg-white rounded-xl shadow-lg p-6 border border-gray-200"
              >
                <h4 className="text-lg font-semibold text-gray-700 text-center mb-4 pb-2 border-b border-gray-200">
                  {column.title}
                </h4>
                <Droppable droppableId={column.id}>
                  {(provided, snapshot) => (
                    <ul
                      {...provided.droppableProps}
                      ref={provided.innerRef}
                      className={`space-y-3 min-h-[300px] p-2 rounded-lg ${
                        snapshot.isDraggingOver ? "bg-blue-50" : ""
                      }`}
                    >
                      {column.items.map((item, index) => (
                        <Draggable
                          key={item.id}
                          draggableId={item.id}
                          index={index}
                        >
                          {(provided, snapshot) => (
                            <li
                              ref={provided.innerRef}
                              {...provided.draggableProps}
                              {...provided.dragHandleProps}
                              className={`border rounded-lg p-3 cursor-move hover:shadow-md transition-all duration-200 ${
                                column.id === "todo"
                                  ? "bg-gray-50 border-gray-200 hover:bg-gray-100"
                                  : column.id === "doing"
                                  ? "bg-orange-50 border-orange-200 border-l-4 border-l-orange-400 hover:bg-orange-100"
                                  : "bg-green-50 border-green-200 border-l-4 border-l-green-500 hover:bg-green-100 opacity-80"
                              } ${
                                snapshot.isDragging
                                  ? "shadow-2xl transform rotate-2"
                                  : ""
                              }`}
                            >
                              <div className="flex items-center">
                                <div
                                  className={`w-3 h-3 rounded-full mr-3 ${
                                    column.id === "todo"
                                      ? "bg-gray-400"
                                      : column.id === "doing"
                                      ? "bg-orange-400"
                                      : "bg-green-500"
                                  }`}
                                ></div>
                                <span
                                  className={`text-gray-700 ${
                                    column.id === "done" ? "line-through" : ""
                                  }`}
                                >
                                  {item.content}
                                </span>
                              </div>
                            </li>
                          )}
                        </Draggable>
                      ))}
                      {provided.placeholder}
                    </ul>
                  )}
                </Droppable>
              </div>
            ))}
          </div>
        </DragDropContext>
      </div>
    </div>
  );
};

export default KanbanBoard;

带手柄的拖拽

import React, { useState } from "react";
import { DragDropContext, Droppable, Draggable } from "@hello-pangea/dnd";

const HandleDragDrop = () => {
  const [items, setItems] = useState([
    { id: "1", title: "重要任务 1", description: "拖拽手柄来重新排序" },
    { id: "2", title: "重要任务 2", description: "拖拽手柄来重新排序" },
    { id: "3", title: "重要任务 3", description: "拖拽手柄来重新排序" },
    { id: "4", title: "重要任务 4", description: "拖拽手柄来重新排序" },
  ]);

  const handleDragEnd = (result) => {
    if (!result.destination) return;

    const newItems = Array.from(items);
    const [reorderedItem] = newItems.splice(result.source.index, 1);
    newItems.splice(result.destination.index, 0, reorderedItem);

    setItems(newItems);
  };

  const deleteItem = (id) => {
    setItems(items.filter((item) => item.id !== id));
  };

  return (
    <div className="min-h-screen bg-gradient-to-br from-purple-50 to-pink-50 py-12 px-4">
      <div className="max-w-2xl mx-auto">
        <h3 className="text-3xl font-bold text-gray-800 text-center mb-8">
          🎯 带手柄的拖拽列表
        </h3>
        <DragDropContext onDragEnd={handleDragEnd}>
          <Droppable droppableId="handle-list">
            {(provided, snapshot) => (
              <ul
                {...provided.droppableProps}
                ref={provided.innerRef}
                className={`space-y-4 ${
                  snapshot.isDraggingOver ? "bg-purple-50 rounded-lg p-4" : ""
                }`}
              >
                {items.map((item, index) => (
                  <Draggable key={item.id} draggableId={item.id} index={index}>
                    {(provided, snapshot) => (
                      <li
                        ref={provided.innerRef}
                        {...provided.draggableProps}
                        className={`bg-white rounded-xl shadow-lg hover:shadow-xl transition-all duration-300 border border-gray-200 overflow-hidden group ${
                          snapshot.isDragging
                            ? "shadow-2xl transform rotate-2"
                            : ""
                        }`}
                      >
                        <div className="flex items-center p-5">
                          {/* 拖拽手柄 */}
                          <div
                            {...provided.dragHandleProps}
                            className="drag-handle flex-shrink-0 mr-4 p-2 rounded-lg bg-gray-50 hover:bg-gray-100 transition-colors duration-200 cursor-move"
                          >
                            <svg
                              className="w-5 h-5 text-gray-400 group-hover:text-gray-600"
                              fill="currentColor"
                              viewBox="0 0 20 20"
                            >
                              <path d="M10 6a2 2 0 110-4 2 2 0 010 4zM10 12a2 2 0 110-4 2 2 0 010 4zM10 18a2 2 0 110-4 2 2 0 010 4z"></path>
                            </svg>
                          </div>

                          {/* 任务内容 */}
                          <div className="flex-1 min-w-0">
                            <h4 className="text-lg font-semibold text-gray-800 truncate">
                              {item.title}
                            </h4>
                            <p className="text-sm text-gray-500 mt-1">
                              {item.description}
                            </p>
                          </div>

                          {/* 删除按钮 */}
                          <button
                            onClick={() => deleteItem(item.id)}
                            className="flex-shrink-0 ml-4 px-5 py-3 bg-red-500 hover:bg-red-600 text-white text-base font-medium rounded-lg transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 flex items-center whitespace-nowrap"
                          >
                            <svg
                              className="w-4 h-4 mr-2"
                              fill="none"
                              stroke="currentColor"
                              viewBox="0 0 24 24"
                            >
                              <path
                                strokeLinecap="round"
                                strokeLinejoin="round"
                                strokeWidth="2"
                                d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
                              ></path>
                            </svg>
                            删除
                          </button>
                        </div>
                      </li>
                    )}
                  </Draggable>
                ))}
                {provided.placeholder}
              </ul>
            )}
          </Droppable>
        </DragDropContext>
      </div>
    </div>
  );
};

export default HandleDragDrop;

核心组件

DragDropContext

拖拽操作的上下文容器,必须包裹所有拖拽相关组件。

<DragDropContext onDragEnd={handleDragEnd}>{/* 拖拽内容 */}</DragDropContext>

Droppable

定义可放置区域,接收拖拽的元素。

<Droppable droppableId="unique-id">
  {(provided, snapshot) => (
    <div ref={provided.innerRef} {...provided.droppableProps}>
      {/* 可拖拽元素 */}
      {provided.placeholder}
    </div>
  )}
</Droppable>

Draggable

定义可拖拽的元素。

<Draggable draggableId="unique-id" index={0}>
  {(provided, snapshot) => (
    <div
      ref={provided.innerRef}
      {...provided.draggableProps}
      {...provided.dragHandleProps}
    >
      {/* 元素内容 */}
    </div>
  )}
</Draggable>

常用配置选项

选项类型默认值描述
droppableIdString放置区域的唯一标识
draggableIdString拖拽元素的唯一标识
indexNumber元素在列表中的索引
isDropDisabledBooleanfalse是否禁用放置
isDragDisabledBooleanfalse是否禁用拖拽
typeString'DEFAULT'拖拽类型,用于分组
modeString'FLUID'拖拽模式(FLUID/SNAP)

常用事件

事件描述参数
onDragStart开始拖拽时触发start
onDragUpdate拖拽过程中触发update
onDragEnd拖拽结束时触发result

result 对象结构

{
  draggableId: 'item-1',
  type: 'DEFAULT',
  source: {
    droppableId: 'list-1',
    index: 0
  },
  destination: {
    droppableId: 'list-2',
    index: 1
  },
  reason: 'DROP'
}

样式和动画

拖拽状态样式

// 拖拽中
snapshot.isDragging && "shadow-2xl transform rotate-2";

// 拖拽悬停
snapshot.isDraggingOver && "bg-blue-50";

// 拖拽准备
snapshot.draggingFromThisWith && "bg-green-50";

自定义动画

/* 拖拽时的样式 */
.dragging {
  transform: rotate(2deg) scale(1.05);
  box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
  z-index: 20;
}

/* 放置区域的样式 */
.droppable-active {
  background-color: #dbeafe;
  border-color: #93c5fd;
}

最佳实践

性能优化:

  • 使用 React.memo 包装 Draggable 组件
  • 避免在拖拽回调中进行复杂计算
  • 合理使用 shouldComponentUpdate

用户体验:

  • 提供清晰的视觉反馈
  • 添加适当的动画效果
  • 支持键盘操作

移动端适配:

  • 测试触摸设备的拖拽体验
  • 确保拖拽区域足够大
  • 优化触摸响应

常见问题解决

1. 拖拽不生效

// 确保 DragDropContext 正确包裹
<DragDropContext onDragEnd={handleDragEnd}>
  <Droppable droppableId="list">
    {(provided) => (
      <div ref={provided.innerRef} {...provided.droppableProps}>
        {/* 内容 */}
        {provided.placeholder}
      </div>
    )}
  </Droppable>
</DragDropContext>

2. 跨列表拖拽

// 使用相同的 type 属性
<Droppable droppableId="list1" type="TASK">
  {/* 内容 */}
</Droppable>

<Droppable droppableId="list2" type="TASK">
  {/* 内容 */}
</Droppable>

3. 条件拖拽

<Draggable draggableId={item.id} index={index} isDragDisabled={item.locked}>
  {/* 内容 */}
</Draggable>

总结

@hello-pangea/dnd 是一个专为 React 设计的高性能拖拽排序库,提供了流畅的用户体验和丰富的自定义选项。通过合理使用其核心组件和配置选项,可以轻松构建各种拖拽排序功能,从简单的列表排序到复杂的看板系统都能完美支持。

该库的优势在于:

  • 与 React 生态完美集成
  • 性能优异,动画流畅
  • 支持触摸设备和键盘操作
  • 提供丰富的自定义选项

无论是简单的任务列表还是复杂的项目管理界面,@hello-pangea/dnd 都能提供优秀的解决方案。

原文链接:https://code.ifrontend.net/archives/999,转载请注明出处。
0

评论0

显示验证码
没有账号?注册  忘记密码?