Bläddra i källkod

GlobalHeader & SiderMenu (#411)

* GlobalHeader & SiderMenu

* Fix login and logout

* Fix test case
偏右 8 år sedan
förälder
incheckning
f67bff3905

+ 157 - 0
src/components/GlobalHeader/index.js

@@ -0,0 +1,157 @@
+import React, { PureComponent } from 'react';
+import { Layout, Menu, Icon, Spin, Tag, Dropdown, Avatar, message } from 'antd';
+import moment from 'moment';
+import groupBy from 'lodash/groupBy';
+import Debounce from 'lodash-decorators/debounce';
+import NoticeIcon from '../../components/NoticeIcon';
+import HeaderSearch from '../../components/HeaderSearch';
+import styles from './index.less';
+
+const { Header } = Layout;
+
+export default class GlobalHeader extends PureComponent {
+  componentDidMount() {
+    this.props.dispatch({
+      type: 'user/fetchCurrent',
+    });
+  }
+  componentWillUnmount() {
+    this.triggerResizeEvent.cancel();
+  }
+  getNoticeData() {
+    const { notices = [] } = this.props;
+    if (notices.length === 0) {
+      return {};
+    }
+    const newNotices = notices.map((notice) => {
+      const newNotice = { ...notice };
+      if (newNotice.datetime) {
+        newNotice.datetime = moment(notice.datetime).fromNow();
+      }
+      // transform id to item key
+      if (newNotice.id) {
+        newNotice.key = newNotice.id;
+      }
+      if (newNotice.extra && newNotice.status) {
+        const color = ({
+          todo: '',
+          processing: 'blue',
+          urgent: 'red',
+          doing: 'gold',
+        })[newNotice.status];
+        newNotice.extra = <Tag color={color} style={{ marginRight: 0 }}>{newNotice.extra}</Tag>;
+      }
+      return newNotice;
+    });
+    return groupBy(newNotices, 'type');
+  }
+  handleNoticeClear = (type) => {
+    message.success(`清空了${type}`);
+    this.props.dispatch({
+      type: 'global/clearNotices',
+      payload: type,
+    });
+  }
+  handleNoticeVisibleChange = (visible) => {
+    if (visible) {
+      this.props.dispatch({
+        type: 'global/fetchNotices',
+      });
+    }
+  }
+  handleMenuClick = ({ key }) => {
+    if (key === 'logout') {
+      this.props.dispatch({
+        type: 'login/logout',
+      });
+    }
+  }
+  toggle = () => {
+    const { collapsed } = this.props;
+    this.props.dispatch({
+      type: 'global/changeLayoutCollapsed',
+      payload: !collapsed,
+    });
+    this.triggerResizeEvent();
+  }
+  @Debounce(600)
+  triggerResizeEvent() { // eslint-disable-line
+    const event = document.createEvent('HTMLEvents');
+    event.initEvent('resize', true, false);
+    window.dispatchEvent(event);
+  }
+  render() {
+    const {
+      currentUser, collapsed, fetchingNotices,
+    } = this.props;
+    const menu = (
+      <Menu className={styles.menu} selectedKeys={[]} onClick={this.handleMenuClick}>
+        <Menu.Item disabled><Icon type="user" />个人中心</Menu.Item>
+        <Menu.Item disabled><Icon type="setting" />设置</Menu.Item>
+        <Menu.Divider />
+        <Menu.Item key="logout"><Icon type="logout" />退出登录</Menu.Item>
+      </Menu>
+    );
+    const noticeData = this.getNoticeData();
+    return (
+      <Header className={styles.header}>
+        <Icon
+          className={styles.trigger}
+          type={collapsed ? 'menu-unfold' : 'menu-fold'}
+          onClick={this.toggle}
+        />
+        <div className={styles.right}>
+          <HeaderSearch
+            className={`${styles.action} ${styles.search}`}
+            placeholder="站内搜索"
+            dataSource={['搜索提示一', '搜索提示二', '搜索提示三']}
+            onSearch={(value) => {
+              console.log('input', value); // eslint-disable-line
+            }}
+            onPressEnter={(value) => {
+              console.log('enter', value); // eslint-disable-line
+            }}
+          />
+          <NoticeIcon
+            className={styles.action}
+            count={currentUser.notifyCount}
+            onItemClick={(item, tabProps) => {
+              console.log(item, tabProps); // eslint-disable-line
+            }}
+            onClear={this.handleNoticeClear}
+            onPopupVisibleChange={this.handleNoticeVisibleChange}
+            loading={fetchingNotices}
+            popupAlign={{ offset: [20, -16] }}
+          >
+            <NoticeIcon.Tab
+              list={noticeData['通知']}
+              title="通知"
+              emptyText="你已查看所有通知"
+              emptyImage="https://gw.alipayobjects.com/zos/rmsportal/wAhyIChODzsoKIOBHcBk.svg"
+            />
+            <NoticeIcon.Tab
+              list={noticeData['消息']}
+              title="消息"
+              emptyText="您已读完所有消息"
+              emptyImage="https://gw.alipayobjects.com/zos/rmsportal/sAuJeJzSKbUmHfBQRzmZ.svg"
+            />
+            <NoticeIcon.Tab
+              list={noticeData['待办']}
+              title="待办"
+              emptyText="你已完成所有待办"
+              emptyImage="https://gw.alipayobjects.com/zos/rmsportal/HsIsxMZiWKrNUavQUXqx.svg"
+            />
+          </NoticeIcon>
+          {currentUser.name ? (
+            <Dropdown overlay={menu}>
+              <span className={`${styles.action} ${styles.account}`}>
+                <Avatar size="small" className={styles.avatar} src={currentUser.avatar} />
+                {currentUser.name}
+              </span>
+            </Dropdown>
+          ) : <Spin size="small" style={{ marginLeft: 8 }} />}
+        </div>
+      </Header>
+    );
+  }
+}

+ 11 - 42
src/layouts/BasicLayout.less

@@ -7,27 +7,18 @@
   position: relative;
 }
 
