import { CheckOutlined , CloseOutlined , DashboardOutlined , DeleteOutlined , PlusOutlined , ReloadOutlined , RetweetOutlined } from '@ant-design/icons' ; import { Button , Empty , message , Spin , Tooltip } from 'antd' ; import classnames from 'classnames' ; import _ from 'lodash' ; import React , { forwardRef , ReactElement , useCallback , useEffect , useImperativeHandle , useMemo , useReducer , useRef , useState } from 'react' ; import { Responsive , WidthProvider } from 'react-grid-layout' ; import KeyEvent from 'react-keyevent' ; import { generateUuid , reducer } from '../utils' ; import Widget from '../widget' ; import WidgetSelector from '../widget/selector' ; import { getWidgetType } from '../widget/utils' ; import { Toolbar } from './components' ; import './index.less' ; import { fetch as fetchApi , update as updateApi } from './service' ; import './style/button.css' ; import './style/empty.css' ; import './style/input.css' ; import './style/message.css' ; import './style/modal.css' ; import './style/spin.css' ; import './style/tooltip.css' ; import { calcMinAndMax , copy , formatLayout } from './utils' ; const ResponsiveReactGridLayout = WidthProvider ( Responsive ) ; export type LayoutItem = { w : number ; h : number ; x : number ; y : number ; i : string ; minW : number ; maxW : number ; minH : number ; maxH : number ; } ; export type LayoutsIF = LayoutItem [ ] ; export interface widgetIF { name : string ; description : string ; tags : string [ ] ; component : ReactElement ; configComponent : ReactElement ; maxLength : number ; snapShot : ImageBitmapSource ; icon : ReactElement ; iconBackground : string ; size : { defaultWidth : number ; defaultHeight : number ; maxWidth : number ; maxHeight : number ; minWidth : number ; minHeight : number ; } ; [ key ] : any ; } interface widgetsIF { [ key ] : widgetIF ; } export interface Dashboard { widgets : widgetsIF ; editMode ? : boolean ; defaultLayout ? : LayoutsIF ; widgetWrapClassName ? : string ; widgetWrapStyle ? : React . CSSProperties ; layout ? : LayoutsIF ; minHeight ? : number ; maxWidgetLength ? : number ; toolbar ? : boolean ; storageKey ? : string ; onLayoutChange ? : ( layout : LayoutsIF ) => void ; onReset ? : ( dirtyCurrentLayout : LayoutsIF , currentLayout : LayoutItem ) => void ; onRemoveWidget ? : ( widget : widgetIF , dirtyCurrentLayout : LayoutsIF , currentLayout : LayoutsIF ) => void ; onAddWidget ? : ( widget : widgetIF , dirtyCurrentLayout : LayoutsIF , currentLayout : LayoutsIF ) => void ; onReload ? : ( currentLayout : LayoutsIF ) => void ; onCancelEdit ? : ( dirtyCurrentLayout : LayoutsIF , currentLayout : LayoutItem ) => void ; onEdit ? : ( currentLayout : LayoutsIF ) => void ; onSave ? : ( currentLayout : LayoutsIF ) => void ; onRevert ? : ( dirtyCurrentLayout : LayoutsIF , currentLayout : LayoutItem ) => void ; [ key ] : any ; } const Dashboard = forwardRef ( ( props , ref ) => { const { storageKey = 'default' , editMode = false , widgets , defaultLayout = [ ] , widgetWrapClassName , widgetWrapStyle , layout : customLayout = null , minHeight = 300 , maxWidgetLength = 20 , toolbar = true , onLayoutChange : _onLayoutChange , onReset , onReload , onRemoveWidget , onAddWidget , onCancelEdit , onEdit , onRevert , onSave , ... restProps } = props ; const [ stateEditMode , setStateEditMode ] = useState ( editMode ) ; const [ loading , setLoading ] = useState ( editMode ) ; const [ state , dispatch ] = useReducer ( reducer , { currentLayout : [ ] , dirtyCurrentLayout : [ ] , } ) ; const { currentLayout = [ ] , dirtyCurrentLayout = [ ] } = state ; const dom = useRef < any > ( null ) ; const fetch = useCallback ( _ . debounce ( async ( ) => { try { const response = await fetchApi ( { id : storageKey } ) ; let layout = _ . isArray ( defaultLayout ) ? defaultLayout : [ ] ; if ( response ) { const resArr = JSON . parse ( response ) . currentLayout ; if ( ! _ . isEmpty ( resArr ) ) { layout = resArr ; } } if ( layout ) { const data = calcMinAndMax ( layout , widgets ) ; dispatch ( { type : 'save' , payload : { currentLayout : data , dirtyCurrentLayout : data , } , } ) ; } setLoading ( false ) ; } catch ( error ) { } } , 200 ) , [ storageKey , widgets ] ) ; const reload = useCallback ( async ( ) => { setLoading ( true ) ; onReload && onReload ( formatLayout ( currentLayout ) ) ; if ( customLayout ) { return ; } fetch ( ) ; } , [ fetch , customLayout ] ) ; const update = useCallback ( async ( payload , callback = ( ) => { } ) => { const layout = payload [ 'layout' ] ; try { const response = await updateApi ( { id : storageKey , data : { currentLayout : layout , } , } ) ; if ( ! response ) { return ; } callback ( ) ; } catch ( error ) { } } , [ storageKey , widgets ] ) ; const onLayoutChange = useCallback ( _ . debounce ( ( layout , layouts ? , callback ? ) => { window . dispatchEvent ( new Event ( 'resize' ) ) ; if ( ! stateEditMode ) { return ; } dispatch ( { type : 'save' , payload : { dirtyCurrentLayout : layout , } , } ) ; _onLayoutChange && _onLayoutChange ( formatLayout ( layout ) ) ; callback && callback ( ) ; } , 300 ) , [ stateEditMode ] ) ; const addWidget = useCallback ( ( widget ) => { if ( dirtyCurrentLayout . length >= maxWidgetLength ) { message . warning ( `超过了最大限制数量${ maxWidgetLength }` + ',' + '不能再添加了' ) ; } const lastItem = dirtyCurrentLayout [ dirtyCurrentLayout . length - 1 ] ; const newLayout = [ ... dirtyCurrentLayout , { w : widget . size . defaultWidth , h : widget . size . defaultHeight , x : 0 , y : lastItem ? lastItem [ 'y' ] + lastItem [ 'h' ] : 0 , i : widget . name + '-' + generateUuid ( ) , minW : widget . size . minWidth , maxW : widget . size . maxWidth , minH : widget . size . minHeight , maxH : widget . size . maxHeight , } , ] ; onAddWidget && onAddWidget ( widget , formatLayout ( dirtyCurrentLayout ) , formatLayout ( newLayout ) ) ; onLayoutChange ( newLayout ) ; message . success ( '添加成功' ) ; } , [ dirtyCurrentLayout , onLayoutChange , maxWidgetLength ] ) ; const removeWidget = useCallback ( ( widgetKey ) => { let removedWidget ; let newLayout = _ . cloneDeep ( dirtyCurrentLayout ) ; newLayout . map ( ( item , index ) => { if ( item [ 'i' ] === widgetKey ) { removedWidget = item ; newLayout . splice ( index , 1 ) ; } } ) ; onRemoveWidget && onRemoveWidget ( removedWidget , formatLayout ( dirtyCurrentLayout ) , formatLayout ( newLayout ) ) ; dispatch ( { type : 'save' , payload : { dirtyCurrentLayout : newLayout , } , } ) ; } , [ dirtyCurrentLayout , onLayoutChange ] ) ; const reset = useCallback ( async ( ) => { onReset && onReset ( [ ] , formatLayout ( currentLayout ) ) ; onLayoutChange ( [ ] ) ; } , [ onLayoutChange ] ) ; const finLayout = useMemo ( ( ) => { return stateEditMode ? dirtyCurrentLayout : currentLayout ; } , [ stateEditMode , dirtyCurrentLayout , currentLayout ] ) ; const cancelEdit = useCallback ( ( ) => { setStateEditMode ( false ) ; onCancelEdit && onCancelEdit ( formatLayout ( dirtyCurrentLayout ) , formatLayout ( currentLayout ) ) ; dispatch ( { type : 'save' , payload : { dirtyCurrentLayout : currentLayout , } , } ) ; } , [ dirtyCurrentLayout , currentLayout ] ) ; const revert = useCallback ( ( ) => { onRevert && onRevert ( formatLayout ( dirtyCurrentLayout ) , formatLayout ( currentLayout ) ) ; dispatch ( { type : 'save' , payload : { dirtyCurrentLayout : currentLayout , } , } ) ; } , [ dirtyCurrentLayout , currentLayout ] ) ; const edit = ( ) => setStateEditMode ( true ) ; const save = useCallback ( ( ) => { dispatch ( { type : 'save' , payload : { currentLayout : dirtyCurrentLayout , widgets , } , } ) ; onSave && onSave ( formatLayout ( dirtyCurrentLayout ) ) ; setStateEditMode ( false ) ; if ( customLayout ) { return ; } update ( { layout : dirtyCurrentLayout , } ) ; } , [ dirtyCurrentLayout , update , customLayout ] ) ; useImperativeHandle ( ref , ( ) => ( { dom : dom . current , reset , removeWidget , addWidget , reload , cancelEdit , edit , revert , save , } ) ) ; const onCtrlShiftC = useCallback ( ( ) => { const res = formatLayout ( currentLayout ) ; copy ( JSON . stringify ( res ) ) ; message . success ( '已复制布局数据到剪切板' ) ; console . log ( 'currentLayout' , res ) ; } , [ currentLayout ] ) ; useEffect ( ( ) => { if ( customLayout ) { return ; } fetch ( ) ; } , [ customLayout ] ) ; useEffect ( ( ) => { setStateEditMode ( editMode ) ; } , [ editMode ] ) ; useEffect ( ( ) => { if ( ! _ . isArray ( customLayout ) ) { return ; } const data = calcMinAndMax ( customLayout , widgets ) ; dispatch ( { type : 'save' , payload : { customLayout : data , dirtyCurrentLayout : data , } , } ) ; } , [ customLayout , widgets ] ) ; return ( < Spin spinning = { loading } style = { { width : '100%' , } } > < KeyEvent style = { { width : '100%' , } } events = { { onCtrlShiftC , } } needFocusing > < div style = { { width : '100%' , } } ref = { dom } > < ResponsiveReactGridLayout className = "react-dashboard-layout" layouts = { { lg : finLayout } } rowHeight = { 30 } isDraggable = { stateEditMode } breakpoints = { { lg : 1200 , md : 800 , sm : 600 , xs : 400 , xxs : 300 } } cols = { { lg : 12 , md : 12 , sm : 2 , xs : 2 , xxs : 2 } } onLayoutChange = { onLayoutChange } isResizable = { stateEditMode } measureBeforeMount > { finLayout . map ( ( item ) => ( < div key = { item . i } className = { classnames ( 'react-dashboard-item' , stateEditMode ? 'react-dashboard-item-edit' : '' ) } > { widgets [ getWidgetType ( item . i , widgets ) ] ? ( < Widget { ... restProps } widgetKey = { item . i } widgetType = { getWidgetType ( item . i , widgets ) } height = { item . h * 40 - 10 } editMode = { stateEditMode } onDeleteWidget = { ( ) => removeWidget ( item . i ) } widgets = { widgets } widgetWrapClassName = { widgetWrapClassName } widgetWrapStyle = { widgetWrapStyle } / > ) : ( < div className = "react-dashboard-aligncenter react-dashboard-full" > < div style = { { textAlign : 'center' } } > < div > { '数据有误' } { item . i } < /div> { stateEditMode && ( < Button icon = { < DeleteOutlined />} size="small" style={{ margin: '10px 0' }} onClick={() => removeWidget(item.i)}> { '删除' } < /Button>)} < /div> < /div>)} < /div>))} < /ResponsiveReactGridLayout> { toolbar && finLayout . length > 0 ? ( ! stateEditMode && ( < div style = { { display : 'flex' , margin : '0 10px' , marginBottom : '10px' , } } > < Button size = "small" icon = { < ReloadOutlined / > } loading = { loading } style = { { marginRight : '10px' } } onClick = { ( ) => reload ( ) } /> < Button size = "small" type = "default" style = { { flex : 1 } } onClick = { edit } > < DashboardOutlined /> { '编辑仪表板' } < /Button> < /div>)) : (<> { ! stateEditMode ? ( < Spin spinning = { loading } > < div className = "react-dashboard-emptyContent" style = { { minHeight } } > { ! loading && ( < Empty description = { < span > { '当前仪表板没有小程序' } < /span>}> < Button size = "small" type = "primary" onClick = { edit } > < DashboardOutlined /> { '编辑仪表板' } < /Button> < /Empty>)} < /div> < /Spin>) : (<div className={classnames('react-dashboard-full', 'react-dashboard-aligncenter')} style={{ minHeight }}> < WidgetSelector widgets = { widgets } currentLayout = { finLayout } addWidget = { addWidget } > < > < Tooltip title = { '添加' } > < Button type = "dashed" shape = "circle" icon = { < PlusOutlined / > } size = "large" / > < /Tooltip> < /> < /WidgetSelector> < /div>)} < />)} { toolbar && stateEditMode && ( < Toolbar fixed = { false } extraRight = { < > < Button size = "small" onClick = { cancelEdit } icon = { < CloseOutlined / > } > { '取消' } < /Button> < Button size = "small" onClick = { revert } icon = { < RetweetOutlined / > } > { '恢复' } < /Button> { ! _ . isEmpty ( finLayout ) && ( < Button size = "small" danger onClick = { reset } icon = { < DeleteOutlined / > } > { '清空' } < /Button>)} < WidgetSelector widgets = { widgets } currentLayout = { finLayout } addWidget = { addWidget } / > < Button size = "small" onClick = { save } type = "primary" icon = { < CheckOutlined / > } > { '保存' } < /Button> < />}/ > ) } < /div> < /KeyEvent> < /Spin>); } ) ; export default Dashboard ;