如何使用React-sortable-hoc和React-draggable拖拽组件实现产品需求?

9,018 阅读5分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第1天,点击查看活动详情

前言

很久很久没有更文了,至于原因那就是忙于准备面试,目前阶段就是等待offer审批中,只是这个时间周期拖得有点长。本来打算不发offer就不更文,可是耗不起了,再耗下去四月的更文活动就要结束了,所以今天还是来写一篇文章参与参与,毕竟我还是乐于分享一些项目中遇到的技术问题啦(羊毛很香,薅羊毛也很爽)。

今天来聊聊这段时间项目中遇到的问题:前端项目中如何使用拖拽组件实现排序效果和移动效果

背景

这段时间很充实既要准备面试相关的问题,平时工作还要接需求进行开发,毕竟站好最后一班岗是必备的职业素养。那这期间接了两个需求,分别是:使用H5实现在CAD中对门窗表的编辑与配置工程做法的编辑页面。这两个需求都不简单,特别是工程做法编辑页面的一些交互功能很是恶心,尽管不好做但最后还是完美解决全优上线了。期间这两个需求都有一个排序功能,即需要手动拖拽进行排序;还有一个需求就是弹窗需要可拖动移动。这次就从这两个需求来介绍react-sortable-hocreact-draggable这两个拖动组件在项目中是如何使用的。

老规矩,先上效果图:

gif2.gif

gif1.gif

gif3.gif

图一、图二是拖动排序效果图,图三则是弹窗拖动效果图。

实现

react-sortable-hoc组件用于实现拖动排序功能,react-draggable组件则用于实现拖动弹窗移动功能。下面就跟着上面gif图顺序来介绍项目中怎么使用这两个组件实现功能的→

一、react-sortable-hoc

react-sortable-hoc是一组react高阶组件,可将任何列表转换为动画,可访问和触摸友好的可排序列表。它可以和现有组件集成,支持拖动手柄、自动滚动、锁定轴和操作事件等功能;有着流畅的动画效果;可水平、垂直拖动;适用于虚拟化库:如react-virtualizedreact-tiny-virtual-listreact-infinite等。

1.1 安装并引入

安装:

$ npm install react-sortable-hoc 或者 yarn add react-sortable-hoc

引入组件:

import { SortableContainer, SortableElement, SortableHandle } from 'react-sortable-hoc';

安装并引入好组件后,下面分别从两个场景介绍它的使用方法:table表格场景列表场景

1.2 table表格场景

table场景使用的是AntdTable组件,利用components属性覆盖table的body元素,从而实现在拖动table行之后可以移动插入并进行排序。

1.2.1 布局代码

首先,根据引入组件可知:

  • sortableContainer 是所有可排序元素的容器
  • sortableElement 是每个可渲染元素的容器
  • sortableHandle 是定义拖拽手柄的容器

接着,为这三个容器分别定义好各自的容器标签,如下:

// table容器
const TableContainer = SortableContainer(props => <tbody {...props} />);

// table行
const TableItem = SortableElement(props => <tr {...props} />);

// 拖拽手柄
const DragHandle = SortableHandle(() => (
    <MenuOutlined title="拖拽可调整顺序" />
));

接着,给容器增加拖拽事件,给拖拽行打上标记,如下:

/**
 * 拖拽容器组件
 * @param props
 * @constructor
 */
const DraggableContainer = props => (
    <TableContainer
        useDragHandle
        helperClass="custom-table-row-dragging"
        onSortEnd={onSortEnd}
        {...props}
    />
);

/**
 * 标记拖拽行
 * @param className
 * @param style
 * @param restProps
 * @constructor
 */
const DraggableBodyRow = ({ className, style, ...restProps }) => {
    const index = dataSource.findIndex(x => x.PkId === restProps['data-row-key']);
    return <TableItem index={index} {...restProps} />;
};

最后,利用components属性覆盖table的body元素,如下:

<Table
    className="custom-table config-table h ofa"
    rowKey="PkId"
    bordered
    sticky={true}
    dataSource={dataSource}
    columns={getColumns()}
    loading={loading}
    pagination={false}
    components={{
        body: {
            wrapper: DraggableContainer,
            row: DraggableBodyRow,
        }
    }}
