BasicLayout.js 7.48 KB
Newer Older
jim's avatar
jim committed
1 2
import React from 'react';
import { Layout } from 'antd';
3
import DocumentTitle from 'react-document-title';
afc163's avatar
afc163 committed
4
import isEqual from 'lodash/isEqual';
陈帅's avatar
陈帅 committed
5
import memoizeOne from 'memoize-one';
6
import { connect } from 'dva';
afc163's avatar
afc163 committed
7 8
import { ContainerQuery } from 'react-container-query';
import classNames from 'classnames';
jim's avatar
jim committed
9
import pathToRegexp from 'path-to-regexp';
afc163's avatar
afc163 committed
10
import { enquireScreen, unenquireScreen } from 'enquire-js';
陈帅's avatar
陈帅 committed
11
import { formatMessage } from 'umi/locale';
12 13
import SiderMenu from '@/components/SiderMenu';
import Authorized from '@/utils/Authorized';
14
import SettingDrawer from '@/components/SettingDrawer';
15
import logo from '../assets/logo.svg';
jim's avatar
jim committed
16 17
import Footer from './Footer';
import Header from './Header';
18
import Context from './MenuContext';
陈帅's avatar
陈帅 committed
19
import Exception403 from '../pages/Exception/403';
20

jim's avatar
jim committed
21
const { Content } = Layout;
ddcat1115's avatar
ddcat1115 committed
22

afc163's avatar
afc163 committed
23 24
// Conversion router to menu.
function formatter(data, parentPath = '', parentAuthority, parentName) {
陈帅's avatar
陈帅 committed
25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
  return data
    .map(item => {
      let locale = 'menu';
      if (parentName && item.name) {
        locale = `${parentName}.${item.name}`;
      } else if (item.name) {
        locale = `menu.${item.name}`;
      } else if (parentName) {
        locale = parentName;
      }
      if (item.path) {
        const result = {
          ...item,
          locale,
          authority: item.authority || parentAuthority,
        };
        if (item.routes) {
          const children = formatter(
            item.routes,
            `${parentPath}${item.path}/`,
            item.authority,
            locale
          );
          // Reduce memory usage
          result.children = children;
        }
        delete result.routes;
        return result;
      }

      return null;
    })
    .filter(item => item);
afc163's avatar
afc163 committed
58 59
}

陈帅's avatar
陈帅 committed
60 61
const memoizeOneFormatter = memoizeOne(formatter, isEqual);

afc163's avatar
afc163 committed
62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
const query = {
  'screen-xs': {
    maxWidth: 575,
  },
  'screen-sm': {
    minWidth: 576,
    maxWidth: 767,
  },
  'screen-md': {
    minWidth: 768,
    maxWidth: 991,
  },
  'screen-lg': {
    minWidth: 992,
    maxWidth: 1199,
  },
  'screen-xl': {
    minWidth: 1200,
yoyo837's avatar
yoyo837 committed
80 81 82 83
    maxWidth: 1599,
  },
  'screen-xxl': {
    minWidth: 1600,
afc163's avatar
afc163 committed
84 85 86
  },
};

87
class BasicLayout extends React.PureComponent {
陈帅's avatar
陈帅 committed
88 89
  constructor(props) {
    super(props);
陈帅's avatar
陈帅 committed
90
    this.getPageTitle = memoizeOne(this.getPageTitle);
91 92
    this.getBreadcrumbNameMap = memoizeOne(this.getBreadcrumbNameMap, isEqual);
    this.breadcrumbNameMap = this.getBreadcrumbNameMap();
陈帅's avatar
陈帅 committed
93
    this.matchParamsPath = memoizeOne(this.matchParamsPath, isEqual);
陈帅's avatar
陈帅 committed
94
  }
95 96 97

  state = {
    rendering: true,
afc163's avatar
afc163 committed
98
    isMobile: false,
陈帅's avatar
陈帅 committed
99
    menuData: this.getMenuData(),
100 101 102
  };

