所见即所得, 通过拖拽实现一个自定义布局

4,817 阅读4分钟

前言

该项目基于react-grid-layoutantd的栅格布局实现。可能不适用所有项目, 但是思路也可以参考。

demo体验地址: 自定义布局

源码地址: 源码

实现功能

通过可视化拖拽区块, 组合区块生成自定义布局代码, 可直接引入有antd的项目中使用。

上面可视化操作后生成的布局代码:

<Row>
    <Col span={10} className="drag-item">
        <Col className="drag-subitem" style={{ height: '10vh' }} span={24}></Col>
        <Col className="drag-subitem" style={{ height: '95vh' }} span={24}></Col>

    </Col>
    <Col span={14} className="drag-item">
        <Col className="drag-subitem" style={{ height: '25vh' }} span={24}></Col>
        <Col className="drag-subitem" style={{ height: '80vh' }} span={24}></Col>

    </Col>
</Row>

代码生成的页面:

实现思路

要想生成任意布局, 栅格布局是最合适的。

这里不考虑特殊情况下的不规则布局。

我们先将布局按列划分, 每一列里面再分成几块, 这样基本就能满足我们大部分的业务场景了。

说起来简单,但是要和可视化拖拽结合起来就没那么简单了。

我们通过拖拽得到每个区块的宽高和位置信息, 借此来确定我们的区块应该放在哪个位置。

然后将其与antd的栅格布局的API结合, 生成我们想要的布局代码。

用过antd的栅格布局的应该知道, Row控制行布局, Col控制列布局。我的思路是:

整个页面都是一个Row, 然后根据拖拽信息划分列, 每个列里都是占满整个列宽的列, 这样就可以往下堆叠。

Col通过span属性控制宽度, 高度我们直接通过style属性加上去。

实现

实现可视化拖拽

这里不得不介绍一下强大的react-grid-layout库了。

git地址: react-grid-layout

我们只需要提供简单的基础信息就能生成可拖拽可伸缩的区块。 这里对API不做过多介绍, 主要将实现。

const dragItem = { x: 14, y: 14, w: 1, h: 1, maxH: 20 }

<GridLayout className="layout"
    onResizeStop={onResizeStop}
    onDragStop={onDragStop}
    preventCollision={true}
    margin={[0, 0]}
    cols={12}
    rowHeight={30}
    width={800}
    style={{ height: 600 }}
>
    {
        Object.keys(dragObj).map(item => <div className="drag-item" style={{ backgroundColor: '#' + ('00000' + (Math.random() * 0x1000000 << 0).toString(16)).slice(-6) }} key={item} data-grid={dragObj[item]}></div>)
    }
</GridLayout>
                

数据驱动视图, 我们通过添加区块事件改变代码中的dragObj, 即可实现往视图中添加区块。

当我们改变了视图中的区块位置或大小时, 我们需要保存当前的区块信息, 方便我们接下来的生成布局代码。

上面代码中的onResizeStoponDragStop事件可以帮我们监听我们的拖拽结束和改变区块大小的动作。

这两个事件做的事情是相同的, 记录当前区块状态, 这里看下事件代码:

    const onResizeStop = (arr) => {
        let obj = {}, layoutObj = {}
        arr.forEach(item => {
            obj[item['i']] = { x: item['x'], y: item['y'], w: item['w'], h: item['h'], maxH: item['maxH'] }
            if (layoutObj[item['x']]) {
                layoutObj[item['x']].push(item)
            } else {
                layoutObj[item['x']] = [item]
            }
        })
        setLayoutObj(layoutObj)
        setDragObj(obj)
    }

这两个事件都会给我们提供一个包含所有区块信息的数组参数, 我们通过对这个参数做解析, 将其改造成我们可以使用的数据结构, 并保存在state中。

解析对象生成代码

当我们通过拖拽生成我们想要的数据结构后, 要生成我们想要的页面, 此时我们需要知道, 区块的位置和宽高如何映射到我们的布局中去

我这里通过约定: 拖拽区域大小为宽800, 高600。 每个区块的基础划分:

将高度20等分, 宽度12等分。

这样:

一个单位的h对应0.5vh的高度;

一个单位的w对应2个单位的span(antd的Col的span是24等分)

有了映射关系, 我们只要把存储区块信息的对象改造成代码串就行了:

    const onCodeGenerator = () => {
        setCodeStr(
            '<Row>\n' 
            + 
            Object.keys(layoutObj).map(item => `    
            <Col span={${layoutObj[item][0].w * 2}} className="drag-item">
            ${layoutObj[item].map(subItem => `      
                <Col className="drag-subitem" style={{height:'${subItem.h * 5}vh'}} span={24}></Col>\n`).join('')}
            </Col>\n`
            ).join('')
            +
            '</Row>'
        )

    }

通过上面的代码进行改造, 就能得到我们想要的代码串, 直接复制, 粘贴到我们的项目中就得到了一个合适的布局。

后话

这算是一个工具性的东西, 我是根据当前公司业务的基础上进行的开发, 所以可能对大家的适用性不是很高, 这里记录下思路, 方便以后有类似的需求可以参考。