Просмотр исходного кода

Mobile menu (#463)

* Increase the sliding menu

* Add a simple animation

* update mobile menu

* update

* update

* update

* rebase master

* recovery import/first
jiang 8 лет назад
Родитель
Сommit
e221476b34

+ 2 - 0
package.json

@@ -25,6 +25,7 @@
     "classnames": "^2.2.5",
     "core-js": "^2.5.1",
     "dva": "^2.1.0",
+    "enquire-js": "^0.1.1",
     "g-cloud": "^1.0.2-beta",
     "g2": "^2.3.13",
     "g2-plugin-slider": "^1.2.1",
@@ -35,6 +36,7 @@
     "numeral": "^2.0.6",
     "prop-types": "^15.5.10",
     "qs": "^6.5.0",
+    "rc-drawer-menu": "^0.5.0",
     "react": "^16.0.0",
     "react-container-query": "^0.9.1",
     "react-document-title": "^2.0.3",

+ 13 - 3
src/components/GlobalHeader/index.js

@@ -1,10 +1,12 @@
 import React, { PureComponent } from 'react';
-import { Layout, Menu, Icon, Spin, Tag, Dropdown, Avatar, message } from 'antd';
+import { Layout, Menu, Icon, Spin, Tag, Dropdown, Avatar, message, Divider } from 'antd';
 import moment from 'moment';
 import groupBy from 'lodash/groupBy';
 import Debounce from 'lodash-decorators/debounce';
+import { Link } from 'dva/router';
 import NoticeIcon from '../../components/NoticeIcon';
 import HeaderSearch from '../../components/HeaderSearch';
+import logo from '../../assets/logo.svg';
 import styles from './index.less';
 
 const { Header } = Layout;
@@ -82,7 +84,7 @@ export default class GlobalHeader extends PureComponent {
   }
   render() {
     const {
-      currentUser, collapsed, fetchingNotices,
+      currentUser, collapsed, fetchingNotices, isMobile,
     } = this.props;
     const menu = (
       <Menu className={styles.menu} selectedKeys={[]} onClick={this.handleMenuClick}>
@@ -95,6 +97,14 @@ export default class GlobalHeader extends PureComponent {
     const noticeData = this.getNoticeData();
     return (
       <Header className={styles.header}>
+        {isMobile && (
+          [(
+            <Link to="/" className={styles.logo} key="logo">
+              <img src={logo} alt="logo" width="32" />
+            </Link>),
+            <Divider type="vertical" key="line" />,
+          ]
+        )}
         <Icon
           className={styles.trigger}
           type={collapsed ? 'menu-unfold' : 'menu-fold'}
@@ -146,7 +156,7 @@ export default class GlobalHeader extends PureComponent {
             <Dropdown overlay={menu}>
               <span className={`${styles.action} ${styles.account}`}>
                 <Avatar size="small" className={styles.avatar} src={currentUser.avatar} />
-                {currentUser.name}
+                <span className={styles.name}>{currentUser.name}</span>
               </span>
             </Dropdown>
           ) : <Spin size="small" style={{ marginLeft: 8 }} />}

+ 44 - 7
src/components/GlobalHeader/index.less

@@ -13,6 +13,20 @@
   }
 }
 
+.logo {
+  height: 64px;
+  line-height: 58px;
+  vertical-align: top;
+  display: inline-block;
+  padding: 0 0 0 24px;
+  cursor: pointer;
+  font-size: 20px;
+  img {
+    display: inline-block;
+    vertical-align: middle;
+  }
+}
+
 .menu {
   :global(.anticon) {
     margin-right: 8px;
@@ -26,19 +40,13 @@ i.trigger {
   font-size: 20px;
   line-height: 64px;
   cursor: pointer;
-  transition: all .3s;
+  transition: all .3s, padding 0s;
   padding: 0 24px;
   &:hover {
     background: @primary-1;
   }
 }
 
-@media screen and (max-width: @screen-xs) {
-  .trigger {
-    display: none;
-  }
-}
-
 .right {
   float: right;
   height: 100%;
@@ -73,3 +81,32 @@ i.trigger {
     }
   }
 }
+
+@media only screen and (max-width: @screen-md) {
+  .header {
+    :global(.ant-divider-vertical) {
+      vertical-align: unset;
+    }
+    .name {
+      display: none;
+    }
+    i.trigger {
+      padding: 0 12px;
+    }
+    .logo {
+      padding-right: 12px;
+      position: relative;
+    }
+    .right {
+      position: absolute;
+      right: 12px;
+      top: 0;
+      background: #fff;
+      .account {
+        .avatar {
+          margin-right: 0;
+        }
+      }
+    }
+  }
+}

+ 162 - 0
src/components/SiderMenu/SiderMenu.js

@@ -0,0 +1,162 @@
+import React, { PureComponent } from 'react';
+import { Layout, Menu, Icon } from 'antd';
+import { Link } from 'dva/router';
+import logo from '../../assets/logo.svg';
+import styles from './index.less';
+import { getMenuData } from '../../common/menu';
+
+const { Sider } = Layout;
+const { SubMenu } = Menu;
+
+export default class SiderMenu extends PureComponent {
+  constructor(props) {
+    super(props);
+    this.menus = getMenuData();
+    this.state = {
+      openKeys: this.getDefaultCollapsedSubMenus(props),
+    };
+  }
+  getDefaultCollapsedSubMenus(props) {
+    const { location: { pathname } } = props || this.props;
+    const snippets = pathname.split('/').slice(1, -1);
+    const currentPathSnippets = snippets.map((item, index) => {
+      const arr = snippets.filter((_, i) => i <= index);
+      return arr.join('/');
+    });
+    let currentMenuSelectedKeys = [];
+    currentPathSnippets.forEach((item) => {
+      currentMenuSelectedKeys = currentMenuSelectedKeys.concat(this.getSelectedMenuKeys(item));
+    });
+    if (currentMenuSelectedKeys.length === 0) {
+      return ['dashboard'];
+    }
+    return currentMenuSelectedKeys;
+  }
+  getFlatMenuKeys(menus) {
+    let keys = [];
+    menus.forEach((item) => {
+      if (item.children) {
+        keys.push(item.path);
+        keys = keys.concat(this.getFlatMenuKeys(item.children));
+      } else {
+        keys.push(item.path);
+      }
+    });
+    return keys;
+  }
+  getSelectedMenuKeys = (path) => {
+    const flatMenuKeys = this.getFlatMenuKeys(this.menus);
+
+    if (flatMenuKeys.indexOf(path.replace(/^\//, '')) > -1) {
+      return [path.replace(/^\//, '')];
+    }
+    if (flatMenuKeys.indexOf(path.replace(/^\//, '').replace(/\/$/, '')) > -1) {
+      return [path.replace(/^\//, '').replace(/\/$/, '')];
+    }
+    return flatMenuKeys.filter((item) => {
+      const itemRegExpStr = `^${item.replace(/:[\w-]+/g, '[\\w-]+')}$`;
+      const itemRegExp = new RegExp(itemRegExpStr);
+      return itemRegExp.test(path.replace(/^\//, ''));
+    });
+  }
+  getNavMenuItems(menusData) {
+    if (!menusData) {
+      return [];
+    }
+    return menusData.map((item) => {
+      if (!item.name) {
+        return null;
+      }
+      let itemPath;
+      if (item.path && item.path.indexOf('http') === 0) {
+        itemPath = item.path;
+      } else {
+        itemPath = `/${item.path || ''}`.replace(/\/+/g, '/');
+      }
+      if (item.children && item.children.some(child => child.name)) {
+        return item.hideInMenu ? null :
+          (
+            <SubMenu
+              title={
+                item.icon ? (
+                  <span>
+                    <Icon type={item.icon} />
+                    <span>{item.name}</span>
+                  </span>
+                ) : item.name
+              }
+              key={item.key || item.path}
+            >
+              {this.getNavMenuItems(item.children)}
+            </SubMenu>
+          );
+      }
+      const icon = item.icon && <Icon type={item.icon} />;
+      return item.hideInMenu ? null :
+        (
+          <Menu.Item key={item.key || item.path}>
+            {
+              /^https?:\/\//.test(itemPath) ? (
+                <a href={itemPath} target={item.target}>
+                  {icon}<span>{item.name}</span>
+                </a>
+              ) : (
+                <Link
+                  to={itemPath}
+                  target={item.target}
+                  replace={itemPath === this.props.location.pathname}
+                  onClick={this.props.isMobile && (() => { this.props.onCollapse(true); })}
+                >
+                  {icon}<span>{item.name}</span>
+                </Link>
+              )
+            }
+          </Menu.Item>
+        );
+    });
+  }
+  handleOpenChange = (openKeys) => {
+    const lastOpenKey = openKeys[openKeys.length - 1];
+    const isMainMenu = this.menus.some(
+      item => lastOpenKey && (item.key === lastOpenKey || item.path === lastOpenKey)
+    );
+    this.setState({
+      openKeys: isMainMenu ? [lastOpenKey] : [...openKeys],
+    });
+  }
+  render() {
+    const { collapsed, location: { pathname }, onCollapse } = this.props;
+    // Don't show popup menu when it is been collapsed
+    const menuProps = collapsed ? {} : {
+      openKeys: this.state.openKeys,
+    };
+    return (
+      <Sider
+        trigger={null}
+        collapsible
+        collapsed={collapsed}
+        breakpoint="md"
+        onCollapse={onCollapse}
+        width={256}
+        className={styles.sider}
+      >
+        <div className={styles.logo}>
+          <Link to="/">
+            <img src={logo} alt="logo" />
+            <h1>Ant Design Pro</h1>
+          </Link>
+        </div>
+        <Menu
+          theme="dark"
+          mode="inline"
+          {...menuProps}
+          onOpenChange={this.handleOpenChange}
+          selectedKeys={this.getSelectedMenuKeys(pathname)}
+          style={{ padding: '16px 0', width: '100%' }}
+        >
+          {this.getNavMenuItems(this.menus)}
+        </Menu>
+      </Sider>
+    );
+  }
+}

+ 26 - 153
src/components/SiderMenu/index.js

@@ -1,167 +1,40 @@
+import 'rc-drawer-menu/assets/index.css';
 import React, { PureComponent } from 'react';
-import { Layout, Menu, Icon } from 'antd';
-import { Link } from 'dva/router';
-import logo from '../../assets/logo.svg';
-import styles from './index.less';
-import { getMenuData } from '../../common/menu';
+import DrawerMenu from 'rc-drawer-menu';
+import SiderMenu from './SiderMenu';
 
-const { Sider } = Layout;
-const { SubMenu } = Menu;
-
-export default class SiderMenu extends PureComponent {
-  constructor(props) {
-    super(props);
-    this.menus = getMenuData();
-    this.state = {
-      openKeys: this.getDefaultCollapsedSubMenus(props),
-    };
-  }
+export default class Index extends PureComponent {
   onCollapse = (collapsed) => {
     this.props.dispatch({
       type: 'global/changeLayoutCollapsed',
       payload: collapsed,
     });
   }
-  getDefaultCollapsedSubMenus(props) {
-    const { location: { pathname } } = props || this.props;
-    const snippets = pathname.split('/').slice(1, -1);
-    const currentPathSnippets = snippets.map((item, index) => {
-      const arr = snippets.filter((_, i) => i <= index);
-      return arr.join('/');
-    });
-    let currentMenuSelectedKeys = [];
-    currentPathSnippets.forEach((item) => {
-      currentMenuSelectedKeys = currentMenuSelectedKeys.concat(this.getSelectedMenuKeys(item));
-    });
-    if (currentMenuSelectedKeys.length === 0) {
-      return ['dashboard'];
-    }
-    return currentMenuSelectedKeys;
-  }
-  getFlatMenuKeys(menus) {
-    let keys = [];
-    menus.forEach((item) => {
-      if (item.children) {
-        keys.push(item.path);
-        keys = keys.concat(this.getFlatMenuKeys(item.children));
-      } else {
-        keys.push(item.path);
-      }
-    });
-    return keys;
-  }
-  getSelectedMenuKeys = (path) => {
-    const flatMenuKeys = this.getFlatMenuKeys(this.menus);
 
-    if (flatMenuKeys.indexOf(path.replace(/^\//, '')) > -1) {
-      return [path.replace(/^\//, '')];
-    }
-    if (flatMenuKeys.indexOf(path.replace(/^\//, '').replace(/\/$/, '')) > -1) {
-      return [path.replace(/^\//, '').replace(/\/$/, '')];
-    }
-    return flatMenuKeys.filter((item) => {
-      const itemRegExpStr = `^${item.replace(/:[\w-]+/g, '[\\w-]+')}$`;
-      const itemRegExp = new RegExp(itemRegExpStr);
-      return itemRegExp.test(path.replace(/^\//, ''));
-    });
-  }
-  getNavMenuItems(menusData) {
-    if (!menusData) {
-      return [];
-    }
-    return menusData.map((item) => {
-      if (!item.name) {
-        return null;
-      }
-      let itemPath;
-      if (item.path && item.path.indexOf('http') === 0) {
-        itemPath = item.path;
-      } else {
-        itemPath = `/${item.path || ''}`.replace(/\/+/g, '/');
-      }
-      if (item.children && item.children.some(child => child.name)) {
-        return item.hideInMenu ? null :
-          (
-            <SubMenu
-              title={
-                item.icon ? (
-                  <span>
-                    <Icon type={item.icon} />
-                    <span>{item.name}</span>
-                  </span>
-                ) : item.name
-              }
-              key={item.key || item.path}
-            >
-              {this.getNavMenuItems(item.children)}
-            </SubMenu>
-          );
-      }
-      const icon = item.icon && <Icon type={item.icon} />;
-      return item.hideInMenu ? null :
-        (
-          <Menu.Item key={item.key || item.path}>
-            {
-              /^https?:\/\//.test(itemPath) ? (
-                <a href={itemPath} target={item.target}>
-                  {icon}<span>{item.name}</span>
-                </a>
-              ) : (
-                <Link
-                  to={itemPath}
-                  target={item.target}
-                  replace={itemPath === this.props.location.pathname}
-                >
-                  {icon}<span>{item.name}</span>
-                </Link>
-              )
-            }
-          </Menu.Item>
-        );
-    });
-  }
-  handleOpenChange = (openKeys) => {
-    const lastOpenKey = openKeys[openKeys.length - 1];
-    const isMainMenu = this.menus.some(
-      item => lastOpenKey && (item.key === lastOpenKey || item.path === lastOpenKey)
-    );
-    this.setState({
-      openKeys: isMainMenu ? [lastOpenKey] : [...openKeys],
-    });
-  }
   render() {
-    const { collapsed, location: { pathname } } = this.props;
-    // Don't show popup menu when it is been collapsed
-    const menuProps = collapsed ? {} : {
-      openKeys: this.state.openKeys,
-    };
-    return (
-      <Sider
-        trigger={null}
-        collapsible
-        collapsed={collapsed}
-        breakpoint="md"
-        onCollapse={this.onCollapse}
-        width={256}
-        className={styles.sider}
+    const { collapsed, isMobile } = this.props;
+    return isMobile ? (
+      <DrawerMenu
+        parent={null}
+        level={null}
+        iconChild={null}
+        open={!collapsed}
+        onMaskClick={() => { this.onCollapse(true); }}
+        width="256px"
       >
-        <div className={styles.logo}>
-          <Link to="/">
-            <img src={logo} alt="logo" />
-            <h1>Ant Design Pro</h1>
-          </Link>
-        </div>
-        <Menu
-          theme="dark"
-          mode="inline"
-          {...menuProps}
-          onOpenChange={this.handleOpenChange}
-          selectedKeys={this.getSelectedMenuKeys(pathname)}
-          style={{ padding: '16px 0', width: '100%' }}
-        >
-          {this.getNavMenuItems(this.menus)}
-        </Menu>
-      </Sider>
+        <SiderMenu
+          {...this.props}
+          isMobile={isMobile}
+          onCollapse={this.onCollapse}
+          collapsed={isMobile ? false : collapsed}
+        />
+      </DrawerMenu>
+    ) : (
+      <SiderMenu
+        {...this.props}
+        isMobile={isMobile}
+        onCollapse={this.onCollapse}
+      />
     );
   }
 }

+ 6 - 2
src/components/SiderMenu/index.less

@@ -1,5 +1,5 @@
 @import "~antd/lib/style/themes/default.less";
-
+@ease-in-out-circ: cubic-bezier(.78, .14, .15, .86);
 .logo {
   height: 64px;
   position: relative;
@@ -23,10 +23,14 @@
     font-weight: 600;
   }
 }
-
 .sider {
   min-height: 100vh;
   box-shadow: 2px 0 6px rgba(0, 21, 41, .35);
   position: relative;
   z-index: 10;
 }
+:global {
+  .drawer .drawer-content {
+    background: #001529;
+  }
+}

+ 19 - 0
src/layouts/BasicLayout.js

@@ -6,6 +6,7 @@ import { connect } from 'dva';
 import { Route, Redirect, Switch } from 'dva/router';
 import { ContainerQuery } from 'react-container-query';
 import classNames from 'classnames';
+import { enquireScreen } from 'enquire-js';
 import GlobalHeader from '../components/GlobalHeader';
 import GlobalFooter from '../components/GlobalFooter';
 import SiderMenu from '../components/SiderMenu';
@@ -55,11 +56,20 @@ const query = {
   },
 };
 
+let isMobile;
+enquireScreen((b) => {
+  isMobile = b;
+});
+
 class BasicLayout extends React.PureComponent {
   static childContextTypes = {
     location: PropTypes.object,
     breadcrumbNameMap: PropTypes.object,
   }
+
+  state = {
+    isMobile,
+  };
   getChildContext() {
     const { location, routerData } = this.props;
     return {
@@ -67,6 +77,13 @@ class BasicLayout extends React.PureComponent {
       breadcrumbNameMap: routerData,
     };
   }
+  componentDidMount() {
+    enquireScreen((b) => {
+      this.setState({
+        isMobile: !!b,
+      });
+    });
+  }
   getPageTitle() {
     const { routerData, location } = this.props;
     const { pathname } = location;
@@ -86,6 +103,7 @@ class BasicLayout extends React.PureComponent {
           collapsed={collapsed}
           location={location}
           dispatch={dispatch}
+          isMobile={this.state.isMobile}
         />
         <Layout>
           <GlobalHeader
@@ -94,6 +112,7 @@ class BasicLayout extends React.PureComponent {
             notices={notices}
             collapsed={collapsed}
             dispatch={dispatch}
+            isMobile={this.state.isMobile}
           />
           <Content style={{ margin: '24px 24px 0', height: '100%' }}>
             <div style={{ minHeight: 'calc(100vh - 260px)' }}>