Optimization and rewriting PrimaryLayout.

This commit is contained in:
zuiidea
2018-09-18 22:04:30 +08:00
parent 5aa187334a
commit 4e7795ec7c
22 changed files with 503 additions and 786 deletions
+3 -7
View File
@@ -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 = {
-69
View File
@@ -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,
}
+2 -2
View File
@@ -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)
},
}
+2 -1
View File
@@ -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",
+36 -78
View File
@@ -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 = (
<span>
<Fragment>
{item.icon ? (
<Icon type={item.icon} style={{ marginRight: 4 }} />
) : (
''
)}
) : null}
{item.name}
</span>
</Fragment>
)
return (
<Breadcrumb.Item key={key}>
{pathArray.length - 1 !== key ? (
<Link
to={
addLangPrefix(
pathToRegexp.compile(item.route || '')(paramMap)
) || '#'
}
>
{content}
</Link>
{paths.length - 1 !== key ? (
<Link to={item.route || '#'}>{content}</Link>
) : (
content
)}
</Breadcrumb.Item>
)
})
}
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 (
<div className={styles.bread}>
<Breadcrumb>{breads}</Breadcrumb>
</div>
<Breadcrumb className={styles.bread}>
{this.generateBreadcrumbs(paths)}
</Breadcrumb>
)
}
}
Bread.propTypes = {
menu: PropTypes.array,
location: PropTypes.object,
routeList: PropTypes.array,
}
export default Bread
+16
View File
@@ -0,0 +1,16 @@
.bread {
margin-bottom: 24px;
:global {
.ant-breadcrumb {
display: flex;
align-items: center;
}
}
}
@media (max-width: 767px) {
.bread {
margin-bottom: 12px;
}
}
+39 -76
View File
@@ -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 (
<Layout.Header className={styles.header}>
{isNavbar ? (
<Popover
placement="bottomLeft"
onVisibleChange={switchMenuPopover}
visible={menuPopoverVisible}
overlayClassName={styles.popovermenu}
trigger="click"
content={<Menus {...menusProps} />}
render() {
const { user, collapsed, onCollapseChange } = this.props
return (
<Layout.Header className={styles.header}>
<div
className={styles.button}
onClick={onCollapseChange.bind(this, !collapsed)}
>
<div className={styles.button}>
<Icon type="bars" />
</div>
</Popover>
) : (
<div className={styles.button} onClick={switchSider}>
<Icon
type={classnames({
'menu-unfold': siderFold,
'menu-fold': !siderFold,
'menu-unfold': collapsed,
'menu-fold': !collapsed,
})}
/>
</div>
)}
<div className={styles.rightWarpper}>
<div className={styles.button}>
<Icon type="mail" />
<div className={styles.rightContainer}>
<Menu mode="horizontal" onClick={this.handleClickMenu}>
<SubMenu
title={
<Fragment>
<Icon type="user" />
{user.username}
</Fragment>
}
>
<Menu.Item key="SignOut">
<Trans>Sign out</Trans>
</Menu.Item>
</SubMenu>
</Menu>
</div>
<Menu mode="horizontal" onClick={handleClickMenu}>
<SubMenu
style={{
float: 'right',
}}
title={
<span>
<Icon type="user" />
{user.username}
</span>
}
>
<Menu.Item key="logout">Sign out</Menu.Item>
</SubMenu>
</Menu>
</div>
</Layout.Header>
)
</Layout.Header>
)
}
}
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
+2 -2
View File
@@ -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 {
-189
View File
@@ -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;
}
}
+81 -142
View File
@@ -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'
state = {
openKeys: store.get('openKeys') || [],
}
onOpenChange = openKeys => {
const { menus } = this.props
const rootSubmenuKeys = menus.filter(_ => !_.menuParentId).map(_ => _.id)
const latestOpenKey = openKeys.find(
key => this.state.openKeys.indexOf(key) === -1
)
const levelMap = {}
// 递归生成菜单
const getMenus = (menuTreeN, siderFoldN) => {
return menuTreeN.map(item => {
if (item.children) {
if (item.menuParentId) {
levelMap[item.id] = item.menuParentId
}
return (
<SubMenu
key={item.id}
title={
<span>
{item.icon && <Icon type={item.icon} />}
{(!siderFoldN || !menuTree.includes(item)) && item.name}
</span>
}
>
{getMenus(item.children, siderFoldN)}
</SubMenu>
)
}
let newOpenKeys = openKeys
if (rootSubmenuKeys.indexOf(latestOpenKey) !== -1) {
newOpenKeys = latestOpenKey ? [latestOpenKey] : []
}
this.setState({
openKeys: newOpenKeys,
})
store.set('openKeys', newOpenKeys)
}
generateMenus = data => {
return data.map(item => {
if (item.children) {
return (
<Menu.Item key={item.id}>
<Navlink
to={addLangPrefix(item.route) || '#'}
style={siderFoldN ? { width: 10 } : {}}
>
{item.icon && <Icon type={item.icon} />}
{item.name}
</Navlink>
</Menu.Item>
<SubMenu
key={item.id}
title={
<Fragment>
{item.icon && <Icon type={item.icon} />}
<span>{item.name}</span>
</Fragment>
}
>
{this.generateMenus(item.children)}
</SubMenu>
)
})
}
const menuItems = getMenus(menuTree, siderFold)
// 保持选中
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 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 menuProps = !siderFold
? {
onOpenChange,
openKeys: navOpenKeys,
}
: {}
// 寻找选中路由
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]))
}
}
}
getPath(current)
return result
}
if (currentMenu) {
defaultSelectedKeys = getPathArray(
menuList,
currentMenu,
'menuParentId',
'id'
return (
<Menu.Item key={item.id}>
<Navlink to={addLangPrefix(item.route) || '#'}>
{item.icon && <Icon type={item.icon} />}
<span>{item.name}</span>
</Navlink>
</Menu.Item>
)
}
})
}
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 (
<Menu
{...menuProps}
mode={siderFold ? 'vertical' : 'inline'}
theme={darkTheme ? 'dark' : 'light'}
selectedKeys={defaultSelectedKeys}
className={styles.sidemenu}
mode="inline"
theme={theme}
onOpenChange={this.onOpenChange}
openKeys={this.state.openKeys}
inlineCollapsed={collapsed}
selectedKeys={selectedKeys}
>
{menuItems}
{this.generateMenus(menuTree)}
</Menu>
)
}
}
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
+33 -35
View File
@@ -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 (
<div>
<div className={styles.logo}>
<Layout.Sider
theme={theme}
breakpoint="md"
trigger={null}
collapsible
collapsed={collapsed}
onBreakpoint={onCollapseChange}
>
<div className={styles.logoContainer}>
<img alt="logo" src={config.logoPath} />
{siderFold ? '' : <span>{config.siteName}</span>}
{collapsed ? null : <h1>{config.siteName}</h1>}
</div>
<Menus {...menusProps} />
{!siderFold ? (
<div className={styles.switchtheme}>
<div className={styles.menuContainer}>
<Menus menus={menus} theme={theme} collapsed={collapsed} />
</div>
{collapsed ? null : (
<div className={styles.switchTheme}>
<span>
<Icon type="bulb" />
<Trans>Switch Theme</Trans>
</span>
<Switch
onChange={changeTheme}
defaultChecked={darkTheme}
onChange={onThemeChange.bind(
this,
theme === 'dark' ? 'light' : 'dark'
)}
defaultChecked={theme === 'dark'}
checkedChildren={i18n.t`Dark`}
unCheckedChildren={i18n.t`Light`}
/>
</div>
) : (
''
)}
</div>
</Layout.Sider>
)
}
}
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
+84
View File
@@ -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;
}
}
+1 -2
View File
@@ -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 }
+1 -46
View File
@@ -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 {
+43 -75
View File
@@ -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' })
menus,
collapsed,
onSignOut() {
dispatch({ type: 'app/signOut' })
},
logout() {
dispatch({ type: 'app/logout' })
},
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' })
},
changeOpenKeys(openKeys) {
window.localStorage.setItem(
`${prefix}navOpenKeys`,
JSON.stringify(openKeys)
)
menus,
theme,
collapsed,
onThemeChange(theme) {
dispatch({
type: 'app/handleNavOpenKeys',
payload: { navOpenKeys: openKeys },
type: 'app/handleThemeChange',
payload: theme,
})
},
onCollapseChange(collapsed) {
dispatch({
type: 'app/handleCollapseChange',
payload: collapsed,
})
},
}
const breadProps = {
menuList,
location,
}
return (
<Fragment>
<Layout
className={classnames({
[styles.dark]: darkTheme,
[styles.light]: !darkTheme,
})}
>
{!isNavbar ? (
<Sider trigger={null} collapsible collapsed={siderFold}>
{siderProps.menuList.length === 0 ? null : (
<MyLayout.Sider {...siderProps} />
)}
</Sider>
) : null}
<Layout>
<MyLayout.Sider {...siderProps} />
<Layout
style={{ height: '100vh', overflow: 'scroll' }}
id="PrimaryLayoutContainer"
id="primaryLayout"
>
<Header {...headerProps} />
<Content>
<Bread {...breadProps} />
<Content className={styles.content}>
<Bread routeList={routeList} />
{hasPermission ? children : <Error />}
</Content>
<BackTop
target={() => document.getElementById('PrimaryLayoutContainer')}
className={styles.backTop}
target={() => document.getElementById('primaryLayout')}
/>
<GlobalFooter copyright={config.copyright} />
</Layout>
+18
View File
@@ -0,0 +1,18 @@
@media (max-width: 767px) {
.content {
padding: 12px;
}
.backTop {
right: 20px;
bottom: 20px;
}
}
.backTop {
right: 50px;
}
.content {
padding: 24px;
}
+21 -50
View File
@@ -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
handleThemeChange(state, { payload }) {
store.set('theme', payload)
state.theme = payload
},
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
},
handleNavOpenKeys(state, { payload: navOpenKeys }) {
return {
...state,
...navOpenKeys,
}
handleCollapseChange(state, { payload }) {
store.set('collapsed', payload)
state.collapsed = payload
},
},
}
+1
View File
@@ -44,6 +44,7 @@
display: inline-block;
font-weight: 700;
color: @primary-color;
.text-gradient();
}
}
+1 -1
View File
@@ -1,5 +1,5 @@
const api = {
queryMenuList: '/menus',
queryRouteList: '/routes',
queryUserInfo: '/user',
logoutUser: '/user/logout',
+13
View File
@@ -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;
}
+102 -11
View File
@@ -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
}
+4
View File
@@ -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"