react-grid-layout:可拖拽缩放网格布局插件

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了react-grid-layout:可拖拽缩放网格布局插件相关的知识,希望对你有一定的参考价值。

参考技术A react-grid-layout

    React-Grid-Layout 是一个网格布局系统,具有响应性并支持断点(breakpoints)。断点布局可以由用户提供或自动生成。

RGL 仅支持 React,不支持 jQuery。

    使用npm安装 React-Grid-Layout:

    样式表:

    下面的示例将生成一个包含三个项目的网格,其中:

    1.    用户将无法拖动或调整项目大小 a

    2.    b 将被限制为最小宽度为 2 个网格块和最大宽度为 4 个网格块

    3.    用户将能够自由拖动和调整项目大小 c

可以选择直接在子项上设置布局属性:

    <ResponsiveReactGridLayout>和<ReactGridLayout>采取width来计算拖动事件位置。在简单的情况下,WidthProvider可以使用HOC在初始化和窗口调整大小事件时自动确定宽度。

    // 除使用 <WidthProvider>情况外必填

     width: number,

    // 如果为 true,容器高度自适应内容

     autoSize: ?boolean = true ,

    //布局中的列数。

     cols: ?number = 12 ,

    // 取消拖拽时的css选择器

     draggableCancel: ?string = '' ,

    // 拖拽时的css选择器

     draggableHandle: ?string = '' ,

    // 紧凑排列类型

     compactType: ?( 'vertical' | 'horizontal' ) = 'vertical' ;

    // 布局,格式为数组对象,例如:

    // x: number, y: number, w: number, h: number

    // 布局中的索引必须与每个项目组件上使用的键匹配。

    // 如果您选择使用自定义键,则可以在布局中指定该键

    // 数组对象,如下所示:

    // i: string, x: number, y: number, w: number, h: number

    //如果父组件没有设置layout,则需要在子组件设置 data-grid

     layout: ?array = null ,

    //  [x, y] 的margin值

     margin: ?[number, number] = [ 10 , 10 ],

    //  [x, y] 的padding值

     containerPadding: ?[number, number] = margin,

    // 行高,可根据 breakpoints 改变

     rowHeight: ?number = 150 ,

    // 放置元素的配置。放置元素是一个从外部拖动某个元素时会出现的虚拟元素。

    //  i - 元素的id

    //  w - 元素的宽

    //  h - 元素的高

     droppingItem?: i : string, w : number, h : number

    // 是否可拖拽

     isDraggable: ?boolean = true ,

    //是否可重置大小

     isResizable: ?boolean = true ,

    //是否可设置边界

     isBounded: ?boolean = false ,

    // 使用 CSS3 translate() 替换position 的top/left ,可提升大约6倍性能

     useCSSTransforms: ?boolean = true ,

    //如果 ResponsiveReactGridLayout 或 ReactGridLayout 的父节点具有 "transform: scale(n)" 属性,应该设置缩放系数以避免拖动时出现渲染伪影。

     transformScale: ?number = 1 ,

    //是否允许重叠

     allowOverlap: ?boolean = false ,

    //如果为 true,则网格项在被拖动时不会改变位置

     preventCollision: ?boolean = false ,

    // 如果为true, 带有`draggable=true`属性的放置元素可被放置在网格上

    //注意:如果使用 Firefox,应该添加

    // `onDragStart=e => e.dataTransfer.setData('text/plain', ' ')` 属性

    // 连同 `draggable=true` 否则此功能将无法正常工作。

    // Firefox 需要 onDragStart 属性来进行拖动初始化

    // https://bugzilla.mozilla.org/show_bug.cgi?id=568313

     isDroppable: ?boolean = false ,

    // 重置大小时的操作位置,默认右下

    // 可选值:

    // 's' - South handle (bottom-center)

    // 'w' - West handle (left-center)

    // 'e' - East handle (right-center)

    // 'n' - North handle (top-center)

    // 'sw' - Southwest handle (bottom-left)

    // 'nw' - Northwest handle (top-left)

    // 'se' - Southeast handle (bottom-right)

    // 'ne' - Northeast handle (top-right)

     resizeHandles: ? Array < 's' | 'w' | 'e' | 'n' | 'sw' | 'nw' | 'se' | 'ne' > = [ 'se' ],

    // 自定义重置大小图标

      resizeHandle?: ReactElement < any > | ((resizeHandleAxis: ResizeHandleAxis , ref: ReactRef < htmlElement > ) => ReactElement < any > ),

    // 布局变化回调

     onLayoutChange: (layout: Layout ) => void ,

    // 下面的所有回调都有签名(layout、oldItem、newItem、placeholder、e、element)。

    // 'start' 和 'stop' 回调为 'placeholder' 传递 `undefined`。

      type ItemCallback = (layout: Layout , oldItem: LayoutItem , newItem: LayoutItem , placeholder: LayoutItem , e: MouseEvent , element: HTMLElement ) => void ,

    // 拖拽开始回调

     onDragStart: ItemCallback ,

    // 拖拽中回调

     onDrag: ItemCallback ,

    // 拖拽停止回调

     onDragStop: ItemCallback ,

    // 开始重置大小回调

     onResizeStart: ItemCallback ,

    // 重置大小中的回调

     onResize: ItemCallback ,

    // 重置大小结束回调

     onResizeStop: ItemCallback ,

    // 当元素从外部放入网格时的回调。

     onDrop : (layout: Layout , item: ? LayoutItem , e: Event ) => void ,

    // 当一个元素从外面拖过网格时调用, 此回调应返回一个对象以动态更改 dropItem大小

     onDropDragOver : (e: DragOverEvent ) => ?( | w?: number, h?: number | | false ),

    // 可以使用它来代替常规 ref 和已弃用的 `ReactDOM.findDOMNode()` 函数

     innerRef: ? React . Ref< "div" > ,

    // Breakpoint名称是任意的,但是必须在cols 和 layouts 对象中匹配。

     breakpoints: ? Object = lg : 1200 , md : 996 , sm : 768 , xs : 480 , xxs : 0 ,

    // 列数

     cols: ? Object = lg : 12 , md : 10 , sm : 6 , xs : 4 , xxs : 2 ,

    //[x,y]的margin

     margin: [number, number] | [breakpoint: $Keys < breakpoints > ]: [number, number],

    // [x,y]的padding

     containerPadding: [number, number] | [breakpoint: $Keys < breakpoints > ]: [number, number],

    // 布局配置

     layouts: [key: $Keys < breakpoints > ]: Layout ,

    // breakpoint发生变更时的回调

     onBreakpointChange: (newBreakpoint: string, newCols: number) => void ,

    // 布局发生变更时的回调

     onLayoutChange: (currentLayout: Layout , allLayouts: [key: $Keys < breakpoints > ]: Layout ) => void ,

    // 宽度改变时的回调,可以根据需要修改布局。

     onWidthChange: (containerWidth: number, margin: [number, number], cols: number, containerPadding: [number, number]) => void ;

     

        // 组件id

         i : string, 

        // 以下单位为网格,不是px

         x : number, 

         y : number, 

         w : number, 

         h : number, 

         minW : ?number = 0 , 

         maxW : ?number = Infinity , 

         minH : ?number = 0 , 

         maxH : ?number = Infinity ,  

        // 如果为 true,则等于 `isDraggable: false, isResizable: false`。

         static : ?boolean = false , 

        // 如果为 false,则不可拖动。覆盖 `static`. 

         isDraggable : ?boolean = true , 

        // 如果为 false,则不可重置大小。覆盖 `static`. 

         isResizable : ?boolean = true ,  

        // 默认情况下,调整大小图标仅显示在右下(东南)角。 

        // 请注意,从顶部或左侧调整大小通常不直观。

         resizeHandles ?: ? Array < 's' | 'w' | 'e' | 'n' | 'sw' | 'nw' | 'se' | 'ne' > = [ 'se' ] 

        //如果为 true 且可拖动,则项目将仅在网格内移动。

         isBounded: ?boolean = false

    

