前言

react-dnd官网地址:https://react-dnd.github.io/react-dnd/about


一、react-dnd 安装

npm install react-dnd react-dnd-html5-backend

二、使用步骤

1. 引入DndProvider

代码如下(示例):

DndProvider 组件为您的应用程序提供 React-DnD 功能。通过backend props 注入

import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import Home from '@/pages/Home';

export default function BaseLayout() {
    return (
        <DndProvider backend={ HTML5Backend }>
            <Home />
        </DndProvider>
    )
}

2. 拖拽,排序

如下图所示:

2.1. 截图

2.2. 代码

1、拖拽的数据都保存在targetBox中,没有数据时,targetBox设置默认可拖拽的区域,同时像子组件传入增删改、移动的方法。

2、通过ref={ drop },使当前元素具备可放置功能

3、useDrop提供的drop方法,该方法主要在模块拖动放置在当前目标上时,会触发

4、当拖动左侧模块到targetBox容器中,触发hover,不存在id,此时显示上下可放置区域,放置时触发drop方法,根据落点新增数据,并且给该数据加上唯一id字段。

5、targetBox中,拖动元素,触发hover 存在id时,移动当前模块,

代码如下(示例):

// index.tsx
import React, { memo, useCallback, useContext, useState, useEffect } from 'react'
import type { FC, Dispatch } from 'react'
import { useDrop } from 'react-dnd'
import update from 'immutability-helper'
import { v4 as uuidv4 } from 'uuid'

import Card from './Card'
import { base, cardBase } from './type'

import styles from './index.scss'

interface TargetBoxProps {
  accept: string
  className?: string,
  dispatch: Dispatch<any>
}

const TargetBox: FC<TargetBoxProps> = memo((props) => {
    const {
      dispatch,
      className,
      accept
    } = props
  const [cards, setCards] = useState<cardBase[]>([])

  // 根据当前id查询,返回card对象,及位置
  const findCard = useCallback(
    (id:  number) => {
      const card = cards.length > 0 ? cards.filter((c) => c.id === id)[0] as base : { } as never
      return {
        card,
        index: cards.indexOf(card),
      }
    },
    [cards],
  )

  // 移动当前模块
  const moveCard = useCallback(
    (id:  number, atIndex:  number) => {
      const { card, index } = findCard(id)
      setCards(
        update(cards, {
          $splice: [
            [index, 1],
            [atIndex, 0, card],
          ],
        }),
      )
    },
    [findCard, cards, setCards],
  )
  // 在当前位置插入模块
  const insertCard = useCallback(
    (atIndex:  number, element:cardBase) => {
      setCards(
        update(cards, {
          $splice: [
            [atIndex, 0, element],
          ],
        }),
      )
    },
    [cards, setCards],
  )
  // 删除当前模块
  const deleteCard = useCallback(
    (atIndex:  number) => {
      setCards(
        update(cards, {
          $splice: [
            [atIndex, 1],
          ],
        }),
      )
    },
    [cards, setCards],
  )

  const onDrop = useCallback((item,monitor)=> {
    const { id: currentId } = item
    if(currentId) return
    let newItem = { ...item, id:uuidv4() }
    insertCard(0, newItem)
  }, [])

  // useDrop react-dnd api
  const [{ isOver, canDrop }, drop] = useDrop({
    accept,
    drop: onDrop,
    collect: (monitor) => ({
      isOver: monitor.isOver(),
      canDrop: monitor.canDrop(),
    }),
  })
  
  const isActive = isOver && canDrop
  let backgroundColor = ''
  if (isActive) {
    backgroundColor = '#F7EDEA'
  }
  
  return (
	<div className={ styles.realBox }>
		{ cards.length === 0 && <div ref={ drop } style={{ background:backgroundColor }} className={ styles.tzTag }>拖拽到此处</div> }
		{ cards.map((item, index) => (
			<Card
				key={ item.id }
				card={ item }
				len={ cards.length }
				moveCard={moveCard}
				findCard={findCard}
				insertCard={ insertCard }
				deleteCard={ deleteCard }
				setProperty={ setProperty }
			/>
		)) }
	</div>
  )
})
export default connect()(TargetBox)

1、通过ref={(node) => drag(drop(node))},使当前元素具备可拖拽可放置功能

2、useDrop api 提供的方法中,drop主要在模块拖动放置在当前目标上时,会触发,useDrop 提供的方法中,hover主要在模块悬停时会触发。

3、useDrag api 提供的方法中,end主要在拖动结束时触发。

代码如下(示例):

