react + react-dnd 实现简单的看板demo

1,797 阅读3分钟

背景

最近领导安排做一个看板 web 端内容,对于这块我还是比较迷茫的,先写一个demo来试试。

实现

构建

这里做一个广告,我自己封装的脚手架工具 groove-cli 。可以快速创建 react ,vue 等项目。

这里使用 groove-cli 来初始化 react 项目。 在拖动这块用到了 react-dnd 这个库。

业务拆解

对于看板的demo,并不需要太复杂的内容,简单可以这样分。 需要实现一个看板页面,看板页面包括多个栏目,一个栏目可以包含多个卡片。

开发

数据结构

因为是demo,所以数据结构并不复杂。

// 看板信息
interface BoardInfoModel {
    sectionList: SectionModel[];
}
// 栏目信息
interface SectionModel {
    sectionId: number;
    sectionName: string;
    cardList: CardModel[];
}
// 卡片信息
interface CardModel {
    cardId: number;
    parentId: number;
    cardName: string;
    cardContent: string;
}

核心功能

对于看板来说,比较核心的功能就是拖动卡片以及栏目。

这里主要是依赖于 react-dnd 这个依赖包,它提供了几个 hooks,像 useDrag和useDrop 这两个hook 是用来实现拖动的核心。

卡片拖动

// 会返回收集过程中的值和拖动元素的ref,还有第三个返回内容就是拖动预览的ref。
const [{display}, dragRef] = useDrag(() => ({
        // 拖动元素类型,用于区分不同元素
        type: 'card',
        // 拖动元素的值。
        item: {value},
        // 在拖动过程中,可以收集不同的值。
        collect: (monitor) => {
            return {
                display: monitor.isDragging() ? 'none' : 'block'
            };
        },
        // 在拖动结束时触发的回调函数
        end: (item, monitor) => {
            const result = monitor.getDropResult();
            emit('moveCard', {
                newCardPost: {...item.value, parentId: result?.name, postion: result.hoverIndex},
                oldCardPost: {...value}
            });
        },
    }), []);
    ...
    // 这边一个想法就是通过外部传入move属性,告诉组件是否要去偏移,是向上偏移还是向下偏移。【代码没有进行优化,比较的丑】
return (
        <div className={styles.cardBox} ref={dragRef} style={{display, transform: typeof move === 'string' ? `translateY(${move === 'down' ? '' : '-'}130px)` : ''}}>
            <div className={styles.cardName}>
                {value.cardName}
            </div>
            <div className={styles.cardContent}>
                {value.cardContent}
            </div>
        </div>
    );
    

卡片的拖动还需要栏目组件的支持,在栏目组件中用到了 useDrop ;

const [{isOver}, drop] = useDrop(() => ({
        // 接受类型
        accept: 'card',
        // 放置的方法,返回结果。
        drop: (item, monitor) => {
            setHoverIndex(null);
            setHoverItem(null);
            let y = monitor.getClientOffset().y;
            let hoverIndexlocal = getHoverIndex(y);
            return {name: value.sectionId, hoverIndex: hoverIndexlocal};
        },
        // 在当前组件中拖动
        hover: (item, monitor) => {
            let y = monitor.getClientOffset().y;
            // 用于记录拖动元素
            setHoverItem(item);
            // 用于记录拖动位置
            setHoverIndex(getHoverIndex(y));
        },
         collect: (monitor) => ({
            isOver: monitor.isOver(),
           
        }),
    }));

在渲染时通过控制 move 属性来去调控

// 栏目中卡片部分的渲染逻辑 还是有可优化的空间的。
value.cardList.map((card: CardModel, index) => {
                            let move = null;
                            //  同列拖动逻辑
                            if (card?.parentId === hoverItem?.value?.parentId) {
                            // 同列会因为从大到小拖动 或者从小到大拖动产生不同的效果
                                let cardIndex = value.cardList.findIndex(item => item.cardId === hoverItem?.value.cardId);

                                let moveD = hoverIndex > cardIndex ? 'down' : 'up';
                                move = hoverIndex <= index ? moveD : null;

                            } else if (card?.parentId !== hoverItem?.value?.parentId) {
                                move = typeof hoverIndex === 'number' ? hoverIndex <= index ? 'down' : null : null;
                            }
                            // 顶部特判
                            if (move === 'up' && index === 0) {
                                move = null;
                            }
                            console.log(value.sectionId, index, hoverIndex, move, isOver);
                            
                            return (
                                <Card key={card.cardId} index={index} value={card} move={(move && isOver) ? move : null}>
                                </Card>
                            );
                        })

数据更新

因为是个简单的demo,我稍微的实现了一个eventBus,在board页面去监听事件。

 on('moveCard', (data) => {
            console.log('on', data);

            let newSectionList = updateData(boardInfo.sectionList, data.oldCardPost, data.newCardPost);
            console.log('处理过的', newSectionList);

            setBoardInfo({
                sectionList: newSectionList
            });
        });

这边同栏和异栏更新还是不太一样的。 同栏这边是移动节点,异栏这边主要是删除原有,新增到别的栏目中、代码实现上比较的多,这边就不贴出来了,大致逻辑是这样。

栏目拖动

实现的方法和卡片拖动基本一致,在board页面使用 useDrop,在 section 使用 useDrag。

最终效果

Kapture 2021-12-07 at 12.06.32.gif

总结

主要是依赖于 react-dnd 提供的能力。目前代码中还是比较多问题的,慢慢优化的。

传送门: github.com/godj1001/gr…