/>

1.2.2 逻辑代码

这里将把拖动后如何提交数据给接口进行排序的思路,只是作为参考,实际工作中自行定义接口逻辑。思路如下:当拖拽行时,将拖拽行拖拽到目标行的前面就设置位置为1标识目标前,反之则是目标后;通过这样传递给后端,后端人员根据位置信息来将前后数据进行自增1或者自减1,从而实现排序。

/**
 * 拖拽结束
 * @param oldIndex
 * @param newIndex 要插入的位置
 */
const onSortEnd = ({oldIndex, newIndex}) => {
    if (oldIndex === newIndex) {
        return;
    }

    let position: number = 0;
    if (oldIndex > newIndex) {
        position = 1; // 目标前
    }else {
        position = 0; // 目标后
    }

    const dragItemPkid: string = dataSource[oldIndex].PkId; // 拖拽行
    const dropItemPkid: string = dataSource[newIndex].PkId;  // 目标行

    // 参数(CAD后台人员定义的参数字段全是首字母大写的...)
    const params: any = {
        Num: position,
        OrderPkid: dragItemPkid,
        TargetPkid: dropItemPkid
    }
    console.log('params', params)
    // 调用接口
    doorWindowHandler.updateDoorWindowSort(params).then(res => {
        if (res.IsSucess) {
            // 排序成功后刷新表格数据即可
            queryReportList();
        } else {
            message.error(res.Message);
        }
    })
}


/**
 * 构造表格列
 */
const getColumns = (): any[] => {
    return [
        {
            className: 'drag-visible',
            title: '排序',
            align: 'center',
            width: 40,
            render: (text: any, record: any, index: number) => <DragHandle />
        },
        {
            title: '图纸',
            className: 'drag-visible',
            align: 'center',
            render: (text: any, record: any, index: number) => {
                // ...
            }
        },
        {
            title: '楼层名',
            className: 'bit-edit-td drag-visible',
            render: (text: any, record: any, index: number) => {
                // ...
            }
        },
        {
            title: '层数',
            className: 'bit-edit-td drag-visible',
            align: 'center',
            render: (text: any, record: any, index: number) => {
                // ...
            }
        },
        {
            title: '文件更新时间',
            className: 'drag-visible',
            dataIndex: 'FileUpdateTimeStr',
            align: 'center',
            width: 140,
            render: (text: any, record: any, index: number) => {
                // ...
            }
        },
        {
            title: '图纸门窗标注读取时间',
            className: 'drag-visible',
            dataIndex: 'ReadLableTimeStr',
            align: 'center',
            width: 140,
        },
        //  ...
    ]
}

1.2.3 样式代码

为拖拽行添加如下样式,便于提升在拖动时的体验;其中为类名为drag-visible添加了显示样式,这是用drag-visible类名来控制拖拽行的某些列的值是否进行展示,上面代码体现了在拖动时只需要显示图纸、楼层名、层数、文件更新时间等列值。

.custom-table-row-dragging {
    background: #fafafa;
    border: 1px solid #ccc;
    z-index: 11;
}
.custom-table-row-dragging td {
    padding: 0 15px;
    visibility: hidden;
}
.custom-table-row-dragging .drag-visible {
    visibility: visible;
}

完成上面的代码编写,即可实现在table表格中实现拖拽排序的功能效果,赶紧去试试→

1.3 列表场景

列表场景要考虑的因素就要比表格多一点,主要它没有集成在其他组件上使用,而是需要自己编码实现的。话不多说,下面就来实现列表场景需求→

首先在引入组件时就需要再引入arrayMove这个方法,它主要用于将移动后的数据排列好后返回。

1.3.1 引入组件

import { SortableContainer, SortableElement, SortableHandle, arrayMove } from 'react-sortable-hoc';

1.3.2 布局代码

同样,和table表格场景一样,需要先为其定义三个容器,但是不用像table那样再为其封装一层打标记和加事件,可直接在容器上使用事件,如下:

// 拖拽容器
const SortContainer = SortableContainer((props: any) => <ul className="flex flex1" {...props} />);

