1.任务需求
实现一个章节目录,要求章和章之间可以进行拖拽排序;每一章内的节可以拖拽排序,并且可以把其他章的小节拖拽到其他章中,章是不可以拖拽到其他章内的,章是可以新建的,小节是请求后端查询数据库返回的。示例如下
首先对技术做了调研,本着提高开发效率,能不自己造轮子就尽量找对应好用的库进行开发的理念上,我对react的相关的拖拽库进行了调研,其中包括react-draggable,react-sortable-hoc,react-beautiful-dnd等拖拽库,不谈其优劣,只谈适合业务场景,最终选择了react-beautiful-dnd这个拖拽库
react-beautiful-dnd英文github地址:https://github.com/atlassian/react-beautiful-dnd
react-beautiful-dnd中文github地址:https://github.com/chinanf-boy/react-beautiful-dnd-zh
react-beautiful-dnd库demo演示地址:https://react-beautiful-dnd.netlify.app/?path=/story/single-vertical-list–basic
文档比较简单,也不是那么易看,网上想搜索相关的例子也没有符合我的需求可供参考的,于是我就拉下了github的源码,去看了下源码,意外的发现,在源码中还有demo的源码,拉下源码后 yarn安装下依赖(不用我过多描述了)然后进入package.json中,找到storybook(那就启动吧,yarn storybook),神奇的一幕出现了,访问9002端口,这些demo就都出现了,然后顺藤摸瓜,找到demo的文件夹storybook,你就看到demo源码了
首先根据需求确定了一下这个数据结构如下图
这个基本用法文档都有说,我这里就不细说了,讲一下具体实现思路以及我的代码,根据文档描述
DragDropContext
作为最外层包裹住可以拖拽的部分
Droppable
可以理解为一个拖拽域,里面的Draggable
包裹的就是可拖拽元素,这些理解了之后对于当前这个场景就可以设计出这样的结构,以下是伪代码
<DragDropContext>
<Droppable>
<Draggable>
{章}
<Droppable>
<Draggable>{节}</Draggable>
<Draggable>{节}</Draggable>
<Draggable>{节}</Draggable>
</Droppable>
</Draggable>
</Droppable>
</DragDropContext>
一个这样的结构就出来的,再参考文档以及示例写出以下代码
let defaultChapter: any = {
id: '6',
name: '默认章',
node_list: [],
}
const [nodeList, setNodeList] = useState([defaultChapter])
- 最外层
<DragDropContext onDragEnd={onDragEnd}>
<div className={css.boardContain}>{board}</div>
</DragDropContext>
- board
const board = (
<Droppable droppableId="board" type="COLUMN" direction="vertical">
{(provided) => (
<div ref={provided.innerRef} {...provided.droppableProps}>
{nodeList?.map((node, index) => (
<Column
key={node?.name}
index={index}
title={node?.name}
node={node}
/>
))}
{provided.placeholder}
</div>
)}
</Droppable>
)
- Column
const Column: React.FC<ColumnProps> = (props) => {
const { title, index, node } = props
return (
<Draggable draggableId={title} index={index} key={title}>
{(provided, snapshot) => (
<div ref={provided.innerRef} {...provided.draggableProps}>
<div
// isDragging={snapshot.isDragging}
{...provided.dragHandleProps}
style={{
marginBottom: '8px',
marginTop: '8px',
fontSize: '16px',
}}
>
{/* {title} */}
<Tooltip title="拖拽排序">
<MenuOutlined
style={{
color: '#666',
marginRight: 5,
}}
/>
</Tooltip>
{index + 1}{' '}
<Input
style={{ width: 200, height: 32, fontSize: '16px' }}
defaultValue={title}
onChange={(e)=>chapterNameChange(e,index)}
/>
{index !== 0 && (
<CloseOutlined
color="#ccc"
size={10}
onClick={() => deleChapterBtnClick(index, node)}
/>
)}
</div>
<ChapterList
preIndex={index + 1}
listId={title}
listType="CHAPT"
list={node.node_list}
/>
</div>
)}
</Draggable>
)
}
- ChapterList
const ChapterList: React.FC<ChapterProps> = (props) => {
const { list, listId, listType, preIndex } = props
return (
<Droppable droppableId={listId} type={listType}>
{(dropProvided, dropSnapshot) => (
<div ref={dropProvided.innerRef} {...dropProvided.droppableProps}>
{list?.map((li, index) => (
<Draggable key={li.id} draggableId={li.id} index={index}>
{(dragProvided, dragSnapshot) => (
<>
<div
ref={dragProvided.innerRef}
{...dragProvided.draggableProps}
key={li.id}
// isDragging={dragSnapshot.isDragging}
>
<div
{...dragProvided.dragHandleProps}
style={{ marginBottom: 13 }}
>
<Tooltip title="拖拽排序">
<MenuOutlined
style={{
color: '#666',
marginRight: 5,
}}
/>
</Tooltip>
<span className={css.listItem}>
{' '}
{preIndex}.{index + 1} {li.name}
</span>
</div>
</div>
</>
)}
</Draggable>
))}
{dropProvided.placeholder}
</div>
)}
</Droppable>
)
}
//最外层要加上onDragEnd才能保存拖拽后的位置哦,主要逻辑其实就是拖拽的这个节或章替换掉目标的位置
const reorder = (
list: any[],
startIndex: number,
endIndex: number,
): any[] => {
const result = Array.from(list)
const [removed] = result.splice(startIndex, 1)
result.splice(endIndex, 0, removed)
return result
}
const onDragEnd = (result: any) => {
const { source, destination, draggableId, type } = result
console.log(
'source',
source,
'destination',
destination,
'draggableId',
draggableId,
'type',
type,
)
if (!destination) {
return
}
if (
source.droppableId === destination.droppableId &&
source.index === destination.index
) {
return
}
if (result.type === 'COLUMN') {
const ordered = reorder(
// this.state.ordered,
nodeList,
source.index,
destination.index,
)
setNodeList([...ordered])
return
}
if (result.type === 'CHAPT') {
const sourceArr = nodeList.filter((n) => n.name == source.droppableId)[0]
.node_list
const desArr = nodeList.filter(
(n) => n.name === destination.droppableId,
)[0].node_list
console.log(sourceArr, desArr)
let moveItem = sourceArr.splice(source.index, 1)[0]
desArr.splice(destination.index, 0, moveItem)
console.log(sourceArr, desArr)
return
}
}
最后
遇到的坑是数不胜数,印象最深刻的就是在最外层如果想让他溢出滚动的话,加overflow:auto是会出问题的,会导致新建的章的位置不被记录,改成overflow:overlay就解决了这个问题。