BasicLayout.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397
  1. import React from 'react';
  2. import PropTypes from 'prop-types';
  3. import { Layout, Menu, Icon, Avatar, Dropdown, Tag, message, Spin } from 'antd';
  4. import DocumentTitle from 'react-document-title';
  5. import { connect } from 'dva';
  6. import { Link, Route, Redirect, Switch } from 'dva/router';
  7. import moment from 'moment';
  8. import groupBy from 'lodash/groupBy';
  9. import { ContainerQuery } from 'react-container-query';
  10. import classNames from 'classnames';
  11. import styles from './BasicLayout.less';
  12. import HeaderSearch from '../components/HeaderSearch';
  13. import NoticeIcon from '../components/NoticeIcon';
  14. import GlobalFooter from '../components/GlobalFooter';
  15. import { getNavData } from '../common/nav';
  16. import { getRouteData } from '../utils/utils';
  17. const { Header, Sider, Content } = Layout;
  18. const { SubMenu } = Menu;
  19. const query = {
  20. 'screen-xs': {
  21. maxWidth: 575,
  22. },
  23. 'screen-sm': {
  24. minWidth: 576,
  25. maxWidth: 767,
  26. },
  27. 'screen-md': {
  28. minWidth: 768,
  29. maxWidth: 991,
  30. },
  31. 'screen-lg': {
  32. minWidth: 992,
  33. maxWidth: 1199,
  34. },
  35. 'screen-xl': {
  36. minWidth: 1200,
  37. },
  38. };
  39. class BasicLayout extends React.PureComponent {
  40. static childContextTypes = {
  41. location: PropTypes.object,
  42. breadcrumbNameMap: PropTypes.object,
  43. }
  44. constructor(props) {
  45. super(props);
  46. // 把一级 Layout 的 children 作为菜单项
  47. this.menus = getNavData().reduce((arr, current) => arr.concat(current.children), []);
  48. this.state = {
  49. openKeys: this.getDefaultCollapsedSubMenus(props),
  50. };
  51. }
  52. getChildContext() {
  53. const { location } = this.props;
  54. const routeData = getRouteData('BasicLayout');
  55. const firstMenuData = getNavData().reduce((arr, current) => arr.concat(current.children), []);
  56. const menuData = this.getMenuData(firstMenuData, '');
  57. const breadcrumbNameMap = {};
  58. routeData.concat(menuData).forEach((item) => {
  59. breadcrumbNameMap[item.path] = item.name;
  60. });
  61. return { location, breadcrumbNameMap };
  62. }
  63. componentDidMount() {
  64. this.props.dispatch({
  65. type: 'user/fetchCurrent',
  66. });
  67. }
  68. componentWillUnmount() {
  69. clearTimeout(this.resizeTimeout);
  70. }
  71. onCollapse = (collapsed) => {
  72. this.props.dispatch({
  73. type: 'global/changeLayoutCollapsed',
  74. payload: collapsed,
  75. });
  76. }
  77. onMenuClick = ({ key }) => {
  78. if (key === 'logout') {
  79. this.props.dispatch({
  80. type: 'login/logout',
  81. });
  82. }
  83. }
  84. getMenuData = (data, parentPath) => {
  85. let arr = [];
  86. data.forEach((item) => {
  87. if (item.children) {
  88. arr.push({ path: `${parentPath}/${item.path}`, name: item.name });
  89. arr = arr.concat(this.getMenuData(item.children, `${parentPath}/${item.path}`));
  90. }
  91. });
  92. return arr;
  93. }
  94. getDefaultCollapsedSubMenus(props) {
  95. const currentMenuSelectedKeys = [...this.getCurrentMenuSelectedKeys(props)];
  96. currentMenuSelectedKeys.splice(-1, 1);
  97. if (currentMenuSelectedKeys.length === 0) {
  98. return ['dashboard'];
  99. }
  100. return currentMenuSelectedKeys;
  101. }
  102. getCurrentMenuSelectedKeys(props) {
  103. const { location: { pathname } } = props || this.props;
  104. const keys = pathname.split('/').slice(1);
  105. if (keys.length === 1 && keys[0] === '') {
  106. return [this.menus[0].key];
  107. }
  108. return keys;
  109. }
  110. getNavMenuItems(menusData, parentPath = '') {
  111. if (!menusData) {
  112. return [];
  113. }
  114. return menusData.map((item) => {
  115. if (!item.name) {
  116. return null;
  117. }
  118. let itemPath;
  119. if (item.path.indexOf('http') === 0) {
  120. itemPath = item.path;
  121. } else {
  122. itemPath = `${parentPath}/${item.path || ''}`.replace(/\/+/g, '/');
  123. }
  124. if (item.children && item.children.some(child => child.name)) {
  125. return (
  126. <SubMenu
  127. title={
  128. item.icon ? (
  129. <span>
  130. <Icon type={item.icon} />
  131. <span>{item.name}</span>
  132. </span>
  133. ) : item.name
  134. }
  135. key={item.key || item.path}
  136. >
  137. {this.getNavMenuItems(item.children, itemPath)}
  138. </SubMenu>
  139. );
  140. }
  141. const icon = item.icon && <Icon type={item.icon} />;
  142. return (
  143. <Menu.Item key={item.key || item.path}>
  144. {
  145. /^https?:\/\//.test(itemPath) ? (
  146. <a href={itemPath} target={item.target}>
  147. {icon}<span>{item.name}</span>
  148. </a>
  149. ) : (
  150. <Link to={itemPath} target={item.target}>
  151. {icon}<span>{item.name}</span>
  152. </Link>
  153. )
  154. }
  155. </Menu.Item>
  156. );
  157. });
  158. }
  159. getPageTitle() {
  160. const { location } = this.props;
  161. const { pathname } = location;
  162. let title = 'Ant Design Pro';
  163. getRouteData('BasicLayout').forEach((item) => {
  164. if (item.path === pathname) {
  165. title = `${item.name} - Ant Design Pro`;
  166. }
  167. });
  168. return title;
  169. }
  170. getNoticeData() {
  171. const { notices = [] } = this.props;
  172. if (notices.length === 0) {
  173. return {};
  174. }
  175. const newNotices = notices.map((notice) => {
  176. const newNotice = { ...notice };
  177. if (newNotice.datetime) {
  178. newNotice.datetime = moment(notice.datetime).fromNow();
  179. }
  180. // transform id to item key
  181. if (newNotice.id) {
  182. newNotice.key = newNotice.id;
  183. }
  184. if (newNotice.extra && newNotice.status) {
  185. const color = ({
  186. todo: '',
  187. processing: 'blue',
  188. urgent: 'red',
  189. doing: 'gold',
  190. })[newNotice.status];
  191. newNotice.extra = <Tag color={color} style={{ marginRight: 0 }}>{newNotice.extra}</Tag>;
  192. }
  193. return newNotice;
  194. });
  195. return groupBy(newNotices, 'type');
  196. }
  197. handleOpenChange = (openKeys) => {
  198. const lastOpenKey = openKeys[openKeys.length - 1];
  199. const isMainMenu = this.menus.some(
  200. item => (item.key === lastOpenKey || item.path === lastOpenKey)
  201. );
  202. this.setState({
  203. openKeys: isMainMenu ? [lastOpenKey] : [...openKeys],
  204. });
  205. }
  206. toggle = () => {
  207. const { collapsed } = this.props;
  208. this.props.dispatch({
  209. type: 'global/changeLayoutCollapsed',
  210. payload: !collapsed,
  211. });
  212. this.resizeTimeout = setTimeout(() => {
  213. const event = document.createEvent('HTMLEvents');
  214. event.initEvent('resize', true, false);
  215. window.dispatchEvent(event);
  216. }, 600);
  217. }
  218. handleNoticeClear = (type) => {
  219. message.success(`清空了${type}`);
  220. this.props.dispatch({
  221. type: 'global/clearNotices',
  222. payload: type,
  223. });
  224. }
  225. handleNoticeVisibleChange = (visible) => {
  226. if (visible) {
  227. this.props.dispatch({
  228. type: 'global/fetchNotices',
  229. });
  230. }
  231. }
  232. render() {
  233. const { currentUser, collapsed, fetchingNotices } = this.props;
  234. const menu = (
  235. <Menu className={styles.menu} selectedKeys={[]} onClick={this.onMenuClick}>
  236. <Menu.Item disabled><Icon type="user" />个人中心</Menu.Item>
  237. <Menu.Item disabled><Icon type="setting" />设置</Menu.Item>
  238. <Menu.Divider />
  239. <Menu.Item key="logout"><Icon type="logout" />退出登录</Menu.Item>
  240. </Menu>
  241. );
  242. const noticeData = this.getNoticeData();
  243. // Don't show popup menu when it is been collapsed
  244. const menuProps = collapsed ? {} : {
  245. openKeys: this.state.openKeys,
  246. };
  247. const layout = (
  248. <Layout>
  249. <Sider
  250. trigger={null}
  251. collapsible
  252. collapsed={collapsed}
  253. breakpoint="md"
  254. onCollapse={this.onCollapse}
  255. width={256}
  256. className={styles.sider}
  257. >
  258. <div className={styles.logo}>
  259. <Link to="/">
  260. <img src="https://gw.alipayobjects.com/zos/rmsportal/iwWyPinUoseUxIAeElSx.svg" alt="logo" />
  261. <h1>Ant Design Pro</h1>
  262. </Link>
  263. </div>
  264. <Menu
  265. theme="dark"
  266. mode="inline"
  267. {...menuProps}
  268. onOpenChange={this.handleOpenChange}
  269. selectedKeys={this.getCurrentMenuSelectedKeys()}
  270. style={{ margin: '16px 0', width: '100%' }}
  271. >
  272. {this.getNavMenuItems(this.menus)}
  273. </Menu>
  274. </Sider>
  275. <Layout>
  276. <Header className={styles.header}>
  277. <Icon
  278. className={styles.trigger}
  279. type={collapsed ? 'menu-unfold' : 'menu-fold'}
  280. onClick={this.toggle}
  281. />
  282. <div className={styles.right}>
  283. <HeaderSearch
  284. className={`${styles.action} ${styles.search}`}
  285. placeholder="站内搜索"
  286. dataSource={['搜索提示一', '搜索提示二', '搜索提示三']}
  287. onSearch={(value) => {
  288. console.log('input', value); // eslint-disable-line
  289. }}
  290. onPressEnter={(value) => {
  291. console.log('enter', value); // eslint-disable-line
  292. }}
  293. />
  294. <NoticeIcon
  295. className={styles.action}
  296. count={currentUser.notifyCount}
  297. onItemClick={(item, tabProps) => {
  298. console.log(item, tabProps); // eslint-disable-line
  299. }}
  300. onClear={this.handleNoticeClear}
  301. onPopupVisibleChange={this.handleNoticeVisibleChange}
  302. loading={fetchingNotices}
  303. popupAlign={{ offset: [20, -16] }}
  304. >
  305. <NoticeIcon.Tab
  306. list={noticeData['通知']}
  307. title="通知"
  308. emptyText="你已查看所有通知"
  309. emptyImage="https://gw.alipayobjects.com/zos/rmsportal/wAhyIChODzsoKIOBHcBk.svg"
  310. />
  311. <NoticeIcon.Tab
  312. list={noticeData['消息']}
  313. title="消息"
  314. emptyText="您已读完所有消息"
  315. emptyImage="https://gw.alipayobjects.com/zos/rmsportal/sAuJeJzSKbUmHfBQRzmZ.svg"
  316. />
  317. <NoticeIcon.Tab
  318. list={noticeData['待办']}
  319. title="待办"
  320. emptyText="你已完成所有待办"
  321. emptyImage="https://gw.alipayobjects.com/zos/rmsportal/HsIsxMZiWKrNUavQUXqx.svg"
  322. />
  323. </NoticeIcon>
  324. {currentUser.name ? (
  325. <Dropdown overlay={menu}>
  326. <span className={`${styles.action} ${styles.account}`}>
  327. <Avatar size="small" className={styles.avatar} src={currentUser.avatar} />
  328. {currentUser.name}
  329. </span>
  330. </Dropdown>
  331. ) : <Spin size="small" style={{ marginLeft: 8 }} />}
  332. </div>
  333. </Header>
  334. <Content style={{ margin: '24px 24px 0', height: '100%' }}>
  335. <Switch>
  336. {
  337. getRouteData('BasicLayout').map(item =>
  338. (
  339. <Route
  340. exact={item.exact}
  341. key={item.path}
  342. path={item.path}
  343. component={item.component}
  344. />
  345. )
  346. )
  347. }
  348. <Redirect to="/dashboard/analysis" />
  349. </Switch>
  350. <GlobalFooter
  351. links={[{
  352. title: 'Pro 首页',
  353. href: 'http://pro.ant.design',
  354. blankTarget: true,
  355. }, {
  356. title: 'GitHub',
  357. href: 'https://github.com/ant-design/ant-design-pro',
  358. blankTarget: true,
  359. }, {
  360. title: 'Ant Design',
  361. href: 'http://ant.design',
  362. blankTarget: true,
  363. }]}
  364. copyright={
  365. <div>
  366. Copyright <Icon type="copyright" /> 2017 蚂蚁金服体验技术部出品
  367. </div>
  368. }
  369. />
  370. </Content>
  371. </Layout>
  372. </Layout>
  373. );
  374. return (
  375. <DocumentTitle title={this.getPageTitle()}>
  376. <ContainerQuery query={query}>
  377. {params => <div className={classNames(params)}>{layout}</div>}
  378. </ContainerQuery>
  379. </DocumentTitle>
  380. );
  381. }
  382. }
  383. export default connect(state => ({
  384. currentUser: state.user.currentUser,
  385. collapsed: state.global.collapsed,
  386. fetchingNotices: state.global.fetchingNotices,
  387. notices: state.global.notices,
  388. }))(BasicLayout);