// 拖拽元素
const SortItemLi = SortableElement((props: any) => <li className='flex' {...props} />);

// 拖拽手柄
const DragHandle = SortableHandle(() => <MenuOutlined className="f12" style={{cursor: 'move', color: '#1890FF'}} />);

可以看到,这里使用的是ulli标签实现的,样式可以自行设置。接着,将需要排序的列表使用SortContainer组件SortItemLi组件包裹即可, 如下:

<SortContainer
    axis={'xy'}
    useDragHandle
    onSortEnd={(values) => onSortEnd(values, v.lineId)}
>
    <div className="list flexic">
        {
            v.practiceIdNameList.map((item: any, index: number) =>
                <SortItemLi
                    index={index}
                    key={index}
                >
                    {
                        rowSortLine === v.lineId
                            ?
                            <div className="drag-handle">
                                <DragHandle />
                            </div>
                            :
                            null
                    }
                    <div
                        className="item"
                        style={{cursor: rowSortLine === v.lineId ? 'move' : 'default'}}
                        key={item.practiceId}
                    >{item.practiceName}</div>
                </SortItemLi>
            )
        }
        {
            rowSortLine === v.lineId
                ?
                <div className="sort-option flexic">
                    <Button className="mr8" type="link" size="small" onClick={() => rowSortSubmit(v.lineId)}>保存</Button>
                    <Button type="link" size="small" onClick={() => cancelSort(v)}>取消</Button>
                </div>
                :
                <div className='sort' onClick={() => rowSort(v)}>手动排序</div>
        }
    </div>
</SortContainer>

可以看到SortContainer组件内多了一个axis属性,其实它就是用于控制元素是水平拖动还是垂直拖动还是两者都行的属性;看文档可以知道它的默认值是'y',表示垂直方向拖动;若要设置只能横向拖动就设置为'x',水平垂直都能拖动就设置为'xy';上面代码设置的就是水平垂直方向都能拖拽,主要考虑的是元素可能很多并且换行了,这样若只设置一个方向就不能实现需求,故设置为'xy'。如下图:

gif4.gif

还有从两个场景都可以看到代码都设置了拖拽手柄,这是为了告诉用户元素是可拖动的,增加用户的使用体验。至于react-sortable-hoc的属性和方法可以详见API文档

1.3.3 逻辑代码

其实这里演示的逻辑代码是由于数据结构和接口逻辑使然,接口需要的参数是排序好的ID集合,故不能像table表格场景那样进行逻辑编写,再加上需求需要将顺序排好之后点击保存数据才进行变化,故逻辑和之前的场景有些许不同。先看这里的数据结构,如下图:

image.png

/**
 * 表格行排序
 * @param v
 */
const rowSort = (v: any) => {
    const {lineId} = v;
    setRowSortLine(lineId);
}


/**
 * 拖拽结束
 * @param values
 * @param lineId
 */
const onSortEnd = (values: any, lineId: string) => {
    const {oldIndex, newIndex} = values;
    let newData: any[] = [...dataSource];
    let temp: any = dataSource.filter(v => lineId === v.lineId)[0];
    let data: any[] = [...temp.practiceIdNameList];
    practiceIdNameListOrigin.current = JSON.parse(JSON.stringify(temp.practiceIdNameList));
    let list: any = [];
    if (oldIndex !== newIndex) {
        list = arrayMove(data, oldIndex, newIndex);
    } else {
        list = data;
    }
    newData.forEach(v => {
        if (lineId === v.lineId) {
            v.practiceIdNameList = list
        }
    });
    let arr: any[] = getGroupData(newData);
    setDataSource(arr);
}


/**
 * 排序提交
 * @param lineId
 */
const rowSortSubmit = (lineId: string) => {
    let temp: any = dataSource.filter(v => lineId === v.lineId)[0];
    let ids: string[] = temp.practiceIdNameList.map(v => v.practiceId);
    request(
        '/project/v2/practice/sort',
        {practiceIdList: ids},
        {baseURL: globalProjectMethodURL}
    ).then(res => {
        setRowSortLine('');
        queryTableList(typeId);
    })
}


