背景
"我看竞品的平台就很炫酷呀,我们也要实现一样的效果"
"两周时间够了吧,业务着急用"
又见到了熟悉的话语,身为一个技术人,对话中的是是非非都无关紧要,单从技术讨论功能的可行性
本篇文章会简单介绍一下拖拽拼装页面的基础。因为要做到从一个固定菜单拖拽组件到界面,用react-dnd的drag和drop功能能适配拖拽功能,并且能通过acceptItem指定应该接收的组件
初始化
需要安装react-dnd及其对应依赖react-dnd-html5-backend
yarn add react-dnd react-dnd-html5-backend
在根目录(如_app.tsx)中初始化dndProvider
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { MainLayout as Layout } from '@/page-components/layout/MainLayout';
require('@/styles/global.less');
function CustomApp({ Component, pageProps }: any) {
return (
<DndProvider backend={HTML5Backend}>
<div id="root">
<Layout>
<Component {...pageProps} />
</Layout>
</div>
</DndProvider>
);
}
export default CustomApp;
react-dnd是由两个部分组成的,一个是 drag(拖)组件,一个是drop(drop)组件,这同一个组件可以同时成为drag和drop(既可拖也可以拽)。
创建页面
我们的目标是做一个可拖拽的组件菜单,能防止到页面区域,那么菜单需要是一个drag组件,这里type非常重要,比如你声明一个type为menu的drag组件,那么只有accept中有menu的drop区域可以放置这个组件
import { useDrag, useDrop } from 'react-dnd'
const Component = (compProps: ComponentProps) => {
const { componentType } = compProps.data;
const currentPath = `${compProps.rowIndex}-${compProps.compIndex}`;
const [, drag] = useDrag({
type: 'component',
item: {
type: 'component',
path: currentPath,
data: compProps.data
},
});
return <div ref={drag} className="component">
{compProps?.isMenu ? (menuComponent?.[componentType] || null) : (registeredComponent?.[componentType] || null)}
</div>
}
那么页面区域里怎么做到排序呢,这就要用到两个页面区域间放置看不到的drop区域。这其中overing表示监控在拖拽的组件越过zone上空的时候的变量
const DropZone = (props: DropZoneProps) => {
const { swapPosition, path } = props;
const [{ overing }, drop] = useDrop({
accept: ['component', 'menu'],
drop(item: any) {
// console.log('触发了drop', item, path)
swapPosition(item, path);
},
collect(monitor) {
return {
overing: monitor.isOver()
}
}
});
return <div ref={drop}
className={`drop-zone ${props.className} ${overing ? 'focus' : ''}`}
style={{
height: 20,
background: overing ? '#027cdc' : 'red'
}}
></div>
}
包裹容器,用途就是根据data来渲染页面中的components, 当然还有component前后看不见的放置区域
function RowContainer(rowContainerProps: RowContainerProps) {
const ref = useRef(null);
const { data, swapPosition } = rowContainerProps;
const currentPath = `${rowContainerProps.rowIndex}` // 标注当前位置
return <div ref={ref} className="rowContainer">
{
data?.map((item, index) => {
return <div key={`comp_id_${item.id}`}>
<DropZone className="drop-zone-horizental" swapPosition={swapPosition} path={`${currentPath}-${index}`}></DropZone>
<Component data={item}
rowIndex={rowContainerProps.rowIndex}
compIndex={index}
></Component>
</div>
})
}
<DropZone swapPosition={swapPosition} className="drop-zone-horizental" path={`${currentPath}-${data?.length}`}></DropZone>
</div>
}
那么当放置的时候怎么触发交换呢,这就要用到swapPosition方法, 如果是菜单拖拽进放置区域,那么直接添加组件,如果是页面内拖拽,那么交换他们的位置
// item 被拖拽的物体 path2: 目标路径
const swapPosition = (item: dragItem, path2: string) => {
const newList = {...cardListTotal}
// 如果是menu拖过来的不用删除
if (item.path.indexOf('menu') === -1) {
const pathForm = item.path.split('-')
// 删除原来的item
newList[parseInt(pathForm[0])].splice(parseInt(pathForm[1]), 1)
}
const pathTo = path2.split('-')
newList[parseInt(pathTo[0])].splice(parseInt(pathTo[1]), 0, item.data)
setCardListTotal(newList)
}
整体效果
全部代码
import React, {useEffect, useRef, useState, useContext, useCallback} from 'react';
import { useDrag, useDrop } from 'react-dnd'
/**
* 注册组件区域
*/
interface LayoutItem {
type: string;
id: string;
componentType: string
}
// 拖拽时候传的数据
interface dragItem {
type: string,
path: string,
data: LayoutItem
}
const style= {
borderStyle:'solid',
background: 'skyblue',
width: 200,
lineHeight: '60px',
padding: '0 20px',
border: '1px solid #000',
margin: '0 10px',
cursor: 'move',
}
const registeredComponent: Record<string, any> = {
component1: <div style={style}>component1</div>,
component2: <div style={style}>component2</div>,
component3: <div style={style}>component3</div>
}
const menuComponent: Record<string, any> = {
component1: <div style={style}>菜单1</div>,
component2: <div style={style}>菜单2</div>,
component3: <div style={style}>菜单3</div>
}
interface ComponentProps {
data: LayoutItem,
rowIndex: number | string; // 行index
compIndex: number; // 组件index
isMenu?: boolean; // 是否是菜单样式
}
const Component = (compProps: ComponentProps) => {
const { componentType } = compProps.data;
const currentPath = `${compProps.rowIndex}-${compProps.compIndex}`;
const [, drag] = useDrag({
type: 'component',
item: {
type: 'component',
path: currentPath,
data: compProps.data
},
});
return <div ref={drag} className="component">
{compProps?.isMenu ? (menuComponent?.[componentType] || null) : (registeredComponent?.[componentType] || null)}
</div>
}
/**
* 注册组件区域结束
*/
/**
* 组件容器区域
*/
interface RowContainerProps {
data: LayoutItem[];
rowIndex: number;
swapPosition: (item: dragItem, path2: string) => void
}
function RowContainer(rowContainerProps: RowContainerProps) {
const ref = useRef(null);
const { data, swapPosition } = rowContainerProps;
const currentPath = `${rowContainerProps.rowIndex}` // 标注当前位置
return <div ref={ref} className="rowContainer">
{
data?.map((item, index) => {
return <div key={`comp_id_${item.id}`}>
<DropZone className="drop-zone-horizental" swapPosition={swapPosition} path={`${currentPath}-${index}`}></DropZone>
<Component data={item}
rowIndex={rowContainerProps.rowIndex}
compIndex={index}
></Component>
</div>
})
}
<DropZone swapPosition={swapPosition} className="drop-zone-horizental" path={`${currentPath}-${data?.length}`}></DropZone>
</div>
}
/**
* 组件容器区域结束
*/
/**
* 交换区域
*/
interface DropZoneProps {
className: string;
path: string;
swapPosition: (item: dragItem, path: string,) => void
}
const DropZone = (props: DropZoneProps) => {
const { swapPosition, path } = props;
const [{ overing }, drop] = useDrop({
accept: ['component', 'menu'],
drop(item: any) {
// console.log('触发了drop', item, path)
swapPosition(item, path);
},
collect(monitor) {
return {
overing: monitor.isOver()
}
}
});
return <div ref={drop}
className={`drop-zone ${props.className} ${overing ? 'focus' : ''}`}
style={{
height: 20,
background: overing ? '#027cdc' : 'red'
}}
></div>
}
/**
* 交换区域结束
*/
const MissionList = () => {
const [cardListTotal, setCardListTotal] = useState<LayoutItem[][]>([
// 交集池
[
{
id: '0',
type: 'component',
componentType: 'component1'
},
{
id: '1',
type: 'component',
componentType: 'component2'
},
],
// 并集池
[
{
id: '5',
type: 'component',
componentType: 'component3'
},
{
id: '6',
type: 'component',
componentType: 'component1'
},
],
// 排除池
[]
]
)
const [menuList, setMenuList] = useState([{
id: '8',
type: 'component',
componentType: 'component3'
}])
// item 被拖拽的物体 path2: 目标路径
const swapPosition = (item: dragItem, path2: string) => {
const newList = {...cardListTotal}
// 如果是menu拖过来的不用删除
if (item.path.indexOf('menu') === -1) {
const pathForm = item.path.split('-')
// 删除原来的item
newList[parseInt(pathForm[0])].splice(parseInt(pathForm[1]), 1)
}
const pathTo = path2.split('-')
newList[parseInt(pathTo[0])].splice(parseInt(pathTo[1]), 0, item.data)
setCardListTotal(newList)
}
return (
<div style={{display: 'flex'}}>
<div style={{border: '1px solid #ccc', minWidth: 200}}>
<div>左侧菜单</div>
<Component data={menuList[0]}
rowIndex='menu'
compIndex={0}
></Component>
</div>
<div style={{border: '1px solid #ccc', minWidth: 200}}>
<div>交集区</div>
<RowContainer swapPosition={swapPosition} rowIndex={0} data={cardListTotal[0]}></RowContainer>
</div>
<div style={{border: '1px solid #ccc', minWidth: 200}}>
<div>并集区</div>
<RowContainer swapPosition={swapPosition} rowIndex={1} data={cardListTotal[1]}></RowContainer>
</div>
<div style={{border: '1px solid #ccc', minWidth: 200}}>
<div>排除区</div>
<RowContainer swapPosition={swapPosition} rowIndex={2} data={cardListTotal[2]}></RowContainer>
</div>
</div>
);
};
export default MissionList;