Unverified Commit 27562512 authored by 陈帅's avatar 陈帅 Committed by GitHub

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
parent 35239ea7
// 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',
],
},
};
...@@ -55,6 +55,7 @@ ...@@ -55,6 +55,7 @@
"react-document-title": "^2.0.3", "react-document-title": "^2.0.3",
"react-dom": "^16.5.1", "react-dom": "^16.5.1",
"react-fittext": "^1.0.0", "react-fittext": "^1.0.0",
"react-media": "^1.8.0",
"react-router-dom": "^4.3.1" "react-router-dom": "^4.3.1"
}, },
"devDependencies": { "devDependencies": {
......
...@@ -3,8 +3,8 @@ import { Menu, Icon } from 'antd'; ...@@ -3,8 +3,8 @@ import { Menu, Icon } from 'antd';
import Link from 'umi/link'; import Link from 'umi/link';
import isEqual from 'lodash/isEqual'; import isEqual from 'lodash/isEqual';
import memoizeOne from 'memoize-one'; import memoizeOne from 'memoize-one';
import pathToRegexp from 'path-to-regexp';
import { urlToList } from '../_utils/pathTools'; import { urlToList } from '../_utils/pathTools';
import { getMenuMatches } from './SiderMenuUtils';
import styles from './index.less'; import styles from './index.less';
const { SubMenu } = Menu; const { SubMenu } = Menu;
...@@ -23,14 +23,6 @@ const getIcon = icon => { ...@@ -23,14 +23,6 @@ const getIcon = icon => {
return 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 { export default class BaseMenu extends PureComponent {
constructor(props) { constructor(props) {
super(props); super(props);
......
import React, { PureComponent } from 'react'; import React, { PureComponent, Suspense } from 'react';
import { Layout } from 'antd'; import { Layout } from 'antd';
import classNames from 'classnames'; import classNames from 'classnames';
import Link from 'umi/link'; import Link from 'umi/link';
import styles from './index.less'; import styles from './index.less';
import BaseMenu, { getMenuMatches } from './BaseMenu'; import PageLoading from '../PageLoading';
import { urlToList } from '../_utils/pathTools'; import { getDefaultCollapsedSubMenus } from './SiderMenuUtils';
const BaseMenu = React.lazy(() => import('./BaseMenu'));
const { Sider } = Layout; 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 { export default class SiderMenu extends PureComponent {
constructor(props) { constructor(props) {
super(props); super(props);
...@@ -67,7 +54,6 @@ export default class SiderMenu extends PureComponent { ...@@ -67,7 +54,6 @@ export default class SiderMenu extends PureComponent {
[styles.fixSiderbar]: fixSiderbar, [styles.fixSiderbar]: fixSiderbar,
[styles.light]: theme === 'light', [styles.light]: theme === 'light',
}); });
return ( return (
<Sider <Sider
trigger={null} trigger={null}
...@@ -85,13 +71,16 @@ export default class SiderMenu extends PureComponent { ...@@ -85,13 +71,16 @@ export default class SiderMenu extends PureComponent {
<h1>Ant Design Pro</h1> <h1>Ant Design Pro</h1>
</Link> </Link>
</div> </div>
<BaseMenu <Suspense fallback={<PageLoading />}>
{...this.props} <BaseMenu
mode="inline" {...this.props}
handleOpenChange={this.handleOpenChange} mode="inline"
style={{ padding: '16px 0', width: '100%' }} handleOpenChange={this.handleOpenChange}
{...defaultProps} onOpenChange={this.handleOpenChange}
/> style={{ padding: '16px 0', width: '100%' }}
{...defaultProps}
/>
</Suspense>
</Sider> </Sider>
); );
} }
......
import { getFlatMenuKeys } from './index'; import { getFlatMenuKeys } from './SiderMenuUtils';
const menu = [ const menu = [
{ {
......
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);
};
import React from 'react'; import React from 'react';
import { Drawer } from 'antd'; import { Drawer } from 'antd';
import SiderMenu from './SiderMenu'; import SiderMenu from './SiderMenu';
import { getFlatMenuKeys } from './SiderMenuUtils';
/** const SiderMenuWrapper = React.memo(props => {
* 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 { isMobile, menuData, collapsed, onCollapse } = props; const { isMobile, menuData, collapsed, onCollapse } = props;
const flatMenuKeys = getFlatMenuKeys(menuData);
return isMobile ? ( return isMobile ? (
<Drawer <Drawer
visible={!collapsed} visible={!collapsed}
...@@ -30,15 +16,11 @@ const SiderMenuWrapper = props => { ...@@ -30,15 +16,11 @@ const SiderMenuWrapper = props => {
height: '100vh', height: '100vh',
}} }}
> >
<SiderMenu <SiderMenu {...props} flatMenuKeys={flatMenuKeys} collapsed={isMobile ? false : collapsed} />
{...props}
flatMenuKeys={getFlatMenuKeys(menuData)}
collapsed={isMobile ? false : collapsed}
/>
</Drawer> </Drawer>
) : ( ) : (
<SiderMenu {...props} flatMenuKeys={getFlatMenuKeys(menuData)} /> <SiderMenu {...props} flatMenuKeys={flatMenuKeys} />
); );
}; });
export default SiderMenuWrapper; export default SiderMenuWrapper;
...@@ -2,6 +2,7 @@ import React, { PureComponent } from 'react'; ...@@ -2,6 +2,7 @@ import React, { PureComponent } 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 styles from './index.less'; import styles from './index.less';
export default class TopNavHeader extends PureComponent { export default class TopNavHeader extends PureComponent {
...@@ -16,8 +17,9 @@ export default class TopNavHeader extends PureComponent { ...@@ -16,8 +17,9 @@ export default class TopNavHeader extends PureComponent {
} }
render() { render() {
const { theme, contentWidth, logo } = this.props; const { theme, contentWidth, menuData, logo } = this.props;
const { maxWidth } = this.state; const { maxWidth } = this.state;
const flatMenuKeys = getFlatMenuKeys(menuData);
return ( return (
<div className={`${styles.head} ${theme === 'light' ? styles.light : ''}`}> <div className={`${styles.head} ${theme === 'light' ? styles.light : ''}`}>
<div <div
...@@ -38,7 +40,11 @@ export default class TopNavHeader extends PureComponent { ...@@ -38,7 +40,11 @@ export default class TopNavHeader extends PureComponent {
maxWidth, maxWidth,
}} }}
> >
<BaseMenu {...this.props} style={{ border: 'none', height: 64 }} /> <BaseMenu
{...this.props}
flatMenuKeys={flatMenuKeys}
style={{ border: 'none', height: 64 }}
/>
</div> </div>
</div> </div>
<RightContent {...this.props} /> <RightContent {...this.props} />
......
...@@ -7,7 +7,7 @@ describe('Homepage', () => { ...@@ -7,7 +7,7 @@ describe('Homepage', () => {
it('it should have logo text', async () => { it('it should have logo text', async () => {
await page.goto(BASE_URL); await page.goto(BASE_URL);
await page.waitForSelector('h1', { await page.waitForSelector('h1', {
timeout: 2000, timeout: 5000,
}); });
const text = await page.evaluate(() => document.getElementsByTagName('h1')[0].innerText); const text = await page.evaluate(() => document.getElementsByTagName('h1')[0].innerText);
expect(text).toContain('Ant Design Pro'); expect(text).toContain('Ant Design Pro');
......
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();
});
});
import React from 'react'; import React, { Suspense } from 'react';
import { Layout } from 'antd'; import { Layout } from 'antd';
import DocumentTitle from 'react-document-title'; import DocumentTitle from 'react-document-title';
import isEqual from 'lodash/isEqual'; import isEqual from 'lodash/isEqual';
...@@ -7,16 +7,19 @@ import { connect } from 'dva'; ...@@ -7,16 +7,19 @@ import { connect } from 'dva';
import { ContainerQuery } from 'react-container-query'; import { ContainerQuery } from 'react-container-query';
import classNames from 'classnames'; import classNames from 'classnames';
import pathToRegexp from 'path-to-regexp'; import pathToRegexp from 'path-to-regexp';
import { enquireScreen, unenquireScreen } from 'enquire-js'; import Media from 'react-media';
import { formatMessage } from 'umi/locale'; import { formatMessage } from 'umi/locale';
import SiderMenu from '@/components/SiderMenu';
import Authorized from '@/utils/Authorized'; import Authorized from '@/utils/Authorized';
import SettingDrawer from '@/components/SettingDrawer';
import logo from '../assets/logo.svg'; import logo from '../assets/logo.svg';
import Footer from './Footer'; import Footer from './Footer';
import Header from './Header'; import Header from './Header';
import Context from './MenuContext'; import Context from './MenuContext';
import Exception403 from '../pages/Exception/403'; 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; const { Content } = Layout;
...@@ -88,8 +91,6 @@ class BasicLayout extends React.PureComponent { ...@@ -88,8 +91,6 @@ class BasicLayout extends React.PureComponent {
} }
state = { state = {
rendering: true,
isMobile: false,
menuData: this.getMenuData(), menuData: this.getMenuData(),
}; };
...@@ -101,27 +102,13 @@ class BasicLayout extends React.PureComponent { ...@@ -101,27 +102,13 @@ class BasicLayout extends React.PureComponent {
dispatch({ dispatch({
type: 'setting/getSetting', 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) { componentDidUpdate(preProps) {
// After changing to phone mode, // After changing to phone mode,
// if collapsed is true, you need to click twice to display // if collapsed is true, you need to click twice to display
this.breadcrumbNameMap = this.getBreadcrumbNameMap(); this.breadcrumbNameMap = this.getBreadcrumbNameMap();
const { isMobile } = this.state; const { collapsed, isMobile } = this.props;
const { collapsed } = this.props;
if (isMobile && !preProps.isMobile && !collapsed) { if (isMobile && !preProps.isMobile && !collapsed) {
this.handleMenuCollapse(false); this.handleMenuCollapse(false);
} }
...@@ -129,7 +116,6 @@ class BasicLayout extends React.PureComponent { ...@@ -129,7 +116,6 @@ class BasicLayout extends React.PureComponent {
componentWillUnmount() { componentWillUnmount() {
cancelAnimationFrame(this.renderRef); cancelAnimationFrame(this.renderRef);
unenquireScreen(this.enquireHandler);
} }
getContext() { getContext() {
...@@ -187,8 +173,7 @@ class BasicLayout extends React.PureComponent { ...@@ -187,8 +173,7 @@ class BasicLayout extends React.PureComponent {
}; };
getLayoutStyle = () => { getLayoutStyle = () => {
const { isMobile } = this.state; const { fixSiderbar, isMobile, collapsed, layout } = this.props;
const { fixSiderbar, collapsed, layout } = this.props;
if (fixSiderbar && layout !== 'topmenu' && !isMobile) { if (fixSiderbar && layout !== 'topmenu' && !isMobile) {
return { return {
paddingLeft: collapsed ? '80px' : '256px', paddingLeft: collapsed ? '80px' : '256px',
...@@ -213,15 +198,14 @@ class BasicLayout extends React.PureComponent { ...@@ -213,15 +198,14 @@ class BasicLayout extends React.PureComponent {
}); });
}; };
renderSettingDrawer() { renderSettingDrawer = () => {
// Do not render SettingDrawer in production // Do not render SettingDrawer in production
// unless it is deployed in preview.pro.ant.design as demo // unless it is deployed in preview.pro.ant.design as demo
const { rendering } = this.state; if (process.env.NODE_ENV === 'production' && APP_TYPE !== 'site') {
if ((rendering || process.env.NODE_ENV === 'production') && APP_TYPE !== 'site') {
return null; return null;
} }
return <SettingDrawer />; return <SettingDrawer />;
} };
render() { render() {
const { const {
...@@ -229,8 +213,9 @@ class BasicLayout extends React.PureComponent { ...@@ -229,8 +213,9 @@ class BasicLayout extends React.PureComponent {
layout: PropsLayout, layout: PropsLayout,
children, children,
location: { pathname }, location: { pathname },
isMobile,
} = this.props; } = this.props;
const { isMobile, menuData } = this.state; const { menuData } = this.state;
const isTop = PropsLayout === 'topmenu'; const isTop = PropsLayout === 'topmenu';
const routerConfig = this.matchParamsPath(pathname); const routerConfig = this.matchParamsPath(pathname);
const layout = ( const layout = (
...@@ -282,7 +267,7 @@ class BasicLayout extends React.PureComponent { ...@@ -282,7 +267,7 @@ class BasicLayout extends React.PureComponent {
)} )}
</ContainerQuery> </ContainerQuery>
</DocumentTitle> </DocumentTitle>
{this.renderSettingDrawer()} <Suspense fallback={<PageLoading />}>{this.renderSettingDrawer()}</Suspense>
</React.Fragment> </React.Fragment>
); );
} }
...@@ -292,4 +277,8 @@ export default connect(({ global, setting }) => ({ ...@@ -292,4 +277,8 @@ export default connect(({ global, setting }) => ({
collapsed: global.collapsed, collapsed: global.collapsed,
layout: setting.layout, layout: setting.layout,
...setting, ...setting,
}))(BasicLayout); }))(props => (
<Media query="(max-width: 599px)">
{isMobile => <BasicLayout {...props} isMobile={isMobile} />}
</Media>
));
This diff is collapsed.
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;
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;
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;
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;
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;
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment