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

news2024/12/31 5:47:10

使用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>
  )
}


export default WidgetLoadingSpin;

最后是一些简单的CSS样式

.dashboard-widget-loading {
  display: flex;
  justify-content: center;
  align-items: center;
  width: 100%;
  height: 100%
}

.dashboard-widget-wrapper {
  background: white;
}

.dashboard-widget-wrapper:hover {
  box-shadow: 5px 5px 10px rgba(0, 0, 0, 0.9)
}

.dashboard-widget-header {
  display: none;
}

.dashboard-widget-header-icon {
  margin: 4px;
  opacity: 0.7;
}

.dashboard-widget-header-icon:hover {
  color: #00508E;
}


.dashboard-widget-wrapper:hover .dashboard-widget-header {
  position: absolute;
  right: 7px;
  top: 2px;
  cursor: pointer;
  z-index: 999;
  display: block;
}

参考

https://github.com/react-grid-layout/react-grid-layout

https://github.com/Bilif/react-drag-grid

react-grid-layout实现拖拽,网格布局

React-grid-layout 一个支持拖拽的栅格布局库

echarts-for-react

在 React 中使用 echarts-for-react / react ,如,柱状图,折线图,饼图

react-grid-layout核心功能实现

React的动态加载(lazy import)

深入理解React:懒加载(lazy)实现原理

react-grid-layout 使用说明

React-Grid-Layout

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

React 实现炫酷的可拖拽网格布局

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/43188.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

游戏源码编程软件,对于新手来说十分友好,纯中文的界面让所有功能都一目了然,操作相当简单

这是一款免费的图像化编程工具,使用者无需会任何编程语言即可通过拼接积木的形式搭建出属于自己的程序。 编程猫kitten使用教程: 1、打开软件,进入软件主界面,运行界面如下图。 2、点击文件,可选择打开、新建、打开本地作品等。 3、可在方框内输入作品名称,快速进行输入…

安静!听听AI眼中岛国老师的声音~

大家好&#xff0c;我是鸟哥。一个半路出家的程序员。 最近在折腾自己的微信机器人&#xff0c;除了自动回复&#xff0c;自动拉群等常规的功能外&#xff0c;我准备给它赋予一些AI功能&#xff0c;毕竟这两年人工智能火的一塌糊涂。例如前段时间风靡朋友圈的人物头像动漫化&am…

如何保存/同步多架构容器 Docker 镜像

前言 随着容器、芯片技术的进一步发展&#xff0c;以及绿色、节能、信创等方面的要求&#xff0c;多 CPU 架构的场景越来越常见。典型的应用场景包括&#xff1a; 信创&#xff1a;x86 服务器 鲲鹏 ARM 等信创服务器&#xff1b;个人电脑&#xff1a;苹果 Mac M1 Windows 电…

Triton Inference Server 环境配置

本人环境 Ubuntu18.04&#xff0c;3090显卡&#xff0c;显卡驱动版本510.85.02&#xff0c;cuda版本11.6&#xff0c;docker版本20.10.12(注意&#xff1a;docker一定要通过apt安装&#xff0c;用snap安装会报错) 安装步骤 1. 根据驱动版本和cuda版本下载对应版本的Triton D…

java计算机毕业设计ssm驾校预约考试管理系统a3cf7(附源码、数据库)

java计算机毕业设计ssm驾校预约考试管理系统a3cf7&#xff08;附源码、数据库&#xff09; 项目运行 环境配置&#xff1a; Jdk1.8 Tomcat8.5 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#…

第四章. Pandas进阶—日期数据处理

第四章. Pandas进阶 4.7 日期数据处理 1.DataFrame的日期数据转换&#xff08;to_datetime&#xff09; 在日常工作中&#xff0c;常见的日期方式有很多种&#xff0c;例如’20221123’&#xff0c;‘2022.11.23’&#xff0c;‘2022/11/23’&#xff0c;‘23/11/2022’&#…

Deepwalk详解

算法思想 源于word2vec &#xff0c;word2vec通过语料库中的句子序列来描述词与词之间的共现关系。进而学习到词语的向量表示&#xff0c;deepwalk则使用图中的节点与节点的共像现关系来学习节点的向量表示。这种借鉴的前提是点在图中的分布和词在句子中的分布都是幂律分布。 …

关于MuLoginWebGL介绍说明:

WebGL就是俗称的硬件显卡型号的意思&#xff0c;在MuLogin中我们提供了多个平台和品牌的显卡芯片指纹。 我们在做实验时&#xff0c;Chrome浏览器和Internet Explorer&#xff08;Edge&#xff09;测试取WebGL vendor 会有两种不同值 &#xff0c;Chrome 取为 Google Inc. Int…

