简介
@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>
常用配置选项
选项 | 类型 | 默认值 | 描述 |
---|---|---|---|
droppableId | String | – | 放置区域的唯一标识 |
draggableId | String | – | 拖拽元素的唯一标识 |
index | Number | – | 元素在列表中的索引 |
isDropDisabled | Boolean | false | 是否禁用放置 |
isDragDisabled | Boolean | false | 是否禁用拖拽 |
type | String | 'DEFAULT' | 拖拽类型,用于分组 |
mode | String | '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