  componentDidMount() {
afc163's avatar
afc163 committed
103 104 105 106 107 108 109
    const { dispatch } = this.props;
    dispatch({
      type: 'user/fetchCurrent',
    });
    dispatch({
      type: 'setting/getSetting',
    });
110 111 112 113 114
    this.renderRef = requestAnimationFrame(() => {
      this.setState({
        rendering: false,
      });
    });
afc163's avatar
afc163 committed
115 116 117 118 119 120 121 122
    this.enquireHandler = enquireScreen(mobile => {
      const { isMobile } = this.state;
      if (isMobile !== mobile) {
        this.setState({
          isMobile: mobile,
        });
      }
    });
123 124
  }

125 126 127
  componentDidUpdate(preProps) {
    // After changing to phone mode,
    // if collapsed is true, you need to click twice to display
128
    this.breadcrumbNameMap = this.getBreadcrumbNameMap();
129 130 131 132 133
    const { isMobile } = this.state;
    const { collapsed } = this.props;
    if (isMobile && !preProps.isMobile && !collapsed) {
      this.handleMenuCollapse(false);
    }
134 135 136 137
  }

  componentWillUnmount() {
    cancelAnimationFrame(this.renderRef);
afc163's avatar
afc163 committed
138
    unenquireScreen(this.enquireHandler);
139 140
  }

jim's avatar
jim committed
141
  getContext() {
陈帅's avatar
陈帅 committed
142
    const { location } = this.props;
ddcat1115's avatar
ddcat1115 committed
143 144
    return {
      location,
陈帅's avatar
陈帅 committed
145
      breadcrumbNameMap: this.breadcrumbNameMap,
ddcat1115's avatar
ddcat1115 committed
146
    };
147
  }
148

149 150 151 152
  getMenuData() {
    const {
      route: { routes },
    } = this.props;
陈帅's avatar
陈帅 committed
153
    return memoizeOneFormatter(routes);
154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174
  }

