diff --git a/src/components/GlobalHeader/index.js b/src/components/GlobalHeader/index.js new file mode 100644 index 0000000000000000000000000000000000000000..fa7aac798af33437358893cb285d011b1e54b15e --- /dev/null +++ b/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 = {newNotice.extra}; + } + 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 = ( + + 个人中心 + 设置 + + 退出登录 + + ); + const noticeData = this.getNoticeData(); + return ( +
+ +
+ { + console.log('input', value); // eslint-disable-line + }} + onPressEnter={(value) => { + console.log('enter', value); // eslint-disable-line + }} + /> + { + console.log(item, tabProps); // eslint-disable-line + }} + onClear={this.handleNoticeClear} + onPopupVisibleChange={this.handleNoticeVisibleChange} + loading={fetchingNotices} + popupAlign={{ offset: [20, -16] }} + > + + + + + {currentUser.name ? ( + + + + {currentUser.name} + + + ) : } +
+
+ ); + } +} diff --git a/src/layouts/BasicLayout.less b/src/components/GlobalHeader/index.less similarity index 65% rename from src/layouts/BasicLayout.less rename to src/components/GlobalHeader/index.less index dc40a1e56b0039212736afecdea7e496c9b4b6c7..dc9ac299e2f25763194434f4399c451039500ce4 100644 --- a/src/layouts/BasicLayout.less +++ b/src/components/GlobalHeader/index.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; -} diff --git a/src/components/SiderMenu/index.js b/src/components/SiderMenu/index.js new file mode 100644 index 0000000000000000000000000000000000000000..5a6d58f47755f8140335decfe160633096e7dfe3 --- /dev/null +++ b/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 ( + + + {item.name} + + ) : item.name + } + key={item.key || item.path} + > + {this.getNavMenuItems(item.children, itemPath)} + + ); + } + const icon = item.icon && ; + return ( + + { + /^https?:\/\//.test(itemPath) ? ( + + {icon}{item.name} + + ) : ( + + {icon}{item.name} + + ) + } + + ); + }); + } + 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 ( + +
+ + logo +

Ant Design Pro

+ +
+ + {this.getNavMenuItems(this.menus)} + +
+ ); + } +} diff --git a/src/components/SiderMenu/index.less b/src/components/SiderMenu/index.less new file mode 100644 index 0000000000000000000000000000000000000000..15d256f31ef174d08378e9c4dec88725c06013bc --- /dev/null +++ b/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; +} diff --git a/src/e2e/home.e2e.js b/src/e2e/home.e2e.js index 6de577b969659d8e5727133fe5d33d06d6e295cf..61b0cd83a89568f5a88364c15196f2ff5b1064e4 100644 --- a/src/e2e/home.e2e.js +++ b/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('

Ant Design Pro

'); }); }); diff --git a/src/layouts/BasicLayout.js b/src/layouts/BasicLayout.js index a46cb5728c6a4cbca0550609defed4daf4c4f753..cf7aeb6634eeaadc88650c8525e1cb9b1577c1f0 100644 --- a/src/layouts/BasicLayout.js +++ b/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 ( - - - {item.name} - - ) : item.name - } - key={item.key || item.path} - > - {this.getNavMenuItems(item.children, itemPath)} - - ); - } - const icon = item.icon && ; - return ( - - { - /^https?:\/\//.test(itemPath) ? ( - - {icon}{item.name} - - ) : ( - - {icon}{item.name} - - ) - } - - ); - }); - } 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 = {newNotice.extra}; + 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 = ( - - 个人中心 - 设置 - - 退出登录 - - ); - 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 = ( - -
- - logo -

Ant Design Pro

- -
- - {this.getNavMenuItems(this.menus)} - -
+ navData={navData} + location={location} + dispatch={dispatch} + /> -
- -
- { - console.log('input', value); // eslint-disable-line - }} - onPressEnter={(value) => { - console.log('enter', value); // eslint-disable-line - }} - /> - { - console.log(item, tabProps); // eslint-disable-line - }} - onClear={this.handleNoticeClear} - onPopupVisibleChange={this.handleNoticeVisibleChange} - loading={fetchingNotices} - popupAlign={{ offset: [20, -16] }} - > - - - - - {currentUser.name ? ( - - - - {currentUser.name} - - - ) : } -
-
+