React中使用dnd-kit实现拖拽排序

发布于:2025-04-10 ⋅ 阅读:(42) ⋅ 点赞:(0)

使用dnd-kit实现拖拽排序

效果展示

gifimg4

实现源码

安装依赖

dad-kit github地址

yarn add @dnd-kit/core @dnd-kit/sortable @dnd-kit/utilities @dnd-kit/modifiers

这几个包的作用

  • @dnd-kit/core:核心库,提供基本的拖拽功能。
  • @dnd-kit/sortable:扩展库,提供排序功能和工具。
  • @dnd-kit/modifiers:修饰库,提供拖拽行为的限制和修饰功能。
  • @dnd-kit/utilities:工具库,提供 CSS 和实用工具函数。上述演示的平滑移动的样式就是来源于这个包。

实现代码

最外层使用 DndContext 组件,然后内层使用 SortableContext,并且传入列表信息,SortableContext 内层正常显示循环出来的DOM元素

import React, { ChangeEvent, FC, useState } from 'react'
import useGetComponentInfo from '../../hooks/useGetComponentInfo'
import { Button, Flex, Input, message, Space } from 'antd'
import style from './Layerlib.module.scss'
import { EyeInvisibleOutlined, LockOutlined, DragOutlined } from '@ant-design/icons'
import classNames from 'classnames'
import { useDispatch } from 'react-redux'
import {
  changeSelectedId,
  changeComponentTitle,
  componentInfoType,
  hiddenComponent,
  toogleLock,
  reorderComponents
} from '../../store/componentsReducer'

import type { DragEndEvent, DragMoveEvent } from '@dnd-kit/core'
import { DndContext } from '@dnd-kit/core'
import { arrayMove, SortableContext, rectSortingStrategy, useSortable } from '@dnd-kit/sortable'
import { restrictToParentElement } from '@dnd-kit/modifiers'
import { CSS } from '@dnd-kit/utilities'

export default function Layerlib() {
  // 记录当前正在修改的id
  const [changingId, setChangingId] = useState('')
  const { componentsList, selectedId } = useGetComponentInfo()
  const dispatch = useDispatch()

  // 点击组件
  function handleClick(component: componentInfoType) {
    const { _id, isHidden } = component

    if (isHidden) {
      message.error('不能选中被隐藏的组件')
      return
    }

    if (_id !== selectedId) {
      dispatch(changeSelectedId(_id))
      setChangingId('')
    } else {
      setChangingId(_id)
    }
  }

  // 修改组件标题
  function handleChangeTitle(event: ChangeEvent<HTMLInputElement>, id: string) {
    const value = event.target.value.trim()
    if (!value) return
    dispatch(changeComponentTitle({ _id: id, title: value }))
  }

  // 切换组件隐藏和显示
  function handleToggleHidden(component: componentInfoType) {
    const { _id, isHidden } = component
    dispatch(hiddenComponent({ _id, isHidden: !isHidden }))
  }

  // 切换锁定
  function handleToggleLock(component: componentInfoType) {
    const { _id } = component
    dispatch(toogleLock({ _id }))
  }

  // 拖动排序
  const getMoveIndex = (array: componentInfoType[], dragItem: DragMoveEvent) => {
    const { active, over } = dragItem
    const activeIndex = array.findIndex(item => item._id === active.id)
    const overIndex = array.findIndex(item => item._id === over?.id)

    // 处理未找到索引的情况
    return {
      activeIndex: activeIndex !== -1 ? activeIndex : 0,
      overIndex: overIndex !== -1 ? overIndex : activeIndex
    }
  }

  // 拖动结束
  const dragEndEvent = (dragItem: DragEndEvent) => {
    const { active, over } = dragItem
    if (!active || !over) return // 处理边界情况

    const moveDataList = [...componentsList]
    const { activeIndex, overIndex } = getMoveIndex(moveDataList, dragItem)

    if (activeIndex !== overIndex) {
      const newDataList = arrayMove(moveDataList, activeIndex, overIndex)
      // 关键:更新一下最新的组件数据
      dispatch(reorderComponents(newDataList))
    }
  }

  type DraggableItemProps = {
    component: componentInfoType
  }
  // 拖拽的子组件
  const DraggableItem: FC<DraggableItemProps> = ({ component }) => {
    const { _id, isLock, isHidden } = component
    const { setNodeRef, attributes, listeners, transform, transition } = useSortable({
      id: _id,
      transition: {
        duration: 500,
        easing: 'cubic-bezier(0.25, 1, 0.5, 1)'
      }
    })
    const styles = {
      transform: CSS.Transform.toString(transform),
      transition
    }

    // 动态样式
    const componentClass = classNames({
      [style.check]: _id === selectedId,
      [style.hidden]: isHidden
    })

    return (
      <Flex
        key={_id}
        align="center"
        justify="space-between"
        gap={10}
        className={style.layerItem}
        ref={setNodeRef}
        style={styles}
      >
        <div className={componentClass} style={{ flex: 1 }} onClick={() => handleClick(component)}>
          {changingId === _id ? (
            <Input
              value={component.title}
              autoFocus
              onChange={event => handleChangeTitle(event, component._id)}
              onPressEnter={() => setChangingId('')}
              onBlur={() => setChangingId('')}
            />
          ) : (
            component.title
          )}
        </div>
        <Space>
          {/* 拖动排序 */}
          <Button
            size="small"
            shape="circle"
            icon={<DragOutlined />}
            type="text"
            style={{
              cursor: 'move'
            }}
            {...attributes}
            {...listeners}
          />

          <Button
            size="small"
            shape="circle"
            icon={<EyeInvisibleOutlined />}
            type={isHidden ? 'primary' : 'text'}
            onClick={() => handleToggleHidden(component)}
          />
          <Button
            size="small"
            shape="circle"
            icon={<LockOutlined />}
            type={isLock ? 'primary' : 'text'}
            onClick={() => handleToggleLock(component)}
          />
        </Space>
      </Flex>
    )
  }

  return (
    <DndContext onDragEnd={dragEndEvent} modifiers={[restrictToParentElement]}>
      <SortableContext items={componentsList.map(item => item._id)} strategy={rectSortingStrategy}>
        <div className="drag-container">
          {componentsList.map(component => (
            <DraggableItem key={component._id} component={component} />
          ))}
        </div>
      </SortableContext>
    </DndContext>
  )
}

