使用react-grid-layout和react-full-screen实现一个可自定义和全屏展示的dashboard页面

news2024/12/25 12:46:43

文章目录

    • 使用react-grid-layout和react-full-screen实现一个可自定义和全屏展示的dashboard页面
      • 具体实现代码展示
        • 主展示页面
        • 懒加载组件
        • 组件加载时展示的组件
        • dashboard菜单组件
        • 具体的图表组件
      • Demo演示

使用react-grid-layout和react-full-screen实现一个可自定义和全屏展示的dashboard页面

之前的博文中已经讲了如何使用react-grid-layoutecharts-for-react实现一个支持拖拽的自定义响应式dashboard页面, 还有使用react-sizeme解决了侧栏(抽屉)展开或隐藏时不会自适应容器大小的问题

参考:

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

《使用react-sizeme解决react-grid-layout中侧栏(抽屉)展开或隐藏时不会自适应容器大小的问题》

接下来我们需要做的功能如下:

  1. 我们可以全局锁定和解锁dashboard中的图表组件,让其不允许拖动和缩放。
  2. 懒加载对应的图表组件显示到dashboard中
  3. 实时保存当前界面的布局到localstorage,以便下次进入页面可以展示上一次编辑的dashboard布局
  4. 实现dashboard全屏展示,这里会用到react-full-screen

具体实现代码展示

主展示页面

在这里插入图片描述

import React, {useLayoutEffect, useState} from "react";
import 'react-grid-layout/css/styles.css'
import 'react-resizable/css/styles.css'
import {findIndex} from "lodash";
import './dashboard.css'
import {CloseOutlined, LockOutlined, QuestionCircleOutlined, UnlockOutlined} from "@ant-design/icons";
import ReactGridLayout from "react-grid-layout";
import {isEmpty} from "lodash-es";
import {withSize} from 'react-sizeme';
import LazyWidget from "@/pages/Dashboard/Detail/Widget/LazyWidget";
import DashboardMenuButton from "@/pages/Dashboard/Detail/DashboardMenuButton";
import {FullScreen, useFullScreenHandle} from "react-full-screen";

interface DashboardWidgetInfo {
  widgetName: string,
  layout: ReactGridLayout.Layout
}


function DashboardGird({size: {width}}: any) {
  const [widgets, setWidgets] = useState<DashboardWidgetInfo[]>([]);
  const handle = useFullScreenHandle();

  useLayoutEffect(() => {
    const layoutJson = localStorage.getItem("dashboard_layout");
    if (isEmpty(layoutJson)) {
      return;
    }
    setWidgets(JSON.parse(layoutJson as string));
  }, []);
  const getLayouts: any = () => {
    return widgets.map(item => {
      return {
        ...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) => {
    return (<LazyWidget widgetName={widgetName}/>)
  }

  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) % 12;
    const widgetName = x % 2 == 0 ? 'BarChartWidget' : 'PieChartWidget';
    const index = findIndex(widgets, (w) => w.widgetName === widgetName);
    if (index !== -1) {
      return;
    }
    const newWidgets = [...widgets, {
      widgetName: widgetName,
      layout: {i: widgetName, x: x, y: Infinity, w: 3, h: 2, static: false}
    }] as DashboardWidgetInfo[];
    setWidgets(newWidgets);
  }

  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);
    localStorage.setItem("dashboard_layout", JSON.stringify(widgets));
  }


  const lockAllWidgets = () => {
    setWidgets(widgets.map(w => {
      return {
        ...w,
        layout: {
          ...w.layout,
          static: true
        }
      }
    }) as DashboardWidgetInfo[]);
  }

  const unlockAllWidgets = () => {
    setWidgets(widgets.map(w => {
      return {
        ...w,
        layout: {
          ...w.layout,
          static: false
        }
      }
    }) as DashboardWidgetInfo[]);
  }

  const enterFullScreen = () => {
    handle.enter();
  }


  const menuConfig = {
    addWidget: onAddWidget,
    lockAllWidgets: lockAllWidgets,
    unlockAllWidgets: unlockAllWidgets,
    enterFullScreen: enterFullScreen
  }

  return (
      <FullScreen handle={handle}>
        <div>
          <DashboardMenuButton {...menuConfig}/>
          <ReactGridLayout
              cols={12}
              rowHeight={100}
              width={width}
              autoSize={true}
              isDraggable={true}
              isResizable={true}
              isBounded={true}
              layout={getLayouts()}
              className={'layouts'}
              onLayoutChange={onLayoutChange}>
            {widgets?.map(item => createWidget(item))}
          </ReactGridLayout>
        </div>
      </FullScreen>
  );
}

