拖拽组件 react-dnd 拖动排序的使用

8,082 阅读4分钟

今天做自己的项目时,对于展示页轮播图顺序的修改这块地方,思前想后,觉得用拖拽排序的方式才舒服。于是折腾了一下react-dnd,特地记录下来。

react-dnd文档地址: 官方文档

一.首先

有两个依赖要安装。

cnpm i react-dnd react-dnd-html5-backend -S

要实现拖拽排序,首先需要让盒子可以被拖动,被拖动的同时还要能够接受别人的拖动,上预览:

二.相关api

DndProvider (所有的拖动操作都要在这里面)

1.属性

必填项backend,通过它将其注入后端,可以使用React-dnd随附的HTML5后端,除了自定义React-dnd后端代码。

useDrag (一个hook,使之能拖动)

1.参数(详细请阅读文档)

是一个js对象,里面必须定义item,值必须是字符串!

2.返回值

是一个js数组,在这个数组中:

1.下标0:包含来自collect函数的收集属性的对象,即参数对象中的可选成员collect,若不写则返回 {}

2.下标1:拖动源的连接器功能(类型为函数,参数类型必须是React.RefObject<any> | React.ReactElement | Element | null),把ref丢进去就完事。

3.下标2:用于拖动预览的连接器功能。这可以附加到DOM的预览部分。这里用不着

useDrop (一个hook,使之能接受拖动)

1.参数(详细请阅读文档)

是一个js对象,里面必须定义accept,值必须是字符串!

2.返回值

是一个js数组,在这个数组中:

1.下标0:包含来自collect函数的收集属性的对象,即参数对象中的可选成员collect,若不写则返回 {}

2.下标1:放置目标的连接器功能(类型为函数,参数类型同useDrag中描述),把ref丢进去就完事。

三.开始

(出了这三件套就可以打团了)

新建一个组件,用于作为拖动源和接受拖动的组件。

DragDropBox.jsx

import React, { useRef } from 'react'
import { useDrop, useDrag } from 'react-dnd'

export default ({ id, text, index, changePosition, className }) => {
    const ref = useRef(null)
    // 因为没有定义收集函数,所以返回值数组第一项不要
    const [, drop] = useDrop({ 
        accept: 'DragDropBox', // 只对useDrag的type的值为DragDropBox时才做出反应
        hover: (item, monitor) => { // 这里用节流可能会导致拖动排序不灵敏
            if (!ref.current) return
            let dragIndex = item.index
            let hoverIndex = index
            if (dragIndex === hoverIndex) return // 如果回到自己的坑,那就什么都不做
            changePosition(dragIndex, hoverIndex)  // 调用传入的方法完成交换
            item.index = hoverIndex // 将当前当前移动到Box的index赋值给当前拖动的box,不然会出现两个盒子疯狂抖动!
        }
    })
    
const [{ isDragging }, drag] = useDrag({
      item: {
          type: 'DragDropBox',
          id,
          index,
          text
      },
      collect: monitor => ({
          isDragging: monitor.isDragging() // css样式需要
      })
  })
    return (
    	// ref 这样处理可以使得这个组件既可以被拖动也可以接受拖动
        <div ref={ drag(drop(ref)) } style={{ opacity: isDragging ? 0.5 : 1 }} className={ className.dragBox }>
            <h2>{`第 ${ index + 1 } 屏`}</h2>
            <h2 style={{ fontSize: '30px' }}>{ id + '.' + text }</h2>
        </div>
    )
}

再新建一个组件,引用它。

Swipe.jsx

import React, { useState, useEffect } from 'react'
import style from './swipe.module.scss'
import { DndProvider } from 'react-dnd'
import { HTML5Backend } from 'react-dnd-html5-backend'
import DragDropBox from './DragDropBox'
import { cloneDeep } from 'lodash'
import Card from '@/component/card/card' // 类似饿了么ui的el-card,把他当成div吧

import {
    MinusOutlined
} from '@ant-design/icons'
import { Button, Popconfirm, message } from 'antd'

const initData = [
    {
        id: 1,
        text: '111'
    },
    {
        id: 2,
        text: '222'
    },
    {
        id: 3,
        text: '333'
    },
    {
        id: 4,
        text: '444'
    },
    {
        id: 5,
        text: '555'
    },
    {
        id: 6,
        text: '666'
    }
]

