Analysis.js 21 KB

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