Analysis.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659
  1. import React, { Component } from 'react';
  2. import { connect } from 'dva';
  3. import { formatMessage, FormattedMessage } from 'umi/locale';
  4. import {
  5. Row,
  6. Col,
  7. Icon,
  8. Card,
  9. Tabs,
  10. Table,
  11. Radio,
  12. DatePicker,
  13. Tooltip,
  14. Menu,
  15. Dropdown,
  16. } from 'antd';
  17. import {
  18. ChartCard,
  19. MiniArea,
  20. MiniBar,
  21. MiniProgress,
  22. Field,
  23. Bar,
  24. Pie,
  25. TimelineChart,
  26. } from 'components/Charts';
  27. import Trend from 'components/Trend';
  28. import NumberInfo from 'components/NumberInfo';
  29. import numeral from 'numeral';
  30. import GridContent from '../layouts/GridContent';
  31. import Yuan from '../../utils/Yuan';
  32. import { getTimeDistance } from '../../utils/utils';
  33. import styles from './Analysis.less';
  34. const { TabPane } = Tabs;
  35. const { RangePicker } = DatePicker;
  36. const rankingListData = [];
  37. for (let i = 0; i < 7; i += 1) {
  38. rankingListData.push({
  39. title: `工专路 ${i} 号店`,
  40. total: 323234,
  41. });
  42. }
  43. @connect(({ chart, loading }) => ({
  44. chart,
  45. loading: loading.effects['chart/fetch'],
  46. }))
  47. class Analysis extends Component {
  48. constructor(props) {
  49. super(props);
  50. this.rankingListData = [];
  51. for (let i = 0; i < 7; i += 1) {
  52. this.rankingListData.push({
  53. title: formatMessage({ id: 'app.analysis.test' }, { no: i }),
  54. total: 323234,
  55. });
  56. }
  57. this.state = {
  58. salesType: 'all',
  59. currentTabKey: '',
  60. rangePickerValue: getTimeDistance('year'),
  61. };
  62. }
  63. state = {
  64. salesType: 'all',
  65. currentTabKey: '',
  66. rangePickerValue: getTimeDistance('year'),
  67. };
  68. componentDidMount() {
  69. const { dispatch } = this.props;
  70. dispatch({
  71. type: 'chart/fetch',
  72. });
  73. }
  74. componentWillUnmount() {
  75. const { dispatch } = this.props;
  76. dispatch({
  77. type: 'chart/clear',
  78. });
  79. }
  80. handleChangeSalesType = e => {
  81. this.setState({
  82. salesType: e.target.value,
  83. });
  84. };
  85. handleTabChange = key => {
  86. this.setState({
  87. currentTabKey: key,
  88. });
  89. };
  90. handleRangePickerChange = rangePickerValue => {
  91. const { dispatch } = this.props;
  92. this.setState({
  93. rangePickerValue,
  94. });
  95. dispatch({
  96. type: 'chart/fetchSalesData',
  97. });
  98. };
  99. selectDate = type => {
  100. const { dispatch } = this.props;
  101. this.setState({
  102. rangePickerValue: getTimeDistance(type),
  103. });
  104. dispatch({
  105. type: 'chart/fetchSalesData',
  106. });
  107. };
  108. isActive(type) {
  109. const { rangePickerValue } = this.state;
  110. const value = getTimeDistance(type);
  111. if (!rangePickerValue[0] || !rangePickerValue[1]) {
  112. return;
  113. }
  114. if (
  115. rangePickerValue[0].isSame(value[0], 'day') &&
  116. rangePickerValue[1].isSame(value[1], 'day')
  117. ) {
  118. return styles.currentDate;
  119. }
  120. }
  121. render() {
  122. const { rangePickerValue, salesType, currentTabKey } = this.state;
  123. const { chart, loading } = this.props;
  124. const {
  125. visitData,
  126. visitData2,
  127. salesData,
  128. searchData,
  129. offlineData,
  130. offlineChartData,
  131. salesTypeData,
  132. salesTypeDataOnline,
  133. salesTypeDataOffline,
  134. } = chart;
  135. const salesPieData =
  136. salesType === 'all'
  137. ? salesTypeData
  138. : salesType === 'online'
  139. ? salesTypeDataOnline
  140. : salesTypeDataOffline;
  141. const menu = (
  142. <Menu>
  143. <Menu.Item>操作一</Menu.Item>
  144. <Menu.Item>操作二</Menu.Item>
  145. </Menu>
  146. );
  147. const iconGroup = (
  148. <span className={styles.iconGroup}>
  149. <Dropdown overlay={menu} placement="bottomRight">
  150. <Icon type="ellipsis" />
  151. </Dropdown>
  152. </span>
  153. );
  154. const salesExtra = (
  155. <div className={styles.salesExtraWrap}>
  156. <div className={styles.salesExtra}>
  157. <a className={this.isActive('today')} onClick={() => this.selectDate('today')}>
  158. <FormattedMessage id="app.analysis.all-day" defaultMessage="All Day" />
  159. </a>
  160. <a className={this.isActive('week')} onClick={() => this.selectDate('week')}>
  161. <FormattedMessage id="app.analysis.all-week" defaultMessage="All Week" />
  162. </a>
  163. <a className={this.isActive('month')} onClick={() => this.selectDate('month')}>
  164. <FormattedMessage id="app.analysis.all-month" defaultMessage="All Month" />
  165. </a>
  166. <a className={this.isActive('year')} onClick={() => this.selectDate('year')}>
  167. <FormattedMessage id="app.analysis.all-year" defaultMessage="All Year" />
  168. </a>
  169. </div>
  170. <RangePicker
  171. value={rangePickerValue}
  172. onChange={this.handleRangePickerChange}
  173. style={{ width: 256 }}
  174. />
  175. </div>
  176. );
  177. const columns = [
  178. {
  179. title: <FormattedMessage id="app.analysis.table.rank" defaultMessage="Rank" />,
  180. dataIndex: 'index',
  181. key: 'index',
  182. },
  183. {
  184. title: (
  185. <FormattedMessage
  186. id="app.analysis.table.search-keyword"
  187. defaultMessage="Search keyword"
  188. />
  189. ),
  190. dataIndex: 'keyword',
  191. key: 'keyword',
  192. render: text => <a href="/">{text}</a>,
  193. },
  194. {
  195. title: <FormattedMessage id="app.analysis.table.users" defaultMessage="Users" />,
  196. dataIndex: 'count',
  197. key: 'count',
  198. sorter: (a, b) => a.count - b.count,
  199. className: styles.alignRight,
  200. },
  201. {
  202. title: (
  203. <FormattedMessage id="app.analysis.table.weekly-range" defaultMessage="Weekly Range" />
  204. ),
  205. dataIndex: 'range',
  206. key: 'range',
  207. sorter: (a, b) => a.range - b.range,
  208. render: (text, record) => (
  209. <Trend flag={record.status === 1 ? 'down' : 'up'}>
  210. <span style={{ marginRight: 4 }}>
  211. {text}
  212. %
  213. </span>
  214. </Trend>
  215. ),
  216. align: 'right',
  217. },
  218. ];
  219. const activeKey = currentTabKey || (offlineData[0] && offlineData[0].name);
  220. const CustomTab = ({ data, currentTabKey: currentKey }) => (
  221. <Row gutter={8} style={{ width: 138, margin: '8px 0' }}>
  222. <Col span={12}>
  223. <NumberInfo
  224. title={data.name}
  225. subTitle={
  226. <FormattedMessage
  227. id="app.analysis.conversion-rate"
  228. defaultMessage="Conversion Rate"
  229. />
  230. }
  231. gap={2}
  232. total={`${data.cvr * 100}%`}
  233. theme={currentKey !== data.name && 'light'}
  234. />
  235. </Col>
  236. <Col span={12} style={{ paddingTop: 36 }}>
  237. <Pie
  238. animate={false}
  239. color={currentKey !== data.name && '#BDE4FF'}
  240. inner={0.55}
  241. tooltip={false}
  242. margin={[0, 0, 0, 0]}
  243. percent={data.cvr * 100}
  244. height={64}
  245. />
  246. </Col>
  247. </Row>
  248. );
  249. const topColResponsiveProps = {
  250. xs: 24,
  251. sm: 12,
  252. md: 12,
  253. lg: 12,
  254. xl: 6,
  255. style: { marginBottom: 24 },
  256. };
  257. return (
  258. <GridContent>
  259. <Row gutter={24}>
  260. <Col {...topColResponsiveProps}>
  261. <ChartCard
  262. bordered={false}
  263. title={
  264. <FormattedMessage id="app.analysis.total-sales" defaultMessage="Total Sales" />
  265. }
  266. action={
  267. <Tooltip
  268. title={
  269. <FormattedMessage id="app.analysis.introduce" defaultMessage="introduce" />
  270. }
  271. >
  272. <Icon type="info-circle-o" />
  273. </Tooltip>
  274. }
  275. loading={loading}
  276. total={() => <Yuan>126560</Yuan>}
  277. footer={
  278. <Field
  279. label={
  280. <FormattedMessage id="app.analysis.day-sales" defaultMessage="Day Sales" />
  281. }
  282. value={`¥${numeral(12423).format('0,0')}`}
  283. />
  284. }
  285. contentHeight={46}
  286. >
  287. <Trend flag="up" style={{ marginRight: 16 }}>
  288. <FormattedMessage id="app.analysis.week" defaultMessage="Weekly Changes" />
  289. <span className={styles.trendText}>12%</span>
  290. </Trend>
  291. <Trend flag="down">
  292. <FormattedMessage id="app.analysis.day" defaultMessage="Daily Changes" />
  293. <span className={styles.trendText}>11%</span>
  294. </Trend>
  295. </ChartCard>
  296. </Col>
  297. <Col {...topColResponsiveProps}>
  298. <ChartCard
  299. bordered={false}
  300. loading={loading}
  301. title={<FormattedMessage id="app.analysis.visits" defaultMessage="visits" />}
  302. action={
  303. <Tooltip
  304. title={
  305. <FormattedMessage id="app.analysis.introduce" defaultMessage="introduce" />
  306. }
  307. >
  308. <Icon type="info-circle-o" />
  309. </Tooltip>
  310. }
  311. total={numeral(8846).format('0,0')}
  312. footer={
  313. <Field
  314. label={
  315. <FormattedMessage id="app.analysis.day-visits" defaultMessage="Day Visits" />
  316. }
  317. value={numeral(1234).format('0,0')}
  318. />
  319. }
  320. contentHeight={46}
  321. >
  322. <MiniArea color="#975FE4" data={visitData} />
  323. </ChartCard>
  324. </Col>
  325. <Col {...topColResponsiveProps}>
  326. <ChartCard
  327. bordered={false}
  328. loading={loading}
  329. title={<FormattedMessage id="app.analysis.payments" defaultMessage="Payments" />}
  330. action={
  331. <Tooltip
  332. title={
  333. <FormattedMessage id="app.analysis.introduce" defaultMessage="Introduce" />
  334. }
  335. >
  336. <Icon type="info-circle-o" />
  337. </Tooltip>
  338. }
  339. total={numeral(6560).format('0,0')}
  340. footer={
  341. <Field
  342. label={
  343. <FormattedMessage
  344. id="app.analysis.conversion-rate"
  345. defaultMessage="Conversion Rate"
  346. />
  347. }
  348. value="60%"
  349. />
  350. }
  351. contentHeight={46}
  352. >
  353. <MiniBar data={visitData} />
  354. </ChartCard>
  355. </Col>
  356. <Col {...topColResponsiveProps}>
  357. <ChartCard
  358. loading={loading}
  359. bordered={false}
  360. title={
  361. <FormattedMessage
  362. id="app.analysis.operational-effect"
  363. defaultMessage="Operational Effect"
  364. />
  365. }
  366. action={
  367. <Tooltip
  368. title={
  369. <FormattedMessage id="app.analysis.introduce" defaultMessage="introduce" />
  370. }
  371. >
  372. <Icon type="info-circle-o" />
  373. </Tooltip>
  374. }
  375. total="78%"
  376. footer={
  377. <div style={{ whiteSpace: 'nowrap', overflow: 'hidden' }}>
  378. <Trend flag="up" style={{ marginRight: 16 }}>
  379. <FormattedMessage id="app.analysis.week" defaultMessage="Weekly changes" />
  380. <span className={styles.trendText}>12%</span>
  381. </Trend>
  382. <Trend flag="down">
  383. <FormattedMessage id="app.analysis.day" defaultMessage="Weekly changes" />
  384. <span className={styles.trendText}>11%</span>
  385. </Trend>
  386. </div>
  387. }
  388. contentHeight={46}
  389. >
  390. <MiniProgress percent={78} strokeWidth={8} target={80} color="#13C2C2" />
  391. </ChartCard>
  392. </Col>
  393. </Row>
  394. <Card loading={loading} bordered={false} bodyStyle={{ padding: 0 }}>
  395. <div className={styles.salesCard}>
  396. <Tabs tabBarExtraContent={salesExtra} size="large" tabBarStyle={{ marginBottom: 24 }}>
  397. <TabPane
  398. tab={<FormattedMessage id="app.analysis.sales" defaultMessage="Sales" />}
  399. key="sales"
  400. >
  401. <Row>
  402. <Col xl={16} lg={12} md={12} sm={24} xs={24}>
  403. <div className={styles.salesBar}>
  404. <Bar
  405. height={295}
  406. title={
  407. <FormattedMessage
  408. id="app.analysis.sales-trend"
  409. defaultMessage="Sales Trend"
  410. />
  411. }
  412. data={salesData}
  413. />
  414. </div>
  415. </Col>
  416. <Col xl={8} lg={12} md={12} sm={24} xs={24}>
  417. <div className={styles.salesRank}>
  418. <h4 className={styles.rankingTitle}>
  419. <FormattedMessage
  420. id="app.analysis.sales-ranking"
  421. defaultMessage="Sales Ranking"
  422. />
  423. </h4>
  424. <ul className={styles.rankingList}>
  425. {this.rankingListData.map((item, i) => (
  426. <li key={item.title}>
  427. <span className={i < 3 ? styles.active : ''}>{i + 1}</span>
  428. <span>{item.title}</span>
  429. <span>{numeral(item.total).format('0,0')}</span>
  430. </li>
  431. ))}
  432. </ul>
  433. </div>
  434. </Col>
  435. </Row>
  436. </TabPane>
  437. <TabPane
  438. tab={<FormattedMessage id="app.analysis.visits" defaultMessage="Visits" />}
  439. key="views"
  440. >
  441. <Row>
  442. <Col xl={16} lg={12} md={12} sm={24} xs={24}>
  443. <div className={styles.salesBar}>
  444. <Bar
  445. height={292}
  446. title={
  447. <FormattedMessage
  448. id="app.analysis.visits-trend"
  449. defaultMessage="Visits Trend"
  450. />
  451. }
  452. data={salesData}
  453. />
  454. </div>
  455. </Col>
  456. <Col xl={8} lg={12} md={12} sm={24} xs={24}>
  457. <div className={styles.salesRank}>
  458. <h4 className={styles.rankingTitle}>
  459. <FormattedMessage
  460. id="app.analysis.visits-ranking"
  461. defaultMessage="Visits Ranking"
  462. />
  463. </h4>
  464. <ul className={styles.rankingList}>
  465. {this.rankingListData.map((item, i) => (
  466. <li key={item.title}>
  467. <span className={i < 3 ? styles.active : ''}>{i + 1}</span>
  468. <span>{item.title}</span>
  469. <span>{numeral(item.total).format('0,0')}</span>
  470. </li>
  471. ))}
  472. </ul>
  473. </div>
  474. </Col>
  475. </Row>
  476. </TabPane>
  477. </Tabs>
  478. </div>
  479. </Card>
  480. <Row gutter={24}>
  481. <Col xl={12} lg={24} md={24} sm={24} xs={24}>
  482. <Card
  483. loading={loading}
  484. bordered={false}
  485. title={
  486. <FormattedMessage
  487. id="app.analysis.online-top-search"
  488. defaultMessage="Online Top Search"
  489. />
  490. }
  491. extra={iconGroup}
  492. style={{ marginTop: 24 }}
  493. >
  494. <Row gutter={68}>
  495. <Col sm={12} xs={24} style={{ marginBottom: 24 }}>
  496. <NumberInfo
  497. subTitle={
  498. <span>
  499. <FormattedMessage
  500. id="app.analysis.search-users"
  501. defaultMessage="search users"
  502. />
  503. <Tooltip
  504. title={
  505. <FormattedMessage
  506. id="app.analysis.introduce"
  507. defaultMessage="introduce"
  508. />
  509. }
  510. >
  511. <Icon style={{ marginLeft: 8 }} type="info-circle-o" />
  512. </Tooltip>
  513. </span>
  514. }
  515. gap={8}
  516. total={numeral(12321).format('0,0')}
  517. status="up"
  518. subTotal={17.1}
  519. />
  520. <MiniArea line height={45} data={visitData2} />
  521. </Col>
  522. <Col sm={12} xs={24} style={{ marginBottom: 24 }}>
  523. <NumberInfo
  524. subTitle={
  525. <FormattedMessage
  526. id="app.analysis.per-capita-search"
  527. defaultMessage="Per Capita Search"
  528. />
  529. }
  530. total={2.7}
  531. status="down"
  532. subTotal={26.2}
  533. gap={8}
  534. />
  535. <MiniArea line height={45} data={visitData2} />
  536. </Col>
  537. </Row>
  538. <Table
  539. rowKey={record => record.index}
  540. size="small"
  541. columns={columns}
  542. dataSource={searchData}
  543. pagination={{
  544. style: { marginBottom: 0 },
  545. pageSize: 5,
  546. }}
  547. />
  548. </Card>
  549. </Col>
  550. <Col xl={12} lg={24} md={24} sm={24} xs={24}>
  551. <Card
  552. loading={loading}
  553. className={styles.salesCard}
  554. bordered={false}
  555. title={
  556. <FormattedMessage
  557. id="app.analysis.the-proportion-of-sales"
  558. defaultMessage="The Proportion of Sales"
  559. />
  560. }
  561. bodyStyle={{ padding: 24 }}
  562. extra={
  563. <div className={styles.salesCardExtra}>
  564. {iconGroup}
  565. <div className={styles.salesTypeRadio}>
  566. <Radio.Group value={salesType} onChange={this.handleChangeSalesType}>
  567. <Radio.Button value="all">
  568. <FormattedMessage id="app.analysis.channel.all" defaultMessage="ALL" />
  569. </Radio.Button>
  570. <Radio.Button value="online">
  571. <FormattedMessage
  572. id="app.analysis.channel.online"
  573. defaultMessage="Online"
  574. />
  575. </Radio.Button>
  576. <Radio.Button value="stores">
  577. <FormattedMessage
  578. id="app.analysis.channel.stores"
  579. defaultMessage="Stores"
  580. />
  581. </Radio.Button>
  582. </Radio.Group>
  583. </div>
  584. </div>
  585. }
  586. style={{ marginTop: 24, minHeight: 509 }}
  587. >
  588. <h4 style={{ marginTop: 8, marginBottom: 32 }}>
  589. <FormattedMessage id="app.analysis.sales" defaultMessage="Sales" />
  590. </h4>
  591. <Pie
  592. hasLegend
  593. subTitle={<FormattedMessage id="app.analysis.sales" defaultMessage="Sales" />}
  594. total={() => <Yuan>{salesPieData.reduce((pre, now) => now.y + pre, 0)}</Yuan>}
  595. data={salesPieData}
  596. valueFormat={value => <Yuan>{value}</Yuan>}
  597. height={248}
  598. lineWidth={4}
  599. />
  600. </Card>
  601. </Col>
  602. </Row>
  603. <Card
  604. loading={loading}
  605. className={styles.offlineCard}
  606. bordered={false}
  607. bodyStyle={{ padding: '0 0 32px 0' }}
  608. style={{ marginTop: 32 }}
  609. >
  610. <Tabs activeKey={activeKey} onChange={this.handleTabChange}>
  611. {offlineData.map(shop => (
  612. <TabPane tab={<CustomTab data={shop} currentTabKey={activeKey} />} key={shop.name}>
  613. <div style={{ padding: '0 24px' }}>
  614. <TimelineChart
  615. height={400}
  616. data={offlineChartData}
  617. titleMap={{
  618. y1: <FormattedMessage id="app.analysis.traffic" defaultMessage="Traffic" />,
  619. y2: <FormattedMessage id="app.analysis.payments" defaultMessage="Payments" />,
  620. }}
  621. />
  622. </div>
  623. </TabPane>
  624. ))}
  625. </Tabs>
  626. </Card>
  627. </GridContent>
  628. );
  629. }
  630. }
  631. export default Analysis;