拖拽React库使用

0 阅读2分钟

DnD拖拽库使用

@dnd-kit/core 是 dnd-kit 的核心模块,通常与其他模块(如 @dnd-kit/sortable@dnd-kit/utilities)结合使用,以实现更高级的功能。

@dnd-kit/core 是一个现代化的拖拽和放置(Drag and Drop, DnD)库,专为 React 应用设计,提供了灵活且高性能的拖拽功能。它是 dnd-kit 的核心模块,提供了基础的拖拽逻辑和事件处理。

核心功能:

  1. 拖拽和放置的核心逻辑

    • 提供拖拽的检测、拖拽目标的管理、拖拽事件的处理等功能。
    • 支持复杂的拖拽场景,如嵌套拖拽、动态拖拽目标等。
  2. 高性能

    • 使用现代浏览器 API(如 Pointer Events 和 MutationObserver),性能优于传统的 HTML5 DnD API。
  3. 完全可控

    • 提供了对拖拽行为的完全控制,允许开发者自定义拖拽的触发条件、约束、动画等。
  4. 无 UI 限制

    • 不强制使用特定的 UI 库或组件,开发者可以自由选择如何渲染拖拽元素。
  5. 事件驱动

    • 提供了丰富的事件回调(如 onDragStartonDragOveronDragEnd),方便开发者监听和处理拖拽行为。

使用场景:

  • 列表排序:实现拖拽排序功能,如任务管理工具中的任务列表。
  • 拖拽上传:支持文件或元素的拖拽上传。
  • 复杂布局:支持拖拽调整布局或嵌套拖拽。
  • 游戏开发:实现拖拽交互的游戏场景。

使用示例

image.png

'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>