背景
最近领导安排做一个看板 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。
最终效果
总结
主要是依赖于 react-dnd 提供的能力。目前代码中还是比较多问题的,慢慢优化的。