export default withSize({refreshMode: 'debounce', refreshRate: 60})(DashboardGird);

懒加载组件

import React, {useMemo} from "react";
import WidgetLoadingSpin from "@/pages/Dashboard/Detail/WidgetLoadingSpin";

function LazyWidget({widgetName}: any) {
  const LazyComponent = React.lazy(() => import('@/pages/Dashboard/Detail/Widget/' + widgetName));
  return useMemo(() => (<React.Suspense fallback={<WidgetLoadingSpin/>}>
    <LazyComponent/>
  </React.Suspense>), [widgetName]);
}


export default LazyWidget

组件加载时展示的组件

在这里插入图片描述

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;

dashboard菜单组件

在这里插入图片描述

import React, {useState} from "react";
import {Button, Dropdown, MenuProps} from "antd";
import {
  AppstoreAddOutlined,
  AppstoreOutlined,
  FullscreenOutlined,
  LockOutlined,
  RedoOutlined,
  UnlockOutlined
} from "@ant-design/icons";
import Draggable, {DraggableBounds, DraggableData, DraggableEvent} from 'react-draggable'
import './dashboard.css'
interface DashboardMenuProps {
  addWidget: () => void,
  lockAllWidgets: () => void,
  unlockAllWidgets: () => void,
  enterFullScreen: () => void
}

const DashboardMenuButton = (props: DashboardMenuProps) => {
  const [bound, setBound] = useState<DraggableBounds>({left: 0, top: 0, bottom: 0, right: 0})
  const onStart = (event: DraggableEvent, draggableData: DraggableData) => {
    const {clientWidth, clientHeight} = window?.document?.documentElement;
    const targetRect = document.getElementById("draggable-dashboard-menu-button")?.getBoundingClientRect();
    const rightSpaceWidth = 5;
    if (targetRect) {
      setBound({
        left: -targetRect?.left + draggableData?.x,
        right: clientWidth - (targetRect?.right - draggableData?.x) - rightSpaceWidth,
        top: -targetRect?.top + draggableData?.y,
        bottom: clientHeight - (targetRect?.bottom - draggableData?.y)
      })
    }
  };


  const items: MenuProps['items'] = [
    {
      key: 'addWidget',
      label: (
          <AppstoreAddOutlined onClick={props.addWidget}/>
      ),
    },
    {
      key: 'lockAll',
      label: (
          <LockOutlined onClick={props.lockAllWidgets}/>
      ),
    },
    {
      key: 'unlockAll',
      label: (
          <UnlockOutlined onClick={props.unlockAllWidgets}/>
      ),
    },
    {
      key: 'refreshAll',
      label: (
          <RedoOutlined/>
      ),
    },
    {
      key: 'fullScreen',
      label: (
          <FullscreenOutlined onClick={props.enterFullScreen}/>
      ),
    }
  ];

  return (<Draggable bounds={bound} handle={'.draggable-dashboard-button'}
                     onStart={(event, uiData) => onStart(event, uiData)}>
    <div className={'dashboard-menu-button'}
         id={'draggable-dashboard-menu-button'}>
      <Dropdown menu={{items}} placement="topRight" arrow trigger={['click']} overlayClassName={'dashboard-menu-button-dropdown'}>
        <Button type="primary" icon={<AppstoreOutlined/>} size={'large'} className={'draggable-dashboard-button'}/>
      </Dropdown>
    </div>
  </Draggable>)


}

export default DashboardMenuButton;

具体的图表组件

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

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

