Commit e221476b authored by jiang's avatar jiang Committed by ddcat1115

Mobile menu (#463)

* Increase the sliding menu

* Add a simple animation

* update mobile menu

* update

* update

* update

* rebase master

* recovery import/first
parent ce84e3f7
import React, { PureComponent } from 'react'; 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 moment from 'moment';
import groupBy from 'lodash/groupBy'; import groupBy from 'lodash/groupBy';
import Debounce from 'lodash-decorators/debounce'; import Debounce from 'lodash-decorators/debounce';
import { Link } from 'dva/router';
import NoticeIcon from '../../components/NoticeIcon'; import NoticeIcon from '../../components/NoticeIcon';
import HeaderSearch from '../../components/HeaderSearch'; import HeaderSearch from '../../components/HeaderSearch';
import logo from '../../assets/logo.svg';
import styles from './index.less'; import styles from './index.less';
const { Header } = Layout; const { Header } = Layout;
...@@ -82,7 +84,7 @@ export default class GlobalHeader extends PureComponent { ...@@ -82,7 +84,7 @@ export default class GlobalHeader extends PureComponent {
} }
render() { render() {
const { const {
currentUser, collapsed, fetchingNotices, currentUser, collapsed, fetchingNotices, isMobile,
} = this.props; } = this.props;
const menu = ( const menu = (
<Menu className={styles.menu} selectedKeys={[]} onClick={this.handleMenuClick}> <Menu className={styles.menu} selectedKeys={[]} onClick={this.handleMenuClick}>
...@@ -95,6 +97,14 @@ export default class GlobalHeader extends PureComponent { ...@@ -95,6 +97,14 @@ export default class GlobalHeader extends PureComponent {
const noticeData = this.getNoticeData(); const noticeData = this.getNoticeData();
return ( return (
<Header className={styles.header}> <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 <Icon
className={styles.trigger} className={styles.trigger}
type={collapsed ? 'menu-unfold' : 'menu-fold'} type={collapsed ? 'menu-unfold' : 'menu-fold'}
...@@ -146,7 +156,7 @@ export default class GlobalHeader extends PureComponent { ...@@ -146,7 +156,7 @@ export default class GlobalHeader extends PureComponent {
<Dropdown overlay={menu}> <Dropdown overlay={menu}>
<span className={`${styles.action} ${styles.account}`}> <span className={`${styles.action} ${styles.account}`}>
<Avatar size="small" className={styles.avatar} src={currentUser.avatar} /> <Avatar size="small" className={styles.avatar} src={currentUser.avatar} />
{currentUser.name} <span className={styles.name}>{currentUser.name}</span>
</span> </span>
</Dropdown> </Dropdown>
) : <Spin size="small" style={{ marginLeft: 8 }} />} ) : <Spin size="small" style={{ marginLeft: 8 }} />}
......
...@@ -13,6 +13,20 @@ ...@@ -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 { .menu {
:global(.anticon) { :global(.anticon) {
margin-right: 8px; margin-right: 8px;
...@@ -26,19 +40,13 @@ i.trigger { ...@@ -26,19 +40,13 @@ i.trigger {
font-size: 20px; font-size: 20px;
line-height: 64px; line-height: 64px;
cursor: pointer; cursor: pointer;
transition: all .3s; transition: all .3s, padding 0s;
padding: 0 24px; padding: 0 24px;
&:hover { &:hover {
background: @primary-1; background: @primary-1;
} }
} }
@media screen and (max-width: @screen-xs) {
.trigger {
display: none;
}
}
.right { .right {
float: right; float: right;
height: 100%; height: 100%;
...@@ -73,3 +81,32 @@ i.trigger { ...@@ -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;
}
}
}
}
}
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>
);
}
}
import 'rc-drawer-menu/assets/index.css';
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import { Layout, Menu, Icon } from 'antd'; import DrawerMenu from 'rc-drawer-menu';
import { Link } from 'dva/router'; import SiderMenu from './SiderMenu';
import logo from '../../assets/logo.svg';
import styles from './index.less';
import { getMenuData } from '../../common/menu';
const { Sider } = Layout; export default class Index extends PureComponent {
const { SubMenu } = Menu;
export default class SiderMenu extends PureComponent {
constructor(props) {
super(props);
this.menus = getMenuData();
this.state = {
openKeys: this.getDefaultCollapsedSubMenus(props),
};
}
onCollapse = (collapsed) => { onCollapse = (collapsed) => {
this.props.dispatch({ this.props.dispatch({
type: 'global/changeLayoutCollapsed', type: 'global/changeLayoutCollapsed',
payload: collapsed, 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() { render() {
const { collapsed, location: { pathname } } = this.props; const { collapsed, isMobile } = this.props;
// Don't show popup menu when it is been collapsed return isMobile ? (
const menuProps = collapsed ? {} : { <DrawerMenu
openKeys: this.state.openKeys, parent={null}
}; level={null}
return ( iconChild={null}
<Sider open={!collapsed}
trigger={null} onMaskClick={() => { this.onCollapse(true); }}
collapsible width="256px"
collapsed={collapsed}
breakpoint="md"
onCollapse={this.onCollapse}
width={256}
className={styles.sider}
> >
<div className={styles.logo}> <SiderMenu
<Link to="/"> {...this.props}
<img src={logo} alt="logo" /> isMobile={isMobile}
<h1>Ant Design Pro</h1> onCollapse={this.onCollapse}
</Link> collapsed={isMobile ? false : collapsed}
</div> />
<Menu </DrawerMenu>
theme="dark" ) : (
mode="inline" <SiderMenu
{...menuProps} {...this.props}
onOpenChange={this.handleOpenChange} isMobile={isMobile}
selectedKeys={this.getSelectedMenuKeys(pathname)} onCollapse={this.onCollapse}
style={{ padding: '16px 0', width: '100%' }} />
>
{this.getNavMenuItems(this.menus)}
</Menu>
</Sider>
); );
} }
} }
@import "~antd/lib/style/themes/default.less"; @import "~antd/lib/style/themes/default.less";
@ease-in-out-circ: cubic-bezier(.78, .14, .15, .86);
.logo { .logo {
height: 64px; height: 64px;
position: relative; position: relative;
...@@ -23,10 +23,14 @@ ...@@ -23,10 +23,14 @@
font-weight: 600; font-weight: 600;
} }
} }
.sider { .sider {
min-height: 100vh; min-height: 100vh;
box-shadow: 2px 0 6px rgba(0, 21, 41, .35); box-shadow: 2px 0 6px rgba(0, 21, 41, .35);
position: relative; position: relative;
z-index: 10; z-index: 10;
} }
:global {
.drawer .drawer-content {
background: #001529;
}
}
...@@ -6,6 +6,7 @@ import { connect } from 'dva'; ...@@ -6,6 +6,7 @@ import { connect } from 'dva';
import { Route, Redirect, Switch } from 'dva/router'; import { Route, Redirect, Switch } from 'dva/router';
import { ContainerQuery } from 'react-container-query'; import { ContainerQuery } from 'react-container-query';
import classNames from 'classnames'; import classNames from 'classnames';
import { enquireScreen } from 'enquire-js';
import GlobalHeader from '../components/GlobalHeader'; import GlobalHeader from '../components/GlobalHeader';
import GlobalFooter from '../components/GlobalFooter'; import GlobalFooter from '../components/GlobalFooter';
import SiderMenu from '../components/SiderMenu'; import SiderMenu from '../components/SiderMenu';
...@@ -55,11 +56,20 @@ const query = { ...@@ -55,11 +56,20 @@ const query = {
}, },
}; };
let isMobile;
enquireScreen((b) => {
isMobile = b;
});
class BasicLayout extends React.PureComponent { class BasicLayout extends React.PureComponent {
static childContextTypes = { static childContextTypes = {
location: PropTypes.object, location: PropTypes.object,
breadcrumbNameMap: PropTypes.object, breadcrumbNameMap: PropTypes.object,
} }
state = {
isMobile,
};
getChildContext() { getChildContext() {
const { location, routerData } = this.props; const { location, routerData } = this.props;
return { return {
...@@ -67,6 +77,13 @@ class BasicLayout extends React.PureComponent { ...@@ -67,6 +77,13 @@ class BasicLayout extends React.PureComponent {
breadcrumbNameMap: routerData, breadcrumbNameMap: routerData,
}; };
} }
componentDidMount() {
enquireScreen((b) => {
this.setState({
isMobile: !!b,
});
});
}
getPageTitle() { getPageTitle() {
const { routerData, location } = this.props; const { routerData, location } = this.props;
const { pathname } = location; const { pathname } = location;
...@@ -86,6 +103,7 @@ class BasicLayout extends React.PureComponent { ...@@ -86,6 +103,7 @@ class BasicLayout extends React.PureComponent {
collapsed={collapsed} collapsed={collapsed}
location={location} location={location}
dispatch={dispatch} dispatch={dispatch}
isMobile={this.state.isMobile}
/> />
<Layout> <Layout>
<GlobalHeader <GlobalHeader
...@@ -94,6 +112,7 @@ class BasicLayout extends React.PureComponent { ...@@ -94,6 +112,7 @@ class BasicLayout extends React.PureComponent {
notices={notices} notices={notices}
collapsed={collapsed} collapsed={collapsed}
dispatch={dispatch} dispatch={dispatch}
isMobile={this.state.isMobile}
/> />
<Content style={{ margin: '24px 24px 0', height: '100%' }}> <Content style={{ margin: '24px 24px 0', height: '100%' }}>
<div style={{ minHeight: 'calc(100vh - 260px)' }}> <div style={{ minHeight: 'calc(100vh - 260px)' }}>
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment