mirror of
https://github.com/zuiidea/antd-admin
synced 2026-10-20 22:43:43 +00:00
Optimization and rewriting PrimaryLayout.
This commit is contained in:
+3
-7
@@ -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 = {
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
.bread {
|
||||
margin-bottom: 24px;
|
||||
|
||||
:global {
|
||||
.ant-breadcrumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.bread {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
@media (max-width: 767px) {
|
||||
.content {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.backTop {
|
||||
right: 20px;
|
||||
bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.backTop {
|
||||
right: 50px;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 24px;
|
||||
}
|
||||
+21
-50
@@ -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
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
display: inline-block;
|
||||
font-weight: 700;
|
||||
color: @primary-color;
|
||||
.text-gradient();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
const api = {
|
||||
queryMenuList: '/menus',
|
||||
queryRouteList: '/routes',
|
||||
|
||||
queryUserInfo: '/user',
|
||||
logoutUser: '/user/logout',
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user