  /**
   * 获取面包屑映射
   * @param {Object} menuData 菜单配置
   */
  getBreadcrumbNameMap() {
    const routerMap = {};
    const mergeMenuAndRouter = data => {
      data.forEach(menuItem => {
        if (menuItem.children) {
          mergeMenuAndRouter(menuItem.children);
        }
        // Reduce memory usage
        routerMap[menuItem.path] = menuItem;
      });
    };
    mergeMenuAndRouter(this.getMenuData());
    return routerMap;
  }

陈帅's avatar
陈帅 committed
175 176 177 178 179 180 181
  matchParamsPath = pathname => {
    const pathKey = Object.keys(this.breadcrumbNameMap).find(key =>
      pathToRegexp(key).test(pathname)
    );
    return this.breadcrumbNameMap[pathKey];
  };

陈帅's avatar
陈帅 committed
182
  getPageTitle = pathname => {
陈帅's avatar
陈帅 committed
183 184
    const currRouterData = this.matchParamsPath(pathname);

陈帅's avatar
陈帅 committed
185 186
    if (!currRouterData) {
      return 'Ant Design Pro';
ddcat1115's avatar
ddcat1115 committed
187
    }
陈帅's avatar
陈帅 committed
188
    const message = formatMessage({
陈帅's avatar
陈帅 committed
189 190 191 192 193
      id: currRouterData.locale || currRouterData.name,
      defaultMessage: currRouterData.name,
    });
    return `${message} - Ant Design Pro`;
  };
陈帅's avatar
陈帅 committed
194

jim's avatar
jim committed
195
  getLayoutStyle = () => {
196
    const { isMobile } = this.state;
陈帅's avatar
陈帅 committed
197
    const { fixSiderbar, collapsed, layout } = this.props;
198
    if (fixSiderbar && layout !== 'topmenu' && !isMobile) {
jim's avatar
jim committed
199
      return {
200
        paddingLeft: collapsed ? '80px' : '256px',
jim's avatar
jim committed
201 202 203 204
      };
    }
    return null;
  };
陈帅's avatar
陈帅 committed
205

jim's avatar
jim committed
206 207 208 209 210 211 212
  getContentStyle = () => {
    const { fixedHeader } = this.props;
    return {
      margin: '24px 24px 0',
      paddingTop: fixedHeader ? 64 : 0,
    };
  };
陈帅's avatar
陈帅 committed
213

jim's avatar
jim committed
214
  handleMenuCollapse = collapsed => {
陈帅's avatar
陈帅 committed
215 216
    const { dispatch } = this.props;
    dispatch({
217 218 219
      type: 'global/changeLayoutCollapsed',
      payload: collapsed,
    });
jim's avatar
jim committed
220
  };
221

222
  renderSettingDrawer() {
kennylbj's avatar
kennylbj committed
223 224
    // Do not render SettingDrawer in production
    // unless it is deployed in preview.pro.ant.design as demo
225 226 227 228 229 230 231
    const { rendering } = this.state;
    if ((rendering || process.env.NODE_ENV === 'production') && APP_TYPE !== 'site') {
      return null;
    }
    return <SettingDrawer />;
  }

232
  render() {
陈帅's avatar
陈帅 committed
233
    const {
afc163's avatar
afc163 committed
234
      navTheme,
陈帅's avatar
陈帅 committed
235
      layout: PropsLayout,
愚道's avatar
愚道 committed
236
      children,
陈帅's avatar
陈帅 committed
237
      location: { pathname },
陈帅's avatar
陈帅 committed
238
    } = this.props;
陈帅's avatar
陈帅 committed
239
    const { isMobile, menuData } = this.state;
陈帅's avatar
陈帅 committed
240
    const isTop = PropsLayout === 'topmenu';
陈帅's avatar
陈帅 committed
241
    const routerConfig = this.matchParamsPath(pathname);
afc163's avatar
afc163 committed
242 243
    const layout = (
      <Layout>
jim's avatar
jim committed
244
        {isTop && !isMobile ? null : (
jim's avatar
jim committed
245 246 247
          <SiderMenu
            logo={logo}
            Authorized={Authorized}
afc163's avatar
afc163 committed
248
            theme={navTheme}
jim's avatar
jim committed
249
            onCollapse={this.handleMenuCollapse}
250
            menuData={menuData}
251
            isMobile={isMobile}
jim's avatar
jim committed
252
            {...this.props}
jim's avatar
jim committed
253 254
          />
        )}
陈帅's avatar
陈帅 committed
255 256 257 258 259 260
        <Layout
          style={{
            ...this.getLayoutStyle(),
            minHeight: '100vh',
          }}
        >
261 262 263 264
          <Header
            menuData={menuData}
            handleMenuCollapse={this.handleMenuCollapse}
            logo={logo}
265
            isMobile={isMobile}
266 267
            {...this.props}
          />
陈帅's avatar
陈帅 committed
268 269 270 271 272
          <Content style={this.getContentStyle()}>
            <Authorized authority={routerConfig.authority} noMatch={<Exception403 />}>
              {children}
            </Authorized>
          </Content>
jim's avatar
jim committed
273
          <Footer />
274
        </Layout>
afc163's avatar
afc163 committed
275 276 277
      </Layout>
    );
    return (
陈帅's avatar
陈帅 committed
278 279 280 281 282 283 284 285 286 287
      <React.Fragment>
        <DocumentTitle title={this.getPageTitle(pathname)}>
          <ContainerQuery query={query}>
            {params => (
              <Context.Provider value={this.getContext()}>
                <div className={classNames(params)}>{layout}</div>
              </Context.Provider>
            )}
          </ContainerQuery>
        </DocumentTitle>
288
        {this.renderSettingDrawer()}
陈帅's avatar
陈帅 committed
289
      </React.Fragment>
290 291 292 293
    );
  }
}

jim's avatar
jim committed
294
export default connect(({ global, setting }) => ({
Andreas Cederström's avatar
Andreas Cederström committed
295
  collapsed: global.collapsed,
jim's avatar
jim committed
296 297
  layout: setting.layout,
  ...setting,
陈帅's avatar
陈帅 committed
298
}))(BasicLayout);