const Swipe = () => {
    const [isChangePosition, setIsChangePosition] = useState(false)
    const [boxList, setBoxList] = useState(initData)

    useEffect(() => {
        sessionStorage.initData = JSON.stringify(initData)
        return () => {
            sessionStorage.removeItem('initData')
        }
    }, [])
    
    const changePosition = (dragIndex, hoverIndex) => {
        let data = cloneDeep(boxList)
        let temp = data[dragIndex]
        // 交换位置
        data[dragIndex] = data[hoverIndex]
        data[hoverIndex] = temp
        setBoxList(data)
    }

    // 更换顺序按钮
    const changePositionBtn = () => {
        message.info('请拖拽图片进行排序!')
        setIsChangePosition(true)
    }

    // 取消交换位置
    const cancelChangePosition = () => {
        setIsChangePosition(false)
        setBoxList(JSON.parse(sessionStorage.getItem('initData')))
    }

    // 保存修改
    const saveChangePosition = () => {
        setIsChangePosition(false)
        sessionStorage.initData = JSON.stringify(boxList)
        // 在这里发送请求更新后端数据
        message.success('已更新顺序!')
    }

    const deleteImgOne = id => {
        console.log(id)
    }
    
    return(
        <div style={{ height: '100%' }}>
            <Card bottom>
                {
                    isChangePosition ?
                        <>
                            <Button onClick={ cancelChangePosition } style={{ marginRight: '15px' }}>取消</Button>
                            <Button type="primary" onClick={ saveChangePosition }>保存</Button>
                        </>
                        :
                        <Button type="primary" onClick={ changePositionBtn }>更换顺序</Button>
                }
            </Card>
            <Card>
                {
                    isChangePosition ?
                        <DndProvider backend={ HTML5Backend }>
                            <div className={ style.dragBoxContainer }>
                                {
                                    boxList.map((value, i)=>
                                        <DragDropBox
                                            className={ style }
                                            key={ value.id }
                                            index={ i }
                                            id={ value.id }
                                            text={ value.text }
                                            changePosition={ changePosition }
                                        />
                                    )
                                }
                            </div>
                        </DndProvider>
                        :
                        <div className={ style.dragBoxContainer }>
                            {
                                boxList.map(value=> (
                                    <div
                                        key={ value.id }
                                        className={ [ style.dragBox, style.deleteIcon ].join(' ') }
                                    >
                                        <Popconfirm
                                            title="确定要删除这张图片吗?"
                                            placement="top"
                                            onConfirm={ () => deleteImgOne(value.id) }
                                            okText="确定"
                                            cancelText="取消"
                                        >
                                            <div className={ style.deleteIconBox }>
                                                <MinusOutlined className={ style.deleteIcon }/>
                                            </div>
                                        </Popconfirm>
                                        { value.id } - { value.text }
                                    </div>
                                ))
                            }
                            <div className={ [style.dragBox, style.addImgBox].join(' ') }/>
                        </div>
                }
            </Card>
        </div>
    )
}

export default Swipe

Swipe.jsx 中的样式文件

swipe.module.scss

.card{
 height: 200px;
 width: 200px;
 background-color: pink;
 margin: 0 15px 15px 0;
}

.dragBoxContainer{
 width: 100%;
 height: 100%;
 display: grid;
 grid-template-columns: repeat(auto-fill, 360px);
 grid-template-rows: repeat(auto-fill, 180px);
 grid-gap: 15px;
 justify-content: center;
 align-items: center;
}

.dragBox{
 position: relative;
 display: flex;
 flex-direction: column;
 height: 180px;
 width: 100%;
 background-color: pink;
 padding: 10px;
 overflow: hidden;
 border-radius: 12px;
 h2{
   font-size: 20px;
 }
 .imgBox{
   flex-grow: 1;
   width: 100%;
   background-color: #fff;
 }
 .deleteIconBox{
   content: '';
   display: block;
   width: 0;
   height: 0;
   border-left: 40px solid transparent;
   border-top: 40px solid red;
   position: absolute;
   right: 0;
   top: 0;
   cursor: pointer;
 }
 .deleteIcon{
   color: #fff;
   font-size: 16px;
   position: absolute;
   bottom: 18px;
   right: 4px;
 }
}

.addImgBox{
 border: 1px dashed #ccc;
 background-color: transparent;
}