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 @@
"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": {
......
......@@ -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);
......
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>
<Suspense fallback={<PageLoading />}>
<BaseMenu
{...this.props}
mode="inline"
handleOpenChange={this.handleOpenChange}
onOpenChange={this.handleOpenChange}
style={{ padding: '16px 0', width: '100%' }}
{...defaultProps}
/>
</Suspense>
</Sider>
);
}
......
import { getFlatMenuKeys } from './index';
import { getFlatMenuKeys } from './SiderMenuUtils';
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 { 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;
......@@ -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} />
......
......@@ -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');
......
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 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>
));
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}
<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}
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%"
selectDate={this.selectDate}
/>
}
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>
<Row gutter={24}>
<Col xl={12} lg={24} md={24} sm={24} xs={24}>
<Card
<Suspense fallback={null}>
<TopSearch
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"
visitData2={visitData2}
selectDate={this.selectDate}
searchData={searchData}
dropdownGroup={dropdownGroup}
/>
<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>
</Suspense>
</Col>
<Col xl={12} lg={24} md={24} sm={24} xs={24}>
<Card
<Suspense fallback={null}>
<ProportionSales
dropdownGroup={dropdownGroup}
salesType={salesType}
loading={loading}
className={styles.salesCard}
bordered={false}
title={
<FormattedMessage
id="app.analysis.the-proportion-of-sales"
defaultMessage="The Proportion of Sales"
salesPieData={salesPieData}
handleChangeSalesType={this.handleChangeSalesType}
/>
}
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}
/>
</Card>
</Suspense>
</Col>
</Row>
<Card
<Suspense fallback={null}>
<OfflineData
activeKey={activeKey}
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' }),
}}
offlineData={offlineData}
offlineChartData={offlineChartData}
handleTabChange={this.handleTabChange}
/>
</div>
</TabPane>
))}
</Tabs>
</Card>
</Suspense>
</GridContent>
);
}
......
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