-.logo {
-  height: 64px;
-  position: relative;
-  line-height: 64px;
-  padding-left: (@menu-collapsed-width - 32px) / 2;
-  transition: all .3s;
-  background: #002140;
-  overflow: hidden;
-  img {
-    display: inline-block;
-    vertical-align: middle;
-    height: 32px;
+:global {
+  .ant-layout {
+    overflow-x: hidden;
   }
-  h1 {
-    color: #fff;
-    display: inline-block;
-    vertical-align: middle;
-    font-size: 20px;
-    margin: 0 0 0 12px;
-    font-family: 'Myriad Pro', 'Helvetica Neue', Arial, Helvetica, sans-serif;
-    font-weight: 600;
+}
+
+.menu {
+  :global(.anticon) {
+    margin-right: 8px;
+  }
+  :global(.ant-dropdown-menu-item) {
+    width: 160px;
   }
 }
 
@@ -82,25 +73,3 @@ i.trigger {
     }
   }
 }
-
-.menu {
-  :global(.anticon) {
-    margin-right: 8px;
-  }
-  :global(.ant-dropdown-menu-item) {
-    width: 160px;
-  }
-}
-
-:global {
-  .ant-layout {
-    overflow-x: hidden;
-  }
-}
-
-.sider {
-  min-height: 100vh;
-  box-shadow: 2px 0 6px rgba(0, 21, 41, .35);
-  position: relative;
-  z-index: 10;
-}

+ 139 - 0
src/components/SiderMenu/index.js

@@ -0,0 +1,139 @@
+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';
+
+const { Sider } = Layout;
+const { SubMenu } = Menu;
+
+export default class SiderMenu extends PureComponent {
+  constructor(props) {
+    super(props);
+    // 把一级 Layout 的 children 作为菜单项
+    this.menus = props.navData.reduce((arr, current) => arr.concat(current.children), []);
+    this.state = {
+      openKeys: this.getDefaultCollapsedSubMenus(props),
+    };
+  }
+  onCollapse = (collapsed) => {
+    this.props.dispatch({
+      type: 'global/changeLayoutCollapsed',
+      payload: collapsed,
+    });
+  }
+  getDefaultCollapsedSubMenus(props) {
+    const currentMenuSelectedKeys = [...this.getCurrentMenuSelectedKeys(props)];
+    currentMenuSelectedKeys.splice(-1, 1);
+    if (currentMenuSelectedKeys.length === 0) {
+      return ['dashboard'];
+    }
+    return currentMenuSelectedKeys;
+  }
+  getCurrentMenuSelectedKeys(props) {
+    const { location: { pathname } } = props || this.props;
+    const keys = pathname.split('/').slice(1);
+    if (keys.length === 1 && keys[0] === '') {
+      return [this.menus[0].key];
+    }
+    return keys;
+  }
+  getNavMenuItems(menusData, parentPath = '') {
+    if (!menusData) {
+      return [];
+    }
+    return menusData.map((item) => {
+      if (!item.name) {
+        return null;
+      }
+      let itemPath;
+      if (item.path.indexOf('http') === 0) {
+        itemPath = item.path;
+      } else {
+        itemPath = `${parentPath}/${item.path || ''}`.replace(/\/+/g, '/');
+      }
+      if (item.children && item.children.some(child => child.name)) {
+        return (
+          <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, itemPath)}
+          </SubMenu>
+        );
+      }
+      const icon = item.icon && <Icon type={item.icon} />;
+      return (
+        <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 } = 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}
+      >
+        <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.getCurrentMenuSelectedKeys()}
+          style={{ margin: '16px 0', width: '100%' }}
+        >
+          {this.getNavMenuItems(this.menus)}
+        </Menu>
+      </Sider>
+    );
+  }
+}

