DnD拖拽库使用
@dnd-kit/core 是 dnd-kit 的核心模块,通常与其他模块(如 @dnd-kit/sortable、@dnd-kit/utilities)结合使用,以实现更高级的功能。
@dnd-kit/core 是一个现代化的拖拽和放置(Drag and Drop, DnD)库,专为 React 应用设计,提供了灵活且高性能的拖拽功能。它是 dnd-kit 的核心模块,提供了基础的拖拽逻辑和事件处理。
核心功能:
-
拖拽和放置的核心逻辑:
- 提供拖拽的检测、拖拽目标的管理、拖拽事件的处理等功能。
- 支持复杂的拖拽场景,如嵌套拖拽、动态拖拽目标等。
-
高性能:
- 使用现代浏览器 API(如
Pointer Events和MutationObserver),性能优于传统的 HTML5 DnD API。
- 使用现代浏览器 API(如
-
完全可控:
- 提供了对拖拽行为的完全控制,允许开发者自定义拖拽的触发条件、约束、动画等。
-
无 UI 限制:
- 不强制使用特定的 UI 库或组件,开发者可以自由选择如何渲染拖拽元素。
-
事件驱动:
- 提供了丰富的事件回调(如
onDragStart、onDragOver、onDragEnd),方便开发者监听和处理拖拽行为。
- 提供了丰富的事件回调(如
使用场景:
- 列表排序:实现拖拽排序功能,如任务管理工具中的任务列表。
- 拖拽上传:支持文件或元素的拖拽上传。
- 复杂布局:支持拖拽调整布局或嵌套拖拽。
- 游戏开发:实现拖拽交互的游戏场景。
使用示例
'use client';
import React from 'react';
import {DragOutlined, PlusOutlined } from '@ant-design/icons';
import {
DndContext,
DragOverlay,
closestCenter,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
useDndContext,
} from '@dnd-kit/core';
import {
arrayMove,
SortableContext,
useSortable,
verticalListSortingStrategy,
} from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { Button, Input, Space, Tooltip } from 'antd';
import Image from 'next/image';
import dragIcon from '@/public/icons/agent/drag.svg';
import deleteIcon from '@/public/icons/card/delete.svg';
import { OptionItem } from '../../types';
const SortableOptionItem = ({
option,
onDelete,
onUpdate,
}: {
option: OptionItem;
onDelete: (id: string) => void;
onUpdate: (id: string, value: string) => void;
}) => {
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({
id: option.id,
});
console.log('option111');
console.log(option);
const style = {
transform: CSS.Transform.toString(transform),
transition,
userSelect: 'none' as const,
};
return (
<div ref={setNodeRef} style={style} {...attributes}>
<Input
prefix={
<div {...listeners} style={{ cursor: 'grab' }}>
<Image src={dragIcon} alt="drag" style={{ cursor: 'pointer' }} />
</div>
}
suffix={
<Tooltip title="删除">
<Image
onClick={e => {
e.stopPropagation(); // 防止触发 input focus
onDelete(option.id);
}}
src={deleteIcon}
alt="delete"
width={16}
height={16}
style={{ cursor: 'pointer' }}
/>
</Tooltip>
}
value={option.value}
onChange={e => onUpdate(option.id, e.target.value)}
placeholder="请输入选项"
/>
</div>
);
};
export interface DraggableOptionsListProps {
value?: OptionItem[];
onChange?: (items: OptionItem[]) => void;
}
const DraggableOptionsList: React.FC<DraggableOptionsListProps> = ({ value, onChange }) => {
const options = value ?? [];
const generateId = () => `opt-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
const handleAdd = () => {
const newOptions = [...options, { id: generateId(), value: '' }];
onChange?.(newOptions);
};
const handleDelete = (id: string) => {
const newOptions = options.filter(item => item.id !== id);
onChange?.(newOptions);
};
const handleUpdate = (id: string, newValue: string) => {
const newOptions = options.map(item => (item.id === id ? { ...item, value: newValue } : item));
onChange?.(newOptions);
};
const sensors = useSensors(useSensor(PointerSensor), useSensor(KeyboardSensor));
const handleDragEnd = (event: any) => {
const { active, over } = event;
if (!over || active.id === over.id) return;
const oldIndex = options.findIndex(item => item.id === active.id);
const newIndex = options.findIndex(item => item.id === over.id);
const newOptions = arrayMove(options, oldIndex, newIndex);
onChange?.(newOptions);
};
const { active } = useDndContext();
return (
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
<SortableContext items={options.map(item => item.id)} strategy={verticalListSortingStrategy}>
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{options.map(option => (
<SortableOptionItem
key={option.id}
option={option}
onDelete={handleDelete}
onUpdate={handleUpdate}
/>
))}
</div>
</SortableContext>
<Button
block
type="primary"
icon={<PlusOutlined />}
onClick={handleAdd}
style={{ marginTop: 8, height: 32 }}
>
添加选项
</Button>
<DragOverlay dropAnimation={null}>
{active ? (
<div
style={{
padding: '8px 12px',
background: '#fff',
border: '1px solid #e8e8e8',
borderRadius: 4,
}}
>
<Space size={8} align="center">
<DragOutlined style={{ fontSize: 12, color: '#999' }} />
<span>{options.find(item => item.id === active.id)?.value || '未命名'}</span>
</Space>
</div>
) : null}
</DragOverlay>
</DndContext>
);
};
export default DraggableOptionsList;
- 使用
const [optionItems, setOptionItems] = useState<{ id: string; value: string }[]>([]);
<Form.Item
label={<span className="formLabel">选项</span>}
className="formItem"
name="options"
rules={[{ required: true, message: '请至少添加一个选项' }]}
getValueProps={(value: string[] = []) => ({
value: value.map((v, idx) => ({ id: `opt-${idx}`, value: v })),
})}
getValueFromEvent={(items: OptionItem[]) => {
return items.map(item => item.value);
}}
>
<DraggableOptionsList value={optionItems} onChange={setOptionItems} />
</Form.Item>