Просмотр исходного кода

lazy Analysis (#2927)

* lazy Analysis

* lazy loading menu

* remove hard code

* fix style warning

* fix loading height waring

* fix offineData Loading error

* lazy load menu

* fix login test eroor

* fix #2950 ,fix topmeun error

* add args of puppeteer

* add layout=topmenu e2e test
陈帅 7 лет назад
Родитель
Сommit
2756251241

+ 14 - 0
jest-puppeteer.config.js

@@ -0,0 +1,14 @@
+// ps https://github.com/GoogleChrome/puppeteer/issues/3120
+module.exports = {
+  launch: {
+    args: [
+      '--disable-gpu',
+      '--disable-dev-shm-usage',
+      '--disable-setuid-sandbox',
+      '--no-first-run',
+      '--no-sandbox',
+      '--no-zygote',
+      '--single-process',
+    ],
+  },
+};

+ 1 - 0
package.json

@@ -55,6 +55,7 @@
     "react-document-title": "^2.0.3",
     "react-dom": "^16.5.1",
     "react-fittext": "^1.0.0",
+    "react-media": "^1.8.0",
     "react-router-dom": "^4.3.1"
   },
   "devDependencies": {

+ 1 - 9
src/components/SiderMenu/BaseMenu.js

@@ -3,8 +3,8 @@ import { Menu, Icon } from 'antd';
 import Link from 'umi/link';
 import isEqual from 'lodash/isEqual';
 import memoizeOne from 'memoize-one';
-import pathToRegexp from 'path-to-regexp';
 import { urlToList } from '../_utils/pathTools';
+import { getMenuMatches } from './SiderMenuUtils';
 import styles from './index.less';
 
 const { SubMenu } = Menu;
@@ -23,14 +23,6 @@ const getIcon = icon => {
   return icon;
 };
 
-export const getMenuMatches = (flatMenuKeys, path) =>
-  flatMenuKeys.filter(item => {
-    if (item) {
-      return pathToRegexp(item).test(path);
-    }
-    return false;
-  });
-
 export default class BaseMenu extends PureComponent {
   constructor(props) {
     super(props);

+ 14 - 25
src/components/SiderMenu/SiderMenu.js

@@ -1,27 +1,14 @@
-import React, { PureComponent } from 'react';
+import React, { PureComponent, Suspense } from 'react';
 import { Layout } from 'antd';
 import classNames from 'classnames';
 import Link from 'umi/link';
 import styles from './index.less';
-import BaseMenu, { getMenuMatches } from './BaseMenu';
-import { urlToList } from '../_utils/pathTools';
+import PageLoading from '../PageLoading';
+import { getDefaultCollapsedSubMenus } from './SiderMenuUtils';
 
+const BaseMenu = React.lazy(() => import('./BaseMenu'));
 const { Sider } = Layout;
 
-/**
- * 获得菜单子节点
- * @memberof SiderMenu
- */
-const getDefaultCollapsedSubMenus = props => {
-  const {
-    location: { pathname },
-    flatMenuKeys,
-  } = props;
-  return urlToList(pathname)
-    .map(item => getMenuMatches(flatMenuKeys, item)[0])
-    .filter(item => item);
-};
-
 export default class SiderMenu extends PureComponent {
   constructor(props) {
     super(props);
@@ -67,7 +54,6 @@ export default class SiderMenu extends PureComponent {
       [styles.fixSiderbar]: fixSiderbar,
       [styles.light]: theme === 'light',
     });
-
     return (
       <Sider
         trigger={null}
@@ -85,13 +71,16 @@ export default class SiderMenu extends PureComponent {
             <h1>Ant Design Pro</h1>
           </Link>
         </div>
-        <BaseMenu
-          {...this.props}
-          mode="inline"
-          handleOpenChange={this.handleOpenChange}
-          style={{ padding: '16px 0', width: '100%' }}
-          {...defaultProps}
-        />
+        <Suspense fallback={<PageLoading />}>
+          <BaseMenu
+            {...this.props}
+            mode="inline"
+            handleOpenChange={this.handleOpenChange}
+            onOpenChange={this.handleOpenChange}
+            style={{ padding: '16px 0', width: '100%' }}
+            {...defaultProps}
+          />
+        </Suspense>
       </Sider>
     );
   }

+ 1 - 1
src/components/SiderMenu/SiderMenu.test.js

@@ -1,4 +1,4 @@
-import { getFlatMenuKeys } from './index';
+import { getFlatMenuKeys } from './SiderMenuUtils';
 
 const menu = [
   {

+ 39 - 0
src/components/SiderMenu/SiderMenuUtils.js

@@ -0,0 +1,39 @@
+import pathToRegexp from 'path-to-regexp';
+import { urlToList } from '../_utils/pathTools';
+
+/**
+ * Recursively flatten the data
+ * [{path:string},{path:string}] => {path,path2}
+ * @param  menus
+ */
+export const getFlatMenuKeys = menuData => {
+  let keys = [];
+  menuData.forEach(item => {
+    keys.push(item.path);
+    if (item.children) {
+      keys = keys.concat(getFlatMenuKeys(item.children));
+    }
+  });
+  return keys;
+};
+
+export const getMenuMatches = (flatMenuKeys, path) =>
+  flatMenuKeys.filter(item => {
+    if (item) {
+      return pathToRegexp(item).test(path);
+    }
+    return false;
+  });
+/**
+ * 获得菜单子节点
+ * @memberof SiderMenu
+ */
+export const getDefaultCollapsedSubMenus = props => {
+  const {
+    location: { pathname },
+    flatMenuKeys,
+  } = props;
+  return urlToList(pathname)
+    .map(item => getMenuMatches(flatMenuKeys, item)[0])
+    .filter(item => item);
+};

+ 6 - 24
src/components/SiderMenu/index.js

@@ -1,25 +1,11 @@
 import React from 'react';
 import { Drawer } from 'antd';
 import SiderMenu from './SiderMenu';
+import { getFlatMenuKeys } from './SiderMenuUtils';
 
-/**
- * Recursively flatten the data
- * [{path:string},{path:string}] => {path,path2}
- * @param  menus
- */
-export const getFlatMenuKeys = menuData => {
-  let keys = [];
-  menuData.forEach(item => {
-    keys.push(item.path);
-    if (item.children) {
-      keys = keys.concat(getFlatMenuKeys(item.children));
-    }
-  });
-  return keys;
-};
-
-const SiderMenuWrapper = props => {
+const SiderMenuWrapper = React.memo(props => {
   const { isMobile, menuData, collapsed, onCollapse } = props;
+  const flatMenuKeys = getFlatMenuKeys(menuData);
   return isMobile ? (
     <Drawer
       visible={!collapsed}
@@ -30,15 +16,11 @@ const SiderMenuWrapper = props => {
         height: '100vh',
       }}
     >
-      <SiderMenu
-        {...props}
-        flatMenuKeys={getFlatMenuKeys(menuData)}
-        collapsed={isMobile ? false : collapsed}
-      />
+      <SiderMenu {...props} flatMenuKeys={flatMenuKeys} collapsed={isMobile ? false : collapsed} />
     </Drawer>
   ) : (
-    <SiderMenu {...props} flatMenuKeys={getFlatMenuKeys(menuData)} />
+    <SiderMenu {...props} flatMenuKeys={flatMenuKeys} />
   );
-};
+});
 
 export default SiderMenuWrapper;

+ 8 - 2
src/components/TopNavHeader/index.js

@@ -2,6 +2,7 @@ import React, { PureComponent } 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';
 
 export default class TopNavHeader extends PureComponent {
@@ -16,8 +17,9 @@ export default class TopNavHeader extends PureComponent {
   }
 
   render() {
-    const { theme, contentWidth, logo } = this.props;
+    const { theme, contentWidth, menuData, logo } = this.props;
     const { maxWidth } = this.state;
+    const flatMenuKeys = getFlatMenuKeys(menuData);
     return (
       <div className={`${styles.head} ${theme === 'light' ? styles.light : ''}`}>
         <div
@@ -38,7 +40,11 @@ export default class TopNavHeader extends PureComponent {
                 maxWidth,
               }}
             >
-              <BaseMenu {...this.props} style={{ border: 'none', height: 64 }} />
+              <BaseMenu
+                {...this.props}
+                flatMenuKeys={flatMenuKeys}
+                style={{ border: 'none', height: 64 }}
+              />
             </div>
           </div>
           <RightContent {...this.props} />

+ 1 - 1
src/e2e/home.e2e.js

@@ -7,7 +7,7 @@ describe('Homepage', () => {
   it('it should have logo text', async () => {
     await page.goto(BASE_URL);
     await page.waitForSelector('h1', {
-      timeout: 2000,
+      timeout: 5000,
     });
     const text = await page.evaluate(() => document.getElementsByTagName('h1')[0].innerText);
     expect(text).toContain('Ant Design Pro');

+ 18 - 0
src/e2e/topMenu.e2e.js

@@ -0,0 +1,18 @@
+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();
+  });
+});

+ 20 - 31
src/layouts/BasicLayout.js

@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { Suspense } from 'react';
 import { Layout } from 'antd';
 import DocumentTitle from 'react-document-title';
 import isEqual from 'lodash/isEqual';
@@ -7,16 +7,19 @@ import { connect } from 'dva';
 import { ContainerQuery } from 'react-container-query';
 import classNames from 'classnames';
 import pathToRegexp from 'path-to-regexp';
-import { enquireScreen, unenquireScreen } from 'enquire-js';
+import Media from 'react-media';
 import { formatMessage } from 'umi/locale';
-import SiderMenu from '@/components/SiderMenu';
 import Authorized from '@/utils/Authorized';
-import SettingDrawer from '@/components/SettingDrawer';
 import logo from '../assets/logo.svg';
 import Footer from './Footer';
 import Header from './Header';
 import Context from './MenuContext';
 import Exception403 from '../pages/Exception/403';
+import PageLoading from '@/components/PageLoading';
+import SiderMenu from '@/components/SiderMenu';
+
+// lazy load SettingDrawer
+const SettingDrawer = React.lazy(() => import('@/components/SettingDrawer'));
 
 const { Content } = Layout;
 
@@ -88,8 +91,6 @@ class BasicLayout extends React.PureComponent {
   }
 
   state = {
-    rendering: true,
-    isMobile: false,
     menuData: this.getMenuData(),
   };
 
@@ -101,27 +102,13 @@ class BasicLayout extends React.PureComponent {
     dispatch({
       type: 'setting/getSetting',
     });
-    this.renderRef = requestAnimationFrame(() => {
-      this.setState({
-        rendering: false,
-      });
-    });
-    this.enquireHandler = enquireScreen(mobile => {
-      const { isMobile } = this.state;
-      if (isMobile !== mobile) {
-        this.setState({
-          isMobile: mobile,
-        });
-      }
-    });
   }
 
   componentDidUpdate(preProps) {
     // After changing to phone mode,
     // if collapsed is true, you need to click twice to display
     this.breadcrumbNameMap = this.getBreadcrumbNameMap();
-    const { isMobile } = this.state;
-    const { collapsed } = this.props;
+    const { collapsed, isMobile } = this.props;
     if (isMobile && !preProps.isMobile && !collapsed) {
       this.handleMenuCollapse(false);
     }
@@ -129,7 +116,6 @@ class BasicLayout extends React.PureComponent {
 
   componentWillUnmount() {
     cancelAnimationFrame(this.renderRef);
-    unenquireScreen(this.enquireHandler);
   }
 
   getContext() {
@@ -187,8 +173,7 @@ class BasicLayout extends React.PureComponent {
   };
 
   getLayoutStyle = () => {
-    const { isMobile } = this.state;
-    const { fixSiderbar, collapsed, layout } = this.props;
+    const { fixSiderbar, isMobile, collapsed, layout } = this.props;
     if (fixSiderbar && layout !== 'topmenu' && !isMobile) {
       return {
         paddingLeft: collapsed ? '80px' : '256px',
@@ -213,15 +198,14 @@ class BasicLayout extends React.PureComponent {
     });
   };
 
-  renderSettingDrawer() {
+  renderSettingDrawer = () => {
     // Do not render SettingDrawer in production
     // unless it is deployed in preview.pro.ant.design as demo
-    const { rendering } = this.state;
-    if ((rendering || process.env.NODE_ENV === 'production') && APP_TYPE !== 'site') {
+    if (process.env.NODE_ENV === 'production' && APP_TYPE !== 'site') {
       return null;
     }
     return <SettingDrawer />;
-  }
+  };
 
   render() {
     const {
@@ -229,8 +213,9 @@ class BasicLayout extends React.PureComponent {
       layout: PropsLayout,
       children,
       location: { pathname },
+      isMobile,
     } = this.props;
-    const { isMobile, menuData } = this.state;
+    const { menuData } = this.state;
     const isTop = PropsLayout === 'topmenu';
     const routerConfig = this.matchParamsPath(pathname);
     const layout = (
@@ -282,7 +267,7 @@ class BasicLayout extends React.PureComponent {
             )}
           </ContainerQuery>
         </DocumentTitle>
-        {this.renderSettingDrawer()}
+        <Suspense fallback={<PageLoading />}>{this.renderSettingDrawer()}</Suspense>
       </React.Fragment>
     );
   }
@@ -292,4 +277,8 @@ export default connect(({ global, setting }) => ({
   collapsed: global.collapsed,
   layout: setting.layout,
   ...setting,
-}))(BasicLayout);
+}))(props => (
+  <Media query="(max-width: 599px)">
+    {isMobile => <BasicLayout {...props} isMobile={isMobile} />}
+  </Media>
+));

+ 52 - 559
src/pages/Dashboard/Analysis.js

@@ -1,70 +1,28 @@
-import React, { Component } from 'react';
+import React, { Component, Suspense } from 'react';
 import { connect } from 'dva';
-import { formatMessage, FormattedMessage } from 'umi/locale';
-import {
-  Row,
-  Col,
-  Icon,
-  Card,
-  Tabs,
-  Table,
-  Radio,
-  DatePicker,
-  Tooltip,
-  Menu,
-  Dropdown,
-} from 'antd';
-import {
-  ChartCard,
-  MiniArea,
-  MiniBar,
-  MiniProgress,
-  Field,
-  Bar,
-  Pie,
-  TimelineChart,
-} from '@/components/Charts';
-import Trend from '@/components/Trend';
-import NumberInfo from '@/components/NumberInfo';
-import numeral from 'numeral';
+import { Row, Col, Icon, Menu, Dropdown } from 'antd';
+
 import GridContent from '@/components/PageHeaderWrapper/GridContent';
-import Yuan from '@/utils/Yuan';
 import { getTimeDistance } from '@/utils/utils';
 
 import styles from './Analysis.less';
+import PageLoading from '@/components/PageLoading';
 
-const { TabPane } = Tabs;
-const { RangePicker } = DatePicker;
-
-const rankingListData = [];
-for (let i = 0; i < 7; i += 1) {
-  rankingListData.push({
-    title: `工专路 ${i} 号店`,
-    total: 323234,
-  });
-}
+const IntroduceRow = React.lazy(() => import('./IntroduceRow'));
+const SalesCard = React.lazy(() => import('./SalesCard'));
+const TopSearch = React.lazy(() => import('./TopSearch'));
+const ProportionSales = React.lazy(() => import('./ProportionSales'));
+const OfflineData = React.lazy(() => import('./OfflineData'));
 
 @connect(({ chart, loading }) => ({
   chart,
   loading: loading.effects['chart/fetch'],
 }))
 class Analysis extends Component {
-  constructor(props) {
-    super(props);
-    this.rankingListData = [];
-    for (let i = 0; i < 7; i += 1) {
-      this.rankingListData.push({
-        title: formatMessage({ id: 'app.analysis.test' }, { no: i }),
-        total: 323234,
-      });
-    }
-  }
-
   state = {
     salesType: 'all',
     currentTabKey: '',
     rangePickerValue: getTimeDistance('year'),
-    loading: true,
   };
 
   componentDidMount() {
@@ -73,11 +31,6 @@ class Analysis extends Component {
       dispatch({
         type: 'chart/fetch',
       });
-      this.timeoutId = setTimeout(() => {
-        this.setState({
-          loading: false,
-        });
-      }, 600);
     });
   }
 
@@ -124,7 +77,7 @@ class Analysis extends Component {
     });
   };
 
-  isActive(type) {
+  isActive = type => {
     const { rangePickerValue } = this.state;
     const value = getTimeDistance(type);
     if (!rangePickerValue[0] || !rangePickerValue[1]) {
@@ -137,11 +90,11 @@ class Analysis extends Component {
       return styles.currentDate;
     }
     return '';
-  }
+  };
 
   render() {
-    const { rangePickerValue, salesType, loading: stateLoading, currentTabKey } = this.state;
-    const { chart, loading: propsLoading } = this.props;
+    const { rangePickerValue, salesType, currentTabKey } = this.state;
+    const { chart, loading } = this.props;
     const {
       visitData,
       visitData2,
@@ -153,7 +106,6 @@ class Analysis extends Component {
       salesTypeDataOnline,
       salesTypeDataOffline,
     } = chart;
-    const loading = propsLoading || stateLoading;
     let salesPieData;
     if (salesType === 'all') {
       salesPieData = salesTypeData;
@@ -167,7 +119,7 @@ class Analysis extends Component {
       </Menu>
     );
 
-    const iconGroup = (
+    const dropdownGroup = (
       <span className={styles.iconGroup}>
         <Dropdown overlay={menu} placement="bottomRight">
           <Icon type="ellipsis" />
@@ -175,515 +127,56 @@ class Analysis extends Component {
       </span>
     );
 
-    const salesExtra = (
-      <div className={styles.salesExtraWrap}>
-        <div className={styles.salesExtra}>
-          <a className={this.isActive('today')} onClick={() => this.selectDate('today')}>
-            <FormattedMessage id="app.analysis.all-day" defaultMessage="All Day" />
-          </a>
-          <a className={this.isActive('week')} onClick={() => this.selectDate('week')}>
-            <FormattedMessage id="app.analysis.all-week" defaultMessage="All Week" />
-          </a>
-          <a className={this.isActive('month')} onClick={() => this.selectDate('month')}>
-            <FormattedMessage id="app.analysis.all-month" defaultMessage="All Month" />
-          </a>
-          <a className={this.isActive('year')} onClick={() => this.selectDate('year')}>
-            <FormattedMessage id="app.analysis.all-year" defaultMessage="All Year" />
-          </a>
-        </div>
-        <RangePicker
-          value={rangePickerValue}
-          onChange={this.handleRangePickerChange}
-          style={{ width: 256 }}
-        />
-      </div>
-    );
-
-    const columns = [
-      {
-        title: <FormattedMessage id="app.analysis.table.rank" defaultMessage="Rank" />,
-        dataIndex: 'index',
-        key: 'index',
-      },
-      {
-        title: (
-          <FormattedMessage
-            id="app.analysis.table.search-keyword"
-            defaultMessage="Search keyword"
-          />
-        ),
-        dataIndex: 'keyword',
-        key: 'keyword',
-        render: text => <a href="/">{text}</a>,
-      },
-      {
-        title: <FormattedMessage id="app.analysis.table.users" defaultMessage="Users" />,
-        dataIndex: 'count',
-        key: 'count',
-        sorter: (a, b) => a.count - b.count,
-        className: styles.alignRight,
-      },
-      {
-        title: (
-          <FormattedMessage id="app.analysis.table.weekly-range" defaultMessage="Weekly Range" />
-        ),
-        dataIndex: 'range',
-        key: 'range',
-        sorter: (a, b) => a.range - b.range,
-        render: (text, record) => (
-          <Trend flag={record.status === 1 ? 'down' : 'up'}>
-            <span style={{ marginRight: 4 }}>{text}%</span>
-          </Trend>
-        ),
-        align: 'right',
-      },
-    ];
-
     const activeKey = currentTabKey || (offlineData[0] && offlineData[0].name);
 
-    const CustomTab = ({ data, currentTabKey: currentKey }) => (
-      <Row gutter={8} style={{ width: 138, margin: '8px 0' }}>
-        <Col span={12}>
-          <NumberInfo
-            title={data.name}
-            subTitle={
-              <FormattedMessage
-                id="app.analysis.conversion-rate"
-                defaultMessage="Conversion Rate"
-              />
-            }
-            gap={2}
-            total={`${data.cvr * 100}%`}
-            theme={currentKey !== data.name && 'light'}
-          />
-        </Col>
-        <Col span={12} style={{ paddingTop: 36 }}>
-          <Pie
-            animate={false}
-            color={currentKey !== data.name && '#BDE4FF'}
-            inner={0.55}
-            tooltip={false}
-            margin={[0, 0, 0, 0]}
-            percent={data.cvr * 100}
-            height={64}
-          />
-        </Col>
-      </Row>
-    );
-
-    const topColResponsiveProps = {
-      xs: 24,
-      sm: 12,
-      md: 12,
-      lg: 12,
-      xl: 6,
-      style: { marginBottom: 24 },
-    };
-
     return (
       <GridContent>
-        <Row gutter={24}>
-          <Col {...topColResponsiveProps}>
-            <ChartCard
-              bordered={false}
-              title={
-                <FormattedMessage id="app.analysis.total-sales" defaultMessage="Total Sales" />
-              }
-              action={
-                <Tooltip
-                  title={
-                    <FormattedMessage id="app.analysis.introduce" defaultMessage="Introduce" />
-                  }
-                >
-                  <Icon type="info-circle-o" />
-                </Tooltip>
-              }
-              loading={loading}
-              total={() => <Yuan>126560</Yuan>}
-              footer={
-                <Field
-                  label={
-                    <FormattedMessage id="app.analysis.day-sales" defaultMessage="Daily Sales" />
-                  }
-                  value={`¥${numeral(12423).format('0,0')}`}
-                />
-              }
-              contentHeight={46}
-            >
-              <Trend flag="up" style={{ marginRight: 16 }}>
-                <FormattedMessage id="app.analysis.week" defaultMessage="Weekly Changes" />
-                <span className={styles.trendText}>12%</span>
-              </Trend>
-              <Trend flag="down">
-                <FormattedMessage id="app.analysis.day" defaultMessage="Daily Changes" />
-                <span className={styles.trendText}>11%</span>
-              </Trend>
-            </ChartCard>
-          </Col>
-
-          <Col {...topColResponsiveProps}>
-            <ChartCard
-              bordered={false}
-              loading={loading}
-              title={<FormattedMessage id="app.analysis.visits" defaultMessage="Visits" />}
-              action={
-                <Tooltip
-                  title={
-                    <FormattedMessage id="app.analysis.introduce" defaultMessage="Introduce" />
-                  }
-                >
-                  <Icon type="info-circle-o" />
-                </Tooltip>
-              }
-              total={numeral(8846).format('0,0')}
-              footer={
-                <Field
-                  label={
-                    <FormattedMessage id="app.analysis.day-visits" defaultMessage="Daily Visits" />
-                  }
-                  value={numeral(1234).format('0,0')}
-                />
-              }
-              contentHeight={46}
-            >
-              <MiniArea color="#975FE4" data={visitData} />
-            </ChartCard>
-          </Col>
-          <Col {...topColResponsiveProps}>
-            <ChartCard
-              bordered={false}
-              loading={loading}
-              title={<FormattedMessage id="app.analysis.payments" defaultMessage="Payments" />}
-              action={
-                <Tooltip
-                  title={
-                    <FormattedMessage id="app.analysis.introduce" defaultMessage="Introduce" />
-                  }
-                >
-                  <Icon type="info-circle-o" />
-                </Tooltip>
-              }
-              total={numeral(6560).format('0,0')}
-              footer={
-                <Field
-                  label={
-                    <FormattedMessage
-                      id="app.analysis.conversion-rate"
-                      defaultMessage="Conversion Rate"
-                    />
-                  }
-                  value="60%"
-                />
-              }
-              contentHeight={46}
-            >
-              <MiniBar data={visitData} />
-            </ChartCard>
-          </Col>
-          <Col {...topColResponsiveProps}>
-            <ChartCard
-              loading={loading}
-              bordered={false}
-              title={
-                <FormattedMessage
-                  id="app.analysis.operational-effect"
-                  defaultMessage="Operational Effect"
-                />
-              }
-              action={
-                <Tooltip
-                  title={
-                    <FormattedMessage id="app.analysis.introduce" defaultMessage="Introduce" />
-                  }
-                >
-                  <Icon type="info-circle-o" />
-                </Tooltip>
-              }
-              total="78%"
-              footer={
-                <div style={{ whiteSpace: 'nowrap', overflow: 'hidden' }}>
-                  <Trend flag="up" style={{ marginRight: 16 }}>
-                    <FormattedMessage id="app.analysis.week" defaultMessage="Weekly Changes" />
-                    <span className={styles.trendText}>12%</span>
-                  </Trend>
-                  <Trend flag="down">
-                    <FormattedMessage id="app.analysis.day" defaultMessage="Weekly Changes" />
-                    <span className={styles.trendText}>11%</span>
-                  </Trend>
-                </div>
-              }
-              contentHeight={46}
-            >
-              <MiniProgress percent={78} strokeWidth={8} target={80} color="#13C2C2" />
-            </ChartCard>
-          </Col>
-        </Row>
-
-        <Card loading={loading} bordered={false} bodyStyle={{ padding: 0 }}>
-          <div className={styles.salesCard}>
-            <Tabs tabBarExtraContent={salesExtra} size="large" tabBarStyle={{ marginBottom: 24 }}>
-              <TabPane
-                tab={<FormattedMessage id="app.analysis.sales" defaultMessage="Sales" />}
-                key="sales"
-              >
-                <Row>
-                  <Col xl={16} lg={12} md={12} sm={24} xs={24}>
-                    <div className={styles.salesBar}>
-                      <Bar
-                        height={295}
-                        title={
-                          <FormattedMessage
-                            id="app.analysis.sales-trend"
-                            defaultMessage="Sales Trend"
-                          />
-                        }
-                        data={salesData}
-                      />
-                    </div>
-                  </Col>
-                  <Col xl={8} lg={12} md={12} sm={24} xs={24}>
-                    <div className={styles.salesRank}>
-                      <h4 className={styles.rankingTitle}>
-                        <FormattedMessage
-                          id="app.analysis.sales-ranking"
-                          defaultMessage="Sales Ranking"
-                        />
-                      </h4>
-                      <ul className={styles.rankingList}>
-                        {this.rankingListData.map((item, i) => (
-                          <li key={item.title}>
-                            <span
-                              className={`${styles.rankingItemNumber} ${
-                                i < 3 ? styles.active : ''
-                              }`}
-                            >
-                              {i + 1}
-                            </span>
-                            <span className={styles.rankingItemTitle} title={item.title}>
-                              {item.title}
-                            </span>
-                            <span className={styles.rankingItemValue}>
-                              {numeral(item.total).format('0,0')}
-                            </span>
-                          </li>
-                        ))}
-                      </ul>
-                    </div>
-                  </Col>
-                </Row>
-              </TabPane>
-              <TabPane
-                tab={<FormattedMessage id="app.analysis.visits" defaultMessage="Visits" />}
-                key="views"
-              >
-                <Row>
-                  <Col xl={16} lg={12} md={12} sm={24} xs={24}>
-                    <div className={styles.salesBar}>
-                      <Bar
-                        height={292}
-                        title={
-                          <FormattedMessage
-                            id="app.analysis.visits-trend"
-                            defaultMessage="Visits Trend"
-                          />
-                        }
-                        data={salesData}
-                      />
-                    </div>
-                  </Col>
-                  <Col xl={8} lg={12} md={12} sm={24} xs={24}>
-                    <div className={styles.salesRank}>
-                      <h4 className={styles.rankingTitle}>
-                        <FormattedMessage
-                          id="app.analysis.visits-ranking"
-                          defaultMessage="Visits Ranking"
-                        />
-                      </h4>
-                      <ul className={styles.rankingList}>
-                        {this.rankingListData.map((item, i) => (
-                          <li key={item.title}>
-                            <span
-                              className={`${styles.rankingItemNumber} ${
-                                i < 3 ? styles.active : ''
-                              }`}
-                            >
-                              {i + 1}
-                            </span>
-                            <span className={styles.rankingItemTitle} title={item.title}>
-                              {item.title}
-                            </span>
-                            <span>{numeral(item.total).format('0,0')}</span>
-                          </li>
-                        ))}
-                      </ul>
-                    </div>
-                  </Col>
-                </Row>
-              </TabPane>
-            </Tabs>
-          </div>
-        </Card>
-
+        <Suspense fallback={<PageLoading />}>
+          <IntroduceRow loading={loading} visitData={visitData} />
+        </Suspense>
+        <Suspense fallback={null}>
+          <SalesCard
+            rangePickerValue={rangePickerValue}
+            salesData={salesData}
+            isActive={this.isActive}
+            handleRangePickerChange={this.handleRangePickerChange}
+            loading={loading}
+            selectDate={this.selectDate}
+          />
+        </Suspense>
         <Row gutter={24}>
           <Col xl={12} lg={24} md={24} sm={24} xs={24}>
-            <Card
-              loading={loading}
-              bordered={false}
-              title={
-                <FormattedMessage
-                  id="app.analysis.online-top-search"
-                  defaultMessage="Online Top Search"
-                />
-              }
-              extra={iconGroup}
-              style={{ marginTop: 24 }}
-            >
-              <Row gutter={68}>
-                <Col sm={12} xs={24} style={{ marginBottom: 24 }}>
-                  <NumberInfo
-                    subTitle={
-                      <span>
-                        <FormattedMessage
-                          id="app.analysis.search-users"
-                          defaultMessage="search users"
-                        />
-                        <Tooltip
-                          title={
-                            <FormattedMessage
-                              id="app.analysis.introduce"
-                              defaultMessage="introduce"
-                            />
-                          }
-                        >
-                          <Icon style={{ marginLeft: 8 }} type="info-circle-o" />
-                        </Tooltip>
-                      </span>
-                    }
-                    gap={8}
-                    total={numeral(12321).format('0,0')}
-                    status="up"
-                    subTotal={17.1}
-                  />
-                  <MiniArea line height={45} data={visitData2} />
-                </Col>
-                <Col sm={12} xs={24} style={{ marginBottom: 24 }}>
-                  <NumberInfo
-                    subTitle={
-                      <span>
-                        <FormattedMessage
-                          id="app.analysis.per-capita-search"
-                          defaultMessage="Per Capita Search"
-                        />
-                        <Tooltip
-                          title={
-                            <FormattedMessage
-                              id="app.analysis.introduce"
-                              defaultMessage="introduce"
-                            />
-                          }
-                        >
-                          <Icon style={{ marginLeft: 8 }} type="info-circle-o" />
-                        </Tooltip>
-                      </span>
-                    }
-                    total={2.7}
-                    status="down"
-                    subTotal={26.2}
-                    gap={8}
-                  />
-                  <MiniArea line height={45} data={visitData2} />
-                </Col>
-              </Row>
-              <Table
-                rowKey={record => record.index}
-                size="small"
-                columns={columns}
-                dataSource={searchData}
-                pagination={{
-                  style: { marginBottom: 0 },
-                  pageSize: 5,
-                }}
+            <Suspense fallback={null}>
+              <TopSearch
+                loading={loading}
+                visitData2={visitData2}
+                selectDate={this.selectDate}
+                searchData={searchData}
+                dropdownGroup={dropdownGroup}
               />
-            </Card>
+            </Suspense>
           </Col>
           <Col xl={12} lg={24} md={24} sm={24} xs={24}>
-            <Card
-              loading={loading}
-              className={styles.salesCard}
-              bordered={false}
-              title={
-                <FormattedMessage
-                  id="app.analysis.the-proportion-of-sales"
-                  defaultMessage="The Proportion of Sales"
-                />
-              }
-              bodyStyle={{ padding: 24 }}
-              extra={
-                <div className={styles.salesCardExtra}>
-                  {iconGroup}
-                  <div className={styles.salesTypeRadio}>
-                    <Radio.Group value={salesType} onChange={this.handleChangeSalesType}>
-                      <Radio.Button value="all">
-                        <FormattedMessage id="app.analysis.channel.all" defaultMessage="ALL" />
-                      </Radio.Button>
-                      <Radio.Button value="online">
-                        <FormattedMessage
-                          id="app.analysis.channel.online"
-                          defaultMessage="Online"
-                        />
-                      </Radio.Button>
-                      <Radio.Button value="stores">
-                        <FormattedMessage
-                          id="app.analysis.channel.stores"
-                          defaultMessage="Stores"
-                        />
-                      </Radio.Button>
-                    </Radio.Group>
-                  </div>
-                </div>
-              }
-              style={{ marginTop: 24, minHeight: 509 }}
-            >
-              <h4 style={{ marginTop: 8, marginBottom: 32 }}>
-                <FormattedMessage id="app.analysis.sales" defaultMessage="Sales" />
-              </h4>
-              <Pie
-                hasLegend
-                subTitle={<FormattedMessage id="app.analysis.sales" defaultMessage="Sales" />}
-                total={() => <Yuan>{salesPieData.reduce((pre, now) => now.y + pre, 0)}</Yuan>}
-                data={salesPieData}
-                valueFormat={value => <Yuan>{value}</Yuan>}
-                height={248}
-                lineWidth={4}
+            <Suspense fallback={null}>
+              <ProportionSales
+                dropdownGroup={dropdownGroup}
+                salesType={salesType}
+                loading={loading}
+                salesPieData={salesPieData}
+                handleChangeSalesType={this.handleChangeSalesType}
               />
-            </Card>
+            </Suspense>
           </Col>
         </Row>
-
-        <Card
-          loading={loading}
-          className={styles.offlineCard}
-          bordered={false}
-          bodyStyle={{ padding: '0 0 32px 0' }}
-          style={{ marginTop: 32 }}
-        >
-          <Tabs activeKey={activeKey} onChange={this.handleTabChange}>
-            {offlineData.map(shop => (
-              <TabPane tab={<CustomTab data={shop} currentTabKey={activeKey} />} key={shop.name}>
-                <div style={{ padding: '0 24px' }}>
-                  <TimelineChart
-                    height={400}
-                    data={offlineChartData}
-                    titleMap={{
-                      y1: formatMessage({ id: 'app.analysis.traffic' }),
-                      y2: formatMessage({ id: 'app.analysis.payments' }),
-                    }}
-                  />
-                </div>
-              </TabPane>
-            ))}
-          </Tabs>
-        </Card>
+        <Suspense fallback={null}>
+          <OfflineData
+            activeKey={activeKey}
+            loading={loading}
+            offlineData={offlineData}
+            offlineChartData={offlineChartData}
+            handleTabChange={this.handleTabChange}
+          />
+        </Suspense>
       </GridContent>
     );
   }

+ 144 - 0
src/pages/Dashboard/IntroduceRow.js

@@ -0,0 +1,144 @@
+import React, { memo } from 'react';
+import { Row, Col, Icon, Tooltip } from 'antd';
+import { FormattedMessage } from 'umi/locale';
+import styles from './Analysis.less';
+import { ChartCard, MiniArea, MiniBar, MiniProgress, Field } from '@/components/Charts';
+import Trend from '@/components/Trend';
+import numeral from 'numeral';
+import Yuan from '@/utils/Yuan';
+
+const topColResponsiveProps = {
+  xs: 24,
+  sm: 12,
+  md: 12,
+  lg: 12,
+  xl: 6,
+  style: { marginBottom: 24 },
+};
+
+const IntroduceRow = memo(({ loading, visitData }) => (
+  <Row gutter={24}>
+    <Col {...topColResponsiveProps}>
+      <ChartCard
+        bordered={false}
+        title={<FormattedMessage id="app.analysis.total-sales" defaultMessage="Total Sales" />}
+        action={
+          <Tooltip
+            title={<FormattedMessage id="app.analysis.introduce" defaultMessage="Introduce" />}
+          >
+            <Icon type="info-circle-o" />
+          </Tooltip>
+        }
+        loading={loading}
+        total={() => <Yuan>126560</Yuan>}
+        footer={
+          <Field
+            label={<FormattedMessage id="app.analysis.day-sales" defaultMessage="Daily Sales" />}
+            value={`¥${numeral(12423).format('0,0')}`}
+          />
+        }
+        contentHeight={46}
+      >
+        <Trend flag="up" style={{ marginRight: 16 }}>
+          <FormattedMessage id="app.analysis.week" defaultMessage="Weekly Changes" />
+          <span className={styles.trendText}>12%</span>
+        </Trend>
+        <Trend flag="down">
+          <FormattedMessage id="app.analysis.day" defaultMessage="Daily Changes" />
+          <span className={styles.trendText}>11%</span>
+        </Trend>
+      </ChartCard>
+    </Col>
+
+    <Col {...topColResponsiveProps}>
+      <ChartCard
+        bordered={false}
+        loading={loading}
+        title={<FormattedMessage id="app.analysis.visits" defaultMessage="Visits" />}
+        action={
+          <Tooltip
+            title={<FormattedMessage id="app.analysis.introduce" defaultMessage="Introduce" />}
+          >
+            <Icon type="info-circle-o" />
+          </Tooltip>
+        }
+        total={numeral(8846).format('0,0')}
+        footer={
+          <Field
+            label={<FormattedMessage id="app.analysis.day-visits" defaultMessage="Daily Visits" />}
+            value={numeral(1234).format('0,0')}
+          />
+        }
+        contentHeight={46}
+      >
+        <MiniArea color="#975FE4" data={visitData} />
+      </ChartCard>
+    </Col>
+    <Col {...topColResponsiveProps}>
+      <ChartCard
+        bordered={false}
+        loading={loading}
+        title={<FormattedMessage id="app.analysis.payments" defaultMessage="Payments" />}
+        action={
+          <Tooltip
+            title={<FormattedMessage id="app.analysis.introduce" defaultMessage="Introduce" />}
+          >
+            <Icon type="info-circle-o" />
+          </Tooltip>
+        }
+        total={numeral(6560).format('0,0')}
+        footer={
+          <Field
+            label={
+              <FormattedMessage
+                id="app.analysis.conversion-rate"
+                defaultMessage="Conversion Rate"
+              />
+            }
+            value="60%"
+          />
+        }
+        contentHeight={46}
+      >
+        <MiniBar data={visitData} />
+      </ChartCard>
+    </Col>
+    <Col {...topColResponsiveProps}>
+      <ChartCard
+        loading={loading}
+        bordered={false}
+        title={
+          <FormattedMessage
+            id="app.analysis.operational-effect"
+            defaultMessage="Operational Effect"
+          />
+        }
+        action={
+          <Tooltip
+            title={<FormattedMessage id="app.analysis.introduce" defaultMessage="Introduce" />}
+          >
+            <Icon type="info-circle-o" />
+          </Tooltip>
+        }
+        total="78%"
+        footer={
+          <div style={{ whiteSpace: 'nowrap', overflow: 'hidden' }}>
+            <Trend flag="up" style={{ marginRight: 16 }}>
+              <FormattedMessage id="app.analysis.week" defaultMessage="Weekly Changes" />
+              <span className={styles.trendText}>12%</span>
+            </Trend>
+            <Trend flag="down">
+              <FormattedMessage id="app.analysis.day" defaultMessage="Weekly Changes" />
+              <span className={styles.trendText}>11%</span>
+            </Trend>
+          </div>
+        }
+        contentHeight={26}
+      >
+        <MiniProgress percent={78} strokeWidth={8} target={80} color="#13C2C2" />
+      </ChartCard>
+    </Col>
+  </Row>
+));
+
+export default IntroduceRow;

+ 65 - 0
src/pages/Dashboard/OfflineData.js

@@ -0,0 +1,65 @@
+import React, { memo } from 'react';
+import { Card, Tabs, Row, Col } from 'antd';
+import { formatMessage, FormattedMessage } from 'umi/locale';
+import styles from './Analysis.less';
+import { TimelineChart, Pie } from '@/components/Charts';
+import NumberInfo from '@/components/NumberInfo';
+
+const CustomTab = ({ data, currentTabKey: currentKey }) => (
+  <Row gutter={8} style={{ width: 138, margin: '8px 0' }}>
+    <Col span={12}>
+      <NumberInfo
+        title={data.name}
+        subTitle={
+          <FormattedMessage id="app.analysis.conversion-rate" defaultMessage="Conversion Rate" />
+        }
+        gap={2}
+        total={`${data.cvr * 100}%`}
+        theme={currentKey !== data.name && 'light'}
+      />
+    </Col>
+    <Col span={12} style={{ paddingTop: 36 }}>
+      <Pie
+        animate={false}
+        color={currentKey !== data.name && '#BDE4FF'}
+        inner={0.55}
+        tooltip={false}
+        margin={[0, 0, 0, 0]}
+        percent={data.cvr * 100}
+        height={64}
+      />
+    </Col>
+  </Row>
+);
+
+const { TabPane } = Tabs;
+
+const OfflineData = memo(
+  ({ activeKey, loading, offlineData, offlineChartData, handleTabChange }) => (
+    <Card
+      loading={loading}
+      className={styles.offlineCard}
+      bordered={false}
+      style={{ marginTop: 32 }}
+    >
+      <Tabs activeKey={activeKey} onChange={handleTabChange}>
+        {offlineData.map(shop => (
+          <TabPane tab={<CustomTab data={shop} currentTabKey={activeKey} />} key={shop.name}>
+            <div style={{ padding: '0 24px' }}>
+              <TimelineChart
+                height={400}
+                data={offlineChartData}
+                titleMap={{
+                  y1: formatMessage({ id: 'app.analysis.traffic' }),
+                  y2: formatMessage({ id: 'app.analysis.payments' }),
+                }}
+              />
+            </div>
+          </TabPane>
+        ))}
+      </Tabs>
+    </Card>
+  )
+);
+
+export default OfflineData;

+ 63 - 0
src/pages/Dashboard/ProportionSales.js

@@ -0,0 +1,63 @@
+import React, { memo } from 'react';
+import { Card, Radio } from 'antd';
+import { FormattedMessage } from 'umi/locale';
+import styles from './Analysis.less';
+import { Pie } from '@/components/Charts';
+import Yuan from '@/utils/Yuan';
+
+const ProportionSales = memo(
+  ({ dropdownGroup, salesType, loading, salesPieData, handleChangeSalesType }) => (
+    <Card
+      loading={loading}
+      className={styles.salesCard}
+      bordered={false}
+      title={
+        <FormattedMessage
+          id="app.analysis.the-proportion-of-sales"
+          defaultMessage="The Proportion of Sales"
+        />
+      }
+      bodyStyle={{ padding: 24 }}
+      extra={
+        <div className={styles.salesCardExtra}>
+          {dropdownGroup}
+          <div className={styles.salesTypeRadio}>
+            <Radio.Group value={salesType} onChange={handleChangeSalesType}>
+              <Radio.Button value="all">
+                <FormattedMessage id="app.analysis.channel.all" defaultMessage="ALL" />
+              </Radio.Button>
+              <Radio.Button value="online">
+                <FormattedMessage id="app.analysis.channel.online" defaultMessage="Online" />
+              </Radio.Button>
+              <Radio.Button value="stores">
+                <FormattedMessage id="app.analysis.channel.stores" defaultMessage="Stores" />
+              </Radio.Button>
+            </Radio.Group>
+          </div>
+        </div>
+      }
+      style={{ marginTop: 24 }}
+    >
+      <div
+        style={{
+          minHeight: 380,
+        }}
+      >
+        <h4 style={{ marginTop: 8, marginBottom: 32 }}>
+          <FormattedMessage id="app.analysis.sales" defaultMessage="Sales" />
+        </h4>
+        <Pie
+          hasLegend
+          subTitle={<FormattedMessage id="app.analysis.sales" defaultMessage="Sales" />}
+          total={() => <Yuan>{salesPieData.reduce((pre, now) => now.y + pre, 0)}</Yuan>}
+          data={salesPieData}
+          valueFormat={value => <Yuan>{value}</Yuan>}
+          height={248}
+          lineWidth={4}
+        />
+      </div>
+    </Card>
+  )
+);
+
+export default ProportionSales;

+ 150 - 0
src/pages/Dashboard/SalesCard.js

@@ -0,0 +1,150 @@
+import React, { memo } from 'react';
+import { Row, Col, Card, Tabs, DatePicker } from 'antd';
+import { FormattedMessage, formatMessage } from 'umi/locale';
+import numeral from 'numeral';
+import styles from './Analysis.less';
+import { Bar } from '@/components/Charts';
+
+const { RangePicker } = DatePicker;
+const { TabPane } = Tabs;
+
+const rankingListData = [];
+for (let i = 0; i < 7; i += 1) {
+  rankingListData.push({
+    title: formatMessage({ id: 'app.analysis.test' }, { no: i }),
+    total: 323234,
+  });
+}
+
+const SalesCard = memo(
+  ({ rangePickerValue, salesData, isActive, handleRangePickerChange, loading, selectDate }) => (
+    <Card loading={loading} bordered={false} bodyStyle={{ padding: 0 }}>
+      <div className={styles.salesCard}>
+        <Tabs
+          tabBarExtraContent={
+            <div className={styles.salesExtraWrap}>
+              <div className={styles.salesExtra}>
+                <a className={isActive('today')} onClick={() => selectDate('today')}>
+                  <FormattedMessage id="app.analysis.all-day" defaultMessage="All Day" />
+                </a>
+                <a className={isActive('week')} onClick={() => selectDate('week')}>
+                  <FormattedMessage id="app.analysis.all-week" defaultMessage="All Week" />
+                </a>
+                <a className={isActive('month')} onClick={() => selectDate('month')}>
+                  <FormattedMessage id="app.analysis.all-month" defaultMessage="All Month" />
+                </a>
+                <a className={isActive('year')} onClick={() => selectDate('year')}>
+                  <FormattedMessage id="app.analysis.all-year" defaultMessage="All Year" />
+                </a>
+              </div>
+              <RangePicker
+                value={rangePickerValue}
+                onChange={handleRangePickerChange}
+                style={{ width: 256 }}
+              />
+            </div>
+          }
+          size="large"
+          tabBarStyle={{ marginBottom: 24 }}
+        >
+          <TabPane
+            tab={<FormattedMessage id="app.analysis.sales" defaultMessage="Sales" />}
+            key="sales"
+          >
+            <Row>
+              <Col xl={16} lg={12} md={12} sm={24} xs={24}>
+                <div className={styles.salesBar}>
+                  <Bar
+                    height={295}
+                    title={
+                      <FormattedMessage
+                        id="app.analysis.sales-trend"
+                        defaultMessage="Sales Trend"
+                      />
+                    }
+                    data={salesData}
+                  />
+                </div>
+              </Col>
+              <Col xl={8} lg={12} md={12} sm={24} xs={24}>
+                <div className={styles.salesRank}>
+                  <h4 className={styles.rankingTitle}>
+                    <FormattedMessage
+                      id="app.analysis.sales-ranking"
+                      defaultMessage="Sales Ranking"
+                    />
+                  </h4>
+                  <ul className={styles.rankingList}>
+                    {rankingListData.map((item, i) => (
+                      <li key={item.title}>
+                        <span
+                          className={`${styles.rankingItemNumber} ${i < 3 ? styles.active : ''}`}
+                        >
+                          {i + 1}
+                        </span>
+                        <span className={styles.rankingItemTitle} title={item.title}>
+                          {item.title}
+                        </span>
+                        <span className={styles.rankingItemValue}>
+                          {numeral(item.total).format('0,0')}
+                        </span>
+                      </li>
+                    ))}
+                  </ul>
+                </div>
+              </Col>
+            </Row>
+          </TabPane>
+          <TabPane
+            tab={<FormattedMessage id="app.analysis.visits" defaultMessage="Visits" />}
+            key="views"
+          >
+            <Row>
+              <Col xl={16} lg={12} md={12} sm={24} xs={24}>
+                <div className={styles.salesBar}>
+                  <Bar
+                    height={292}
+                    title={
+                      <FormattedMessage
+                        id="app.analysis.visits-trend"
+                        defaultMessage="Visits Trend"
+                      />
+                    }
+                    data={salesData}
+                  />
+                </div>
+              </Col>
+              <Col xl={8} lg={12} md={12} sm={24} xs={24}>
+                <div className={styles.salesRank}>
+                  <h4 className={styles.rankingTitle}>
+                    <FormattedMessage
+                      id="app.analysis.visits-ranking"
+                      defaultMessage="Visits Ranking"
+                    />
+                  </h4>
+                  <ul className={styles.rankingList}>
+                    {rankingListData.map((item, i) => (
+                      <li key={item.title}>
+                        <span
+                          className={`${styles.rankingItemNumber} ${i < 3 ? styles.active : ''}`}
+                        >
+                          {i + 1}
+                        </span>
+                        <span className={styles.rankingItemTitle} title={item.title}>
+                          {item.title}
+                        </span>
+                        <span>{numeral(item.total).format('0,0')}</span>
+                      </li>
+                    ))}
+                  </ul>
+                </div>
+              </Col>
+            </Row>
+          </TabPane>
+        </Tabs>
+      </div>
+    </Card>
+  )
+);
+
+export default SalesCard;

+ 111 - 0
src/pages/Dashboard/TopSearch.js

@@ -0,0 +1,111 @@
+import React, { memo } from 'react';
+import { Row, Col, Table, Tooltip, Card, Icon } from 'antd';
+import { FormattedMessage } from 'umi/locale';
+import Trend from '@/components/Trend';
+import numeral from 'numeral';
+import styles from './Analysis.less';
+import NumberInfo from '@/components/NumberInfo';
+import { MiniArea } from '@/components/Charts';
+
+const columns = [
+  {
+    title: <FormattedMessage id="app.analysis.table.rank" defaultMessage="Rank" />,
+    dataIndex: 'index',
+    key: 'index',
+  },
+  {
+    title: (
+      <FormattedMessage id="app.analysis.table.search-keyword" defaultMessage="Search keyword" />
+    ),
+    dataIndex: 'keyword',
+    key: 'keyword',
+    render: text => <a href="/">{text}</a>,
+  },
+  {
+    title: <FormattedMessage id="app.analysis.table.users" defaultMessage="Users" />,
+    dataIndex: 'count',
+    key: 'count',
+    sorter: (a, b) => a.count - b.count,
+    className: styles.alignRight,
+  },
+  {
+    title: <FormattedMessage id="app.analysis.table.weekly-range" defaultMessage="Weekly Range" />,
+    dataIndex: 'range',
+    key: 'range',
+    sorter: (a, b) => a.range - b.range,
+    render: (text, record) => (
+      <Trend flag={record.status === 1 ? 'down' : 'up'}>
+        <span style={{ marginRight: 4 }}>{text}%</span>
+      </Trend>
+    ),
+    align: 'right',
+  },
+];
+
+const TopSearch = memo(({ loading, visitData2, searchData, dropdownGroup }) => (
+  <Card
+    loading={loading}
+    bordered={false}
+    title={
+      <FormattedMessage id="app.analysis.online-top-search" defaultMessage="Online Top Search" />
+    }
+    extra={dropdownGroup}
+    style={{ marginTop: 24 }}
+  >
+    <Row gutter={68}>
+      <Col sm={12} xs={24} style={{ marginBottom: 24 }}>
+        <NumberInfo
+          subTitle={
+            <span>
+              <FormattedMessage id="app.analysis.search-users" defaultMessage="search users" />
+              <Tooltip
+                title={<FormattedMessage id="app.analysis.introduce" defaultMessage="introduce" />}
+              >
+                <Icon style={{ marginLeft: 8 }} type="info-circle-o" />
+              </Tooltip>
+            </span>
+          }
+          gap={8}
+          total={numeral(12321).format('0,0')}
+          status="up"
+          subTotal={17.1}
+        />
+        <MiniArea line height={45} data={visitData2} />
+      </Col>
+      <Col sm={12} xs={24} style={{ marginBottom: 24 }}>
+        <NumberInfo
+          subTitle={
+            <span>
+              <FormattedMessage
+                id="app.analysis.per-capita-search"
+                defaultMessage="Per Capita Search"
+              />
+              <Tooltip
+                title={<FormattedMessage id="app.analysis.introduce" defaultMessage="introduce" />}
+              >
+                <Icon style={{ marginLeft: 8 }} type="info-circle-o" />
+              </Tooltip>
+            </span>
+          }
+          total={2.7}
+          status="down"
+          subTotal={26.2}
+          gap={8}
+        />
+        <MiniArea line height={45} data={visitData2} />
+      </Col>
+    </Row>
+    <Table
+      rowKey={record => record.index}
+      size="small"
+      columns={columns}
+      dataSource={searchData}
+      pagination={{
+        style: { marginBottom: 0 },
+        pageSize: 5,
+      }}
+    />
+  </Card>
+));
+
+export default TopSearch;