到这里我们就完成了使用react-grid-layoutreact-full-screen实现一个可自定义和全屏展示的dashboard页面啦~

Demo演示

请添加图片描述

请添加图片描述

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

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

相关文章

线段树模板(Java)

线段树一、线段树概念二、线段树模板1.建树2. 单点修改3.区间查询4.完整代码及测试一、线段树概念 线段树是一种二叉搜索树&#xff0c;与区间树相似&#xff0c;它将一个区间划分成一些单元区间&#xff0c;每个单元区间对应线段树中的一个叶结点。它的主要优势是对于区间求和…

PacBio HiFi 测序动植物基因组项目真实案例测评

HiFi Reads全称High fidelity reads, 是PacBio公司基于Sequel II平台产出的兼具长读长和高准确度的测序序列&#xff0c;该测序模式&#xff08;CCS测序模式&#xff09;一经问世&#xff0c;备受广大组学科研用户关注——其超长读长完美规避了二代测序short reads的天生不足&a…

【密码加密原则三】

目录 1 密码加密原则&#xff08;续&#xff09; 1.1 盐值的优化 1.2 Mybatis中的占位符 1 密码加密原则&#xff08;续&#xff09; 1.1 盐值的优化 为了进一步保障密码安全&#xff0c;可以考虑使用随机的盐值&#xff0c;但是&#xff0c;需要注意&#xff0c;随机的盐…

Java高效率复习-MySQL下篇[MySQL]

前言 本文章的语言描述会比上篇多一些 数据库的创建修改与删除 标识符命名规则 数据库名、表名不得超过30个字符&#xff0c;变量限制为29个必须只能包含A-Z&#xff0c;a-z&#xff0c;0-9&#xff0c;_等63个字符数据库名、表名、字段名等对象名中间不要包含空格同一个My…

生产环境 Nginx后端服务大量TIME-WAIT的解决

netstat -n | awk /^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]} ss -s netstat -nat |awk {print $6}|sort|uniq -c|sort -rn 统计TIME_WAIT 连接的本地地址 netstat -an | grep TIME_WAIT | awk {print $4} | sort | uniq -c | sort -n -k1 尝试抓取 tcp 包 tcpd…

工业CT之三维重建技术

目前&#xff0c;国内现有的工业CT设备绝大多数是基于线阵探测器的断层扫描技术。 该技术主要是通过观察二维图像去发现单层断面上的损伤部位&#xff0c;至于能准确地确定损伤部位的空间位置、大小、几何形状等&#xff0c;仅通过观察二维切片图像是很难实现的。 这个时候就需…

Flink系列之Flink中Checkpoint容错机制

title: Flink系列 三、Flink Checkpoint 容错机制原理概述 ​ Flink 提供了 Exactly once 特性&#xff0c;是依赖于带有 barrier 的分布式快照 可部分重发的数据源功能实现的。而分布式快照中&#xff0c;就保存了 operator 的状态信息。 ​ Flink 的失败恢复依赖于 检查点…

Zabbix技术分享——如何使用zabbix监控华为云RDS

在数字化大背景下&#xff0c;数据是重要的生产资料&#xff0c;这些数据存放在哪里&#xff0c;如何保障数据安全是所有企业都要考虑的事情。华为云RDS凭借安全可靠&#xff0c;可根据业务规模动态扩容的特性&#xff0c;受到越来越多中小企业的青睐&#xff0c;对华为云RDS监…

NR PUSCH power control(一)

这篇看下NR PUSCH power control的相关内容&#xff0c;主要内容集中在38.213 7.1章节&#xff0c;功率计算无非就是一个长公式&#xff0c;根据RRC配置的参数及后续DCI field 的内容作出功率的调整&#xff1b;最初这部分看的就云里雾里的&#xff0c;最近再看&#xff0c;相比…

upload-labs通关

upload-labs通关 shell &#x1f349; 目录upload-labs通关PASS-01、PASS-02PASS-03PASS-04PASS-05PASS-06PASS-07PASS-08PASS-09PASS-10PASS-11PASS-12PASS-13PASS-14PASS-15PASS-16PASS-17PASS-18PASS-19PASS-20PASS-21shell能上传并能解析就算成功 PASS-01、PASS-02 图片…

