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/
|
// https://umijs.org/config/
|
||||
import os from 'os'; |
import os from 'os'; |
||||
import webpackPlugin from './plugin.config'; |
|
||||
import defaultSettings from '../src/defaultSettings'; |
|
||||
import slash from 'slash2'; |
import slash from 'slash2'; |
||||
|
import { IPlugin } from 'umi-types'; |
||||
|
import defaultSettings from './defaultSettings'; |
||||
|
import webpackPlugin from './plugin.config'; |
||||
|
|
||||
const { pwa, primaryColor } = defaultSettings; |
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', |
'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 { Icon } from 'antd'; |
||||
import Link from 'umi/link'; |
import Link from 'umi/link'; |
||||
import Debounce from 'lodash-decorators/debounce'; |
import debounce from 'lodash/debounce'; |
||||
import styles from './index.less'; |
import styles from './index.less'; |
||||
import RightContent from './RightContent'; |
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() { |
componentWillUnmount() { |
||||
this.triggerResizeEvent.cancel(); |
this.triggerResizeEvent.cancel(); |
||||
} |
} |
||||
/* eslint-disable*/ |
triggerResizeEvent = debounce(() => { |
||||
@Debounce(600) |
|
||||
triggerResizeEvent() { |
|
||||
// eslint-disable-line
|
// eslint-disable-line
|
||||
const event = document.createEvent('HTMLEvents'); |
const event = document.createEvent('HTMLEvents'); |
||||
event.initEvent('resize', true, false); |
event.initEvent('resize', true, false); |
||||
window.dispatchEvent(event); |
window.dispatchEvent(event); |
||||
} |
}); |
||||
toggle = () => { |
toggle = () => { |
||||
const { collapsed, onCollapse } = this.props; |
const { collapsed, onCollapse } = this.props; |
||||
onCollapse(!collapsed); |
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 { 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';
|
// import IconFont from '@/components/IconFont';
|
||||
// <IconFont type='icon-demo' className='xxx-xxx' />
|
// <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 Link from 'umi/link'; |
||||
import RightContent from '../GlobalHeader/RightContent'; |
import RightContent from '../GlobalHeader/RightContent'; |
||||
import BaseMenu from '../SiderMenu/BaseMenu'; |
import BaseMenu from '../SiderMenu/BaseMenu'; |
||||
import { getFlatMenuKeys } from '../SiderMenu/SiderMenuUtils'; |
import { getFlatMenuKeys } from '../SiderMenu/SiderMenuUtils'; |
||||
import styles from './index.less'; |
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 = { |
state = { |
||||
maxWidth: undefined, |
maxWidth: undefined, |
||||
}; |
}; |
||||
|
|
||||
|
maim: HTMLDivElement; |
||||
|
|
||||
static getDerivedStateFromProps(props) { |
static getDerivedStateFromProps(props) { |
||||
return { |
return { |
||||
maxWidth: (props.contentWidth === 'Fixed' ? 1200 : window.innerWidth) - 280 - 165 - 40, |
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 React, { Fragment } from 'react'; |
||||
import { Layout, Icon } from 'antd'; |
|
||||
import { GlobalFooter } from 'ant-design-pro'; |
import { GlobalFooter } from 'ant-design-pro'; |
||||
|
|
||||
const { Footer } = Layout; |
const { Footer } = Layout; |
||||
@ -1,3 +1,3 @@ |
|||||
import { createContext } from 'react'; |
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'; |
import request from '@/utils/request'; |
||||
|
|
||||
export async function query() { |
export async function query(): Promise<any> { |
||||
return request('/api/users'); |
return request('/api/users'); |
||||
} |
} |
||||
|
|
||||
export async function queryCurrent() { |
export async function queryCurrent(): Promise<any> { |
||||
return request('/api/currentUser'); |
return request('/api/currentUser'); |
||||
} |
} |
||||
|
|
||||
export async function queryNotices() { |
export async function queryNotices(): Promise<any> { |
||||
return request('/api/notices'); |
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'; |
import { getAuthority } from './authority'; |
||||
|
|
||||
describe('getAuthority should be strong', () => { |
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 isEqual from 'lodash/isEqual'; |
||||
import memoizeOne from 'memoize-one'; |
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)); |
const pathKey = Object.keys(breadcrumbNameMap).find(key => pathToRegexp(key).test(pathname)); |
||||
return breadcrumbNameMap[pathKey]; |
return breadcrumbNameMap[pathKey]; |
||||
}; |
}; |
||||
|
|
||||
const getPageTitle = (pathname, breadcrumbNameMap) => { |
const getPageTitle = (pathname: string, breadcrumbNameMap: object): string => { |
||||
const currRouterData = matchParamsPath(pathname, breadcrumbNameMap); |
const currRouterData = matchParamsPath(pathname, breadcrumbNameMap); |
||||
if (!currRouterData) { |
if (!currRouterData) { |
||||
return title; |
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