BasicLayout.js 11 KB

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