+ 32 - 0
src/components/SiderMenu/index.less

@@ -0,0 +1,32 @@
+@import "~antd/lib/style/themes/default.less";
+
+.logo {
+  height: 64px;
+  position: relative;
+  line-height: 64px;
+  padding-left: (@menu-collapsed-width - 32px) / 2;
+  transition: all .3s;
+  background: #002140;
+  overflow: hidden;
+  img {
+    display: inline-block;
+    vertical-align: middle;
+    height: 32px;
+  }
+  h1 {
+    color: #fff;
+    display: inline-block;
+    vertical-align: middle;
+    font-size: 20px;
+    margin: 0 0 0 12px;
+    font-family: 'Myriad Pro', 'Helvetica Neue', Arial, Helvetica, sans-serif;
+    font-weight: 600;
+  }
+}
+
+.sider {
+  min-height: 100vh;
+  box-shadow: 2px 0 6px rgba(0, 21, 41, .35);
+  position: relative;
+  z-index: 10;
+}

+ 1 - 1
src/e2e/home.e2e.js

@@ -3,7 +3,7 @@ import Nightmare from 'nightmare';
 describe('Homepage', () => {
   it('it should have logo text', async () => {
     const page = Nightmare().goto('http://localhost:8000');
-    const text = await page.evaluate(() => document.body.innerHTML).end();
+    const text = await page.wait('h1').evaluate(() => document.body.innerHTML).end();
     expect(text).toContain('<h1>Ant Design Pro</h1>');
   });
 });

+ 27 - 279
src/layouts/BasicLayout.js

@@ -1,23 +1,17 @@
 import React from 'react';
 import PropTypes from 'prop-types';
-import { Layout, Menu, Icon, Avatar, Dropdown, Tag, message, Spin } from 'antd';
+import { Layout, Icon } from 'antd';
 import DocumentTitle from 'react-document-title';
 import { connect } from 'dva';
-import { Link, Route, Redirect, Switch } from 'dva/router';
-import moment from 'moment';
-import groupBy from 'lodash/groupBy';
+import { Route, Redirect, Switch } from 'dva/router';
 import { ContainerQuery } from 'react-container-query';
 import classNames from 'classnames';
-import Debounce from 'lodash-decorators/debounce';
-import HeaderSearch from '../components/HeaderSearch';
-import NoticeIcon from '../components/NoticeIcon';
+import GlobalHeader from '../components/GlobalHeader';
 import GlobalFooter from '../components/GlobalFooter';
+import SiderMenu from '../components/SiderMenu';
 import NotFound from '../routes/Exception/404';
-import styles from './BasicLayout.less';
-import logo from '../assets/logo.svg';
 