使用react-grid-layout和echarts-for-react实现一个支持拖拽的自定义响应式dashboard页面

文章目录

使用react-grid-layout和echarts-for-react实现一个支持拖拽的自定义响应式dashboard页面

需求概要

在前端工作中,我们会经常遇到自定义dashboard页这样的需求。然后我想做一个能够让用户可以在面板上自由的拖拽,固定(不允许拖拽),拖拉改变大小、新增,删除组件。组件可以是各种echarts图形,也可是各种数据表格。通过各个组件的拖拽组合,从而让用户自定义需要的dashboard页。

我们直接先来看最终的效果

技术栈

那我们这里就会是用到react-grid-layoutecharts-for-react

首先echarts-for-react,顾名思义就是用来绘制echarts图表的,这里不过多解释。然后react-grid-layout 是一个网格布局系统,可以实现响应式的网格布局,并且支持分割点(breakpoints)的设置,灵活运用可以方便的实现拖拽式组件的实现。

具体使用就不多介绍了,可以直接去官网看看,例子很多也很详细:https://github.com/react-grid-layout/react-grid-layout

简单实现

下面是dashboard的主渲染入口

import React, useState from "react";
import 'react-grid-layout/css/styles.css'
import 'react-resizable/css/styles.css'
import Layout, Responsive, WidthProvider from "react-grid-layout";
import Button from "antd";
import findIndex from "lodash";
import './dashboard.css'
import WidgetLoadingSpin from "@/pages/Dashboard/Detail/WidgetLoadingSpin";
import CloseOutlined, LockOutlined, QuestionCircleOutlined, UnlockOutlined from "@ant-design/icons";
const BarChartWidgetLazy = React.lazy(() => import('@/pages/Dashboard/Detail/Widget/BarChartWidget'));
const PieChartWidgetLazy = React.lazy(() => import('@/pages/Dashboard/Detail/Widget/PieChartWidget'));
const ResponsiveReactGridLayout = WidthProvider(Responsive);

