react的拖拽组件库dnd-kit

item.tsx页面

import React, { FC } from 'react';
// 导入 dnd-kit 的核心 hook,用于处理单个可排序元素的逻辑
import { useSortable } from '@dnd-kit/sortable';
// 导入工具库,用于将 transform 数据转换为 CSS 样式字符串
import { CSS } from '@dnd-kit/utilities';

// 定义组件接收的属性类型
type PropsType = {
    id: string;   // 列表项的唯一标识符 (必填)
    title: string; // 列表项显示的标题 (必填)
};

// 使用函数式组件 (FC) 定义 Item 组件
const Item: FC<PropsType> = (props: PropsType) => {
    const { id, title } = props;

    // 使用 useSortable Hook 来处理拖拽逻辑
    // id 必须是唯一的,用于标识当前拖拽元素
    // 返回的对象包含拖拽所需的各种属性和状态:
    const {
        // attributes: 需要添加到元素上的属性(如 role, aria 等可访问性属性)
        attributes,
        // listeners: 包含 mousedown 等事件监听器,用于触发拖拽开始
        listeners,
        // setNodeRef: 必须通过 ref 传递给根元素,用于跟踪拖拽元素的 DOM 节点
        setNodeRef,
        // transform: 包含 translate(x, y) 等数据,在拖拽时更新元素位置
        transform,
        // transition: 包含过渡动画数据,在元素排序交换位置时产生平滑动画
    } = useSortable({ id });

    // 定义样式对象
    const style = {
        // 将 dnd-kit 计算出的 transform 数据转换为 CSS transform 字符串
        // 这样元素才能跟随鼠标移动
        transform: CSS.Transform.toString(transform),
        // 启用过渡动画,当元素在列表中交换位置时会有滑动效果
        transition: undefined,
        // 基础样式:边框、外边距、背景色和文字颜色
        border: '2px solid #ccc',
        margin: '10px 0',
        background: '#f1f1f1',
        color: 'black',
    };

    // 渲染 JSX
    return (
        // 1. ref={setNodeRef}: 让 dnd-kit 能够追踪这个 DOM 元素
        // 2. style={style}: 应用包含拖拽位移和动画的样式
        // 3. {...attributes}: 添加可访问性属性 (如 draggable="true")
        // 4. {...listeners}: 添加鼠标事件监听器 (如 onMouseDown),点击即可拖拽
        <div ref={setNodeRef} style={style} {...attributes} {...listeners}>
            {/* 显示传入的标题内容 */}
            Item {title}
        </div>
    );
};

export default Item;

container.tsx

import React, { FC, useState } from 'react';
// 核心逻辑导入
import {
    DndContext,        // 核心上下文组件,包裹所有可拖拽元素
    closestCenter,     // 碰撞检测算法:以元素中心点来判断是否发生碰撞
    KeyboardSensor,    // 传感器:支持键盘操作(如空格键选中,方向键移动)
    PointerSensor,     // 传感器:支持鼠标(pc)和触摸(移动端)操作
    useSensor,         // Hook:用于创建单个传感器实例
    useSensors,        // Hook:用于组合多个传感器
    DragEndEvent       // TypeScript类型:拖拽结束事件的类型定义
} from '@dnd-kit/core';

// 排序逻辑导入
import {
    arrayMove,                         // 工具函数:用于根据索引移动数组元素
    SortableContext,                   // 组件:为子元素提供排序上下文
    sortableKeyboardCoordinates,       // 键盘传感器辅助函数:处理垂直列表的键盘导航坐标
    verticalListSortingStrategy        // 排序策略:垂直列表的排列方式
} from '@dnd-kit/sortable';

// 导入之前定义的可拖拽子项组件
import Item from './Item';

// 定义列表项的数据结构类型
type ComponentType = {
    fe_id: string;   // 业务数据中的唯一标识符
    title: string;   // 显示的标题文本
};

const Container: FC = () => {
    // 1. 状态定义:初始化列表数据
    // 使用 useState 存储组件列表,数据包含唯一ID和标题
    const [items, setItems] = useState<ComponentType[]>([
        { fe_id: 'c1', title: '组件1' },
        { fe_id: 'c2', title: '组件2' },
        { fe_id: 'c3', title: '组件3' }
    ]);

    // 2. 传感器配置:定义用户如何触发拖拽行为
    // useSensors 组合了鼠标/触摸 和 键盘 两种触发方式
    const sensors = useSensors(
        // 鼠标/触摸传感器:监听 mousedown 或 touchstart 事件来启动拖拽
        useSensor(PointerSensor),
        
        // 键盘传感器:监听键盘事件进行拖拽
        useSensor(KeyboardSensor, {
            // coordinateGetter: 告诉键盘传感器如何计算移动坐标
            // sortableKeyboardCoordinates 是 dnd-kit 提供的专门用于列表排序的坐标计算方法
            coordinateGetter: sortableKeyboardCoordinates,
        })
    );

    // 3. 拖拽结束处理函数
    // 当用户松开鼠标或触发放置时调用
    function handleDragEnd(event: DragEndEvent) {
        const { active, over } = event;

        // 边界情况处理:
        // 如果拖拽结束的位置为空(例如拖到了可视区域外),直接返回不做处理
        if (over == null) return;
        
        // 只有当拖拽源 (active) 和 放置目标 (over) 不是同一个元素时才执行排序
        if (active.id !== over.id) {
            // 更新状态
            setItems((items) => {
                // 查找被拖拽元素的旧索引
                const oldIndex = items.findIndex(c => c.fe_id === active.id);
                // 查找目标放置位置的新索引
                const newIndex = items.findIndex(c => c.fe_id === over.id);

                // 使用 dnd-kit 提供的 arrayMove 工具函数
                // 它会返回一个新数组,将 oldIndex 位置的元素移动到 newIndex
                // 这样可以保证状态的不可变性 (Immutability)
                return arrayMove(items, oldIndex, newIndex);
            });
        }
    }

    // 4. 数据适配
    // dnd-kit 的 SortableContext 要求 items 数组中的每个对象必须包含 'id' 字段
    // 我们的数据源使用的是 'fe_id',因此需要做一个映射转换
    const itemsWithId = items.map(c => ({
        ...c,      // 保留原有属性
        id: c.fe_id // 增加 id 字段以满足 dnd-kit 的要求
    }));

    // 5. 渲染视图
    return (
        {/* 
            DndContext:整个拖拽系统的根容器
            sensors:传入传感器配置,决定如何启动拖拽
            collisionDetection:碰撞检测策略,closestCenter 表示以元素中心点最近者为准
            onDragEnd:拖拽结束时的回调函数
        */}
        <DndContext 
            sensors={sensors}
            collisionDetection={closestCenter}
            onDragEnd={handleDragEnd}
        >
            {/* 
                SortableContext:排序功能的上下文
                items:传入包含 id 的数据列表
                strategy:排序策略,verticalListSortingStrategy 表示垂直列表排列
            */}
            <SortableContext 
                items={itemsWithId}
                strategy={verticalListSortingStrategy}
            >
                {/* 
                    渲染列表项
                    遍历 itemsWithId,为每个数据项生成一个 Item 组件
                    key:React 列表渲染的唯一标识
                    id:传递给 Item 组件的唯一 id (用于 dnd-kit 内部逻辑)
                    title:传递给 Item 组件的显示文本
                */}
                {itemsWithId.map(c => (
                    <Item 
                        key={c.id} 
                        id={c.id} 
                        title={c.title}
                    />
                ))}
            </SortableContext>
        </DndContext>
    );
};

export default Container;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值