初始化 antd-pro

This commit is contained in:
sin
2019-02-27 11:06:55 +08:00
parent 9f4fdb7f6e
commit b2068ae44b
458 changed files with 28090 additions and 2 deletions

View 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>
);
}
}

View 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>
);
}
}

View 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',
]);
});
});

View 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], ['/']);
};

View 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;

View 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;
}
}
}