Browse Source
* merge v3 to v4 * src/components/IconFont * src/components/PageLoading * src/components/SelectLang * src/components/SettingDrawer * remove e2e and test * src/components/TopNavHeader * src/components/GlobalHeader * src/components/HeaderDropdown * src/components/HeaderSearch * src/components/TopNavHeader * fix error * mock * move defaultSettings * global.txs * src/locales * remove lint mock * fix ci test error * change PureComponent to Component, interface IDefaultSettings * Don't prefix interface with I Close: #3706 * strictNullChecks set truepull/3719/head
committed by
GitHub
89 changed files with 772 additions and 643 deletions
@ -1,13 +1,14 @@ |
|||
// https://umijs.org/config/
|
|||
import os from 'os'; |
|||
import webpackPlugin from './plugin.config'; |
|||
import defaultSettings from '../src/defaultSettings'; |
|||
import slash from 'slash2'; |
|||
import { IPlugin } from 'umi-types'; |
|||
import defaultSettings from './defaultSettings'; |
|||
import webpackPlugin from './plugin.config'; |
|||
|
|||
const { pwa, primaryColor } = defaultSettings; |
|||
const { NODE_ENV, APP_TYPE, TEST } = process.env; |
|||
const { APP_TYPE, TEST } = process.env; |
|||
|
|||
const plugins = [ |
|||
const plugins: IPlugin[] = [ |
|||
[ |
|||
'umi-plugin-react', |
|||
{ |
|||
@ -1,36 +0,0 @@ |
|||
import config from '../config/config'; |
|||
|
|||
const RouterConfig = config.routes; |
|||
|
|||
const BASE_URL = `http://localhost:${process.env.PORT || 8000}`; |
|||
|
|||
function formatter(data) { |
|||
return data |
|||
.reduce((pre, item) => { |
|||
pre.push(item.path); |
|||
return pre; |
|||
}, []) |
|||
.filter(item => item); |
|||
} |
|||
|
|||
describe('Homepage', async () => { |
|||
const testPage = path => async () => { |
|||
await page.goto(`${BASE_URL}${path}`); |
|||
await page.waitForSelector('footer', { |
|||
timeout: 2000, |
|||
}); |
|||
const haveFooter = await page.evaluate( |
|||
() => document.getElementsByTagName('footer').length > 0 |
|||
); |
|||
expect(haveFooter).toBeTruthy(); |
|||
}; |
|||
|
|||
beforeAll(async () => { |
|||
jest.setTimeout(1000000); |
|||
await page.setCacheEnabled(false); |
|||
}); |
|||
const routers = formatter(RouterConfig[1].routes); |
|||
routers.forEach(route => { |
|||
it(`test pages ${route}`, testPage(route)); |
|||
}); |
|||
}); |
|||
@ -1,15 +0,0 @@ |
|||
const BASE_URL = `http://localhost:${process.env.PORT || 8000}`; |
|||
|
|||
describe('Homepage', () => { |
|||
beforeAll(async () => { |
|||
jest.setTimeout(1000000); |
|||
}); |
|||
it('it should have logo text', async () => { |
|||
await page.goto(BASE_URL); |
|||
await page.waitForSelector('h1', { |
|||
timeout: 5000, |
|||
}); |
|||
const text = await page.evaluate(() => document.getElementsByTagName('h1')[0].innerText); |
|||
expect(text).toContain('Ant Design Pro'); |
|||
}); |
|||
}); |
|||
@ -1,18 +0,0 @@ |
|||
const BASE_URL = `http://localhost:${process.env.PORT || 8000}`; |
|||
|
|||
describe('Homepage', () => { |
|||
beforeAll(async () => { |
|||
jest.setTimeout(1000000); |
|||
}); |
|||
it('topmenu should have footer', async () => { |
|||
const params = '/form/basic-form?navTheme=light&layout=topmenu'; |
|||
await page.goto(`${BASE_URL}${params}`); |
|||
await page.waitForSelector('footer', { |
|||
timeout: 2000, |
|||
}); |
|||
const haveFooter = await page.evaluate( |
|||
() => document.getElementsByTagName('footer').length > 0 |
|||
); |
|||
expect(haveFooter).toBeTruthy(); |
|||
}); |
|||
}); |
|||
@ -1,34 +0,0 @@ |
|||
import config from '../config/config'; |
|||
|
|||
const RouterConfig = config.routes; |
|||
|
|||
const BASE_URL = `http://localhost:${process.env.PORT || 8000}`; |
|||
|
|||
function formatter(data) { |
|||
return data |
|||
.reduce((pre, item) => { |
|||
pre.push(item.path); |
|||
return pre; |
|||
}, []) |
|||
.filter(item => item); |
|||
} |
|||
|
|||
describe('Homepage', () => { |
|||
const testPage = path => async () => { |
|||
await page.goto(`${BASE_URL}${path}`); |
|||
await page.waitForSelector('footer', { |
|||
timeout: 2000, |
|||
}); |
|||
const haveFooter = await page.evaluate( |
|||
() => document.getElementsByTagName('footer').length > 0 |
|||
); |
|||
expect(haveFooter).toBeTruthy(); |
|||
}; |
|||
|
|||
beforeAll(async () => { |
|||
jest.setTimeout(1000000); |
|||
}); |
|||
formatter(RouterConfig[0].routes).forEach(route => { |
|||
it(`test pages ${route}`, testPage(route)); |
|||
}); |
|||
}); |
|||
@ -1,22 +1,30 @@ |
|||
import React, { PureComponent } from 'react'; |
|||
import React, { Component } from 'react'; |
|||
import { Icon } from 'antd'; |
|||
import Link from 'umi/link'; |
|||
import Debounce from 'lodash-decorators/debounce'; |
|||
import debounce from 'lodash/debounce'; |
|||
import styles from './index.less'; |
|||
import RightContent from './RightContent'; |
|||
|
|||
export default class GlobalHeader extends PureComponent { |
|||
interface GlobalHeaderProps { |
|||
collapsed?: boolean; |
|||
onCollapse?: (collapsed: boolean) => void; |
|||
isMobile?: boolean; |
|||
logo?: string; |
|||
onNoticeClear?: (type: string) => void; |
|||
onMenuClick?: ({ key: string }) => void; |
|||
onNoticeVisibleChange?: (b: boolean) => void; |
|||
} |
|||
|
|||
export default class GlobalHeader extends Component<GlobalHeaderProps> { |
|||
componentWillUnmount() { |
|||
this.triggerResizeEvent.cancel(); |
|||
} |
|||
/* eslint-disable*/ |
|||
@Debounce(600) |
|||
triggerResizeEvent() { |
|||
triggerResizeEvent = debounce(() => { |
|||
// eslint-disable-line
|
|||
const event = document.createEvent('HTMLEvents'); |
|||
event.initEvent('resize', true, false); |
|||
window.dispatchEvent(event); |
|||
} |
|||
}); |
|||
toggle = () => { |
|||
const { collapsed, onCollapse } = this.props; |
|||
onCollapse(!collapsed); |
|||
@ -1,2 +0,0 @@ |
|||
import * as React from 'react'; |
|||
export default class HeaderDropdown extends React.Component<any, any> {} |
|||
@ -1,13 +0,0 @@ |
|||
import React, { PureComponent } from 'react'; |
|||
import { Dropdown } from 'antd'; |
|||
import classNames from 'classnames'; |
|||
import styles from './index.less'; |
|||
|
|||
export default class HeaderDropdown extends PureComponent { |
|||
render() { |
|||
const { overlayClassName, ...props } = this.props; |
|||
return ( |
|||
<Dropdown overlayClassName={classNames(styles.container, overlayClassName)} {...props} /> |
|||
); |
|||
} |
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
import React, { Component } from 'react'; |
|||
import { Dropdown } from 'antd'; |
|||
import classNames from 'classnames'; |
|||
import styles from './index.less'; |
|||
|
|||
declare type OverlayFunc = () => React.ReactNode; |
|||
|
|||
interface HeaderDropdownProps { |
|||
overlayClassName?: string; |
|||
overlay: React.ReactNode | OverlayFunc; |
|||
placement?: 'bottomLeft' | 'bottomRight' | 'topLeft' | 'topCenter' | 'topRight' | 'bottomCenter'; |
|||
} |
|||
|
|||
export default class HeaderDropdown extends Component<HeaderDropdownProps> { |
|||
render() { |
|||
const { overlayClassName, ...props } = this.props; |
|||
|
|||
return ( |
|||
<Dropdown overlayClassName={classNames(styles.container, overlayClassName)} {...props} /> |
|||
); |
|||
} |
|||
} |
|||
@ -1,15 +0,0 @@ |
|||
import * as React from 'react'; |
|||
export interface IHeaderSearchProps { |
|||
placeholder?: string; |
|||
dataSource?: string[]; |
|||
defaultOpen?: boolean; |
|||
open?: boolean; |
|||
onSearch?: (value: string) => void; |
|||
onChange?: (value: string) => void; |
|||
onVisibleChange?: (visible: boolean) => void; |
|||
onPressEnter?: (value: string) => void; |
|||
style?: React.CSSProperties; |
|||
className?: string; |
|||
} |
|||
|
|||
export default class HeaderSearch extends React.Component<IHeaderSearchProps, any> {} |
|||
@ -1,6 +1,8 @@ |
|||
import { Icon } from 'antd'; |
|||
import { iconfontUrl as scriptUrl } from '../../defaultSettings'; |
|||
import defaultSettings from '../../../config/defaultSettings'; |
|||
|
|||
const { iconfontUrl } = defaultSettings; |
|||
const scriptUrl = iconfontUrl; |
|||
// 使用:
|
|||
// import IconFont from '@/components/IconFont';
|
|||
// <IconFont type='icon-demo' className='xxx-xxx' />
|
|||
@ -1,49 +0,0 @@ |
|||
import React, { PureComponent } from 'react'; |
|||
import { formatMessage, setLocale, getLocale } from 'umi/locale'; |
|||
import { Menu, Icon } from 'antd'; |
|||
import classNames from 'classnames'; |
|||
import HeaderDropdown from '../HeaderDropdown'; |
|||
import styles from './index.less'; |
|||
|
|||
export default class SelectLang extends PureComponent { |
|||
changeLang = ({ key }) => { |
|||
setLocale(key); |
|||
}; |
|||
|
|||
render() { |
|||
const { className } = this.props; |
|||
const selectedLang = getLocale(); |
|||
const locales = ['zh-CN', 'zh-TW', 'en-US', 'pt-BR']; |
|||
const languageLabels = { |
|||
'zh-CN': '简体中文', |
|||
'zh-TW': '繁体中文', |
|||
'en-US': 'English', |
|||
'pt-BR': 'Português', |
|||
}; |
|||
const languageIcons = { |
|||
'zh-CN': '🇨🇳', |
|||
'zh-TW': '🇭🇰', |
|||
'en-US': '🇬🇧', |
|||
'pt-BR': '🇧🇷', |
|||
}; |
|||
const langMenu = ( |
|||
<Menu className={styles.menu} selectedKeys={[selectedLang]} onClick={this.changeLang}> |
|||
{locales.map(locale => ( |
|||
<Menu.Item key={locale}> |
|||
<span role="img" aria-label={languageLabels[locale]}> |
|||
{languageIcons[locale]} |
|||
</span>{' '} |
|||
{languageLabels[locale]} |
|||
</Menu.Item> |
|||
))} |
|||
</Menu> |
|||
); |
|||
return ( |
|||
<HeaderDropdown overlay={langMenu} placement="bottomRight"> |
|||
<span className={classNames(styles.dropDown, className)}> |
|||
<Icon type="global" title={formatMessage({ id: 'navBar.lang' })} /> |
|||
</span> |
|||
</HeaderDropdown> |
|||
); |
|||
} |
|||
} |
|||
@ -0,0 +1,51 @@ |
|||
import React from 'react'; |
|||
import { formatMessage, setLocale, getLocale } from 'umi-plugin-locale'; |
|||
import { Menu, Icon } from 'antd'; |
|||
import classNames from 'classnames'; |
|||
import HeaderDropdown from '../HeaderDropdown'; |
|||
import styles from './index.less'; |
|||
|
|||
interface SelectLangProps { |
|||
className?: string; |
|||
} |
|||
const SelectLang: React.SFC<SelectLangProps> = props => { |
|||
const { className } = props; |
|||
const selectedLang = getLocale(); |
|||
const changeLang = ({ key }) => { |
|||
setLocale(key); |
|||
}; |
|||
const locales = ['zh-CN', 'zh-TW', 'en-US', 'pt-BR']; |
|||
const languageLabels = { |
|||
'zh-CN': '简体中文', |
|||
'zh-TW': '繁体中文', |
|||
'en-US': 'English', |
|||
'pt-BR': 'Português', |
|||
}; |
|||
const languageIcons = { |
|||
'zh-CN': '🇨🇳', |
|||
'zh-TW': '🇭🇰', |
|||
'en-US': '🇬🇧', |
|||
'pt-BR': '🇧🇷', |
|||
}; |
|||
const langMenu = ( |
|||
<Menu className={styles.menu} selectedKeys={[selectedLang]} onClick={changeLang}> |
|||
{locales.map(locale => ( |
|||
<Menu.Item key={locale}> |
|||
<span role="img" aria-label={languageLabels[locale]}> |
|||
{languageIcons[locale]} |
|||
</span>{' '} |
|||
{languageLabels[locale]} |
|||
</Menu.Item> |
|||
))} |
|||
</Menu> |
|||
); |
|||
return ( |
|||
<HeaderDropdown overlay={langMenu} placement="bottomRight"> |
|||
<span className={classNames(styles.dropDown, className)}> |
|||
<Icon type="global" title={formatMessage({ id: 'navBar.lang' })} /> |
|||
</span> |
|||
</HeaderDropdown> |
|||
); |
|||
}; |
|||
|
|||
export default SelectLang; |
|||
@ -1,39 +0,0 @@ |
|||
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', |
|||
]); |
|||
}); |
|||
}); |
|||
@ -1,16 +1,52 @@ |
|||
import React, { PureComponent } from 'react'; |
|||
import React, { Component } from 'react'; |
|||
import Link from 'umi/link'; |
|||
import RightContent from '../GlobalHeader/RightContent'; |
|||
import BaseMenu from '../SiderMenu/BaseMenu'; |
|||
import { getFlatMenuKeys } from '../SiderMenu/SiderMenuUtils'; |
|||
import styles from './index.less'; |
|||
import { title } from '../../defaultSettings'; |
|||
import defaultSettings from '../../../config/defaultSettings'; |
|||
|
|||
export default class TopNavHeader extends PureComponent { |
|||
export declare type CollapseType = 'clickTrigger' | 'responsive'; |
|||
export declare type SiderTheme = 'light' | 'dark'; |
|||
export declare type MenuMode = |
|||
| 'vertical' |
|||
| 'vertical-left' |
|||
| 'vertical-right' |
|||
| 'horizontal' |
|||
| 'inline'; |
|||
|
|||
const { title } = defaultSettings; |
|||
interface TopNavHeaderProps { |
|||
theme: SiderTheme; |
|||
contentWidth?: string; |
|||
menuData?: any[]; |
|||
logo?: string; |
|||
mode?: MenuMode; |
|||
flatMenuKeys?: any[]; |
|||
onCollapse?: (collapsed: boolean, type?: CollapseType) => void; |
|||
isMobile?: boolean; |
|||
openKeys?: any; |
|||
className?: string; |
|||
collapsed?: boolean; |
|||
handleOpenChange?: (openKeys: any[]) => void; |
|||
style?: React.CSSProperties; |
|||
onOpenChange?: (openKeys: string[]) => void; |
|||
onNoticeClear?: (type: string) => void; |
|||
onMenuClick?: ({ key: string }) => void; |
|||
onNoticeVisibleChange?: (b: boolean) => void; |
|||
} |
|||
|
|||
interface TopNavHeaderState { |
|||
maxWidth: undefined | number; |
|||
} |
|||
|
|||
export default class TopNavHeader extends Component<TopNavHeaderProps, TopNavHeaderState> { |
|||
state = { |
|||
maxWidth: undefined, |
|||
}; |
|||
|
|||
maim: HTMLDivElement; |
|||
|
|||
static getDerivedStateFromProps(props) { |
|||
return { |
|||
maxWidth: (props.contentWidth === 'Fixed' ? 1200 : window.innerWidth) - 280 - 165 - 40, |
|||
@ -1,17 +0,0 @@ |
|||
import { urlToList } from './pathTools'; |
|||
|
|||
describe('test urlToList', () => { |
|||
it('A path', () => { |
|||
expect(urlToList('/userinfo')).toEqual(['/userinfo']); |
|||
}); |
|||
it('Secondary path', () => { |
|||
expect(urlToList('/userinfo/2144')).toEqual(['/userinfo', '/userinfo/2144']); |
|||
}); |
|||
it('Three paths', () => { |
|||
expect(urlToList('/userinfo/2144/addr')).toEqual([ |
|||
'/userinfo', |
|||
'/userinfo/2144', |
|||
'/userinfo/2144/addr', |
|||
]); |
|||
}); |
|||
}); |
|||
@ -1,173 +0,0 @@ |
|||
import React, { Suspense } from 'react'; |
|||
import { Layout } from 'antd'; |
|||
import DocumentTitle from 'react-document-title'; |
|||
import { connect } from 'dva'; |
|||
import { ContainerQuery } from 'react-container-query'; |
|||
import classNames from 'classnames'; |
|||
import Media from 'react-media'; |
|||
import logo from '../assets/logo.svg'; |
|||
import Footer from './Footer'; |
|||
import Header from './Header'; |
|||
import Context from './MenuContext'; |
|||
import PageLoading from '@/components/PageLoading'; |
|||
import SiderMenu from '@/components/SiderMenu'; |
|||
import getPageTitle from '@/utils/getPageTitle'; |
|||
import styles from './BasicLayout.less'; |
|||
|
|||
// lazy load SettingDrawer
|
|||
const SettingDrawer = React.lazy(() => import('@/components/SettingDrawer')); |
|||
|
|||
const { Content } = Layout; |
|||
|
|||
const query = { |
|||
'screen-xs': { |
|||
maxWidth: 575, |
|||
}, |
|||
'screen-sm': { |
|||
minWidth: 576, |
|||
maxWidth: 767, |
|||
}, |
|||
'screen-md': { |
|||
minWidth: 768, |
|||
maxWidth: 991, |
|||
}, |
|||
'screen-lg': { |
|||
minWidth: 992, |
|||
maxWidth: 1199, |
|||
}, |
|||
'screen-xl': { |
|||
minWidth: 1200, |
|||
maxWidth: 1599, |
|||
}, |
|||
'screen-xxl': { |
|||
minWidth: 1600, |
|||
}, |
|||
}; |
|||
|
|||
class BasicLayout extends React.Component { |
|||
componentDidMount() { |
|||
const { |
|||
dispatch, |
|||
route: { routes, authority }, |
|||
} = this.props; |
|||
dispatch({ |
|||
type: 'user/fetchCurrent', |
|||
}); |
|||
dispatch({ |
|||
type: 'setting/getSetting', |
|||
}); |
|||
dispatch({ |
|||
type: 'menu/getMenuData', |
|||
payload: { routes, authority }, |
|||
}); |
|||
} |
|||
|
|||
getContext() { |
|||
const { location, breadcrumbNameMap } = this.props; |
|||
return { |
|||
location, |
|||
breadcrumbNameMap, |
|||
}; |
|||
} |
|||
|
|||
getLayoutStyle = () => { |
|||
const { fixSiderbar, isMobile, collapsed, layout } = this.props; |
|||
if (fixSiderbar && layout !== 'topmenu' && !isMobile) { |
|||
return { |
|||
paddingLeft: collapsed ? '80px' : '256px', |
|||
}; |
|||
} |
|||
return null; |
|||
}; |
|||
|
|||
handleMenuCollapse = collapsed => { |
|||
const { dispatch } = this.props; |
|||
dispatch({ |
|||
type: 'global/changeLayoutCollapsed', |
|||
payload: collapsed, |
|||
}); |
|||
}; |
|||
|
|||
renderSettingDrawer = () => { |
|||
// Do not render SettingDrawer in production
|
|||
// unless it is deployed in preview.pro.ant.design as demo
|
|||
if (process.env.NODE_ENV === 'production' && APP_TYPE !== 'site') { |
|||
return null; |
|||
} |
|||
return <SettingDrawer />; |
|||
}; |
|||
|
|||
render() { |
|||
const { |
|||
navTheme, |
|||
layout: PropsLayout, |
|||
children, |
|||
location: { pathname }, |
|||
isMobile, |
|||
menuData, |
|||
breadcrumbNameMap, |
|||
fixedHeader, |
|||
} = this.props; |
|||
|
|||
const isTop = PropsLayout === 'topmenu'; |
|||
const contentStyle = !fixedHeader ? { paddingTop: 0 } : {}; |
|||
const layout = ( |
|||
<Layout> |
|||
{isTop && !isMobile ? null : ( |
|||
<SiderMenu |
|||
logo={logo} |
|||
theme={navTheme} |
|||
onCollapse={this.handleMenuCollapse} |
|||
menuData={menuData} |
|||
isMobile={isMobile} |
|||
{...this.props} |
|||
/> |
|||
)} |
|||
<Layout |
|||
style={{ |
|||
...this.getLayoutStyle(), |
|||
minHeight: '100vh', |
|||
}} |
|||
> |
|||
<Header |
|||
menuData={menuData} |
|||
handleMenuCollapse={this.handleMenuCollapse} |
|||
logo={logo} |
|||
isMobile={isMobile} |
|||
{...this.props} |
|||
/> |
|||
<Content className={styles.content} style={contentStyle}> |
|||
{children} |
|||
</Content> |
|||
<Footer /> |
|||
</Layout> |
|||
</Layout> |
|||
); |
|||
return ( |
|||
<React.Fragment> |
|||
<DocumentTitle title={getPageTitle(pathname, breadcrumbNameMap)}> |
|||
<ContainerQuery query={query}> |
|||
{params => ( |
|||
<Context.Provider value={this.getContext()}> |
|||
<div className={classNames(params)}>{layout}</div> |
|||
</Context.Provider> |
|||
)} |
|||
</ContainerQuery> |
|||
</DocumentTitle> |
|||
<Suspense fallback={<PageLoading />}>{this.renderSettingDrawer()}</Suspense> |
|||
</React.Fragment> |
|||
); |
|||
} |
|||
} |
|||
|
|||
export default connect(({ global, setting, menu: menuModel }) => ({ |
|||
collapsed: global.collapsed, |
|||
layout: setting.layout, |
|||
menuData: menuModel.menuData, |
|||
breadcrumbNameMap: menuModel.breadcrumbNameMap, |
|||
...setting, |
|||
}))(props => ( |
|||
<Media query="(max-width: 599px)"> |
|||
{isMobile => <BasicLayout {...props} isMobile={isMobile} />} |
|||
</Media> |
|||
)); |
|||
@ -0,0 +1,153 @@ |
|||
import PageLoading from '@/components/PageLoading'; |
|||
import SiderMenu from '@/components/SiderMenu'; |
|||
import getPageTitle from '@/utils/getPageTitle'; |
|||
import { Layout } from 'antd'; |
|||
import classNames from 'classnames'; |
|||
import { connect } from 'dva'; |
|||
import React, { Suspense, useState } from 'react'; |
|||
import { ContainerQuery } from 'react-container-query'; |
|||
import DocumentTitle from 'react-document-title'; |
|||
import useMedia from 'react-media-hook2'; |
|||
import logo from '../assets/logo.svg'; |
|||
import styles from './BasicLayout.less'; |
|||
import Footer from './Footer'; |
|||
import Header from './Header'; |
|||
import Context from './MenuContext'; |
|||
|
|||
// lazy load SettingDrawer
|
|||
const SettingDrawer = React.lazy(() => import('@/components/SettingDrawer')); |
|||
|
|||
const { Content } = Layout; |
|||
|
|||
const query = { |
|||
'screen-xs': { |
|||
maxWidth: 575, |
|||
}, |
|||
'screen-sm': { |
|||
minWidth: 576, |
|||
maxWidth: 767, |
|||
}, |
|||
'screen-md': { |
|||
minWidth: 768, |
|||
maxWidth: 991, |
|||
}, |
|||
'screen-lg': { |
|||
minWidth: 992, |
|||
maxWidth: 1199, |
|||
}, |
|||
'screen-xl': { |
|||
minWidth: 1200, |
|||
maxWidth: 1599, |
|||
}, |
|||
'screen-xxl': { |
|||
minWidth: 1600, |
|||
}, |
|||
}; |
|||
|
|||
export declare type SiderTheme = 'light' | 'dark'; |
|||
|
|||
interface BasicLayoutProps { |
|||
dispatch: (args: any) => void; |
|||
// wait for https://github.com/umijs/umi/pull/2036
|
|||
route: any; |
|||
breadcrumbNameMap: object; |
|||
fixSiderbar: boolean; |
|||
layout: string; |
|||
navTheme: SiderTheme; |
|||
menuData: any[]; |
|||
fixedHeader: boolean; |
|||
location: Location; |
|||
collapsed: boolean; |
|||
} |
|||
|
|||
interface BasicLayoutContext { |
|||
location: Location; |
|||
breadcrumbNameMap: object; |
|||
} |
|||
|
|||
const BasicLayout: React.SFC<BasicLayoutProps> = props => { |
|||
const { |
|||
breadcrumbNameMap, |
|||
dispatch, |
|||
children, |
|||
collapsed, |
|||
fixedHeader, |
|||
fixSiderbar, |
|||
layout: PropsLayout, |
|||
location, |
|||
menuData, |
|||
navTheme, |
|||
route: { routes, authority }, |
|||
} = props; |
|||
useState(() => { |
|||
dispatch({ type: 'user/fetchCurrent' }); |
|||
dispatch({ type: 'setting/getSetting' }); |
|||
dispatch({ type: 'menu/getMenuData', payload: { routes, authority } }); |
|||
}); |
|||
const isTop = PropsLayout === 'topmenu'; |
|||
const contentStyle = !fixedHeader ? { paddingTop: 0 } : {}; |
|||
const isMobile = useMedia({ id: 'BasicLayout', query: '(max-width: 599px)' })[0]; |
|||
const hasLeftPadding = fixSiderbar && PropsLayout !== 'topmenu' && !isMobile; |
|||
const getContext = (): BasicLayoutContext => ({ location, breadcrumbNameMap }); |
|||
const handleMenuCollapse = (payload: boolean) => |
|||
dispatch({ type: 'global/changeLayoutCollapsed', payload }); |
|||
// Do not render SettingDrawer in production
|
|||
// unless it is deployed in preview.pro.ant.design as demo
|
|||
const renderSettingDrawer = () => |
|||
!(process.env.NODE_ENV === 'production' && APP_TYPE !== 'site') && <SettingDrawer />; |
|||
|
|||
const layout = ( |
|||
<Layout> |
|||
{isTop && !isMobile ? null : ( |
|||
<SiderMenu |
|||
logo={logo} |
|||
theme={navTheme} |
|||
onCollapse={handleMenuCollapse} |
|||
menuData={menuData} |
|||
isMobile={isMobile} |
|||
{...props} |
|||
/> |
|||
)} |
|||
<Layout |
|||
style={{ |
|||
paddingLeft: hasLeftPadding ? (collapsed ? 80 : 256) : void 0, |
|||
minHeight: '100vh', |
|||
}} |
|||
> |
|||
<Header |
|||
menuData={menuData} |
|||
handleMenuCollapse={handleMenuCollapse} |
|||
logo={logo} |
|||
isMobile={isMobile} |
|||
{...props} |
|||
/> |
|||
<Content className={styles.content} style={contentStyle}> |
|||
{children} |
|||
</Content> |
|||
<Footer /> |
|||
</Layout> |
|||
</Layout> |
|||
); |
|||
return ( |
|||
<React.Fragment> |
|||
<DocumentTitle title={getPageTitle(location.pathname, breadcrumbNameMap)}> |
|||
<ContainerQuery query={query}> |
|||
{params => ( |
|||
<Context.Provider value={getContext()}> |
|||
<div className={classNames(params)}>{layout}</div> |
|||
</Context.Provider> |
|||
)} |
|||
</ContainerQuery> |
|||
</DocumentTitle> |
|||
<Suspense fallback={<PageLoading />}>{renderSettingDrawer()}</Suspense> |
|||
</React.Fragment> |
|||
); |
|||
}; |
|||
|
|||
export default connect(({ global, setting, menu: menuModel }) => ({ |
|||
collapsed: global.collapsed, |
|||
layout: setting.layout, |
|||
menuData: menuModel.menuData, |
|||
breadcrumbNameMap: menuModel.breadcrumbNameMap, |
|||
...setting, |
|||
}))(BasicLayout); |
|||
@ -1,3 +0,0 @@ |
|||
import React from 'react'; |
|||
|
|||
export default ({ children }) => <div>{children}</div>; |
|||
@ -0,0 +1,9 @@ |
|||
import React, { ReactNode, SFC } from 'react'; |
|||
|
|||
interface BlankLayoutProps { |
|||
children: ReactNode; |
|||
} |
|||
|
|||
const Layout: SFC<BlankLayoutProps> = ({ children }) => <div>{children}</div>; |
|||
|
|||
export default Layout; |
|||
@ -1,5 +1,5 @@ |
|||
import { Icon, Layout } from 'antd'; |
|||
import React, { Fragment } from 'react'; |
|||
import { Layout, Icon } from 'antd'; |
|||
import { GlobalFooter } from 'ant-design-pro'; |
|||
|
|||
const { Footer } = Layout; |
|||
@ -1,3 +1,3 @@ |
|||
import { createContext } from 'react'; |
|||
|
|||
export default createContext(); |
|||
export default createContext({}); |
|||
@ -1,44 +0,0 @@ |
|||
import React from 'react'; |
|||
import Redirect from 'umi/redirect'; |
|||
import pathToRegexp from 'path-to-regexp'; |
|||
import { connect } from 'dva'; |
|||
import Authorized from '@/utils/Authorized'; |
|||
|
|||
function AuthComponent({ children, location, routerData, currentCuser }) { |
|||
const isLogin = currentCuser && currentCuser.name; |
|||
|
|||
const getRouteAuthority = (pathname, routeData) => { |
|||
const routes = routeData.slice(); // clone
|
|||
|
|||
const getAuthority = (routeDatas, path) => { |
|||
let authorities; |
|||
routeDatas.forEach(route => { |
|||
// check partial route
|
|||
if (pathToRegexp(`${route.path}(.*)`).test(path)) { |
|||
if (route.authority) { |
|||
authorities = route.authority; |
|||
} |
|||
// is exact route?
|
|||
if (!pathToRegexp(route.path).test(path) && route.routes) { |
|||
authorities = getAuthority(route.routes, path); |
|||
} |
|||
} |
|||
}); |
|||
return authorities; |
|||
}; |
|||
|
|||
return getAuthority(routes, pathname); |
|||
}; |
|||
return ( |
|||
<Authorized |
|||
authority={getRouteAuthority(location.pathname, routerData)} |
|||
noMatch={isLogin ? <Redirect to="/exception/403" /> : <Redirect to="/user/login" />} |
|||
> |
|||
{children} |
|||
</Authorized> |
|||
); |
|||
} |
|||
export default connect(({ menu: menuModel, user: userModel }) => ({ |
|||
routerData: menuModel.routerData, |
|||
currentCuser: userModel.currentCuser, |
|||
}))(AuthComponent); |
|||
@ -0,0 +1,45 @@ |
|||
import Authorized from '@/utils/Authorized'; |
|||
import { connect } from 'dva'; |
|||
import pathToRegexp from 'path-to-regexp'; |
|||
import React from 'react'; |
|||
import Redirect from 'umi/redirect'; |
|||
import { UserModelState } from '../models/user'; |
|||
|
|||
interface AuthComponentProps { |
|||
location: Location; |
|||
routerData: any[]; |
|||
user: UserModelState; |
|||
} |
|||
|
|||
const AuthComponent: React.SFC<AuthComponentProps> = ({ children, location, routerData, user }) => { |
|||
const { currentUser } = user; |
|||
const isLogin = currentUser && currentUser.name; |
|||
const getRouteAuthority = (path, routeData) => { |
|||
let authorities; |
|||
routeData.forEach(route => { |
|||
// match prefix
|
|||
if (pathToRegexp(`${route.path}(.*)`).test(path)) { |
|||
authorities = route.authority || authorities; |
|||
|
|||
// get children authority recursively
|
|||
if (route.routes) { |
|||
authorities = getRouteAuthority(path, route.routes) || authorities; |
|||
} |
|||
} |
|||
}); |
|||
return authorities; |
|||
}; |
|||
return ( |
|||
<Authorized |
|||
authority={getRouteAuthority(location.pathname, routerData)} |
|||
noMatch={isLogin ? <Redirect to="/exception/403" /> : <Redirect to="/user/login" />} |
|||
> |
|||
{children} |
|||
</Authorized> |
|||
); |
|||
}; |
|||
|
|||
export default connect(({ menu: menuModel, user }) => ({ |
|||
routerData: menuModel.routerData, |
|||
user, |
|||
}))(AuthComponent); |
|||
@ -1,13 +1,13 @@ |
|||
import request from '@/utils/request'; |
|||
|
|||
export async function query() { |
|||
export async function query(): Promise<any> { |
|||
return request('/api/users'); |
|||
} |
|||
|
|||
export async function queryCurrent() { |
|||
export async function queryCurrent(): Promise<any> { |
|||
return request('/api/currentUser'); |
|||
} |
|||
|
|||
export async function queryNotices() { |
|||
export async function queryNotices(): Promise<any> { |
|||
return request('/api/notices'); |
|||
} |
|||
@ -0,0 +1,12 @@ |
|||
declare module '*.css'; |
|||
declare module '*.less'; |
|||
declare module '*.scss'; |
|||
declare module '*.sass'; |
|||
declare module '*.svg'; |
|||
declare module '*.png'; |
|||
declare module '*.jpg'; |
|||
declare module '*.jpeg'; |
|||
declare module '*.gif'; |
|||
declare module '*.bmp'; |
|||
declare module '*.tiff'; |
|||
declare var APP_TYPE: string; |
|||
@ -1,3 +1,4 @@ |
|||
import 'jest'; |
|||
import { getAuthority } from './authority'; |
|||
|
|||
describe('getAuthority should be strong', () => { |
|||
@ -1,15 +1,26 @@ |
|||
import { formatMessage } from 'umi/locale'; |
|||
import pathToRegexp from 'path-to-regexp'; |
|||
import isEqual from 'lodash/isEqual'; |
|||
import memoizeOne from 'memoize-one'; |
|||
import { menu, title } from '../defaultSettings'; |
|||
import pathToRegexp from 'path-to-regexp'; |
|||
import { formatMessage } from 'umi-plugin-locale'; |
|||
import defaultSettings from '../../config/defaultSettings'; |
|||
|
|||
const { menu, title } = defaultSettings; |
|||
|
|||
interface RouterData { |
|||
name: string; |
|||
locale: string; |
|||
authority?: string[]; |
|||
children?: any[]; |
|||
icon?: string; |
|||
path: string; |
|||
} |
|||
|
|||
export const matchParamsPath = (pathname, breadcrumbNameMap) => { |
|||
export const matchParamsPath = (pathname: string, breadcrumbNameMap: object): RouterData => { |
|||
const pathKey = Object.keys(breadcrumbNameMap).find(key => pathToRegexp(key).test(pathname)); |
|||
return breadcrumbNameMap[pathKey]; |
|||
}; |
|||
|
|||
const getPageTitle = (pathname, breadcrumbNameMap) => { |
|||
const getPageTitle = (pathname: string, breadcrumbNameMap: object): string => { |
|||
const currRouterData = matchParamsPath(pathname, breadcrumbNameMap); |
|||
if (!currRouterData) { |
|||
return title; |
|||
@ -1,38 +0,0 @@ |
|||
import { isUrl } from './utils'; |
|||
|
|||
describe('isUrl tests', () => { |
|||
it('should return false for invalid and corner case inputs', () => { |
|||
expect(isUrl([])).toBeFalsy(); |
|||
expect(isUrl({})).toBeFalsy(); |
|||
expect(isUrl(false)).toBeFalsy(); |
|||
expect(isUrl(true)).toBeFalsy(); |
|||
expect(isUrl(NaN)).toBeFalsy(); |
|||
expect(isUrl(null)).toBeFalsy(); |
|||
expect(isUrl(undefined)).toBeFalsy(); |
|||
expect(isUrl()).toBeFalsy(); |
|||
expect(isUrl('')).toBeFalsy(); |
|||
}); |
|||
|
|||
it('should return false for invalid URLs', () => { |
|||
expect(isUrl('foo')).toBeFalsy(); |
|||
expect(isUrl('bar')).toBeFalsy(); |
|||
expect(isUrl('bar/test')).toBeFalsy(); |
|||
expect(isUrl('http:/example.com/')).toBeFalsy(); |
|||
expect(isUrl('ttp://example.com/')).toBeFalsy(); |
|||
}); |
|||
|
|||
it('should return true for valid URLs', () => { |
|||
expect(isUrl('http://example.com/')).toBeTruthy(); |
|||
expect(isUrl('https://example.com/')).toBeTruthy(); |
|||
expect(isUrl('http://example.com/test/123')).toBeTruthy(); |
|||
expect(isUrl('https://example.com/test/123')).toBeTruthy(); |
|||
expect(isUrl('http://example.com/test/123?foo=bar')).toBeTruthy(); |
|||
expect(isUrl('https://example.com/test/123?foo=bar')).toBeTruthy(); |
|||
expect(isUrl('http://www.example.com/')).toBeTruthy(); |
|||
expect(isUrl('https://www.example.com/')).toBeTruthy(); |
|||
expect(isUrl('http://www.example.com/test/123')).toBeTruthy(); |
|||
expect(isUrl('https://www.example.com/test/123')).toBeTruthy(); |
|||
expect(isUrl('http://www.example.com/test/123?foo=bar')).toBeTruthy(); |
|||
expect(isUrl('https://www.example.com/test/123?foo=bar')).toBeTruthy(); |
|||
}); |
|||
}); |
|||
Loading…
Reference in new issue