2023年湖北监理工程师考试科目有哪些?考试题型什么样子的?

2023年湖北监理工程师考试科目有哪些&#xff1f;考试题型什么样子的&#xff1f; 一、监理工程师考试科目&#xff1a; 监理工程师考试一共考四科 1. 《建设工程监理基本理论和相关法规》(客观题) 2. 《建设工程合同管理》(客观题) 3. 《建设工程目标控制》(客观题) 4. 《…

社交电商时代,切勿剑走偏锋,始终以产品为中心,模式为辅助

社交电商这个名词近期十分火&#xff0c;参与这个方式的人数以亿计&#xff0c;可以这样说“十亿人民九亿商&#xff0c;八亿人在做电商”。 我们感悟&#xff1a;“传统电商火热&#xff0c;社交电商更火”&#xff01;那么什么是社交电商呢&#xff1f;社交电商概念&#xff…

必须了解的海外新闻稿写作要点 ️

随着经济全球化的发展&#xff0c;中国企业走向世界是必然的趋势。媒介易小编发现了全球的海外消费者一般了解一个品牌都是去搜索引擎搜索&#xff0c;所以确认海外媒体投放新闻稿是中国企业走向世界必经之路&#xff0c;是密不可分的哟。 新闻稿是公司或机构向媒体发送的手稿。…

Python学习基础笔记十四——函数参数

函数参数这块在前面的博客中没有展开&#xff0c;现在专门整理出来&#xff1a; 1、参数的数量&#xff1a; 1&#xff09;没有参数&#xff1a;就是定义函数和调用函数的括号中都不写内容。 2&#xff09;有一个参数&#xff1a;可以是任何数据类型。 3&#xff09;有多个参…

学习python中的数据结构

数据结构 链表和数组 数组 Python的list是由数组来实现的 有序的元素序列, 在内存中表现为一块连续的内存区域; 链表 通过指针将无序的列表链接起来, 每个节点都存储着当前节点的值和下一个节点的内存地址 链表和数组有什么区别? 实现有序的方式是不一样的, 数组是连续的内…

[附源码]SSM计算机毕业设计网上鞋店管理系统JAVA

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

Pan-cancer image-based detection of clinically actionable genetic alternations

目录 摘要 前言 结果 深度学习模型优化 从组织病理图像预测泛癌基因突变 摘要 肿瘤的分子突变可导致肿瘤细胞及其微环境的表型变化。常规组织病理切片可以反映出这种形态学改变。本研究表明深度学习方法能直接从常规病理图像中推断出广泛的基因突变、肿瘤分子亚型、基因表达…

中纺集团×StarRocks:构建企业级数据资产管理的实践

中纺集团经过“十三五”信息化建设&#xff0c;生成了大量宝贵的数据资源&#xff0c;但也存在信息孤岛、系统壁垒、数据质量等问题。中纺结合企业实际需求&#xff0c;按照集团“十四五”信息化规划中有关数据中台的建设计划与目标要求&#xff0c;历经半年多的测试比选&#…

使用扩展有效对齐 SwiftUI 内容,创建自定义 SwiftUI 方法以快速对齐项目并使您的代码看起来简洁明了(教程含源码)

在开发 iOS 应用程序时,对齐内容可能是一个耗时的过程。如果应用程序有多个屏幕,则需要在不同的地方完成这件事,并可能导致看起来杂乱无章的视图。 作为一个始终致力于让我的代码看起来简单和流线型的人,实现目标所需的大量Spacer()元素常常让我恼火,这就是为什么当我发…

配置Nginx和其他应用的HTTPS访问

使用tomcat或者weblogic部署的应用默认都是http访问的&#xff0c;如果通过https访问&#xff0c;需要ssl证书。tomcat或者weblogic可以配置&#xff1b; 同时&#xff0c;另一种方法&#xff0c;https网站中&#xff0c;如果接口服务是http的&#xff0c;那么请求接口就会被拒…

[美国访问学者J1]签证的材料准备

对于美国访问学者J1签证材料的准备&#xff0c;在这里知识人网老师和大家分享一下&#xff1a; 1. 有效护照&#xff1a;如果您的护照将在距您预计抵美日期的六个月内过期、或已损坏、或护照上已无空白的签证签发页, 请在前来面谈之前先申请一本新护照。 2. DS-160表格确认页。…

vulnhub靶机darkhole

靶机下载地址&#xff1a;DarkHole: 1 ~ VulnHub Kali ip:192.168.174.128 靶机ip&#xff1a;192.168.174.135 靶机ip发现 sudo arp-scan -l 开放端口扫描 nmap -p- -sV -A 192.168.174.135 发现开启了22端口和80端口 目录扫描 gobuster dir -u http://192.168.174.135…