初始化 antd-pro
This commit is contained in:
166
admin-web/src/components/SiderMenu/BaseMenu.js
Normal file
166
admin-web/src/components/SiderMenu/BaseMenu.js
Normal file
@@ -0,0 +1,166 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { Menu, Icon } from 'antd';
|
||||
import Link from 'umi/link';
|
||||
import { urlToList } from '../_utils/pathTools';
|
||||
import { getMenuMatches } from './SiderMenuUtils';
|
||||
import { isUrl } from '@/utils/utils';
|
||||
import styles from './index.less';
|
||||
import IconFont from '@/components/IconFont';
|
||||
|
||||
const { SubMenu } = Menu;
|
||||
|
||||
// Allow menu.js config icon as string or ReactNode
|
||||
// icon: 'setting',
|
||||
// icon: 'icon-geren' #For Iconfont ,
|
||||
// icon: 'http://demo.com/icon.png',
|
||||
// icon: <Icon type="setting" />,
|
||||
const getIcon = icon => {
|
||||
if (typeof icon === 'string') {
|
||||
if (isUrl(icon)) {
|
||||
return <Icon component={() => <img src={icon} alt="icon" className={styles.icon} />} />;
|
||||
}
|
||||
if (icon.startsWith('icon-')) {
|
||||
return <IconFont type={icon} />;
|
||||
}
|
||||
return <Icon type={icon} />;
|
||||
}
|
||||
return icon;
|
||||
};
|
||||
|
||||
export default class BaseMenu extends PureComponent {
|
||||
/**
|
||||
* 获得菜单子节点
|
||||
* @memberof SiderMenu
|
||||
*/
|
||||
getNavMenuItems = (menusData, parent) => {
|
||||
if (!menusData) {
|
||||
return [];
|
||||
}
|
||||
return menusData
|
||||
.filter(item => item.name && !item.hideInMenu)
|
||||
.map(item => this.getSubMenuOrItem(item, parent))
|
||||
.filter(item => item);
|
||||
};
|
||||
|
||||
// Get the currently selected menu
|
||||
getSelectedMenuKeys = pathname => {
|
||||
const { flatMenuKeys } = this.props;
|
||||
return urlToList(pathname).map(itemPath => getMenuMatches(flatMenuKeys, itemPath).pop());
|
||||
};
|
||||
|
||||
/**
|
||||
* get SubMenu or Item
|
||||
*/
|
||||
getSubMenuOrItem = item => {
|
||||
// doc: add hideChildrenInMenu
|
||||
if (item.children && !item.hideChildrenInMenu && item.children.some(child => child.name)) {
|
||||
const { name } = item;
|
||||
return (
|
||||
<SubMenu
|
||||
title={
|
||||
item.icon ? (
|
||||
<span>
|
||||
{getIcon(item.icon)}
|
||||
<span>{name}</span>
|
||||
</span>
|
||||
) : (
|
||||
name
|
||||
)
|
||||
}
|
||||
key={item.path}
|
||||
>
|
||||
{this.getNavMenuItems(item.children)}
|
||||
</SubMenu>
|
||||
);
|
||||
}
|
||||
return <Menu.Item key={item.path}>{this.getMenuItemPath(item)}</Menu.Item>;
|
||||
};
|
||||
|
||||
/**
|
||||
* 判断是否是http链接.返回 Link 或 a
|
||||
* Judge whether it is http link.return a or Link
|
||||
* @memberof SiderMenu
|
||||
*/
|
||||
getMenuItemPath = item => {
|
||||
const { name } = item;
|
||||
const itemPath = this.conversionPath(item.path);
|
||||
const icon = getIcon(item.icon);
|
||||
const { target } = item;
|
||||
// Is it a http link
|
||||
if (/^https?:\/\//.test(itemPath)) {
|
||||
return (
|
||||
<a href={itemPath} target={target}>
|
||||
{icon}
|
||||
<span>{name}</span>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
const { location, isMobile, onCollapse } = this.props;
|
||||
return (
|
||||
<Link
|
||||
to={itemPath}
|
||||
target={target}
|
||||
replace={itemPath === location.pathname}
|
||||
onClick={
|
||||
isMobile
|
||||
? () => {
|
||||
onCollapse(true);
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{icon}
|
||||
<span>{name}</span>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
conversionPath = path => {
|
||||
if (path && path.indexOf('http') === 0) {
|
||||
return path;
|
||||
}
|
||||
return `/${path || ''}`.replace(/\/+/g, '/');
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
openKeys,
|
||||
theme,
|
||||
mode,
|
||||
location: { pathname },
|
||||
className,
|
||||
collapsed,
|
||||
} = this.props;
|
||||
// if pathname can't match, use the nearest parent's key
|
||||
let selectedKeys = this.getSelectedMenuKeys(pathname);
|
||||
if (!selectedKeys.length && openKeys) {
|
||||
selectedKeys = [openKeys[openKeys.length - 1]];
|
||||
}
|
||||
let props = {};
|
||||
if (openKeys && !collapsed) {
|
||||
props = {
|
||||
openKeys: openKeys.length === 0 ? [...selectedKeys] : openKeys,
|
||||
};
|
||||
}
|
||||
const { handleOpenChange, style, menuData } = this.props;
|
||||
const cls = classNames(className, {
|
||||
'top-nav-menu': mode === 'horizontal',
|
||||
});
|
||||
|
||||
return (
|
||||
<Menu
|
||||
key="Menu"
|
||||
mode={mode}
|
||||
theme={theme}
|
||||
onOpenChange={handleOpenChange}
|
||||
selectedKeys={selectedKeys}
|
||||
style={style}
|
||||
className={cls}
|
||||
{...props}
|
||||
>
|
||||
{this.getNavMenuItems(menuData)}
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
}
|
||||
99
admin-web/src/components/SiderMenu/SiderMenu.js
Normal file
99
admin-web/src/components/SiderMenu/SiderMenu.js
Normal file
@@ -0,0 +1,99 @@
|
||||
import React, { PureComponent, Suspense } from 'react';
|
||||
import { Layout } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import Link from 'umi/link';
|
||||
import styles from './index.less';
|
||||
import PageLoading from '../PageLoading';
|
||||
import { getDefaultCollapsedSubMenus } from './SiderMenuUtils';
|
||||
import { title } from '../../defaultSettings';
|
||||
|
||||
const BaseMenu = React.lazy(() => import('./BaseMenu'));
|
||||
const { Sider } = Layout;
|
||||
|
||||
let firstMount = true;
|
||||
|
||||
export default class SiderMenu extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
openKeys: getDefaultCollapsedSubMenus(props),
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
firstMount = false;
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props, state) {
|
||||
const { pathname, flatMenuKeysLen } = state;
|
||||
if (props.location.pathname !== pathname || props.flatMenuKeys.length !== flatMenuKeysLen) {
|
||||
return {
|
||||
pathname: props.location.pathname,
|
||||
flatMenuKeysLen: props.flatMenuKeys.length,
|
||||
openKeys: getDefaultCollapsedSubMenus(props),
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
isMainMenu = key => {
|
||||
const { menuData } = this.props;
|
||||
return menuData.some(item => {
|
||||
if (key) {
|
||||
return item.key === key || item.path === key;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
};
|
||||
|
||||
handleOpenChange = openKeys => {
|
||||
const moreThanOne = openKeys.filter(openKey => this.isMainMenu(openKey)).length > 1;
|
||||
this.setState({
|
||||
openKeys: moreThanOne ? [openKeys.pop()] : [...openKeys],
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { logo, collapsed, onCollapse, fixSiderbar, theme, isMobile } = this.props;
|
||||
const { openKeys } = this.state;
|
||||
const defaultProps = collapsed ? {} : { openKeys };
|
||||
|
||||
const siderClassName = classNames(styles.sider, {
|
||||
[styles.fixSiderBar]: fixSiderbar,
|
||||
[styles.light]: theme === 'light',
|
||||
});
|
||||
return (
|
||||
<Sider
|
||||
trigger={null}
|
||||
collapsible
|
||||
collapsed={collapsed}
|
||||
breakpoint="lg"
|
||||
onCollapse={collapse => {
|
||||
if (firstMount || !isMobile) {
|
||||
onCollapse(collapse);
|
||||
}
|
||||
}}
|
||||
width={256}
|
||||
theme={theme}
|
||||
className={siderClassName}
|
||||
>
|
||||
<div className={styles.logo} id="logo">
|
||||
<Link to="/">
|
||||
<img src={logo} alt="logo" />
|
||||
<h1>{title}</h1>
|
||||
</Link>
|
||||
</div>
|
||||
<Suspense fallback={<PageLoading />}>
|
||||
<BaseMenu
|
||||
{...this.props}
|
||||
mode="inline"
|
||||
handleOpenChange={this.handleOpenChange}
|
||||
onOpenChange={this.handleOpenChange}
|
||||
style={{ padding: '16px 0', width: '100%' }}
|
||||
{...defaultProps}
|
||||
/>
|
||||
</Suspense>
|
||||
</Sider>
|
||||
);
|
||||
}
|
||||
}
|
||||
39
admin-web/src/components/SiderMenu/SiderMenu.test.js
Normal file
39
admin-web/src/components/SiderMenu/SiderMenu.test.js
Normal file
@@ -0,0 +1,39 @@
|
||||
import { getFlatMenuKeys } from './SiderMenuUtils';
|
||||
|
||||
const menu = [
|
||||
{
|
||||
path: '/dashboard',
|
||||
children: [
|
||||
{
|
||||
path: '/dashboard/name',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/userinfo',
|
||||
children: [
|
||||
{
|
||||
path: '/userinfo/:id',
|
||||
children: [
|
||||
{
|
||||
path: '/userinfo/:id/info',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const flatMenuKeys = getFlatMenuKeys(menu);
|
||||
|
||||
describe('test convert nested menu to flat menu', () => {
|
||||
it('simple menu', () => {
|
||||
expect(flatMenuKeys).toEqual([
|
||||
'/dashboard',
|
||||
'/dashboard/name',
|
||||
'/userinfo',
|
||||
'/userinfo/:id',
|
||||
'/userinfo/:id/info',
|
||||
]);
|
||||
});
|
||||
});
|
||||
40
admin-web/src/components/SiderMenu/SiderMenuUtils.js
Normal file
40
admin-web/src/components/SiderMenu/SiderMenuUtils.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import pathToRegexp from 'path-to-regexp';
|
||||
import { urlToList } from '../_utils/pathTools';
|
||||
|
||||
/**
|
||||
* Recursively flatten the data
|
||||
* [{path:string},{path:string}] => {path,path2}
|
||||
* @param menus
|
||||
*/
|
||||
export const getFlatMenuKeys = menuData => {
|
||||
let keys = [];
|
||||
menuData.forEach(item => {
|
||||
keys.push(item.path);
|
||||
if (item.children) {
|
||||
keys = keys.concat(getFlatMenuKeys(item.children));
|
||||
}
|
||||
});
|
||||
return keys;
|
||||
};
|
||||
|
||||
export const getMenuMatches = (flatMenuKeys, path) =>
|
||||
flatMenuKeys.filter(item => {
|
||||
if (item) {
|
||||
return pathToRegexp(item).test(path);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
/**
|
||||
* 获得菜单子节点
|
||||
* @memberof SiderMenu
|
||||
*/
|
||||
export const getDefaultCollapsedSubMenus = props => {
|
||||
const {
|
||||
location: { pathname },
|
||||
flatMenuKeys,
|
||||
} = props;
|
||||
return urlToList(pathname)
|
||||
.map(item => getMenuMatches(flatMenuKeys, item)[0])
|
||||
.filter(item => item)
|
||||
.reduce((acc, curr) => [...acc, curr], ['/']);
|
||||
};
|
||||
26
admin-web/src/components/SiderMenu/index.js
Normal file
26
admin-web/src/components/SiderMenu/index.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
import { Drawer } from 'antd';
|
||||
import SiderMenu from './SiderMenu';
|
||||
import { getFlatMenuKeys } from './SiderMenuUtils';
|
||||
|
||||
const SiderMenuWrapper = React.memo(props => {
|
||||
const { isMobile, menuData, collapsed, onCollapse } = props;
|
||||
const flatMenuKeys = getFlatMenuKeys(menuData);
|
||||
return isMobile ? (
|
||||
<Drawer
|
||||
visible={!collapsed}
|
||||
placement="left"
|
||||
onClose={() => onCollapse(true)}
|
||||
style={{
|
||||
padding: 0,
|
||||
height: '100vh',
|
||||
}}
|
||||
>
|
||||
<SiderMenu {...props} flatMenuKeys={flatMenuKeys} collapsed={isMobile ? false : collapsed} />
|
||||
</Drawer>
|
||||
) : (
|
||||
<SiderMenu {...props} flatMenuKeys={flatMenuKeys} />
|
||||
);
|
||||
});
|
||||
|
||||
export default SiderMenuWrapper;
|
||||
105
admin-web/src/components/SiderMenu/index.less
Normal file
105
admin-web/src/components/SiderMenu/index.less
Normal file
@@ -0,0 +1,105 @@
|
||||
@import '~antd/lib/style/themes/default.less';
|
||||
|
||||
@nav-header-height: @layout-header-height;
|
||||
|
||||
.logo {
|
||||
position: relative;
|
||||
height: @nav-header-height;
|
||||
padding-left: (@menu-collapsed-width - 32px) / 2;
|
||||
overflow: hidden;
|
||||
line-height: @nav-header-height;
|
||||
background: #002140;
|
||||
transition: all 0.3s;
|
||||
img {
|
||||
display: inline-block;
|
||||
height: 32px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
h1 {
|
||||
display: inline-block;
|
||||
margin: 0 0 0 12px;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
font-size: 20px;
|
||||
font-family: Avenir, 'Helvetica Neue', Arial, Helvetica, sans-serif;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
.sider {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
min-height: 100vh;
|
||||
box-shadow: 2px 0 6px rgba(0, 21, 41, 0.35);
|
||||
&.fixSiderBar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
box-shadow: 2px 0 8px 0 rgba(29, 35, 41, 0.05);
|
||||
:global {
|
||||
.ant-menu-root {
|
||||
height: ~'calc(100vh - @{nav-header-height})';
|
||||
overflow-y: auto;
|
||||
}
|
||||
.ant-menu-inline {
|
||||
border-right: 0;
|
||||
.ant-menu-item,
|
||||
.ant-menu-submenu-title {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
&.light {
|
||||
background-color: white;
|
||||
box-shadow: 2px 0 8px 0 rgba(29, 35, 41, 0.05);
|
||||
.logo {
|
||||
background: white;
|
||||
box-shadow: 1px 1px 0 0 @border-color-split;
|
||||
h1 {
|
||||
color: @primary-color;
|
||||
}
|
||||
}
|
||||
:global(.ant-menu-light) {
|
||||
border-right-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 14px;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
:global {
|
||||
.top-nav-menu li.ant-menu-item {
|
||||
height: @nav-header-height;
|
||||
line-height: @nav-header-height;
|
||||
}
|
||||
.drawer .drawer-content {
|
||||
background: #001529;
|
||||
}
|
||||
.ant-menu-inline-collapsed {
|
||||
& > .ant-menu-item .sider-menu-item-img + span,
|
||||
&
|
||||
> .ant-menu-item-group
|
||||
> .ant-menu-item-group-list
|
||||
> .ant-menu-item
|
||||
.sider-menu-item-img
|
||||
+ span,
|
||||
& > .ant-menu-submenu > .ant-menu-submenu-title .sider-menu-item-img + span {
|
||||
display: inline-block;
|
||||
max-width: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
.ant-menu-item .sider-menu-item-img + span,
|
||||
.ant-menu-submenu-title .sider-menu-item-img + span {
|
||||
opacity: 1;
|
||||
transition: opacity 0.3s @ease-in-out, width 0.3s @ease-in-out;
|
||||
}
|
||||
.ant-drawer-left {
|
||||
.ant-drawer-body {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user