// card.tsx
import type { Dispatch, FC, ReactElement } from 'react'
import React, { memo, useEffect, useRef, useState, useContext } from 'react'
import { useDrag, useDrop } from 'react-dnd'
import { connect } from 'dva'
import { ItemTypes } from '@/utils/ItemTypes'
import { v4 as uuidv4 } from 'uuid'
import { cardBase, base, stringAndnull, element } from './type'
import styles from './index.scss'
import _ from 'lodash'

interface modelProps {
  currentCard:base,
}

interface CardProps extends modelProps {
  dispatch:Dispatch<any>;
  card:base
  len:number;
  moveCard: (id:  number, to:  number) => void
  findCard: (id:  number) => { index: number, card:cardBase }
  insertCard: (to:  number, element: cardBase) => void
  deleteCard: (to:  number ) => void
  setProperty: (data: base) => void
}

interface OperationTagProps {
  delClick: () => void
  originalIndex:number;
}

const Card: FC<CardProps> = memo(({
  dispatch,
  card,
  len,
  moveCard,
  findCard,
  insertCard,
  deleteCard,
  setProperty,
  currentCard,
}) =>{
  const { id, name, componentName, data, formId } = card
  let nodeRef = useRef<HTMLDivElement | null>(null)
  let positionRef = useRef<stringAndnull>(null)

  const originalIndex = findCard(id).index

  const [{ isDragging }, drag, dragPreview] = useDrag(
    () => ({
      type: ItemTypes.Box,
      item: { id, originalIndex },
      collect: (monitor) => ({
        isDragging: monitor.isDragging(),
      }),
      // 拖动结束触发
      end: (item, monitor) => {
        const { id: droppedId, originalIndex } = item
        const didDrop = monitor.didDrop()
        if (!didDrop) {
          moveCard(droppedId, originalIndex)
          return
        }
      },
    }),
    [id, originalIndex, moveCard],
  )

  const [{ isOver, canDrop }, drop] = useDrop(
    () => ({
      accept: ItemTypes.Box,
      collect: (monitor) => ({
        isOver: monitor.isOver(),
        canDrop: monitor.canDrop(),
      }),
      // 模块悬停时触发
      hover:(item: cardBase, monitor) =>{
        const { id: draggedId } = item
        // 不存在id,说明不是当前targetBox中的数据
        if(!draggedId) {
          const hoverBoundingRect = nodeRef?.current?.getBoundingClientRect() as DOMRect;
          const hoverMiddleY = (hoverBoundingRect?.bottom - hoverBoundingRect?.top) / 2;
          const clientOffset = monitor.getClientOffset();
          if (clientOffset) {
            const hoverClientY = clientOffset.y - hoverBoundingRect.top;
            if (hoverClientY <= hoverMiddleY) {
              positionRef.current = 'top'
            }
            if (hoverClientY > hoverMiddleY) {
              positionRef.current = 'bottom'
            }
          }
          return
        }
        // 存在id,id不同时,移动
        if (draggedId !== id) {
          const { index: overIndex } = findCard(id)
          moveCard(draggedId, overIndex)
        }
        
      },
      // 模块掉落时触发
      drop:(item: cardBase, monitor)=> {
        const { id: currentId } = item
        if(currentId) return
        let { index: overIndex } = findCard(id)
        if(positionRef.current === 'bottom') overIndex += 1
        const newItem = { ...item, id: uuidv4() }
        insertCard(overIndex, newItem)
        
        positionRef.current = null
      },
    }),
    [findCard, moveCard],
  )

  const beforeSetProperty = () => {
    setProperty(card)
  }

  const ondelClick = () => {
    deleteCard(originalIndex)
  }

  const opacity = isDragging ? 0.4 : 1
  const borderStyle = (currentCard && currentCard.id === id) ? styles.borderCard : ''
  return (
    <div className={ styles.dragContainer }>
      <div className={ styles.dragNode } onClick={ beforeSetProperty } ref={ nodeRef as any }>
        { (isOver && canDrop && positionRef.current === 'top') && <div className={ styles.blankTopCard }>拖拽到此处</div> }
        <div className={`${ styles.cardStyle } ${ borderStyle }`} style={{ opacity }} ref={ dragPreview }>
          { name }
        </div>
        { (isOver && canDrop && positionRef.current === 'bottom' && originalIndex !== len-1 ) && <div className={ styles.blankBottomCard }>拖拽到此处</div> }
        { (dragging && canDrop && originalIndex === len-1 && positionRef.current !== 'top') && <div className={ styles.blankBottomCard }>拖拽到此处</div> }
      </div>
    </div>
  )
})

export default connect()(Card)

三、最终实现效果

最终实现效果如下图所示:
提示:拖拽后的渲染、输入同步展示等需配合其他功能实现

在这里插入图片描述

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