文章目录
- 使用react-grid-layout和react-full-screen实现一个可自定义和全屏展示的dashboard页面
- 具体实现代码展示
- 主展示页面
- 懒加载组件
- 组件加载时展示的组件
- dashboard菜单组件
- 具体的图表组件
- Demo演示
使用react-grid-layout和react-full-screen实现一个可自定义和全屏展示的dashboard页面
之前的博文中已经讲了如何使用react-grid-layout
和echarts-for-react
实现一个支持拖拽的自定义响应式dashboard页面, 还有使用react-sizeme
解决了侧栏(抽屉)展开或隐藏时不会自适应容器大小的问题
参考:
《使用react-grid-layout和echarts-for-react实现一个支持拖拽的自定义响应式dashboard页面》
《使用react-sizeme解决react-grid-layout中侧栏(抽屉)展开或隐藏时不会自适应容器大小的问题》
接下来我们需要做的功能如下:
- 我们可以全局锁定和解锁dashboard中的图表组件,让其不允许拖动和缩放。
- 懒加载对应的图表组件显示到dashboard中
- 实时保存当前界面的布局到localstorage,以便下次进入页面可以展示上一次编辑的dashboard布局
- 实现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-layout
和react-full-screen
实现一个可自定义和全屏展示的dashboard页面啦~