From 4e7795ec7c11f04303a8d0043717846bbbd0a5d1 Mon Sep 17 00:00:00 2001 From: zuiidea Date: Tue, 18 Sep 2018 22:04:30 +0800 Subject: [PATCH] Optimization and rewriting PrimaryLayout. --- mock/_utils.js | 10 +- mock/common.js | 69 ---------- mock/{menu.js => route.js} | 4 +- package.json | 3 +- src/components/Layout/Bread.js | 114 +++++----------- src/components/Layout/Bread.less | 16 +++ src/components/Layout/Header.js | 115 ++++++---------- src/components/Layout/Header.less | 4 +- src/components/Layout/Layout.less | 189 -------------------------- src/components/Layout/Menu.js | 219 +++++++++++------------------- src/components/Layout/Sider.js | 68 +++++----- src/components/Layout/Sider.less | 84 ++++++++++++ src/components/Layout/index.js | 3 +- src/layouts/BaseLayout.js | 47 +------ src/layouts/PrimaryLayout.js | 116 ++++++---------- src/layouts/PrimaryLayout.less | 18 +++ src/models/app.js | 71 +++------- src/pages/login/index.less | 1 + src/services/api.js | 2 +- src/themes/mixin.less | 13 ++ src/utils/index.js | 113 +++++++++++++-- yarn.lock | 4 + 22 files changed, 500 insertions(+), 783 deletions(-) delete mode 100644 mock/common.js rename mock/{menu.js => route.js} (97%) create mode 100644 src/components/Layout/Bread.less delete mode 100644 src/components/Layout/Layout.less create mode 100644 src/components/Layout/Sider.less create mode 100644 src/layouts/PrimaryLayout.less diff --git a/mock/_utils.js b/mock/_utils.js index 9ad83ee2..501eb4cb 100644 --- a/mock/_utils.js +++ b/mock/_utils.js @@ -3,17 +3,13 @@ * @param {array} array An array where all values are objects, like [{key:1},{key:2}]. * @param {string} key The key of the object that needs to be queried. * @param {string} value The value of the object that needs to be queried. - * @return {object|null} Return frist object when query success. + * @return {object|undefined} Return frist object when query success. */ export function queryArray(array, key, value) { if (!Array.isArray(array)) { - return null + return } - const item = array.filter(_ => _[key] === value) - if (item.length) { - return item[0] - } - return null + return array.filter(_ => _[key] === value) } export const Constant = { diff --git a/mock/common.js b/mock/common.js deleted file mode 100644 index f4c26b6a..00000000 --- a/mock/common.js +++ /dev/null @@ -1,69 +0,0 @@ -const Mock = require('mockjs') -const config = { - apiPrefix: '/api/v1', -} - -/** - * Query objects that specify keys and values in an array where all values are objects. - * @param {array} array An array where all values are objects, like [{key:1},{key:2}]. - * @param {string} key The key of the object that needs to be queried. - * @param {string} value The value of the object that needs to be queried. - * @return {object|null} Return frist object when query success. - */ -export function queryArray(array, key, value) { - if (!Array.isArray(array)) { - return null - } - const item = array.filter(_ => _[key] === value) - if (item.length) { - return item[0] - } - return null -} - -const NOTFOUND = { - message: 'Not Found', - documentation_url: 'http://localhost:8000/request', -} - -let postId = 0 -const posts = Mock.mock({ - 'data|100': [ - { - id() { - postId += 1 - return postId + 10000 - }, - 'status|1-2': 1, - title: '@title', - author: '@last', - categories: '@word', - tags: '@word', - 'views|10-200': 1, - 'comments|10-200': 1, - visibility: () => { - return Mock.mock( - '@pick(["Public",' + '"Password protected", ' + '"Private"])' - ) - }, - date: '@dateTime', - image() { - return Mock.Random.image( - '100x100', - Mock.Random.color(), - '#757575', - 'png', - this.author.substr(0, 1) - ) - }, - }, - ], -}).data - -module.exports = { - queryArray, - NOTFOUND, - Mock, - posts, - config, -} diff --git a/mock/menu.js b/mock/route.js similarity index 97% rename from mock/menu.js rename to mock/route.js index 25c663d6..00535ba0 100644 --- a/mock/menu.js +++ b/mock/route.js @@ -87,11 +87,11 @@ const database = [ name: 'Rechartst', icon: 'area-chart', route: '/chart/Recharts', - }, + } ] module.exports = { - [`GET ${ApiPrefix}/menus`](req, res) { + [`GET ${ApiPrefix}/routes`](req, res) { res.status(200).json(database) }, } diff --git a/package.json b/package.json index 5fad8a9e..2612714c 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,8 @@ "react-draft-wysiwyg": "^1.12.13", "react-helmet": "^5.2.0", "react-highcharts": "^16.0.2", - "recharts": "^1.1.0" + "recharts": "^1.1.0", + "store": "^2.0.12" }, "devDependencies": { "@lingui/babel-preset-react": "^2.7.0", diff --git a/src/components/Layout/Bread.js b/src/components/Layout/Bread.js index a5c6f6b0..67cc7d67 100644 --- a/src/components/Layout/Bread.js +++ b/src/components/Layout/Bread.js @@ -1,108 +1,66 @@ -import React, { PureComponent } from 'react' +import React, { PureComponent, Fragment } from 'react' import PropTypes from 'prop-types' import { Breadcrumb, Icon } from 'antd' import Link from 'umi/navlink' -import pathToRegexp from 'path-to-regexp' -import { queryArray, addLangPrefix, pathMatchRegexp, deLangPrefix } from 'utils' +import withRouter from 'umi/withRouter' import { withI18n } from '@lingui/react' -import styles from './Layout.less' +import { pathMatchRegexp, queryAncestors } from 'utils' +import styles from './Bread.less' @withI18n() +@withRouter class Bread extends PureComponent { - render() { - const { menuList, location, i18n } = this.props - // 匹配当前路由 - let pathArray = [] - let current - for (let index in menuList) { - if ( - menuList[index].route && - pathMatchRegexp(menuList[index].route, location.pathname) - ) { - current = menuList[index] - break - } - } - - const getPathArray = item => { - pathArray.unshift(item) - if (item.breadcrumbParentId) { - getPathArray(queryArray(menuList, 'id', item.breadcrumbParentId)) - } - } - - let paramMap = {} - if (!current) { - pathArray.push( - menuList[0] || { - id: 1, - icon: 'laptop', - name: i18n.t`Dashboard`, - } - ) - pathArray.push({ - id: 404, - name: i18n.t`Not Found`, - }) - } else { - getPathArray(current) - - let keys = [] - let values = pathToRegexp(current.route, keys).exec( - deLangPrefix(location.pathname) - ) - if (keys.length) { - keys.forEach((currentValue, index) => { - if (typeof currentValue.name !== 'string') { - return - } - paramMap[currentValue.name] = values[index + 1] - }) - } - } - - // 递归查找父级 - const breads = pathArray.map((item, key) => { + generateBreadcrumbs = paths => { + return paths.map((item, key) => { const content = ( - + {item.icon ? ( - ) : ( - '' - )} + ) : null} {item.name} - + ) + return ( - {pathArray.length - 1 !== key ? ( - - {content} - + {paths.length - 1 !== key ? ( + {content} ) : ( content )} ) }) + } + render() { + const { routeList, location, i18n } = this.props + + // Find a route that matches the pathname. + const currentRoute = routeList.find( + _ => _.route && pathMatchRegexp(_.route, location.pathname) + ) + + // Find the breadcrumb navigation of the current route match and all its ancestors. + const paths = currentRoute + ? queryAncestors(routeList, currentRoute, 'breadcrumbParentId').reverse() + : [ + routeList[0], + { + id: 404, + name: i18n.t`Not Found`, + }, + ] return ( -
- {breads} -
+ + {this.generateBreadcrumbs(paths)} + ) } } Bread.propTypes = { - menu: PropTypes.array, - location: PropTypes.object, + routeList: PropTypes.array, } export default Bread diff --git a/src/components/Layout/Bread.less b/src/components/Layout/Bread.less new file mode 100644 index 00000000..d30a5f14 --- /dev/null +++ b/src/components/Layout/Bread.less @@ -0,0 +1,16 @@ +.bread { + margin-bottom: 24px; + + :global { + .ant-breadcrumb { + display: flex; + align-items: center; + } + } +} + +@media (max-width: 767px) { + .bread { + margin-bottom: 12px; + } +} diff --git a/src/components/Layout/Header.js b/src/components/Layout/Header.js index a16843ea..6ff3a5fe 100644 --- a/src/components/Layout/Header.js +++ b/src/components/Layout/Header.js @@ -1,97 +1,60 @@ -import React from 'react' +import React, { PureComponent, Fragment } from 'react' import PropTypes from 'prop-types' -import { Menu, Icon, Popover, Layout } from 'antd' +import { Menu, Icon, Layout } from 'antd' +import { Trans } from '@lingui/react' import classnames from 'classnames' import styles from './Header.less' -import Menus from './Menu' const { SubMenu } = Menu -const Header = ({ - user, - logout, - switchSider, - siderFold, - isNavbar, - menuPopoverVisible, - location, - switchMenuPopover, - navOpenKeys, - changeOpenKeys, - menuList, -}) => { - let handleClickMenu = e => e.key === 'logout' && logout() - const menusProps = { - menuList, - siderFold: false, - darkTheme: false, - isNavbar, - handleClickNavMenu: switchMenuPopover, - location, - navOpenKeys, - changeOpenKeys, +class Header extends PureComponent { + handleClickMenu = e => { + e.key === 'SignOut' && this.props.onSignOut() } - return ( - - {isNavbar ? ( - } + render() { + const { user, collapsed, onCollapseChange } = this.props + + return ( + +
-
- -
- - ) : ( -
- )} -
-
- + +
+ + + + {user.username} + + } + > + + Sign out + + +
- - - - {user.username} - - } - > - Sign out - - -
- - ) + + ) + } } Header.propTypes = { - menuList: PropTypes.array, + menus: PropTypes.array, user: PropTypes.object, - logout: PropTypes.func, - switchSider: PropTypes.func, - siderFold: PropTypes.bool, - isNavbar: PropTypes.bool, - menuPopoverVisible: PropTypes.bool, - location: PropTypes.object, - switchMenuPopover: PropTypes.func, - navOpenKeys: PropTypes.array, - changeOpenKeys: PropTypes.func, + collapsed: PropTypes.bool, + onSignOut: PropTypes.func, + onCollapseChange: PropTypes.func, } export default Header diff --git a/src/components/Layout/Header.less b/src/components/Layout/Header.less index 707dc1e2..1917977f 100644 --- a/src/components/Layout/Header.less +++ b/src/components/Layout/Header.less @@ -1,6 +1,7 @@ @import '~themes/vars.less'; .header { + padding: 0; :global { .ant-menu-submenu-title { height: 56px; @@ -50,9 +51,8 @@ align-items: center; background-color: #fff; - .rightWarpper { + .rightContainer { display: flex; - padding-right: 16px; } .button { diff --git a/src/components/Layout/Layout.less b/src/components/Layout/Layout.less deleted file mode 100644 index 70a11354..00000000 --- a/src/components/Layout/Layout.less +++ /dev/null @@ -1,189 +0,0 @@ -@import '~themes/vars.less'; - -:global { - .ant-layout-header { - padding: 0; - } - - .ant-layout-sider { - transition: all 0.3s; - - .ant-menu-root { - height: ~'calc(100vh - 144px)'; - overflow-x: hidden; - border-right: none; - - &::-webkit-scrollbar-thumb { - background-color: transparent; - } - - &:hover { - &::-webkit-scrollbar-thumb { - background-color: rgba(0, 0, 0, 0.2); - } - } - } - - .ant-menu { - .ant-menu-item, - .ant-menu-submenu-title { - overflow: hidden; - white-space: normal; - } - } - } - - .ant-layout-content { - padding: 24px; - } - - .ant-layout-footer { - text-align: center; - padding: 0 24px; - height: 40px; - font-size: 12px; - line-height: 40px; - } - - .ant-back-top { - right: 50px; - } - - .ant-switch.ant-switch-large { - line-height: 24px; - height: 26px; - - &:after, - &:before { - width: 22px; - height: 22px; - } - } - - .flex-vertical-center { - display: flex; - align-items: center; - } - - .ant-form-item { - margin-bottom: 12px; - } -} - -.logo { - height: 96px; - display: flex; - align-items: center; - justify-content: center; - - img { - width: 40px; - margin-right: 8px; - } - - span { - vertical-align: text-bottom; - font-size: 16px; - text-transform: uppercase; - display: inline-block; - font-weight: 700; - color: @primary-color; - white-space: nowrap; - :local { - animation: fadeLeftIn 500ms linear; - animation-fill-mode: both; - } - } -} - -.sidemenu { - :global { - .ant-menu-item { - width: 100%; - } - } -} - -.switchtheme { - width: 100%; - position: absolute; - bottom: 0; - height: 48px; - background-color: #fff; - border-top: solid 1px #f8f8f8; - display: flex; - justify-content: space-between; - align-items: center; - padding: 0 16px; - overflow: hidden; - z-index: 9; - transition: all 0.3s; - - span { - white-space: nowrap; - overflow: hidden; - font-size: 12px; - } - - :global { - .anticon { - min-width: 14px; - margin-right: 4px; - font-size: 14px; - } - } -} - -.bread { - margin-bottom: 24px; - - :global { - .ant-breadcrumb { - display: flex; - align-items: center; - } - } -} - -.dark { - .switchtheme { - background-color: #000d18; - border-color: #001629; - } -} - -.light { - :global { - .ant-layout-sider { - background: #fff; - } - } -} -@media (max-width: 767px) { - .bread { - margin-bottom: 12px; - } - - :global { - .ant-layout-content { - padding: 12px; - } - - .ant-back-top { - right: 20px; - bottom: 20px; - } - } -} - -@keyframes fadeLeftIn { - 0% { - transform: translateX(5px); - opacity: 0; - } - - 100% { - transform: translateX(0); - opacity: 1; - } -} diff --git a/src/components/Layout/Menu.js b/src/components/Layout/Menu.js index d85ea53d..e3da4609 100644 --- a/src/components/Layout/Menu.js +++ b/src/components/Layout/Menu.js @@ -1,167 +1,106 @@ -import React, { PureComponent } from 'react' +import React, { PureComponent, Fragment } from 'react' import PropTypes from 'prop-types' import { Menu, Icon } from 'antd' import Navlink from 'umi/navlink' -import { arrayToTree, queryArray, pathMatchRegexp, addLangPrefix } from 'utils' -import styles from './Layout.less' +import withRouter from 'umi/withRouter' +import { + arrayToTree, + queryAncestors, + pathMatchRegexp, + addLangPrefix, +} from 'utils' +import store from 'store' const { SubMenu } = Menu -let openKeysFlag = false + +@withRouter class Menus extends PureComponent { - render() { - const { - siderFold, - darkTheme, - navOpenKeys, - changeOpenKeys, - menuList, - location, - } = this.props - // 生成树状 - const menuTree = arrayToTree( - menuList.filter(_ => _.menuParentId !== '-1'), - 'id', - 'menuParentId' - ) - const levelMap = {} + state = { + openKeys: store.get('openKeys') || [], + } - // 递归生成菜单 - const getMenus = (menuTreeN, siderFoldN) => { - return menuTreeN.map(item => { - if (item.children) { - if (item.menuParentId) { - levelMap[item.id] = item.menuParentId - } - return ( - - {item.icon && } - {(!siderFoldN || !menuTree.includes(item)) && item.name} - - } - > - {getMenus(item.children, siderFoldN)} - - ) - } - return ( - - - {item.icon && } - {item.name} - - - ) - }) - } - const menuItems = getMenus(menuTree, siderFold) + onOpenChange = openKeys => { + const { menus } = this.props + const rootSubmenuKeys = menus.filter(_ => !_.menuParentId).map(_ => _.id) - // 保持选中 - const getAncestorKeys = key => { - let map = {} - const getParent = index => { - const result = [String(levelMap[index])] - if (levelMap[result[0]]) { - result.unshift(getParent(result[0])[0]) - } - return result - } - for (let index in levelMap) { - if ({}.hasOwnProperty.call(levelMap, index)) { - map[index] = getParent(index) - } - } - return map[key] || [] - } + const latestOpenKey = openKeys.find( + key => this.state.openKeys.indexOf(key) === -1 + ) - const onOpenChange = openKeys => { - if (navOpenKeys.length) { - changeOpenKeys([]) - openKeysFlag = true - } - const latestOpenKey = openKeys.find(key => !navOpenKeys.includes(key)) - const latestCloseKey = navOpenKeys.find(key => !openKeys.includes(key)) - let nextOpenKeys = [] - if (latestOpenKey) { - nextOpenKeys = getAncestorKeys(latestOpenKey).concat(latestOpenKey) - } - if (latestCloseKey) { - nextOpenKeys = getAncestorKeys(latestCloseKey) - } - changeOpenKeys(nextOpenKeys) + let newOpenKeys = openKeys + if (rootSubmenuKeys.indexOf(latestOpenKey) !== -1) { + newOpenKeys = latestOpenKey ? [latestOpenKey] : [] } - let menuProps = !siderFold - ? { - onOpenChange, - openKeys: navOpenKeys, - } - : {} + this.setState({ + openKeys: newOpenKeys, + }) + store.set('openKeys', newOpenKeys) + } - // 寻找选中路由 - let currentMenu - let defaultSelectedKeys - for (let item of menuList) { - if (item.route && pathMatchRegexp(item.route, location.pathname)) { - if (!navOpenKeys.length && item.menuParentId && !openKeysFlag) - changeOpenKeys([String(item.menuParentId)]) - currentMenu = item - break - } - } - const getPathArray = (array, current, pid, id) => { - let result = [String(current[id])] - const getPath = item => { - if (item && item[pid]) { - if (item[pid] === '-1') { - result.unshift(String(item['breadcrumbParentId'])) - } else { - result.unshift(String(item[pid])) - getPath(queryArray(array, id, item[pid])) - } - } + generateMenus = data => { + return data.map(item => { + if (item.children) { + return ( + + {item.icon && } + {item.name} + + } + > + {this.generateMenus(item.children)} + + ) } - getPath(current) - return result - } - if (currentMenu) { - defaultSelectedKeys = getPathArray( - menuList, - currentMenu, - 'menuParentId', - 'id' + return ( + + + {item.icon && } + {item.name} + + ) - } + }) + } - if (!defaultSelectedKeys) { - defaultSelectedKeys = ['1'] - } + render() { + const { collapsed, theme, menus, location } = this.props + + // Generating tree-structured data for menu content. + const menuTree = arrayToTree(menus, 'id', 'menuParentId') + + // Find a menu that matches the pathname. + const currentMenu = menus.find( + _ => _.route && pathMatchRegexp(_.route, location.pathname) + ) + + // Find the key that should be selected according to the current menu. + const selectedKeys = currentMenu + ? queryAncestors(menus, currentMenu, 'menuParentId').map(_ => _.id) + : [] return ( - {menuItems} + {this.generateMenus(menuTree)} ) } } + Menus.propTypes = { - menuList: PropTypes.array, - siderFold: PropTypes.bool, - darkTheme: PropTypes.bool, - navOpenKeys: PropTypes.array, - changeOpenKeys: PropTypes.func, - location: PropTypes.object, + menus: PropTypes.array, + collapsed: PropTypes.bool, + theme: PropTypes.string, } export default Menus diff --git a/src/components/Layout/Sider.js b/src/components/Layout/Sider.js index 474b05f5..01b02931 100644 --- a/src/components/Layout/Sider.js +++ b/src/components/Layout/Sider.js @@ -1,69 +1,67 @@ import React, { PureComponent } from 'react' import PropTypes from 'prop-types' -import { Icon, Switch } from 'antd' +import { Icon, Switch, Layout } from 'antd' import { withI18n, Trans } from '@lingui/react' import { config } from 'utils' -import styles from './Layout.less' import Menus from './Menu' +import styles from './Sider.less' @withI18n() class Sider extends PureComponent { render() { const { - siderFold, - darkTheme, - location, - changeTheme, - navOpenKeys, - changeOpenKeys, - menuList, i18n, + menus, + theme, + collapsed, + onThemeChange, + onCollapseChange, } = this.props - const menusProps = { - menuList, - siderFold, - darkTheme, - location, - navOpenKeys, - changeOpenKeys, - } return ( -
-
+ +
logo - {siderFold ? '' : {config.siteName}} + {collapsed ? null :

{config.siteName}

}
- - {!siderFold ? ( -
+
+ +
+ {collapsed ? null : ( +
Switch Theme
- ) : ( - '' )} -
+
) } } Sider.propTypes = { - menuList: PropTypes.array, - siderFold: PropTypes.bool, - darkTheme: PropTypes.bool, - location: PropTypes.object, - changeTheme: PropTypes.func, - navOpenKeys: PropTypes.array, - changeOpenKeys: PropTypes.func, + menus: PropTypes.array, + theme: PropTypes.string, + collapsed: PropTypes.bool, + onThemeChange: PropTypes.func, + onCollapseChange: PropTypes.func, } export default Sider diff --git a/src/components/Layout/Sider.less b/src/components/Layout/Sider.less new file mode 100644 index 00000000..eded13d9 --- /dev/null +++ b/src/components/Layout/Sider.less @@ -0,0 +1,84 @@ +@import '~themes/vars.less'; + +.logoContainer { + height: 96px; + display: flex; + align-items: center; + justify-content: center; + + img { + width: 40px; + margin-right: 8px; + } + + h1 { + vertical-align: text-bottom; + font-size: 16px; + text-transform: uppercase; + display: inline-block; + font-weight: 700; + color: @primary-color; + white-space: nowrap; + margin-bottom: 0; + .text-gradient(); + + :local { + animation: fadeRightIn 300ms @ease-in-out; + animation-fill-mode: both; + } + } +} + +.menuContainer { + height: ~'calc(100vh - 144px)'; + overflow-x: hidden; + border-right: none; + flex: 1; + + &::-webkit-scrollbar-thumb { + background-color: transparent; + } + + &:hover { + &::-webkit-scrollbar-thumb { + background-color: rgba(0, 0, 0, 0.2); + } + } +} + +.switchTheme { + width: 100%; + height: 48px; + display: flex; + justify-content: space-between; + align-items: center; + padding: 0 16px; + overflow: hidden; + transition: all 0.3s; + + span { + white-space: nowrap; + overflow: hidden; + font-size: 12px; + } + + :global { + .anticon { + min-width: 14px; + margin-right: 4px; + font-size: 14px; + } + } +} + +@keyframes fadeLeftIn { + 0% { + transform: translateX(5px); + opacity: 0; + } + + 100% { + transform: translateX(0); + opacity: 1; + } +} diff --git a/src/components/Layout/index.js b/src/components/Layout/index.js index c170d876..3ad0894f 100644 --- a/src/components/Layout/index.js +++ b/src/components/Layout/index.js @@ -2,6 +2,5 @@ import Header from './Header' import Menu from './Menu' import Bread from './Bread' import Sider from './Sider' -import styles from './Layout.less' -export { Header, Menu, Bread, Sider, styles } +export { Header, Menu, Bread, Sider } diff --git a/src/layouts/BaseLayout.js b/src/layouts/BaseLayout.js index 4bd1c810..5ad52f51 100644 --- a/src/layouts/BaseLayout.js +++ b/src/layouts/BaseLayout.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types' import { connect } from 'dva' import { Helmet } from 'react-helmet' import { Loader } from 'components' -import { pathMatchRegexp } from 'utils' +import { queryLayout } from 'utils' import NProgress from 'nprogress' import config from 'utils/config' import withRouter from 'umi/withRouter' @@ -17,51 +17,6 @@ const LayoutMap = { public: PublicLayout, } -/** - * Query which layout should be used for the current path based on the configuration. - * @param {layouts} layouts Layout configuration. - * @param {pathname} pathname Path name to be queried. - * @return {string} Return frist object when query success. - */ -const queryLayout = (layouts, pathname) => { - let result = 'public' - - const isMatch = regepx => { - return regepx instanceof RegExp - ? regepx.test(pathname) - : pathMatchRegexp(regepx, pathname) - } - - for (const item of layouts) { - let include = false - let exlude = false - if (item.include) { - for (const regepx of item.include) { - if (isMatch(regepx)) { - include = true - break - } - } - } - - if (include && item.exlude) { - for (const regepx of item.exlude) { - if (isMatch(regepx)) { - exlude = true - break - } - } - } - - if (include && !exlude) { - result = item.name - break - } - } - - return result -} - @withRouter @connect(({ loading }) => ({ loading })) class BaseLayout extends PureComponent { diff --git a/src/layouts/PrimaryLayout.js b/src/layouts/PrimaryLayout.js index ae0e0471..8b58e094 100644 --- a/src/layouts/PrimaryLayout.js +++ b/src/layouts/PrimaryLayout.js @@ -7,114 +7,82 @@ import { connect } from 'dva' import { MyLayout } from 'components' import { BackTop, Layout } from 'antd' import { GlobalFooter } from 'ant-design-pro' -import { classnames, config, pathMatchRegexp } from 'utils' +import { config, pathMatchRegexp } from 'utils' import Error from '../pages/404' +import styles from './PrimaryLayout.less' -const { Content, Sider } = Layout -const { Header, Bread, styles } = MyLayout -const { prefix } = config +const { Content } = Layout +const { Header, Bread } = MyLayout @withRouter @connect(({ app, loading }) => ({ app, loading })) class PrimaryLayout extends PureComponent { render() { - const { children, dispatch, app, location } = this.props - const { - user, - siderFold, - darkTheme, - isNavbar, - menuPopoverVisible, - navOpenKeys, - menuList, - permissions, - } = app - let { pathname } = location - pathname = pathname.startsWith('/') ? pathname : `/${pathname}` - const current = menuList.filter(item => - pathMatchRegexp(item.route || '', pathname) + const { app, location, dispatch, children } = this.props + const { user, theme, routeList, permissions, collapsed } = app + + // Find a route that matches the pathname. + const currentRoute = routeList.find( + _ => _.route && pathMatchRegexp(_.route, location.pathname) ) - const hasPermission = current.length - ? permissions.visit.includes(current[0].id) + + // Query whether you have permission to enter this page + const hasPermission = currentRoute + ? permissions.visit.includes(currentRoute.id) : false + // MenuParentId is equal to -1 is not a available menu. + const menus = routeList.filter(_ => _.menuParentId !== '-1') + const headerProps = { - menuList, user, - location, - siderFold, - isNavbar, - menuPopoverVisible, - navOpenKeys, - switchMenuPopover() { - dispatch({ type: 'app/switchMenuPopver' }) - }, - logout() { - dispatch({ type: 'app/logout' }) + menus, + collapsed, + onSignOut() { + dispatch({ type: 'app/signOut' }) }, - switchSider() { - dispatch({ type: 'app/switchSider' }) - }, - changeOpenKeys(openKeys) { + onCollapseChange(collapsed) { dispatch({ - type: 'app/handleNavOpenKeys', - payload: { navOpenKeys: openKeys }, + type: 'app/handleCollapseChange', + payload: collapsed, }) }, } const siderProps = { - menuList, - location, - siderFold, - darkTheme, - navOpenKeys, - changeTheme() { - dispatch({ type: 'app/switchTheme' }) + menus, + theme, + collapsed, + onThemeChange(theme) { + dispatch({ + type: 'app/handleThemeChange', + payload: theme, + }) }, - changeOpenKeys(openKeys) { - window.localStorage.setItem( - `${prefix}navOpenKeys`, - JSON.stringify(openKeys) - ) + onCollapseChange(collapsed) { dispatch({ - type: 'app/handleNavOpenKeys', - payload: { navOpenKeys: openKeys }, + type: 'app/handleCollapseChange', + payload: collapsed, }) }, } - const breadProps = { - menuList, - location, - } - return ( - - {!isNavbar ? ( - - {siderProps.menuList.length === 0 ? null : ( - - )} - - ) : null} + +
- - + + {hasPermission ? children : } document.getElementById('PrimaryLayoutContainer')} + className={styles.backTop} + target={() => document.getElementById('primaryLayout')} /> diff --git a/src/layouts/PrimaryLayout.less b/src/layouts/PrimaryLayout.less new file mode 100644 index 00000000..bd157d61 --- /dev/null +++ b/src/layouts/PrimaryLayout.less @@ -0,0 +1,18 @@ +@media (max-width: 767px) { + .content { + padding: 12px; + } + + .backTop { + right: 20px; + bottom: 20px; + } +} + +.backTop { + right: 50px; +} + +.content { + padding: 24px; +} diff --git a/src/models/app.js b/src/models/app.js index 04e39e69..8d0389c5 100644 --- a/src/models/app.js +++ b/src/models/app.js @@ -5,11 +5,11 @@ import { router } from 'utils' import { parse, stringify } from 'qs' -import config from 'config' +import store from 'store' import { RoleType } from 'utils/constant' -import { queryMenuList, logoutUser, queryUserInfo } from 'api' - -const { prefix } = config +import { queryLayout } from 'utils' +import { queryRouteList, logoutUser, queryUserInfo } from 'api' +import config from 'config' export default { namespace: 'app', @@ -18,7 +18,7 @@ export default { permissions: { visit: [], }, - menuList: [ + routeList: [ { id: 1, icon: 'laptop', @@ -26,14 +26,10 @@ export default { router: '/dashboard', }, ], - menuPopoverVisible: false, - siderFold: window.localStorage.getItem(`${prefix}siderFold`) === 'true', - darkTheme: window.localStorage.getItem(`${prefix}darkTheme`) === 'true', - isNavbar: document.body.clientWidth < 769, - navOpenKeys: - JSON.parse(window.localStorage.getItem(`${prefix}navOpenKeys`)) || [], locationPathname: '', locationQuery: {}, + theme: store.get('theme') || 'light', + collapsed: store.get('collapsed') || false, }, subscriptions: { setupHistory({ dispatch, history }) { @@ -63,17 +59,18 @@ export default { *query({ payload }, { call, put, select }) { const { success, user } = yield call(queryUserInfo, payload) const { locationPathname } = yield select(_ => _.app) + if (success && user) { - const { list } = yield call(queryMenuList) + const { list } = yield call(queryRouteList) const { permissions } = user - let menuList = list + let routeList = list if ( permissions.role === RoleType.ADMIN || permissions.role === RoleType.DEVELOPER ) { permissions.visit = list.map(item => item.id) } else { - menuList = list.filter(item => { + routeList = list.filter(item => { const cases = [ permissions.visit.includes(item.id), item.mpid @@ -89,7 +86,7 @@ export default { payload: { user, permissions, - menuList, + routeList, }, }) if (location.pathname === '/login') { @@ -97,10 +94,7 @@ export default { pathname: '/dashboard', }) } - } else if ( - config.openPages && - config.openPages.indexOf(locationPathname) < 0 - ) { + } else if (queryLayout(config.layouts, locationPathname) !== 'public') { router.push({ pathname: '/login', search: stringify({ @@ -110,8 +104,8 @@ export default { } }, - *logout({ payload }, { call, put }) { - const data = yield call(logoutUser, parse(payload)) + *signOut({ payload }, { call, put }) { + const data = yield call(logoutUser) if (data.success) { yield put({ type: 'updateState', @@ -133,14 +127,6 @@ export default { throw data } }, - - *changeNavbar(action, { put, select }) { - const { app } = yield select(_ => _) - const isNavbar = document.body.clientWidth < 769 - if (isNavbar !== app.isNavbar) { - yield put({ type: 'handleNavbar', payload: isNavbar }) - } - }, }, reducers: { updateState(state, { payload }) { @@ -150,29 +136,14 @@ export default { } }, - switchSider(state) { - window.localStorage.setItem(`${prefix}siderFold`, !state.siderFold) - state.siderFold = !state.siderFold - }, - - switchTheme(state) { - window.localStorage.setItem(`${prefix}darkTheme`, !state.darkTheme) - state.darkTheme = !state.darkTheme - }, - - switchMenuPopver(state) { - state.menuPopoverVisible = !state.menuPopoverVisible - }, - - handleNavbar(state, { payload }) { - state.isNavbar = payload + handleThemeChange(state, { payload }) { + store.set('theme', payload) + state.theme = payload }, - handleNavOpenKeys(state, { payload: navOpenKeys }) { - return { - ...state, - ...navOpenKeys, - } + handleCollapseChange(state, { payload }) { + store.set('collapsed', payload) + state.collapsed = payload }, }, } diff --git a/src/pages/login/index.less b/src/pages/login/index.less index 4b8d9020..d7642d65 100644 --- a/src/pages/login/index.less +++ b/src/pages/login/index.less @@ -44,6 +44,7 @@ display: inline-block; font-weight: 700; color: @primary-color; + .text-gradient(); } } diff --git a/src/services/api.js b/src/services/api.js index f372b593..14c9d414 100644 --- a/src/services/api.js +++ b/src/services/api.js @@ -1,5 +1,5 @@ const api = { - queryMenuList: '/menus', + queryRouteList: '/routes', queryUserInfo: '/user', logoutUser: '/user/logout', diff --git a/src/themes/mixin.less b/src/themes/mixin.less index 8e5a36e6..d841ab31 100644 --- a/src/themes/mixin.less +++ b/src/themes/mixin.less @@ -13,3 +13,16 @@ text-overflow: ellipsis; overflow: hidden; } + +.text-gradient { + background-image: -webkit-gradient( + linear, + 37.219838% 34.532506%, + 36.425669% 93.178216%, + from(#29cdff), + to(#0a60ff), + color-stop(0.37, #148eff) + ); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; +} diff --git a/src/utils/index.js b/src/utils/index.js index 60a1e786..61949eb7 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -15,31 +15,27 @@ export const { languages, defaultLanguage } = i18n * @param {array} array An array where all values are objects, like [{key:1},{key:2}]. * @param {string} key The key of the object that needs to be queried. * @param {string} value The value of the object that needs to be queried. - * @return {object|null} Return frist object when query success. + * @return {object|undefined} Return frist object when query success. */ export function queryArray(array, key, value) { if (!Array.isArray(array)) { - return null + return } - const item = array.filter(_ => _[key] === value) - if (item.length) { - return item[0] - } - return null + return array.find(_ => _[key] === value) } /** * Convert an array to a tree-structured array. * @param {array} array The Array need to Converted. * @param {string} id The alias of the unique ID of the object in the array. - * @param {string} pid The alias of the parent ID of the object in the array. + * @param {string} parentId The alias of the parent ID of the object in the array. * @param {string} children The alias of children of the object in the array. * @return {array} Return a tree-structured array. */ export function arrayToTree( array, id = 'id', - pid = 'pid', + parentId = 'pid', children = 'children' ) { const result = [] @@ -51,7 +47,7 @@ export function arrayToTree( }) data.forEach(item => { - const hashParent = hash[item[pid]] + const hashParent = hash[item[parentId]] if (hashParent) { !hashParent[children] && (hashParent[children] = []) hashParent[children].push(item) @@ -143,6 +139,101 @@ export const router = myRouter * @param {string} pathname Specify the pathname to match. * @return {array|null} Return the result of the match or null. */ -export const pathMatchRegexp = (regexp, pathname) => { +export function pathMatchRegexp(regexp, pathname) { return pathToRegexp(regexp).exec(deLangPrefix(pathname)) } + +/** + * In an array object, traverse all parent IDs based on the value of an object. + * @param {array} array The Array need to Converted. + * @param {string} current Specify the value of the object that needs to be queried. + * @param {string} parentId The alias of the parent ID of the object in the array. + * @param {string} id The alias of the unique ID of the object in the array. + * @return {array} Return a key array. + */ +export function queryPathKeys(array, current, parentId, id = 'id') { + const result = [current] + const hashMap = new Map() + array.forEach(item => hashMap.set(item[id], item)) + + const getPath = current => { + const currentParentId = hashMap.get(current)[parentId] + if (currentParentId) { + result.push(currentParentId) + getPath(currentParentId) + } + } + + getPath(current) + return result +} + +/** + * In an array of objects, specify an object that traverses the objects whose parent ID matches. + * @param {array} array The Array need to Converted. + * @param {string} current Specify the object that needs to be queried. + * @param {string} parentId The alias of the parent ID of the object in the array. + * @param {string} id The alias of the unique ID of the object in the array. + * @return {array} Return a key array. + */ +export function queryAncestors(array, current, parentId, id = 'id') { + const result = [current] + const hashMap = new Map() + array.forEach(item => hashMap.set(item[id], item)) + + const getPath = current => { + const currentParentId = hashMap.get(current[id])[parentId] + if (currentParentId) { + result.push(hashMap.get(currentParentId)) + getPath(hashMap.get(currentParentId)) + } + } + + getPath(current) + return result +} + +/** + * Query which layout should be used for the current path based on the configuration. + * @param {layouts} layouts Layout configuration. + * @param {pathname} pathname Path name to be queried. + * @return {string} Return frist object when query success. + */ +export function queryLayout(layouts, pathname) { + let result = 'public' + + const isMatch = regepx => { + return regepx instanceof RegExp + ? regepx.test(pathname) + : pathMatchRegexp(regepx, pathname) + } + + for (const item of layouts) { + let include = false + let exlude = false + if (item.include) { + for (const regepx of item.include) { + if (isMatch(regepx)) { + include = true + break + } + } + } + + if (include && item.exlude) { + for (const regepx of item.exlude) { + if (isMatch(regepx)) { + exlude = true + break + } + } + } + + if (include && !exlude) { + result = item.name + break + } + } + + return result +} diff --git a/yarn.lock b/yarn.lock index e9703b56..d76fe63e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11577,6 +11577,10 @@ stealthy-require@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b" +store@^2.0.12: + version "2.0.12" + resolved "https://registry.yarnpkg.com/store/-/store-2.0.12.tgz#8c534e2a0b831f72b75fc5f1119857c44ef5d593" + stream-browserify@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.1.tgz#66266ee5f9bdb9940a4e4514cafb43bb71e5c9db"