Unverified Commit f67bff39 authored by 偏右's avatar 偏右 Committed by GitHub

GlobalHeader & SiderMenu (#411)

* GlobalHeader & SiderMenu

* Fix login and logout

* Fix test case
parent 3e1ad8fb
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>
);
}
}
......@@ -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;
}
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>
);
}
}
@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;
}
......@@ -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>');
});
});
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>;
}
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,
});
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}`));
}
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>
<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"
navData={navData}
location={location}
dispatch={dispatch}
/>
<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"
<Layout>
<GlobalHeader
currentUser={currentUser}
fetchingNotices={fetchingNotices}
notices={notices}
collapsed={collapsed}
dispatch={dispatch}
/>
</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>
<Content style={{ margin: '24px 24px 0', height: '100%' }}>
<div style={{ minHeight: 'calc(100vh - 260px)' }}>
<Switch>
......
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