interface DashboardWidgetInfo 
  widgetName: string,
  layout: Layout


function DashboardGird() 

  const [widgets, setWidgets] = useState<DashboardWidgetInfo[]>([]);
  const [currentCols, setCurrentCols] = useState<number>(12);

  const getLayouts: any = () => 
    return widgets.map(item => item.layout);
  


  const setLayoutStatic = (widget: DashboardWidgetInfo, staticFlag: boolean) => 
    const index = findIndex(widgets, (w: any) => w.widgetName === widget.widgetName);
    if (index !== -1) 
      const updateWidget = widgets[index];
      updateWidget.layout.static = staticFlag;
      widgets.splice(index, 1, ...updateWidget);
      const newWidgets = [...widgets];
      setWidgets(newWidgets);
    
  

  const lockWidget = (widget: DashboardWidgetInfo) => 
    setLayoutStatic(widget, true);
  

  const unlockWidget = (widget: DashboardWidgetInfo) => 
    setLayoutStatic(widget, false);
  

  const onRemoveWidget = (widget: DashboardWidgetInfo) => 
    const widgetIndex = findIndex(widgets, (w: any) => w.layout.i === widget.layout.i);
    if (widgetIndex !== -1) 
      widgets.splice(widgetIndex, 1);
      const newWidgets = [...widgets];
      setWidgets(newWidgets);
    
  

  const getWidgetComponent = (widgetName: string) => 
    if (widgetName === 'PieChartWidget')  //可以改成策略
      return (<React.Suspense fallback=<WidgetLoadingSpin/>>
        <PieChartWidgetLazy/>
      </React.Suspense>);
     else 
      return (<React.Suspense fallback=<WidgetLoadingSpin/>>
        <BarChartWidgetLazy/>
      </React.Suspense>);
    
  


  const createWidget = (widget: DashboardWidgetInfo) => 
    return (
        <div className='dashboard-widget-wrapper' key=widget.layout.i data-grid=widget.layout>
          <span className='dashboard-widget-header'>
            <QuestionCircleOutlined className='dashboard-widget-header-icon'/>
            widget.layout.static ? <LockOutlined className='dashboard-widget-header-icon' onClick=() => unlockWidget(widget)/> : (
                <UnlockOutlined className='dashboard-widget-header-icon' onClick=() => lockWidget(widget)/>)
            <CloseOutlined className='dashboard-widget-header-icon' onClick=() => onRemoveWidget(widget)/>
          </span>
          getWidgetComponent(widget.widgetName)
        </div>
    );
  


  const onAddWidget = () => 
    const x = (widgets.length * 3) % (currentCols);
    const widgetName = x % 2 == 0 ? 'BarChartWidget' : 'PieChartWidget'
    const newWidgets = [...widgets, 
      widgetName: widgetName,
      layout: i: widgetName, x: x, y: Infinity, w: 3, h: 2, static: false
    ] as DashboardWidgetInfo[];
    setWidgets(newWidgets);
  

  const onBreakpointChange = (newBreakpoint: string, newCols: number) => 
    setCurrentCols(newCols);
  

  const onLayoutChange = (layouts: any[]) => 
    for (const layout of layouts) 
      const updateIndex = findIndex(widgets, (w) => w.layout.i === layout.i);
      if (updateIndex !== -1) 
        const updateWidget = widgets[updateIndex];
        updateWidget.layout = layout;
        widgets.splice(updateIndex, 1, ...updateWidget);
      
    
    const newWidgets = [...widgets];
    setWidgets(newWidgets);
  

  return (
      <>
        <Button onClick=onAddWidget>add widget</Button>
        <ResponsiveReactGridLayout
            layouts=getLayouts()
            className='layouts'
            onLayoutChange=onLayoutChange
            onBreakpointChange=onBreakpointChange>
          widgets?.map(item => createWidget(item))
        </ResponsiveReactGridLayout>
      </>
  );