/**
 * 取消排序
 * @param v
 */
const cancelSort = (v: any) => {
    const {lineId} = v;
    let newData: any[] = [...dataSource];
    newData.forEach(v => {
        if (lineId === v.lineId) {
            v.practiceIdNameList = ObjTest.isNotEmptyArray(practiceIdNameListOrigin.current) ? practiceIdNameListOrigin.current :  v.practiceIdNameList
        }
    });
    let arr: any[] = getGroupData(newData);
    setDataSource(arr);
    setRowSortLine('');
}

从代码可以看到,在拖拽结束onSortEnd()的方法中,根据判断元素的序号是否改变然后使用arrayMove()返回排序好的数组,然后重新给表格数据赋值,从而看到已排序好的元素显示;若序号没有则保持原来的数组即可。然后在取消排序时,将practiceIdNameListOrigin.current保存的值还原即可保持原有数据的显示效果。

二、react-draggable

react-draggable经过几年的发展,已经是一个相对比较稳定的库了。在所有react拖拽库里react-draggable是把功能性和易用性平衡得最好的拖拽库,它可以满足多数情况下的拖拽需求。

2.1 安装并引用

$ npm install react-draggable 或者 yarn add react-draggable

引入组件:

import Draggable from 'react-draggable';

2.2 使用

这里的使用方法是介绍其在AntdModal组件中如何使用,毕竟这个场景还是挺常见的,有些小伙伴在使用了Modal组件之后需求需要拖动,不想重写代码就只想在已有代码上去做更改的心态也是常见的,毕竟我也是如此,不想重复造轮子,能做更改即做,实在无法才去重写。弹窗能移动的需求开始我也觉得要到重写的地步了,但是深入查看文档之后,让我发现可以解决的办法。

那就是Modal组件提供了一个modalRender属性,它用于自定义渲染对话框。即可以自定义渲染,而不影响已有的代码结构,故就有了下面的代码演示,如下:

<Modal
    className="bit-replace-modal"
    title='批量替换'
    width={600}
    visible={visible}
    onOk={handleOk}
    onCancel={handleCancel}
    afterClose={() => {modalForm.resetFields()}}
    maskClosable={false}
    modalRender={(node) => (
        // 就是这里!!!加上这个代码你的弹窗就能拖动了
        <Draggable>
            <div ref={draggleRef}>{node}</div>
        </Draggable>
    )}
>
    <Form form={modalForm} labelCol={{ span: 3}} wrapperCol={{ span: 21 }}>
        <Form.Item label="替换范围" name="partIdList" initialValue={[]} className="mb10">
            <TreeSelect
                treeData={treeData}
                showSearch
                allowClear
                multiple
                treeCheckable={true}
                maxTagCount={6}
                fieldNames={{
                    label: 'title',
                    value: 'key',
                    children: 'children'
                }}
                showCheckedStrategy={SHOW_CHILD}
            />
        </Form.Item>
        <Form.Item label="" name="practiceColumnList" labelCol={{span: 0}} wrapperCol={{span: 24}} initialValue={[]}>
            <Checkbox.Group options={checkboxList}/>
        </Form.Item>
        <Form.Item label="查找内容" name="sourceContent" initialValue={''}>
            <Input />
        </Form.Item>
        <Form.Item label="替换为" name="replaceContent" initialValue={''}>
            <Input />
        </Form.Item>
    </Form>
</Modal>

至此,react-sortable-hocreact-draggable组件在实际项目中的使用方法就介绍完了,文章并没有展开延伸介绍组件的每一个属性和方法,因为那样就会很长影响阅读,这里只讲了其在我们项目中使用的一些属性和方法,其他属性和方法还得结合实际需求去参看API文档进行使用,看了这篇文章相信一定能举一反三,后续的相关属性和方法也能掌握。

最后,xdm看文至此,点个赞👍再走哦,3Q^_^

往期精彩文章

后语

伙伴们,如果觉得本文对你有些许帮助,点个👍或者➕个关注在走呗^_^ 。另外如果本文章有问题或有不理解的部分,欢迎大家在评论区评论指出,我们一起讨论共勉。