-const { Header, Sider, Content } = Layout;
-const { SubMenu } = Menu;
+const { Content } = Layout;
 
 const query = {
   'screen-xs': {
@@ -45,14 +39,6 @@ class BasicLayout extends React.PureComponent {
     location: PropTypes.object,
     breadcrumbNameMap: PropTypes.object,
   }
-  constructor(props) {
-    super(props);
-    // 把一级 Layout 的 children 作为菜单项
-    this.menus = props.navData.reduce((arr, current) => arr.concat(current.children), []);
-    this.state = {
-      openKeys: this.getDefaultCollapsedSubMenus(props),
-    };
-  }
   getChildContext() {
     const { location, navData, getRouteData } = this.props;
     const routeData = getRouteData('BasicLayout');
@@ -68,106 +54,6 @@ class BasicLayout extends React.PureComponent {
     });
     return { location, breadcrumbNameMap };
   }
-  componentDidMount() {
-    this.props.dispatch({
-      type: 'user/fetchCurrent',
-    });
-  }
-  componentWillUnmount() {
-    this.triggerResizeEvent.cancel();
-  }
-  onCollapse = (collapsed) => {
-    this.props.dispatch({
-      type: 'global/changeLayoutCollapsed',
-      payload: collapsed,
-    });
-  }
-  onMenuClick = ({ key }) => {
-    if (key === 'logout') {
-      this.props.dispatch({
-        type: 'login/logout',
-      });
-    }
-  }
-  getMenuData = (data, parentPath) => {
-    let arr = [];
-    data.forEach((item) => {
-      if (item.children) {
-        arr.push({ path: `${parentPath}/${item.path}`, name: item.name });
-        arr = arr.concat(this.getMenuData(item.children, `${parentPath}/${item.path}`));
-      }
-    });
-    return arr;
-  }
-  getDefaultCollapsedSubMenus(props) {
-    const currentMenuSelectedKeys = [...this.getCurrentMenuSelectedKeys(props)];
-    currentMenuSelectedKeys.splice(-1, 1);
-    if (currentMenuSelectedKeys.length === 0) {
-      return ['dashboard'];
-    }
-    return currentMenuSelectedKeys;
-  }
-  getCurrentMenuSelectedKeys(props) {
-    const { location: { pathname } } = props || this.props;
-    const keys = pathname.split('/').slice(1);
-    if (keys.length === 1 && keys[0] === '') {
-      return [this.menus[0].key];
-    }
-    return keys;
-  }
-  getNavMenuItems(menusData, parentPath = '') {
-    if (!menusData) {
-      return [];
-    }
-    return menusData.map((item) => {
-      if (!item.name) {
-        return null;
-      }
-      let itemPath;
-      if (item.path.indexOf('http') === 0) {
-        itemPath = item.path;
-      } else {
-        itemPath = `${parentPath}/${item.path || ''}`.replace(/\/+/g, '/');
-      }
-      if (item.children && item.children.some(child => child.name)) {
-        return (
-          <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, itemPath)}
-          </SubMenu>
-        );
-      }
-      const icon = item.icon && <Icon type={item.icon} />;
-      return (
-        <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>
-      );
-    });
-  }
   getPageTitle() {
     const { location, getRouteData } = this.props;
     const { pathname } = location;
@@ -179,175 +65,37 @@ class BasicLayout extends React.PureComponent {
     });
     return title;
   }