代码解释

modifiers

标识符,传入一个标识符数组以限制在父组件进行拖曳的行为。我们代码中使用 restrictToParentElement,限制在父元素内的标识符,主要可选的一些标识符如下:

  • restrictToParentElement:限制在父元素内。
  • restrictToFirstScrollableAncestor:限制在第一个可滚动祖先元素。
  • restrictToVerticalAxis:限制在垂直轴上。
  • restrictToHorizontalAxis:限制在水平轴上。
  • restrictToBoundingRect:限制在指定矩形区域内。
  • snapCenterToCursor:使元素中心对齐到光标。

onDragEnd

顾名思义,就是用户鼠标松开后触发的拖曳事件的回调。触发时会自动传入类型为 DragEndEvent 的对象,我们可以从其中拿出 active 和 over 两个参数来具体处理拖曳事件。

active 包含 正在拖曳的元素的相关信息,over 包含最后鼠标松开时所覆盖到的元素的相关信息。

import { arrayMove } from '@dnd-kit/sortable'

// 拖动排序
const getMoveIndex = (array: componentInfoType[], dragItem: DragMoveEvent) => {
    const { active, over } = dragItem
    const activeIndex = array.findIndex(item => item._id === active.id)
    const overIndex = array.findIndex(item => item._id === over?.id)

    // 处理未找到索引的情况
    return {
        activeIndex: activeIndex !== -1 ? activeIndex : 0,
        overIndex: overIndex !== -1 ? overIndex : activeIndex
    }
}

// 拖动结束
const dragEndEvent = (dragItem: DragEndEvent) => {
    const { active, over } = dragItem
    if (!active || !over) return // 处理边界情况

    const moveDataList = [...componentsList]
    const { activeIndex, overIndex } = getMoveIndex(moveDataList, dragItem)

    if (activeIndex !== overIndex) {
        const newDataList = arrayMove(moveDataList, activeIndex, overIndex)
        // 关键:更新一下最新的组件数据
        dispatch(reorderComponents(newDataList))
    }
}

我们的代码中,从 dragItem 中解构出 active, over 这两个变量,分别代表当前拖拽的组件数据和被覆盖的组件数据,然后通过 @dnd-kit/sortable 提供的 arrayMove 方法得到排序后的组件列表,接着使用dispatch更新Redux中的数据,实现页面展示最新排序的效果


网站公告

今日签到

点亮在社区的每一天
去签到