diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index 9c4c97e1ad91a23e84b499ffdae0eac65a9cb579..2df685a5dd6c44538cf54537cd2ff99ffa7b99ba 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -52,6 +52,10 @@ jobs:
displayName: install
- script: npm run lint
displayName: lint
+ - script: npm run test:all
+ env:
+ PROGRESS: none
+ displayName: test
- script: npm run build
env:
PROGRESS: none
diff --git a/jest-puppeteer.config.js b/jest-puppeteer.config.js
index cded345494729be647ba25d868f38e33ad5eaa01..21b41e4aaf354b1081863e45321f229f7ffe9416 100644
--- a/jest-puppeteer.config.js
+++ b/jest-puppeteer.config.js
@@ -1,6 +1,12 @@
// ps https://github.com/GoogleChrome/puppeteer/issues/3120
module.exports = {
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',
+ ],
},
};
diff --git a/package.json b/package.json
index d69874b5649fb2514261d3dac601084c7b7b226d..21e33fec1fe968d411dc3ee879fc5ba4d54d89ae 100644
--- a/package.json
+++ b/package.json
@@ -5,7 +5,7 @@
"description": "An out-of-box UI solution for enterprise applications",
"scripts": {
"analyze": "cross-env ANALYZE=1 umi build",
- "build": "umi build",
+ "build": "umi build && npm run functions:build",
"dev": "cross-env APP_TYPE=site umi dev",
"dev:no-mock": "cross-env MOCK=none umi dev",
"docker-hub:build": "docker build -f Dockerfile.hub -t ant-design-pro ./",
@@ -73,9 +73,13 @@
"react-container-query": "^0.11.0",
"react-copy-to-clipboard": "^5.0.1",
"react-document-title": "^2.0.3",
+ "react-dom": "^16.7.0",
+ "react-fittext": "^1.0.0",
+ "react-media": "^1.9.2",
"react-media-hook2": "^1.0.2",
"umi": "^2.6.10",
- "umi-request": "^1.0.0"
+ "umi-plugin-react": "^1.7.2",
+ "umi-request": "^1.0.5"
},
"devDependencies": {
"@types/classnames": "^2.2.7",
@@ -110,6 +114,7 @@
"mockjs": "^1.0.1-beta3",
"netlify-lambda": "^1.4.3",
"prettier": "^1.16.4",
+ "serverless-http": "^1.9.1",
"slash2": "^2.0.0",
"stylelint": "^9.10.1",
"stylelint-config-css-modules": "^1.3.0",
diff --git a/src/components/PageHeaderWrapper/GridContent.js b/src/components/PageHeaderWrapper/GridContent.js
new file mode 100644
index 0000000000000000000000000000000000000000..fee6a9318c263249ffd3aaf67fc656e471aa8984
--- /dev/null
+++ b/src/components/PageHeaderWrapper/GridContent.js
@@ -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
{children}
;
+};
+
+export default connect(({ setting }) => ({
+ contentWidth: setting.contentWidth,
+}))(GridContent);
diff --git a/src/components/PageHeaderWrapper/GridContent.less b/src/components/PageHeaderWrapper/GridContent.less
new file mode 100644
index 0000000000000000000000000000000000000000..d5496e9ecb95318c38a30f1369d35e8fcf583758
--- /dev/null
+++ b/src/components/PageHeaderWrapper/GridContent.less
@@ -0,0 +1,10 @@
+.main {
+ width: 100%;
+ height: 100%;
+ min-height: 100%;
+ transition: 0.3s;
+ &.wide {
+ max-width: 1200px;
+ margin: 0 auto;
+ }
+}
diff --git a/src/components/PageHeaderWrapper/breadcrumb.js b/src/components/PageHeaderWrapper/breadcrumb.js
new file mode 100644
index 0000000000000000000000000000000000000000..02fe66fd26530f68bd99e6a3c863f384a82e611d
--- /dev/null
+++ b/src/components/PageHeaderWrapper/breadcrumb.js
@@ -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 ? (
+ {route.breadcrumbName}
+ ) : (
+ {route.breadcrumbName}
+ );
+};
+
+const renderItemLocal = item => {
+ if (item.locale) {
+ return ;
+ }
+ 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 {};
+};
diff --git a/src/components/PageHeaderWrapper/index.js b/src/components/PageHeaderWrapper/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..7a766834c4ac39cd3628409d45fe3cd09bff1fe7
--- /dev/null
+++ b/src/components/PageHeaderWrapper/index.js
@@ -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 ? (
+ {
+ if (onTabChange) {
+ onTabChange(key);
+ }
+ }}
+ tabBarExtraContent={tabBarExtraContent}
+ >
+ {tabList.map(item => (
+
+ ))}
+
+ ) : null;
+};
+
+const PageHeaderWrapper = ({
+ children,
+ contentWidth,
+ wrapperClassName,
+ top,
+ title,
+ content,
+ logo,
+ extraContent,
+ ...restProps
+}) => {
+ return (
+
+ {top}
+ {title && content && (
+
+ {value => {
+ return (
+
+ {title}
+
+ }
+ key="pageheader"
+ {...restProps}
+ breadcrumb={conversionBreadcrumbList({
+ ...value,
+ ...restProps,
+ home: ,
+ })}
+ className={styles.pageHeader}
+ linkElement={Link}
+ footer={renderFooter(restProps)}
+ >
+
+ {logo &&
{logo}
}
+
+
+ {content &&
{content}
}
+ {extraContent &&
{extraContent}
}
+
+
+
+
+ );
+ }}
+
+ )}
+ {children ? (
+
+ {children}
+
+ ) : null}
+
+ );
+};
+
+export default connect(({ setting }) => ({
+ contentWidth: setting.contentWidth,
+}))(PageHeaderWrapper);
diff --git a/src/components/PageHeaderWrapper/index.less b/src/components/PageHeaderWrapper/index.less
new file mode 100644
index 0000000000000000000000000000000000000000..119585bbaef2af9a3a3f4dd90ce882a50a5a98e8
--- /dev/null
+++ b/src/components/PageHeaderWrapper/index.less
@@ -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;
+ }
+}
diff --git a/src/e2e/baseLayout.e2e.js b/src/e2e/baseLayout.e2e.js
new file mode 100644
index 0000000000000000000000000000000000000000..8e534df6a56641f0ba4049d0400bdc1d3e10ed35
--- /dev/null
+++ b/src/e2e/baseLayout.e2e.js
@@ -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));
+ });
+});
diff --git a/src/e2e/topMenu.e2e.js b/src/e2e/topMenu.e2e.js
new file mode 100644
index 0000000000000000000000000000000000000000..09f2987fe7d482b760d54321be4ec67774c8141e
--- /dev/null
+++ b/src/e2e/topMenu.e2e.js
@@ -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();
+ });
+});
diff --git a/src/layouts/Header.tsx b/src/layouts/Header.tsx
index c00145114f495d48102f66325ac6c6cb99b8a0bd..12a160ee9e38c1f78af90744adb8248bf237b945 100644
--- a/src/layouts/Header.tsx
+++ b/src/layouts/Header.tsx
@@ -136,7 +136,10 @@ class HeaderView extends Component {
const isTop = layout === 'topmenu';
const width = this.getHeadWidth();
const HeaderDom = visible ? (
-