-  getNoticeData() {
-    const { notices = [] } = this.props;
-    if (notices.length === 0) {
-      return {};
-    }
-    const newNotices = notices.map((notice) => {
-      const newNotice = { ...notice };
-      if (newNotice.datetime) {
-        newNotice.datetime = moment(notice.datetime).fromNow();
-      }
-      // transform id to item key
-      if (newNotice.id) {
-        newNotice.key = newNotice.id;
-      }
-      if (newNotice.extra && newNotice.status) {
-        const color = ({
-          todo: '',
-          processing: 'blue',
-          urgent: 'red',
-          doing: 'gold',
-        })[newNotice.status];
-        newNotice.extra = <Tag color={color} style={{ marginRight: 0 }}>{newNotice.extra}</Tag>;
+  getMenuData = (data, parentPath) => {
+    let arr = [];
+    data.forEach((item) => {
+      if (item.children) {
+        arr.push({ path: `${parentPath}/${item.path}`, name: item.name });
+        arr = arr.concat(this.getMenuData(item.children, `${parentPath}/${item.path}`));
       }
-      return newNotice;
-    });
-    return groupBy(newNotices, 'type');
-  }
-  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],
     });
-  }
-  toggle = () => {
-    const { collapsed } = this.props;
-    this.props.dispatch({
-      type: 'global/changeLayoutCollapsed',
-      payload: !collapsed,
-    });
-    this.triggerResizeEvent();
-  }
-  @Debounce(600)
-  triggerResizeEvent() { // eslint-disable-line
-    const event = document.createEvent('HTMLEvents');
-    event.initEvent('resize', true, false);
-    window.dispatchEvent(event);
-  }
-  handleNoticeClear = (type) => {
-    message.success(`清空了${type}`);
-    this.props.dispatch({
-      type: 'global/clearNotices',
-      payload: type,
-    });
-  }
-  handleNoticeVisibleChange = (visible) => {
-    if (visible) {
-      this.props.dispatch({
-        type: 'global/fetchNotices',
-      });
-    }
+    return arr;
   }
   render() {
-    const { currentUser, collapsed, fetchingNotices, getRouteData } = this.props;
-
-    const menu = (
-      <Menu className={styles.menu} selectedKeys={[]} onClick={this.onMenuClick}>
-        <Menu.Item disabled><Icon type="user" />个人中心</Menu.Item>
-        <Menu.Item disabled><Icon type="setting" />设置</Menu.Item>
-        <Menu.Divider />
-        <Menu.Item key="logout"><Icon type="logout" />退出登录</Menu.Item>
-      </Menu>
-    );
-    const noticeData = this.getNoticeData();
-
-    // Don't show popup menu when it is been collapsed
-    const menuProps = collapsed ? {} : {
-      openKeys: this.state.openKeys,
-    };
+    const {
+      currentUser, collapsed, fetchingNotices, notices, getRouteData, navData, location, dispatch,
+    } = this.props;
 
     const layout = (
       <Layout>
-        <Sider
-          trigger={null}
-          collapsible
+        <SiderMenu
           collapsed={collapsed}
-          breakpoint="md"
-          onCollapse={this.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.getCurrentMenuSelectedKeys()}
-            style={{ margin: '16px 0', width: '100%' }}
-          >
-            {this.getNavMenuItems(this.menus)}
-          </Menu>
-        </Sider>
+          navData={navData}
+          location={location}
+          dispatch={dispatch}
+        />
         <Layout>
-          <Header className={styles.header}>
-            <Icon
-              className={styles.trigger}
-              type={collapsed ? 'menu-unfold' : 'menu-fold'}
-              onClick={this.toggle}
-            />
-            <div className={styles.right}>
-              <HeaderSearch
-                className={`${styles.action} ${styles.search}`}
-                placeholder="站内搜索"
-                dataSource={['搜索提示一', '搜索提示二', '搜索提示三']}
-                onSearch={(value) => {
-                  console.log('input', value); // eslint-disable-line
-                }}
-                onPressEnter={(value) => {
-                  console.log('enter', value); // eslint-disable-line
-                }}
-              />
-              <NoticeIcon
-                className={styles.action}
-                count={currentUser.notifyCount}
-                onItemClick={(item, tabProps) => {
-                  console.log(item, tabProps); // eslint-disable-line
-                }}
-                onClear={this.handleNoticeClear}
-                onPopupVisibleChange={this.handleNoticeVisibleChange}
-                loading={fetchingNotices}
-                popupAlign={{ offset: [20, -16] }}
-              >
-                <NoticeIcon.Tab
-                  list={noticeData['通知']}
-                  title="通知"
-                  emptyText="你已查看所有通知"
-                  emptyImage="https://gw.alipayobjects.com/zos/rmsportal/wAhyIChODzsoKIOBHcBk.svg"
-                />
-                <NoticeIcon.Tab
-                  list={noticeData['消息']}
-                  title="消息"
-                  emptyText="您已读完所有消息"
-                  emptyImage="https://gw.alipayobjects.com/zos/rmsportal/sAuJeJzSKbUmHfBQRzmZ.svg"
-                />
-                <NoticeIcon.Tab
-                  list={noticeData['待办']}
-                  title="待办"
-                  emptyText="你已完成所有待办"
-                  emptyImage="https://gw.alipayobjects.com/zos/rmsportal/HsIsxMZiWKrNUavQUXqx.svg"
-                />
-              </NoticeIcon>
-              {currentUser.name ? (
-                <Dropdown overlay={menu}>
-                  <span className={`${styles.action} ${styles.account}`}>
-                    <Avatar size="small" className={styles.avatar} src={currentUser.avatar} />
-                    {currentUser.name}
-                  </span>
-                </Dropdown>
-              ) : <Spin size="small" style={{ marginLeft: 8 }} />}
-            </div>
-          </Header>
+          <GlobalHeader
+            currentUser={currentUser}
+            fetchingNotices={fetchingNotices}
+            notices={notices}
+            collapsed={collapsed}
+            dispatch={dispatch}
+          />
           <Content style={{ margin: '24px 24px 0', height: '100%' }}>
             <div style={{ minHeight: 'calc(100vh - 260px)' }}>
               <Switch>