export default DashboardGird

然后接下来是自己的一些自定制化的一些图表或者表格的组件

import React from "react";
import WidgetLoadingSpin from "@/pages/Dashboard/Detail/WidgetLoadingSpin";

const ReactEchartsLazy = React.lazy(() => import('echarts-for-react'));

function PieChartWidget() 
  const getPieChart = () => 
    return 
      color: ['#3AA1FF', '#36CBCB', '#4ECB73', '#FBD338'],
      tooltip: 
        trigger: 'item',
        formatter: 'a <br/>b: c (d%)'
      ,
      grid: 
        left: '3%',
        right: '4%',
        bottom: '3%',
        containLabel: true
      ,
      series: [
        name: '消费能力',
        type: 'pie',
        radius: ['40%', '55%'],
        center: ['50%', '55%'],
        avoidLabelOverlap: true,
        itemStyle: 
          normal: 
            borderColor: '#FFFFFF',
            borderWidth: 2
          
        ,
        label: 
          normal: 
            show: false,
          ,
        ,
        labelLine: 
          normal: 
            show: false
          
        ,
        data: [
          name: 'a',
          value: '20'
        , 
          name: 'b',
          value: '40'
        , 
          name: 'c',
          value: '10'
        , 
          name: 'd',
          value: '10'
        ]
      ]
    ;
  

  return (<React.Suspense fallback=<WidgetLoadingSpin/>>
    <ReactEchartsLazy
        option=getPieChart()
        notMerge=true
        lazyUpdate=true
        style=width: '100%', height: '100%'/>
  </React.Suspense>)


export default PieChartWidget

import React from "react";
import WidgetLoadingSpin from "@/pages/Dashboard/Detail/WidgetLoadingSpin";

const ReactEchartsLazy = React.lazy(() => import('echarts-for-react'));

function BarChartWidget() 

  const getBarChart = () => 
    return 
      tooltip: 
        trigger: 'axis',
        axisPointer: 
          type: 'shadow'
        
      ,
      grid: 
        left: '3%',
        right: '4%',
        bottom: '3%',
        containLabel: true
      ,
      xAxis: [
        type: 'category',
        data: ['2014', '2015', '2016', '2017', '2018', '2019'],
        axisLine: 
          lineStyle: 
            color: '#8FA3B7',//y轴颜色
          
        ,
        axisLabel: 
          show: true,
          textStyle: 
            color: '#6D6D6D',
          
        ,
        axisTick: show: false
      ],
      yAxis: [
        type: 'value',
        splitLine: show: false,
        //max: 700,
        splitNumber: 3,
        axisTick: show: false,
        axisLine: 
          lineStyle: 
            color: '#8FA3B7',//y轴颜色
          
        ,
        axisLabel: 
          show: true,
          textStyle: 
            color: '#6D6D6D',
          
        ,
      ],
      series: [

        
          name: 'a',
          type: 'bar',
          barWidth: '40%',
          itemStyle: 
            normal: 
              color: '#FAD610'
            
          ,
          stack: '信息',
          data: [320, 132, 101, 134, 90, 30]
        ,
        
          name: 'b',
          type: 'bar',
          itemStyle: 
            normal: 
              color: '#27ECCE'
            
          ,
          stack: '信息',
          data: [220, 182, 191, 234, 290, 230]
        ,
        
          name: 'c',
          type: 'bar',
          itemStyle: 
            normal: 
              color: '#4DB3F5'
            
          ,
          stack: '信息',
          data: [150, 132, 201, 154, 90, 130]
        
      ]
    ;
  

  return (
      <React.Suspense fallback=<WidgetLoadingSpin/>>
        <ReactEchartsLazy
            option=getBarChart()
            notMerge=true
            lazyUpdate=true
            style=width: '100%', height: '100%'/>
      </React.Suspense>)



export default BarChartWidget

这里用到了React.lazy,所以还需要定制一下未加载时候渲染出来的组件

import Spin from "antd";
import React from "react";
import './dashboard.css'
function WidgetLoadingSpin()

  return (
      <div className='dashboard-widget-loading'><Spin tip='Loading...'/></div>
  

以上是关于react-grid-layout:可拖拽缩放网格布局插件的主要内容,如果未能解决你的问题,请参考以下文章

vue-draggable-resizable 可拖拽缩放的组件

5分钟实现一个可拖拽矩形

5分钟实现一个可拖拽矩形

H5拖拽 构造拖拽及缩放 pdf展示

android开发游记:ItemTouchHelper 使用RecyclerView打造可拖拽的GridView

基于react-grid-layout实现可视化拖拽