最近要考pmp,哪个培训机构比较好?

你说的几个都是我着重了解过的&#xff0c;作为过来人&#xff0c;把我做的各大机构的优缺点给你参考吧~ PMP 机构排名的话&#xff0c;没有官方数据&#xff0c;网上数据仅供参考。这篇机构对比的文章&#xff0c;主流机构都有&#xff0c;你可以看看 下面说下我收集的每个机…

【数据库数据恢复】无法启动MongoDB服务的数据恢复案例

关于MongoDB数据库&#xff1a; MongoDB数据库存储方式是将文档存储在集合之中&#xff0c;而不是像Oracle、MySQL一样的关系型数据库。 MongoDB数据库是开源数据库&#xff0c;也提供具有附加功能的商业版本。 MongoDB中的数据是以键值对(key-value pairs)的形式显示的&…

[附源码]Python计算机毕业设计Django校友社交系统

项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等等。 环境需要 1.运行环境&#xff1a;最好是python3.7.7&#xff0c;…

“云办公”如何用任务协同工具搞定项目和团队管理?

导语&#xff1a;远程参加会议、团队协同作业、项目负责人进行任务分配、团队成员多人协同编辑文件及同时推进项目、人力部门在线进行审批报销……&#xff0c;随着“云办公”的加速普及&#xff0c;人们只需一台电脑、一部手机、一根网线&#xff0c;就能随时进入办公状态&…

云服务器及域名到期后,公安联网注销指南

云服务器及域名到期后&#xff0c;公安联网注销指南 公安联网备案及注销的操作流程都写在了官方文档中&#xff0c;可以进入全国互联网安全管理服务平台&#xff0c;在下载中心找到并下载 《互联网站安全服务平台操作指南》&#xff0c;按照操作指南进行备案及撤销。 以下图…

JS实现关闭图片窗口

JS实现关闭图片窗口 有趣的小案例池子&#xff1a; JS实现定时器 JS实现关闭图片窗口 JS实现输入检验 获取焦点后隐藏提示内容的输入框 JS实现获取鼠标在画布中的位置 聊天信息框显示消息 JS点击切换背景图 自动切换背景的登录页面 JS制作跟随鼠标移动的图片 JS实现记住用…

K8S Pod控制器详细讲解

文章目录一、Pod控制器介绍二、ReplicaSet(RS)三、Deployment(Deploy)1.镜像更新&#xff1a;2.版本回退3.金丝雀发布/灰度发布四、Horizontal Pod Autoscaler(HPA)五、DaemonSet(DS)六、Job七、CronJob(CJ)结尾一、Pod控制器介绍 Pod是kubernetes的最小管理单元&#xff0c;在…

ArcGIS矢量化并进行拓扑检查

土地利用数据每年都在发生变化&#xff0c;故每年都要根据去年的数据进行修改。请根据以下要求&#xff0c;修改A区域的数据并对B区域已做好的数据进行拓扑检查。 01 数据说明 1. 地类图斑A.shp&#xff1a;A区域需要编辑修改的图斑数据。 2. 影像.tif&#xff1a;编辑A区域…

Docker数据卷自定义Docker镜像

目录 宿主机与容器之间的文件拷贝 引言&#xff1a;利用MySQL镜像安装MySQL服务 从容器中拷贝文件到宿主机 从宿主机拷贝文件到容器 数据卷 数据卷容器 Dockerfile自定义镜像 自定义tomcat8&#xff08;熟悉几乎所有的Dockerfile命令&#xff09; 宿主机与容器之间的文…

集群配置步骤_java培训

配置步骤 复制3个ZooKeeper zookeeper-3.4.9.tar.gz解压后拷贝到/myzookeeper目录下并重新名为zk01&#xff0c;再复制zk01形成zk02、zk03&#xff0c;共计3份 新增目录 进入zk01/02/03分别新建文件夹&#xff0c;mydata、mylog 新建配置文件 分别进入zk01-zk03各自的conf文件…