12 changed files with 439 additions and 4 deletions
@ -1,6 +1,12 @@ |
|||||
// ps https://github.com/GoogleChrome/puppeteer/issues/3120
|
// ps https://github.com/GoogleChrome/puppeteer/issues/3120
|
||||
module.exports = { |
module.exports = { |
||||
launch: { |
launch: { |
||||
args: ['--disable-gpu', '--disable-dev-shm-usage', '--no-first-run', '--no-zygote'], |
args: [ |
||||
|
'--disable-gpu', |
||||
|
'--disable-dev-shm-usage', |
||||
|
'--no-first-run', |
||||
|
'--no-zygote', |
||||
|
'--no-sandbox', |
||||
|
], |
||||
}, |
}, |
||||
}; |
}; |
||||
|
|||||
@ -0,0 +1,16 @@ |
|||||
|
import React from 'react'; |
||||
|
import { connect } from 'dva'; |
||||
|
import styles from './GridContent.less'; |
||||
|
|
||||
|
const GridContent = props => { |
||||
|
const { contentWidth, children } = props; |
||||
|
let className = `${styles.main}`; |
||||
|
if (contentWidth === 'Fixed') { |
||||
|
className = `${styles.main} ${styles.wide}`; |
||||
|
} |
||||
|
return <div className={className}>{children}</div>; |
||||
|
}; |
||||
|
|
||||
|
export default connect(({ setting }) => ({ |
||||
|
contentWidth: setting.contentWidth, |
||||
|
}))(GridContent); |
||||
@ -0,0 +1,10 @@ |
|||||
|
.main { |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
min-height: 100%; |
||||
|
transition: 0.3s; |
||||
|
&.wide { |
||||
|
max-width: 1200px; |
||||
|
margin: 0 auto; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,116 @@ |
|||||
|
import React from 'react'; |
||||
|
import pathToRegexp from 'path-to-regexp'; |
||||
|
import Link from 'umi/link'; |
||||
|
import { FormattedMessage } from 'umi-plugin-react/locale'; |
||||
|
import { urlToList } from '../_utils/pathTools'; |
||||
|
|
||||
|
// 渲染Breadcrumb 子节点
|
||||
|
// Render the Breadcrumb child node
|
||||
|
const itemRender = (route, params, routes, paths) => { |
||||
|
const last = routes.indexOf(route) === routes.length - 1; |
||||
|
return last || !route.component ? ( |
||||
|
<span>{route.breadcrumbName}</span> |
||||
|
) : ( |
||||
|
<Link to={paths.join('/')}>{route.breadcrumbName}</Link> |
||||
|
); |
||||
|
}; |
||||
|
|
||||
|
const renderItemLocal = item => { |
||||
|
if (item.locale) { |
||||
|
return <FormattedMessage id={item.locale} defaultMessage={item.name} />; |
||||
|
} |
||||
|
return item.name; |
||||
|
}; |
||||
|
|
||||
|
export const getBreadcrumb = (breadcrumbNameMap, url) => { |
||||
|
let breadcrumb = breadcrumbNameMap[url]; |
||||
|
if (!breadcrumb) { |
||||
|
Object.keys(breadcrumbNameMap).forEach(item => { |
||||
|
if (pathToRegexp(item).test(url)) { |
||||
|
breadcrumb = breadcrumbNameMap[item]; |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
return breadcrumb || {}; |
||||
|
}; |
||||
|
|
||||
|
export const getBreadcrumbProps = props => { |
||||
|
const { routes, params, location, breadcrumbNameMap } = props; |
||||
|
return { |
||||
|
routes, |
||||
|
params, |
||||
|
routerLocation: location, |
||||
|
breadcrumbNameMap, |
||||
|
}; |
||||
|
}; |
||||
|
|
||||
|
// Generated according to props
|
||||
|
const conversionFromProps = props => { |
||||
|
const { breadcrumbList } = props; |
||||
|
return breadcrumbList.map(item => { |
||||
|
const { title, href } = item; |
||||
|
return { |
||||
|
path: href, |
||||
|
breadcrumbName: title, |
||||
|
}; |
||||
|
}); |
||||
|
}; |
||||
|
|
||||
|
const conversionFromLocation = (routerLocation, breadcrumbNameMap, props) => { |
||||
|
const { home } = props; |
||||
|
// Convert the url to an array
|
||||
|
const pathSnippets = urlToList(routerLocation.pathname); |
||||
|
// Loop data mosaic routing
|
||||
|
const extraBreadcrumbItems = pathSnippets.map(url => { |
||||
|
const currentBreadcrumb = getBreadcrumb(breadcrumbNameMap, url); |
||||
|
if (currentBreadcrumb.inherited) { |
||||
|
return null; |
||||
|
} |
||||
|
const name = renderItemLocal(currentBreadcrumb); |
||||
|
const { hideInBreadcrumb } = currentBreadcrumb; |
||||
|
return name && !hideInBreadcrumb |
||||
|
? { |
||||
|
path: url, |
||||
|
breadcrumbName: name, |
||||
|
} |
||||
|
: null; |
||||
|
}); |
||||
|
// Add home breadcrumbs to your head if defined
|
||||
|
if (home) { |
||||
|
extraBreadcrumbItems.unshift({ |
||||
|
path: '/', |
||||
|
breadcrumbName: home, |
||||
|
}); |
||||
|
} |
||||
|
return extraBreadcrumbItems; |
||||
|
}; |
||||
|
|
||||
|
/** |
||||
|
* 将参数转化为面包屑 |
||||
|
* Convert parameters into breadcrumbs |
||||
|
*/ |
||||
|
export const conversionBreadcrumbList = props => { |
||||
|
const { breadcrumbList } = props; |
||||
|
const { routes, params, routerLocation, breadcrumbNameMap } = getBreadcrumbProps(props); |
||||
|
if (breadcrumbList && breadcrumbList.length) { |
||||
|
return conversionFromProps(); |
||||
|
} |
||||
|
// 如果传入 routes 和 params 属性
|
||||
|
// If pass routes and params attributes
|
||||
|
if (routes && params) { |
||||
|
return { |
||||
|
routes: routes.filter(route => route.breadcrumbName), |
||||
|
params, |
||||
|
itemRender, |
||||
|
}; |
||||
|
} |
||||
|
// 根据 location 生成 面包屑
|
||||
|
// Generate breadcrumbs based on location
|
||||
|
if (routerLocation && routerLocation.pathname) { |
||||
|
return { |
||||
|
routes: conversionFromLocation(routerLocation, breadcrumbNameMap, props), |
||||
|
itemRender, |
||||
|
}; |
||||
|
} |
||||
|
return {}; |
||||
|
}; |
||||
@ -0,0 +1,104 @@ |
|||||
|
import React from 'react'; |
||||
|
import { FormattedMessage } from 'umi-plugin-react/locale'; |
||||
|
import Link from 'umi/link'; |
||||
|
import { PageHeader, Tabs, Typography } from 'antd'; |
||||
|
import { connect } from 'dva'; |
||||
|
import classNames from 'classnames'; |
||||
|
import GridContent from './GridContent'; |
||||
|
import styles from './index.less'; |
||||
|
import MenuContext from '@/layouts/MenuContext'; |
||||
|
import { conversionBreadcrumbList } from './breadcrumb'; |
||||
|
|
||||
|
const { Title } = Typography; |
||||
|
|
||||
|
/** |
||||
|
* render Footer tabList |
||||
|
* In order to be compatible with the old version of the PageHeader |
||||
|
* basically all the functions are implemented. |
||||
|
*/ |
||||
|
const renderFooter = ({ tabList, activeKeyProps, onTabChange, tabBarExtraContent }) => { |
||||
|
return tabList && tabList.length ? ( |
||||
|
<Tabs |
||||
|
className={styles.tabs} |
||||
|
{...activeKeyProps} |
||||
|
onChange={key => { |
||||
|
if (onTabChange) { |
||||
|
onTabChange(key); |
||||
|
} |
||||
|
}} |
||||
|
tabBarExtraContent={tabBarExtraContent} |
||||
|
> |
||||
|
{tabList.map(item => ( |
||||
|
<Tabs.TabPane tab={item.tab} key={item.key} /> |
||||
|
))} |
||||
|
</Tabs> |
||||
|
) : null; |
||||
|
}; |
||||
|
|
||||
|
const PageHeaderWrapper = ({ |
||||
|
children, |
||||
|
contentWidth, |
||||
|
wrapperClassName, |
||||
|
top, |
||||
|
title, |
||||
|
content, |
||||
|
logo, |
||||
|
extraContent, |
||||
|
...restProps |
||||
|
}) => { |
||||
|
return ( |
||||
|
<div style={{ margin: '-24px -24px 0' }} className={classNames(classNames, styles.main)}> |
||||
|
{top} |
||||
|
{title && content && ( |
||||
|
<MenuContext.Consumer> |
||||
|
{value => { |
||||
|
return ( |
||||
|
<PageHeader |
||||
|
wide={contentWidth === 'Fixed'} |
||||
|
title={ |
||||
|
<Title |
||||
|
level={4} |
||||
|
style={{ |
||||
|
marginBottom: 0, |
||||
|
}} |
||||
|
> |
||||
|
{title} |
||||
|
</Title> |
||||
|
} |
||||
|
key="pageheader" |
||||
|
{...restProps} |
||||
|
breadcrumb={conversionBreadcrumbList({ |
||||
|
...value, |
||||
|
...restProps, |
||||
|
home: <FormattedMessage id="menu.home" defaultMessage="Home" />, |
||||
|
})} |
||||
|
className={styles.pageHeader} |
||||
|
linkElement={Link} |
||||
|
footer={renderFooter(restProps)} |
||||
|
> |
||||
|
<div className={styles.detail}> |
||||
|
{logo && <div className={styles.logo}>{logo}</div>} |
||||
|
<div className={styles.main}> |
||||
|
<div className={styles.row}> |
||||
|
{content && <div className={styles.content}>{content}</div>} |
||||
|
{extraContent && <div className={styles.extraContent}>{extraContent}</div>} |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</PageHeader> |
||||
|
); |
||||
|
}} |
||||
|
</MenuContext.Consumer> |
||||
|
)} |
||||
|
{children ? ( |
||||
|
<div className={styles['children-content']}> |
||||
|
<GridContent>{children}</GridContent> |
||||
|
</div> |
||||
|
) : null} |
||||
|
</div> |
||||
|
); |
||||
|
}; |
||||
|
|
||||
|
export default connect(({ setting }) => ({ |
||||
|
contentWidth: setting.contentWidth, |
||||
|
}))(PageHeaderWrapper); |
||||
@ -0,0 +1,110 @@ |
|||||
|
@import '~antd/lib/style/themes/default.less'; |
||||
|
|
||||
|
.children-content { |
||||
|
margin: 24px 24px 0; |
||||
|
} |
||||
|
|
||||
|
.main { |
||||
|
:global { |
||||
|
.ant-page-header { |
||||
|
padding: 16px 32px 0; |
||||
|
background: #fff; |
||||
|
border-bottom: 1px solid #e8e8e8; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.wide { |
||||
|
max-width: 1200px; |
||||
|
margin: auto; |
||||
|
} |
||||
|
.detail { |
||||
|
display: flex; |
||||
|
} |
||||
|
|
||||
|
.row { |
||||
|
display: flex; |
||||
|
width: 100%; |
||||
|
} |
||||
|
|
||||
|
.logo { |
||||
|
flex: 0 1 auto; |
||||
|
margin-right: 16px; |
||||
|
padding-top: 1px; |
||||
|
> img { |
||||
|
display: block; |
||||
|
width: 28px; |
||||
|
height: 28px; |
||||
|
border-radius: @border-radius-base; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.title-content { |
||||
|
margin-bottom: 16px; |
||||
|
} |
||||
|
|
||||
|
@media screen and (max-width: @screen-sm) { |
||||
|
.content { |
||||
|
margin: 24px 0 0; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.title, |
||||
|
.content { |
||||
|
flex: auto; |
||||
|
} |
||||
|
|
||||
|
.extraContent, |
||||
|
.main { |
||||
|
flex: 0 1 auto; |
||||
|
} |
||||
|
|
||||
|
.main { |
||||
|
width: 100%; |
||||
|
} |
||||
|
|
||||
|
.title { |
||||
|
margin-bottom: 16px; |
||||
|
} |
||||
|
|
||||
|
.logo, |
||||
|
.content, |
||||
|
.extraContent { |
||||
|
margin-bottom: 16px; |
||||
|
} |
||||
|
|
||||
|
.extraContent { |
||||
|
min-width: 242px; |
||||
|
margin-left: 88px; |
||||
|
text-align: right; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@media screen and (max-width: @screen-xl) { |
||||
|
.extraContent { |
||||
|
margin-left: 44px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@media screen and (max-width: @screen-lg) { |
||||
|
.extraContent { |
||||
|
margin-left: 20px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@media screen and (max-width: @screen-md) { |
||||
|
.row { |
||||
|
display: block; |
||||
|
} |
||||
|
|
||||
|
.action, |
||||
|
.extraContent { |
||||
|
margin-left: 0; |
||||
|
text-align: left; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@media screen and (max-width: @screen-sm) { |
||||
|
.detail { |
||||
|
display: block; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,37 @@ |
|||||
|
const RouterConfig = []; |
||||
|
const BASE_URL = `http://localhost:${process.env.PORT || 8000}`; |
||||
|
|
||||
|
function formatter(data) { |
||||
|
return data |
||||
|
.reduce((pre, item) => { |
||||
|
if (item.routes) { |
||||
|
pre.push(item.routes[0].path); |
||||
|
} else { |
||||
|
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); |
||||
|
await page.setCacheEnabled(false); |
||||
|
}); |
||||
|
const routers = formatter(RouterConfig[1].routes); |
||||
|
routers.forEach(route => { |
||||
|
it(`test pages ${route}`, testPage(route)); |
||||
|
}); |
||||
|
}); |
||||
@ -0,0 +1,19 @@ |
|||||
|
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(); |
||||
|
}); |
||||
|
}); |
||||
Loading…
Reference in new issue