npm install @dnd-kit/core @dnd-kit/sortable @dnd-kit/utilities
import React, { useState } from 'react';
import {
DndContext,
DragOverlay,
closestCenter,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
} from '@dnd-kit/core';
import {
arrayMove,
SortableContext,
sortableKeyboardCoordinates,
verticalListSortingStrategy,
useSortable,
} from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
// 可排序项组件
const SortableItem = ({ id, index, children }) => {
const {
attributes,
listeners,
setNodeRef,
setActivatorNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id });
const style = {
transform: CSS.Transform.toString(transform),
transition,
position: 'relative',
display: 'flex',
alignItems: 'center',
gap: '12px',
padding: '16px',
margin: '8px 0',
background: isDragging ? 'transparent' : 'white',
borderRadius: '4px',
boxShadow: isDragging ? 'none' : '0 1px 3px rgba(0,0,0,0.12)',
};
return (
<div ref={setNodeRef} style={style} {...attributes}>
{/* 原位置虚线框 */}
{isDragging && (
<div style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
border: '2px dashed #1890ff',
borderRadius: '4px',
opacity: 0.8
}} />
)}
{/* 拖拽手柄 */}
<div
ref={setActivatorNodeRef}
{...listeners}
style={{
cursor: isDragging ? 'grabbing' : 'grab',
padding: '8px',
background: '#f0f0f0',
borderRadius: '4px',
display: 'flex',
flexDirection: 'column',
gap: '4px',
}}
>
{[...Array(3)].map((_, i) => (
<div
key={i}
style={{
width: '4px',
height: '4px',
background: '#666',
borderRadius: '50%',
}}
/>
))}
</div>
<div>{children}</div>
</div>
);
};
// 主组件
const App = () => {
const [items, setItems] = useState([
{ id: '1', text: 'Item 1' },
{ id: '2', text: 'Item 2' },
{ id: '3', text: 'Item 3' },
{ id: '4', text: 'Item 4' },
]);
const [activeId, setActiveId] = useState(null);
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
distance: 8,
},
}),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
})
);
const handleDragStart = ({ active }) => {
setActiveId(active.id);
};
const handleDragEnd = ({ active, over }) => {
setActiveId(null);
if (active.id !== over?.id) {
setItems((items) => {
const oldIndex = items.findIndex((i) => i.id === active.id);
const newIndex = items.findIndex((i) => i.id === over?.id);
return arrayMove(items, oldIndex, newIndex);
});
}
};
return (
<div style={{ maxWidth: '600px', margin: '2rem auto', padding: '0 1rem' }}>
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
>
<SortableContext
items={items}
strategy={verticalListSortingStrategy}
>
{items.map((item, index) => (
<SortableItem key={item.id} id={item.id} index={index}>
{item.text}
</SortableItem>
))}
</SortableContext>
{/* 拖拽预览层 */}
<DragOverlay>
{activeId ? (
<div style={{
padding: '16px',
background: 'white',
borderRadius: '4px',
boxShadow: '0 4px 8px rgba(0,0,0,0.2)',
transformOrigin: 'center center'
}}>
{items.find(item => item.id === activeId)?.text}
</div>
) : null}
</DragOverlay>
</DndContext>
</div>
);
};
export default App;