diff --git a/config/config.ts b/config/config.ts
index c7b5c76b5c14402af430d2097bb4d97421b20ae3..7d55213b386db620f3925feaa175848ce0992862 100644
--- a/config/config.ts
+++ b/config/config.ts
@@ -93,12 +93,11 @@ export default {
Routes: ['src/pages/Authorized'],
authority: ['admin', 'user'],
routes: [
- // dashboard
{
- path: '/',
- name: 'welcome',
- icon: 'smile',
- component: './Welcome',
+ path: '/analysis',
+ name: 'Analysis',
+ icon: 'dashboard',
+ component: './analysis',
},
],
},
diff --git a/package.json b/package.json
index 5004028e88b7f5dec2f157eb4368e095fd495c34..924a704ad8fc5df58a5262e4107b94928d6c465c 100644
--- a/package.json
+++ b/package.json
@@ -20,7 +20,7 @@
"lint-staged": "lint-staged",
"lint-staged:js": "eslint --ext .js",
"lint-staged:ts": "tslint",
- "lint:fix": "eslint --fix --ext .js src mock tests && stylelint --fix 'src/**/*.less' --syntax less",
+ "lint:fix": "eslint --fix --ext .js src tests && stylelint --fix 'src/**/*.less' --syntax less",
"lint:js": "eslint --ext .js src tests",
"lint:prettier": "check-prettier lint",
"lint:style": "stylelint 'src/**/*.less' --syntax less",
@@ -57,6 +57,7 @@
"dependencies": {
"@ant-design/pro-layout": "^4.1.0",
"@antv/data-set": "^0.10.1",
+ "ant-design-pro": "^2.1.1",
"antd": "^3.16.1",
"bizcharts": "^3.4.3",
"bizcharts-plugin-slider": "^2.1.1-beta.1",
@@ -67,6 +68,7 @@
"lodash-decorators": "^6.0.0",
"memoize-one": "^5.0.0",
"moment": "^2.22.2",
+ "numeral": "^2.0.6",
"omit.js": "^1.0.0",
"path-to-regexp": "^2.4.0",
"qs": "^6.7.0",
diff --git a/src/pages/analysis/_mock.ts b/src/pages/analysis/_mock.ts
new file mode 100644
index 0000000000000000000000000000000000000000..29100ede39df287ae10b52429d7d4b588d5126ef
--- /dev/null
+++ b/src/pages/analysis/_mock.ts
@@ -0,0 +1,197 @@
+import moment from 'moment';
+import { IVisitData, IRadarData, IAnalysisData } from './data';
+
+// mock data
+const visitData: IVisitData[] = [];
+const beginDay = new Date().getTime();
+
+const fakeY = [7, 5, 4, 2, 4, 7, 5, 6, 5, 9, 6, 3, 1, 5, 3, 6, 5];
+for (let i = 0; i < fakeY.length; i += 1) {
+ visitData.push({
+ x: moment(new Date(beginDay + 1000 * 60 * 60 * 24 * i)).format('YYYY-MM-DD'),
+ y: fakeY[i],
+ });
+}
+
+const visitData2 = [];
+const fakeY2 = [1, 6, 4, 8, 3, 7, 2];
+for (let i = 0; i < fakeY2.length; i += 1) {
+ visitData2.push({
+ x: moment(new Date(beginDay + 1000 * 60 * 60 * 24 * i)).format('YYYY-MM-DD'),
+ y: fakeY2[i],
+ });
+}
+
+const salesData = [];
+for (let i = 0; i < 12; i += 1) {
+ salesData.push({
+ x: `${i + 1}月`,
+ y: Math.floor(Math.random() * 1000) + 200,
+ });
+}
+const searchData = [];
+for (let i = 0; i < 50; i += 1) {
+ searchData.push({
+ index: i + 1,
+ keyword: `搜索关键词-${i}`,
+ count: Math.floor(Math.random() * 1000),
+ range: Math.floor(Math.random() * 100),
+ status: Math.floor((Math.random() * 10) % 2),
+ });
+}
+const salesTypeData = [
+ {
+ x: '家用电器',
+ y: 4544,
+ },
+ {
+ x: '食用酒水',
+ y: 3321,
+ },
+ {
+ x: '个护健康',
+ y: 3113,
+ },
+ {
+ x: '服饰箱包',
+ y: 2341,
+ },
+ {
+ x: '母婴产品',
+ y: 1231,
+ },
+ {
+ x: '其他',
+ y: 1231,
+ },
+];
+
+const salesTypeDataOnline = [
+ {
+ x: '家用电器',
+ y: 244,
+ },
+ {
+ x: '食用酒水',
+ y: 321,
+ },
+ {
+ x: '个护健康',
+ y: 311,
+ },
+ {
+ x: '服饰箱包',
+ y: 41,
+ },
+ {
+ x: '母婴产品',
+ y: 121,
+ },
+ {
+ x: '其他',
+ y: 111,
+ },
+];
+
+const salesTypeDataOffline = [
+ {
+ x: '家用电器',
+ y: 99,
+ },
+ {
+ x: '食用酒水',
+ y: 188,
+ },
+ {
+ x: '个护健康',
+ y: 344,
+ },
+ {
+ x: '服饰箱包',
+ y: 255,
+ },
+ {
+ x: '其他',
+ y: 65,
+ },
+];
+
+const offlineData = [];
+for (let i = 0; i < 10; i += 1) {
+ offlineData.push({
+ name: `Stores ${i}`,
+ cvr: Math.ceil(Math.random() * 9) / 10,
+ });
+}
+const offlineChartData = [];
+for (let i = 0; i < 20; i += 1) {
+ offlineChartData.push({
+ x: new Date().getTime() + 1000 * 60 * 30 * i,
+ y1: Math.floor(Math.random() * 100) + 10,
+ y2: Math.floor(Math.random() * 100) + 10,
+ });
+}
+
+const radarOriginData = [
+ {
+ name: '个人',
+ ref: 10,
+ koubei: 8,
+ output: 4,
+ contribute: 5,
+ hot: 7,
+ },
+ {
+ name: '团队',
+ ref: 3,
+ koubei: 9,
+ output: 6,
+ contribute: 3,
+ hot: 1,
+ },
+ {
+ name: '部门',
+ ref: 4,
+ koubei: 1,
+ output: 6,
+ contribute: 5,
+ hot: 7,
+ },
+];
+
+const radarData: IRadarData[] = [];
+const radarTitleMap = {
+ ref: '引用',
+ koubei: '口碑',
+ output: '产量',
+ contribute: '贡献',
+ hot: '热度',
+};
+radarOriginData.forEach(item => {
+ Object.keys(item).forEach(key => {
+ if (key !== 'name') {
+ radarData.push({
+ name: item.name,
+ label: radarTitleMap[key],
+ value: item[key],
+ });
+ }
+ });
+});
+
+const getFakeChartData: IAnalysisData = {
+ visitData,
+ visitData2,
+ salesData,
+ searchData,
+ offlineData,
+ offlineChartData,
+ salesTypeData,
+ salesTypeDataOnline,
+ salesTypeDataOffline,
+ radarData,
+};
+
+export default {
+ 'GET /api/analysis/fake_chart_data': getFakeChartData,
+};
diff --git a/src/pages/analysis/components/Charts/Bar/index.tsx b/src/pages/analysis/components/Charts/Bar/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..1eaa6454a9957d3e3dbb0f00c6db286c654cc098
--- /dev/null
+++ b/src/pages/analysis/components/Charts/Bar/index.tsx
@@ -0,0 +1,133 @@
+import React, { Component } from 'react';
+import { Chart, Axis, Tooltip, Geom } from 'bizcharts';
+import Debounce from 'lodash-decorators/debounce';
+import Bind from 'lodash-decorators/bind';
+import autoHeight from '../autoHeight';
+import styles from '../index.less';
+
+export interface IBarProps {
+ title: React.ReactNode;
+ color?: string;
+ padding?: [number, number, number, number];
+ height?: number;
+ data: Array<{
+ x: string;
+ y: number;
+ }>;
+ forceFit?: boolean;
+ autoLabel?: boolean;
+ style?: React.CSSProperties;
+}
+
+class Bar extends Component<
+ IBarProps,
+ {
+ autoHideXLabels: boolean;
+ }
+> {
+ root: HTMLDivElement | undefined;
+ node: HTMLDivElement | undefined;
+
+ state = {
+ autoHideXLabels: false,
+ };
+
+ componentDidMount() {
+ window.addEventListener('resize', this.resize, { passive: true });
+ }
+
+ componentWillUnmount() {
+ window.removeEventListener('resize', this.resize);
+ }
+
+ handleRoot = (n: HTMLDivElement) => {
+ this.root = n;
+ };
+ handleRef = (n: HTMLDivElement) => {
+ this.node = n;
+ };
+
+ @Bind()
+ @Debounce(400)
+ resize() {
+ if (!this.node || !this.node.parentNode) {
+ return;
+ }
+ const canvasWidth = (this.node.parentNode as HTMLDivElement).clientWidth;
+ const { data = [], autoLabel = true } = this.props;
+ if (!autoLabel) {
+ return;
+ }
+ const minWidth = data.length * 30;
+ const { autoHideXLabels } = this.state;
+
+ if (canvasWidth <= minWidth) {
+ if (!autoHideXLabels) {
+ this.setState({
+ autoHideXLabels: true,
+ });
+ }
+ } else if (autoHideXLabels) {
+ this.setState({
+ autoHideXLabels: false,
+ });
+ }
+ }
+
+ render() {
+ const {
+ height = 1,
+ title,
+ forceFit = true,
+ data,
+ color = 'rgba(24, 144, 255, 0.85)',
+ padding,
+ } = this.props;
+
+ const { autoHideXLabels } = this.state;
+
+ const scale = {
+ x: {
+ type: 'cat',
+ },
+ y: {
+ min: 0,
+ },
+ };
+
+ const tooltip: [string, (...args: any[]) => { name?: string; value: string }] = [
+ 'x*y',
+ (x: string, y: string) => ({
+ name: x,
+ value: y,
+ }),
+ ];
+
+ return (
+
+
+ {title &&
{title}
}
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+export default autoHeight()(Bar);
diff --git a/src/pages/analysis/components/Charts/ChartCard/index.less b/src/pages/analysis/components/Charts/ChartCard/index.less
new file mode 100644
index 0000000000000000000000000000000000000000..282f17d9cf32af486e1c13d8d55bec1a9e5076f1
--- /dev/null
+++ b/src/pages/analysis/components/Charts/ChartCard/index.less
@@ -0,0 +1,75 @@
+@import '~antd/lib/style/themes/default.less';
+
+.chartCard {
+ position: relative;
+ .chartTop {
+ position: relative;
+ width: 100%;
+ overflow: hidden;
+ }
+ .chartTopMargin {
+ margin-bottom: 12px;
+ }
+ .chartTopHasMargin {
+ margin-bottom: 20px;
+ }
+ .metaWrap {
+ float: left;
+ }
+ .avatar {
+ position: relative;
+ top: 4px;
+ float: left;
+ margin-right: 20px;
+ img {
+ border-radius: 100%;
+ }
+ }
+ .meta {
+ height: 22px;
+ color: @text-color-secondary;
+ font-size: @font-size-base;
+ line-height: 22px;
+ }
+ .action {
+ position: absolute;
+ top: 4px;
+ right: 0;
+ line-height: 1;
+ cursor: pointer;
+ }
+ .total {
+ height: 38px;
+ margin-top: 4px;
+ margin-bottom: 0;
+ overflow: hidden;
+ color: @heading-color;
+ font-size: 30px;
+ line-height: 38px;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ word-break: break-all;
+ }
+ .content {
+ position: relative;
+ width: 100%;
+ margin-bottom: 12px;
+ }
+ .contentFixed {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ width: 100%;
+ }
+ .footer {
+ margin-top: 8px;
+ padding-top: 9px;
+ border-top: 1px solid @border-color-split;
+ & > * {
+ position: relative;
+ }
+ }
+ .footerMargin {
+ margin-top: 20px;
+ }
+}
diff --git a/src/pages/analysis/components/Charts/ChartCard/index.tsx b/src/pages/analysis/components/Charts/ChartCard/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..11f5f2052c176f10269fb29efd312714c5957c3a
--- /dev/null
+++ b/src/pages/analysis/components/Charts/ChartCard/index.tsx
@@ -0,0 +1,98 @@
+import React from 'react';
+import { Card } from 'antd';
+import classNames from 'classnames';
+import { CardProps } from 'antd/lib/card';
+
+import styles from './index.less';
+
+type totalType = () => React.ReactNode;
+
+const renderTotal = (total?: number | totalType | React.ReactNode) => {
+ if (!total) {
+ return;
+ }
+ let totalDom;
+ switch (typeof total) {
+ case 'undefined':
+ totalDom = null;
+ break;
+ case 'function':
+ totalDom = {total()}
;
+ break;
+ default:
+ totalDom = {total}
;
+ }
+ return totalDom;
+};
+
+export interface IChartCardProps extends CardProps {
+ title: React.ReactNode;
+ action?: React.ReactNode;
+ total?: React.ReactNode | number | (() => React.ReactNode | number);
+ footer?: React.ReactNode;
+ contentHeight?: number;
+ avatar?: React.ReactNode;
+ style?: React.CSSProperties;
+}
+
+class ChartCard extends React.Component {
+ renderContent = () => {
+ const { contentHeight, title, avatar, action, total, footer, children, loading } = this.props;
+ if (loading) {
+ return false;
+ }
+ return (
+
+
+
{avatar}
+
+
+ {title}
+ {action}
+
+ {renderTotal(total)}
+
+
+ {children && (
+
+ )}
+ {footer && (
+
+ {footer}
+
+ )}
+
+ );
+ };
+
+ render() {
+ const {
+ loading = false,
+ contentHeight,
+ title,
+ avatar,
+ action,
+ total,
+ footer,
+ children,
+ ...rest
+ } = this.props;
+ return (
+
+ {this.renderContent()}
+
+ );
+ }
+}
+
+export default ChartCard;
diff --git a/src/pages/analysis/components/Charts/Field/index.less b/src/pages/analysis/components/Charts/Field/index.less
new file mode 100644
index 0000000000000000000000000000000000000000..4124471cb522bf18fb7963675ddeeb3dc217b9e7
--- /dev/null
+++ b/src/pages/analysis/components/Charts/Field/index.less
@@ -0,0 +1,17 @@
+@import '~antd/lib/style/themes/default.less';
+
+.field {
+ margin: 0;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ .label,
+ .number {
+ font-size: @font-size-base;
+ line-height: 22px;
+ }
+ .number {
+ margin-left: 8px;
+ color: @heading-color;
+ }
+}
diff --git a/src/pages/analysis/components/Charts/Field/index.tsx b/src/pages/analysis/components/Charts/Field/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..ee3c12454ead9e275a7ed71dba1a20c79b083cf8
--- /dev/null
+++ b/src/pages/analysis/components/Charts/Field/index.tsx
@@ -0,0 +1,18 @@
+import React from 'react';
+
+import styles from './index.less';
+
+export interface IFieldProps {
+ label: React.ReactNode;
+ value: React.ReactNode;
+ style?: React.CSSProperties;
+}
+
+const Field: React.SFC = ({ label, value, ...rest }) => (
+
+ {label}
+ {value}
+
+);
+
+export default Field;
diff --git a/src/pages/analysis/components/Charts/Gauge/index.tsx b/src/pages/analysis/components/Charts/Gauge/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..b5d33c60421e690358310fa418b909dbe2e30aca
--- /dev/null
+++ b/src/pages/analysis/components/Charts/Gauge/index.tsx
@@ -0,0 +1,177 @@
+import React from 'react';
+import { Chart, Geom, Axis, Coord, Guide, Shape } from 'bizcharts';
+import autoHeight from '../autoHeight';
+
+const { Arc, Html, Line } = Guide;
+
+export interface IGaugeProps {
+ title: React.ReactNode;
+ color?: string;
+ height?: number;
+ bgColor?: number;
+ percent: number;
+ forceFit?: boolean;
+ style?: React.CSSProperties;
+ formatter: (value: string) => string;
+}
+
+const defaultFormatter = (val: string): string => {
+ switch (val) {
+ case '2':
+ return '差';
+ case '4':
+ return '中';
+ case '6':
+ return '良';
+ case '8':
+ return '优';
+ default:
+ return '';
+ }
+};
+
+Shape.registerShape!('point', 'pointer', {
+ drawShape(cfg: any, group: any) {
+ let point = cfg.points[0];
+ point = (this as any).parsePoint(point);
+ const center = (this as any).parsePoint({
+ x: 0,
+ y: 0,
+ });
+ group.addShape('line', {
+ attrs: {
+ x1: center.x,
+ y1: center.y,
+ x2: point.x,
+ y2: point.y,
+ stroke: cfg.color,
+ lineWidth: 2,
+ lineCap: 'round',
+ },
+ });
+ return group.addShape('circle', {
+ attrs: {
+ x: center.x,
+ y: center.y,
+ r: 6,
+ stroke: cfg.color,
+ lineWidth: 3,
+ fill: '#fff',
+ },
+ });
+ },
+});
+
+class Gauge extends React.Component {
+ render() {
+ const {
+ title,
+ height = 1,
+ percent,
+ forceFit = true,
+ formatter = defaultFormatter,
+ color = '#2F9CFF',
+ bgColor = '#F0F2F5',
+ } = this.props;
+ const cols = {
+ value: {
+ type: 'linear',
+ min: 0,
+ max: 10,
+ tickCount: 6,
+ nice: true,
+ },
+ };
+ const renderHtml = () => `
+
+
${title}
+
+ ${(data[0].value * 10).toFixed(2)}%
+
+
`;
+ const data = [{ value: percent / 10 }];
+ const textStyle: {
+ fontSize: number;
+ fill: string;
+ textAlign: 'center';
+ } = {
+ fontSize: 12,
+ fill: 'rgba(0, 0, 0, 0.65)',
+ textAlign: 'center',
+ };
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+export default autoHeight()(Gauge);
diff --git a/src/pages/analysis/components/Charts/MiniArea/index.tsx b/src/pages/analysis/components/Charts/MiniArea/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..5684aa51fb6c8cf3bbe5457c38a2dbe0168ef3c0
--- /dev/null
+++ b/src/pages/analysis/components/Charts/MiniArea/index.tsx
@@ -0,0 +1,133 @@
+import React from 'react';
+import { Chart, Axis, Tooltip, Geom } from 'bizcharts';
+import autoHeight from '../autoHeight';
+import styles from '../index.less';
+
+export interface IAxis {
+ title: any;
+ line: any;
+ gridAlign: any;
+ labels: any;
+ tickLine: any;
+ grid: any;
+}
+
+export interface IMiniAreaProps {
+ color?: string;
+ height?: number;
+ borderColor?: string;
+ line?: boolean;
+ animate?: boolean;
+ xAxis?: IAxis;
+ forceFit?: boolean;
+ scale?: { x: any; y: any };
+ yAxis?: IAxis;
+ borderWidth?: number;
+ data: Array<{
+ x: number | string;
+ y: number;
+ }>;
+}
+
+class MiniArea extends React.Component {
+ render() {
+ const {
+ height = 1,
+ data = [],
+ forceFit = true,
+ color = 'rgba(24, 144, 255, 0.2)',
+ borderColor = '#1089ff',
+ scale = { x: {}, y: {} },
+ borderWidth = 2,
+ line,
+ xAxis,
+ yAxis,
+ animate = true,
+ } = this.props;
+
+ const padding: [number, number, number, number] = [36, 5, 30, 5];
+
+ const scaleProps = {
+ x: {
+ type: 'cat',
+ range: [0, 1],
+ ...scale!.x,
+ },
+ y: {
+ min: 0,
+ ...scale!.y,
+ },
+ };
+
+ const tooltip: [string, (...args: any[]) => { name?: string; value: string }] = [
+ 'x*y',
+ (x: string, y: string) => ({
+ name: x,
+ value: y,
+ }),
+ ];
+
+ const chartHeight = height + 54;
+
+ return (
+
+
+ {height > 0 && (
+
+
+
+
+
+ {line ? (
+
+ ) : (
+
+ )}
+
+ )}
+
+
+ );
+ }
+}
+
+export default autoHeight()(MiniArea);
diff --git a/src/pages/analysis/components/Charts/MiniBar/index.tsx b/src/pages/analysis/components/Charts/MiniBar/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..d0fe9d4c160ddac885cf1246d455f675ba1822be
--- /dev/null
+++ b/src/pages/analysis/components/Charts/MiniBar/index.tsx
@@ -0,0 +1,61 @@
+import React from 'react';
+import { Chart, Tooltip, Geom } from 'bizcharts';
+import autoHeight from '../autoHeight';
+import styles from '../index.less';
+
+export interface IMiniBarProps {
+ color?: string;
+ height?: number;
+ data: Array<{
+ x: number | string;
+ y: number;
+ }>;
+ forceFit?: boolean;
+ style?: React.CSSProperties;
+}
+
+class MiniBar extends React.Component {
+ render() {
+ const { height = 0, forceFit = true, color = '#1890FF', data = [] } = this.props;
+
+ const scale = {
+ x: {
+ type: 'cat',
+ },
+ y: {
+ min: 0,
+ },
+ };
+
+ const padding: [number, number, number, number] = [36, 5, 30, 5];
+
+ const tooltip: [string, (...args: any[]) => { name?: string; value: string }] = [
+ 'x*y',
+ (x: string, y: string) => ({
+ name: x,
+ value: y,
+ }),
+ ];
+
+ // for tooltip not to be hide
+ const chartHeight = height + 54;
+
+ return (
+
+ );
+ }
+}
+export default autoHeight()(MiniBar);
diff --git a/src/pages/analysis/components/Charts/MiniProgress/index.less b/src/pages/analysis/components/Charts/MiniProgress/index.less
new file mode 100644
index 0000000000000000000000000000000000000000..e1e0b4fc5169615814efe60821f39dc3e1bc58b9
--- /dev/null
+++ b/src/pages/analysis/components/Charts/MiniProgress/index.less
@@ -0,0 +1,37 @@
+@import '~antd/lib/style/themes/default.less';
+
+.miniProgress {
+ position: relative;
+ width: 100%;
+ padding: 5px 0;
+ .progressWrap {
+ position: relative;
+ background-color: @background-color-base;
+ }
+ .progress {
+ width: 0;
+ height: 100%;
+ background-color: @primary-color;
+ border-radius: 1px 0 0 1px;
+ transition: all 0.4s cubic-bezier(0.08, 0.82, 0.17, 1) 0s;
+ }
+ .target {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ z-index: 9;
+ width: 20px;
+ span {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 2px;
+ height: 4px;
+ border-radius: 100px;
+ }
+ span:last-child {
+ top: auto;
+ bottom: 0;
+ }
+ }
+}
diff --git a/src/pages/analysis/components/Charts/MiniProgress/index.tsx b/src/pages/analysis/components/Charts/MiniProgress/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..c0d1507dd4c5ff0d6d36f40897a910910df7a737
--- /dev/null
+++ b/src/pages/analysis/components/Charts/MiniProgress/index.tsx
@@ -0,0 +1,43 @@
+import React from 'react';
+import { Tooltip } from 'antd';
+import styles from './index.less';
+
+export interface IMiniProgressProps {
+ target: number;
+ targetLabel?: string;
+ color?: string;
+ strokeWidth?: number;
+ percent?: number;
+ style?: React.CSSProperties;
+}
+
+const MiniProgress: React.SFC = ({
+ targetLabel,
+ target,
+ color = 'rgb(19, 194, 194)',
+ strokeWidth,
+ percent,
+}) => {
+ return (
+
+ );
+};
+
+export default MiniProgress;
diff --git a/src/pages/analysis/components/Charts/Pie/index.less b/src/pages/analysis/components/Charts/Pie/index.less
new file mode 100644
index 0000000000000000000000000000000000000000..fc961b41df8831d2da6e7cf987b89e3624133fbc
--- /dev/null
+++ b/src/pages/analysis/components/Charts/Pie/index.less
@@ -0,0 +1,94 @@
+@import '~antd/lib/style/themes/default.less';
+
+.pie {
+ position: relative;
+ .chart {
+ position: relative;
+ }
+ &.hasLegend .chart {
+ width: ~'calc(100% - 240px)';
+ }
+ .legend {
+ position: absolute;
+ top: 50%;
+ right: 0;
+ min-width: 200px;
+ margin: 0 20px;
+ padding: 0;
+ list-style: none;
+ transform: translateY(-50%);
+ li {
+ height: 22px;
+ margin-bottom: 16px;
+ line-height: 22px;
+ cursor: pointer;
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+ }
+ .dot {
+ position: relative;
+ top: -1px;
+ display: inline-block;
+ width: 8px;
+ height: 8px;
+ margin-right: 8px;
+ border-radius: 8px;
+ }
+ .line {
+ display: inline-block;
+ width: 1px;
+ height: 16px;
+ margin-right: 8px;
+ background-color: @border-color-split;
+ }
+ .legendTitle {
+ color: @text-color;
+ }
+ .percent {
+ color: @text-color-secondary;
+ }
+ .value {
+ position: absolute;
+ right: 0;
+ }
+ .title {
+ margin-bottom: 8px;
+ }
+ .total {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ max-height: 62px;
+ text-align: center;
+ transform: translate(-50%, -50%);
+ & > h4 {
+ height: 22px;
+ margin-bottom: 8px;
+ color: @text-color-secondary;
+ font-weight: normal;
+ font-size: 14px;
+ line-height: 22px;
+ }
+ & > p {
+ display: block;
+ height: 32px;
+ color: @heading-color;
+ font-size: 1.2em;
+ line-height: 32px;
+ white-space: nowrap;
+ }
+ }
+}
+
+.legendBlock {
+ &.hasLegend .chart {
+ width: 100%;
+ margin: 0 0 32px 0;
+ }
+ .legend {
+ position: relative;
+ transform: none;
+ }
+}
diff --git a/src/pages/analysis/components/Charts/Pie/index.tsx b/src/pages/analysis/components/Charts/Pie/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..9ed8a9a3956c6ea0b9f4590188c0370881f7c32f
--- /dev/null
+++ b/src/pages/analysis/components/Charts/Pie/index.tsx
@@ -0,0 +1,308 @@
+import React, { Component } from 'react';
+import { Chart, Tooltip, Geom, Coord } from 'bizcharts';
+import { DataView } from '@antv/data-set';
+import { Divider } from 'antd';
+import classNames from 'classnames';
+import ReactFitText from 'react-fittext';
+import Debounce from 'lodash-decorators/debounce';
+import Bind from 'lodash-decorators/bind';
+import autoHeight from '../autoHeight';
+
+import styles from './index.less';
+export interface IPieProps {
+ animate?: boolean;
+ color?: string;
+ colors?: string[];
+ selected?: boolean;
+ height?: number;
+ margin?: [number, number, number, number];
+ hasLegend?: boolean;
+ padding?: [number, number, number, number];
+ percent?: number;
+ data?: Array<{
+ x: string | string;
+ y: number;
+ }>;
+ inner?: number;
+ lineWidth?: number;
+ forceFit?: boolean;
+ style?: React.CSSProperties;
+ className?: string;
+ total?: React.ReactNode | number | (() => React.ReactNode | number);
+ title?: React.ReactNode;
+ tooltip?: boolean;
+ valueFormat?: (value: string) => string | React.ReactNode;
+ subTitle?: React.ReactNode;
+}
+interface IPieState {
+ legendData: Array<{ checked: boolean; x: string; color: string; percent: number; y: string }>;
+ legendBlock: boolean;
+}
+class Pie extends Component {
+ state: IPieState = {
+ legendData: [],
+ legendBlock: false,
+ };
+
+ requestRef: number | undefined;
+ root!: HTMLDivElement;
+ chart: G2.Chart | undefined;
+
+ componentDidMount() {
+ window.addEventListener(
+ 'resize',
+ () => {
+ this.requestRef = requestAnimationFrame(() => this.resize());
+ },
+ { passive: true },
+ );
+ }
+
+ componentDidUpdate(preProps: IPieProps) {
+ const { data } = this.props;
+ if (data !== preProps.data) {
+ // because of charts data create when rendered
+ // so there is a trick for get rendered time
+ this.getLegendData();
+ }
+ }
+
+ componentWillUnmount() {
+ if (this.requestRef) {
+ window.cancelAnimationFrame(this.requestRef);
+ }
+ window.removeEventListener('resize', this.resize);
+ if (this.resize) {
+ (this.resize as any).cancel();
+ }
+ }
+
+ getG2Instance = (chart: G2.Chart) => {
+ this.chart = chart;
+ requestAnimationFrame(() => {
+ this.getLegendData();
+ this.resize();
+ });
+ };
+
+ // for custom lengend view
+ getLegendData = () => {
+ if (!this.chart) return;
+ const geom = this.chart.getAllGeoms()[0]; // 获取所有的图形
+ if (!geom) return;
+ const items = geom.get('dataArray') || []; // 获取图形对应的
+
+ const legendData = items.map((item: { color: any; _origin: any }[]) => {
+ /* eslint no-underscore-dangle:0 */
+ const origin = item[0]._origin;
+ origin.color = item[0].color;
+ origin.checked = true;
+ return origin;
+ });
+
+ this.setState({
+ legendData,
+ });
+ };
+ handleRoot = (n: HTMLDivElement) => {
+ this.root = n;
+ };
+
+ handleLegendClick = (item: any, i: string | number) => {
+ const newItem = item;
+ newItem.checked = !newItem.checked;
+
+ const { legendData } = this.state;
+ legendData[i] = newItem;
+
+ const filteredLegendData = legendData.filter(l => l.checked).map(l => l.x);
+
+ if (this.chart) {
+ this.chart.filter('x', val => filteredLegendData.indexOf(val + '') > -1);
+ }
+
+ this.setState({
+ legendData,
+ });
+ };
+
+ // for window resize auto responsive legend
+ @Bind()
+ @Debounce(300)
+ resize() {
+ const { hasLegend } = this.props;
+ const { legendBlock } = this.state;
+ if (!hasLegend || !this.root) {
+ window.removeEventListener('resize', this.resize);
+ return;
+ }
+ if (
+ this.root &&
+ this.root.parentNode &&
+ (this.root.parentNode as HTMLElement).clientWidth <= 380
+ ) {
+ if (!legendBlock) {
+ this.setState({
+ legendBlock: true,
+ });
+ }
+ } else if (legendBlock) {
+ this.setState({
+ legendBlock: false,
+ });
+ }
+ }
+
+ render() {
+ const {
+ valueFormat,
+ subTitle,
+ total,
+ hasLegend = false,
+ className,
+ style,
+ height = 0,
+ forceFit = true,
+ percent,
+ color,
+ inner = 0.75,
+ animate = true,
+ colors,
+ lineWidth = 1,
+ } = this.props;
+
+ const { legendData, legendBlock } = this.state;
+ const pieClassName = classNames(styles.pie, className, {
+ [styles.hasLegend]: !!hasLegend,
+ [styles.legendBlock]: legendBlock,
+ });
+
+ const {
+ data: propsData,
+ selected: propsSelected = true,
+ tooltip: propsTooltip = true,
+ } = this.props;
+
+ let data = propsData || [];
+ let selected = propsSelected;
+ let tooltip = propsTooltip;
+
+ const defaultColors = colors;
+ data = data || [];
+ selected = selected || true;
+ tooltip = tooltip || true;
+ let formatColor;
+
+ const scale = {
+ x: {
+ type: 'cat',
+ range: [0, 1],
+ },
+ y: {
+ min: 0,
+ },
+ };
+
+ if (percent || percent === 0) {
+ selected = false;
+ tooltip = false;
+ formatColor = (value: string) => {
+ if (value === '占比') {
+ return color || 'rgba(24, 144, 255, 0.85)';
+ }
+ return '#F0F2F5';
+ };
+
+ data = [
+ {
+ x: '占比',
+ y: parseFloat(percent + ''),
+ },
+ {
+ x: '反比',
+ y: 100 - parseFloat(percent + ''),
+ },
+ ];
+ }
+
+ const tooltipFormat: [string, (...args: any[]) => { name?: string; value: string }] = [
+ 'x*percent',
+ (x: string, p: number) => ({
+ name: x,
+ value: `${(p * 100).toFixed(2)}%`,
+ }),
+ ];
+
+ const padding = [12, 0, 12, 0] as [number, number, number, number];
+
+ const dv = new DataView();
+ dv.source(data).transform({
+ type: 'percent',
+ field: 'y',
+ dimension: 'x',
+ as: 'percent',
+ });
+
+ return (
+
+
+
+
+ {!!tooltip && }
+
+
+
+
+ {(subTitle || total) && (
+
+ {subTitle &&
{subTitle}
}
+ {/* eslint-disable-next-line */}
+ {total && (
+
{typeof total === 'function' ? total() : total}
+ )}
+
+ )}
+
+
+
+ {hasLegend && (
+
+ {legendData.map((item, i) => (
+ - this.handleLegendClick(item, i)}>
+
+ {item.x}
+
+
+ {`${(Number.isNaN(item.percent) ? 0 : item.percent * 100).toFixed(2)}%`}
+
+ {valueFormat ? valueFormat(item.y) : item.y}
+
+ ))}
+
+ )}
+
+ );
+ }
+}
+
+export default autoHeight()(Pie);
diff --git a/src/pages/analysis/components/Charts/TagCloud/index.less b/src/pages/analysis/components/Charts/TagCloud/index.less
new file mode 100644
index 0000000000000000000000000000000000000000..db8e4dabfdd3f1fd4566ff22f55962648c369c49
--- /dev/null
+++ b/src/pages/analysis/components/Charts/TagCloud/index.less
@@ -0,0 +1,6 @@
+.tagCloud {
+ overflow: hidden;
+ canvas {
+ transform-origin: 0 0;
+ }
+}
diff --git a/src/pages/analysis/components/Charts/TagCloud/index.tsx b/src/pages/analysis/components/Charts/TagCloud/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..fc809826df3e4d1f607ec83bb812d63f41fcbdd3
--- /dev/null
+++ b/src/pages/analysis/components/Charts/TagCloud/index.tsx
@@ -0,0 +1,206 @@
+import React, { Component } from 'react';
+import { Chart, Geom, Coord, Shape, Tooltip } from 'bizcharts';
+import DataSet from '@antv/data-set';
+import Debounce from 'lodash-decorators/debounce';
+import Bind from 'lodash-decorators/bind';
+import classNames from 'classnames';
+import autoHeight from '../autoHeight';
+import styles from './index.less';
+
+/* eslint no-underscore-dangle: 0 */
+/* eslint no-param-reassign: 0 */
+
+const imgUrl = 'https://gw.alipayobjects.com/zos/rmsportal/gWyeGLCdFFRavBGIDzWk.png';
+
+export interface ITagCloudProps {
+ data: Array<{
+ name: string;
+ value: number;
+ }>;
+ height?: number;
+ className?: string;
+ style?: React.CSSProperties;
+}
+
+interface ITagCloudState {
+ dv: any;
+ height?: number;
+ width: number;
+}
+
+class TagCloud extends Component {
+ state = {
+ dv: null,
+ height: 0,
+ width: 0,
+ };
+ isUnmount!: boolean;
+ requestRef!: number;
+
+ root: HTMLDivElement | undefined;
+ imageMask: HTMLImageElement | undefined;
+
+ componentDidMount() {
+ requestAnimationFrame(() => {
+ this.initTagCloud();
+ this.renderChart(this.props);
+ });
+ window.addEventListener('resize', this.resize, { passive: true });
+ }
+
+ componentDidUpdate(preProps?: ITagCloudProps) {
+ const { data } = this.props;
+ if (preProps && JSON.stringify(preProps.data) !== JSON.stringify(data)) {
+ this.renderChart(this.props);
+ }
+ }
+ componentWillUnmount() {
+ this.isUnmount = true;
+ window.cancelAnimationFrame(this.requestRef);
+ window.removeEventListener('resize', this.resize);
+ }
+ resize = () => {
+ this.requestRef = requestAnimationFrame(() => {
+ this.renderChart(this.props);
+ });
+ };
+ saveRootRef = (node: HTMLDivElement) => {
+ this.root = node;
+ };
+
+ initTagCloud = () => {
+ function getTextAttrs(cfg: {
+ x?: any;
+ y?: any;
+ style?: any;
+ opacity?: any;
+ origin?: any;
+ color?: any;
+ }) {
+ return Object.assign({}, cfg.style, {
+ fillOpacity: cfg.opacity,
+ fontSize: cfg.origin._origin.size,
+ rotate: cfg.origin._origin.rotate,
+ text: cfg.origin._origin.text,
+ textAlign: 'center',
+ fontFamily: cfg.origin._origin.font,
+ fill: cfg.color,
+ textBaseline: 'Alphabetic',
+ });
+ }
+
+ (Shape as any).registerShape('point', 'cloud', {
+ drawShape(
+ cfg: { x: any; y: any },
+ container: { addShape: (arg0: string, arg1: { attrs: any }) => void },
+ ) {
+ const attrs = getTextAttrs(cfg);
+ return container.addShape('text', {
+ attrs: Object.assign(attrs, {
+ x: cfg.x,
+ y: cfg.y,
+ }),
+ });
+ },
+ });
+ };
+
+ @Bind()
+ @Debounce(500)
+ renderChart(nextProps: ITagCloudProps) {
+ // const colors = ['#1890FF', '#41D9C7', '#2FC25B', '#FACC14', '#9AE65C'];
+ const { data, height } = nextProps || this.props;
+
+ if (data.length < 1 || !this.root) {
+ return;
+ }
+
+ const h = height;
+ const w = this.root.offsetWidth;
+
+ const onload = () => {
+ const dv = new DataSet.View().source(data);
+ const range = dv.range('value');
+ const [min, max] = range;
+ dv.transform({
+ type: 'tag-cloud',
+ fields: ['name', 'value'],
+ imageMask: this.imageMask,
+ font: 'Verdana',
+ size: [w, h], // 宽高设置最好根据 imageMask 做调整
+ padding: 0,
+ timeInterval: 5000, // max execute time
+ rotate() {
+ return 0;
+ },
+ fontSize(d: { value: number }) {
+ // eslint-disable-next-line
+ return Math.pow((d.value - min) / (max - min), 2) * (17.5 - 5) + 5;
+ },
+ });
+
+ if (this.isUnmount) {
+ return;
+ }
+
+ this.setState({
+ dv,
+ width: w,
+ height: h,
+ });
+ };
+
+ if (!this.imageMask) {
+ this.imageMask = new Image();
+ this.imageMask.crossOrigin = '';
+ this.imageMask.src = imgUrl;
+
+ this.imageMask.onload = onload;
+ } else {
+ onload();
+ }
+ }
+
+ render() {
+ const { className, height } = this.props;
+ const { dv, width, height: stateHeight } = this.state;
+
+ return (
+
+ {dv && (
+
+
+
+
+
+ )}
+
+ );
+ }
+}
+
+export default autoHeight()(TagCloud);
diff --git a/src/pages/analysis/components/Charts/TimelineChart/index.less b/src/pages/analysis/components/Charts/TimelineChart/index.less
new file mode 100644
index 0000000000000000000000000000000000000000..1751975692135769eebdcaf89ffafcf6b3037cb8
--- /dev/null
+++ b/src/pages/analysis/components/Charts/TimelineChart/index.less
@@ -0,0 +1,3 @@
+.timelineChart {
+ background: #fff;
+}
diff --git a/src/pages/analysis/components/Charts/TimelineChart/index.tsx b/src/pages/analysis/components/Charts/TimelineChart/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..2ccc37283623e27ff1cd48e7c569a683b1199684
--- /dev/null
+++ b/src/pages/analysis/components/Charts/TimelineChart/index.tsx
@@ -0,0 +1,133 @@
+import React from 'react';
+import { Chart, Tooltip, Geom, Legend, Axis } from 'bizcharts';
+import DataSet from '@antv/data-set';
+import Slider from 'bizcharts-plugin-slider';
+import autoHeight from '../autoHeight';
+import styles from './index.less';
+
+export interface ITimelineChartProps {
+ data: Array<{
+ x: number;
+ y1: number;
+ y2: number;
+ }>;
+ title?: string;
+ titleMap: { y1: string; y2: string };
+ padding?: [number, number, number, number];
+ height?: number;
+ style?: React.CSSProperties;
+ borderWidth?: number;
+}
+
+class TimelineChart extends React.Component {
+ render() {
+ const {
+ title,
+ height = 400,
+ padding = [60, 20, 40, 40] as [number, number, number, number],
+ titleMap = {
+ y1: 'y1',
+ y2: 'y2',
+ },
+ borderWidth = 2,
+ data: sourceData,
+ } = this.props;
+
+ const data = Array.isArray(sourceData) ? sourceData : [{ x: 0, y1: 0, y2: 0 }];
+
+ data.sort((a, b) => a.x - b.x);
+
+ let max;
+ if (data[0] && data[0].y1 && data[0].y2) {
+ max = Math.max(
+ [...data].sort((a, b) => b.y1 - a.y1)[0].y1,
+ [...data].sort((a, b) => b.y2 - a.y2)[0].y2,
+ );
+ }
+
+ const ds = new DataSet({
+ state: {
+ start: data[0].x,
+ end: data[data.length - 1].x,
+ },
+ });
+
+ const dv = ds.createView();
+ dv.source(data)
+ .transform({
+ type: 'filter',
+ callback: (obj: { x: string }) => {
+ const date = obj.x;
+ return date <= ds.state.end && date >= ds.state.start;
+ },
+ })
+ .transform({
+ type: 'map',
+ callback(row: { y1: string; y2: string }) {
+ const newRow = { ...row };
+ newRow[titleMap.y1] = row.y1;
+ newRow[titleMap.y2] = row.y2;
+ return newRow;
+ },
+ })
+ .transform({
+ type: 'fold',
+ fields: [titleMap.y1, titleMap.y2], // 展开字段集
+ key: 'key', // key字段
+ value: 'value', // value字段
+ });
+
+ const timeScale = {
+ type: 'time',
+ tickInterval: 60 * 60 * 1000,
+ mask: 'HH:mm',
+ range: [0, 1],
+ };
+
+ const cols = {
+ x: timeScale,
+ value: {
+ max,
+ min: 0,
+ },
+ };
+
+ const SliderGen = () => (
+ {
+ ds.setState('start', startValue);
+ ds.setState('end', endValue);
+ }}
+ />
+ );
+
+ return (
+
+
+ {title &&
{title}
}
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+export default autoHeight()(TimelineChart);
diff --git a/src/pages/analysis/components/Charts/WaterWave/index.less b/src/pages/analysis/components/Charts/WaterWave/index.less
new file mode 100644
index 0000000000000000000000000000000000000000..2e75f21464300dd1d443329943b363b16fee1e97
--- /dev/null
+++ b/src/pages/analysis/components/Charts/WaterWave/index.less
@@ -0,0 +1,28 @@
+@import '~antd/lib/style/themes/default.less';
+
+.waterWave {
+ position: relative;
+ display: inline-block;
+ transform-origin: left;
+ .text {
+ position: absolute;
+ top: 32px;
+ left: 0;
+ width: 100%;
+ text-align: center;
+ span {
+ color: @text-color-secondary;
+ font-size: 14px;
+ line-height: 22px;
+ }
+ h4 {
+ color: @heading-color;
+ font-size: 24px;
+ line-height: 32px;
+ }
+ }
+ .waterWaveCanvasWrapper {
+ transform: scale(0.5);
+ transform-origin: 0 0;
+ }
+}
diff --git a/src/pages/analysis/components/Charts/WaterWave/index.tsx b/src/pages/analysis/components/Charts/WaterWave/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..9dba82160b99756506399cfac2416c08206bf78e
--- /dev/null
+++ b/src/pages/analysis/components/Charts/WaterWave/index.tsx
@@ -0,0 +1,230 @@
+import React, { Component } from 'react';
+import autoHeight from '../autoHeight';
+import styles from './index.less';
+
+/* eslint no-return-assign: 0 */
+/* eslint no-mixed-operators: 0 */
+// riddle: https://riddle.alibaba-inc.com/riddles/2d9a4b90
+
+export interface IWaterWaveProps {
+ title: React.ReactNode;
+ color?: string;
+ height?: number;
+ percent: number;
+ style?: React.CSSProperties;
+}
+
+class WaterWave extends Component {
+ state = {
+ radio: 1,
+ };
+ timer: number = 0;
+ root: HTMLDivElement | undefined | null;
+ node: HTMLCanvasElement | undefined | null;
+
+ componentDidMount() {
+ this.renderChart();
+ this.resize();
+ window.addEventListener(
+ 'resize',
+ () => {
+ requestAnimationFrame(() => this.resize());
+ },
+ { passive: true },
+ );
+ }
+
+ componentDidUpdate(props: IWaterWaveProps) {
+ const { percent } = this.props;
+ if (props.percent !== percent) {
+ // 不加这个会造成绘制缓慢
+ this.renderChart('update');
+ }
+ }
+
+ componentWillUnmount() {
+ cancelAnimationFrame(this.timer);
+ if (this.node) {
+ this.node.innerHTML = '';
+ }
+ window.removeEventListener('resize', this.resize);
+ }
+
+ resize = () => {
+ if (this.root) {
+ const { height = 1 } = this.props;
+ const { offsetWidth } = this.root.parentNode as HTMLElement;
+ this.setState({
+ radio: offsetWidth < height ? offsetWidth / height : 1,
+ });
+ }
+ };
+ renderChart(type?: string) {
+ const { percent, color = '#1890FF' } = this.props;
+ const data = percent / 100;
+ const self = this;
+ cancelAnimationFrame(this.timer);
+
+ if (!this.node || (data !== 0 && !data)) {
+ return;
+ }
+
+ const canvas = this.node;
+ const ctx = canvas.getContext('2d');
+ if (!ctx) {
+ return;
+ }
+ const canvasWidth = canvas.width;
+ const canvasHeight = canvas.height;
+ const radius = canvasWidth / 2;
+ const lineWidth = 2;
+ const cR = radius - lineWidth;
+
+ ctx.beginPath();
+ ctx.lineWidth = lineWidth * 2;
+
+ const axisLength = canvasWidth - lineWidth;
+ const unit = axisLength / 8;
+ const range = 0.2; // 振幅
+ let currRange = range;
+ const xOffset = lineWidth;
+ let sp = 0; // 周期偏移量
+ let currData = 0;
+ const waveupsp = 0.005; // 水波上涨速度
+
+ let arcStack: number[][] = [];
+ const bR = radius - lineWidth;
+ const circleOffset = -(Math.PI / 2);
+ let circleLock = true;
+
+ for (let i = circleOffset; i < circleOffset + 2 * Math.PI; i += 1 / (8 * Math.PI)) {
+ arcStack.push([radius + bR * Math.cos(i), radius + bR * Math.sin(i)]);
+ }
+
+ const cStartPoint = arcStack.shift() as number[];
+ ctx.strokeStyle = color;
+ ctx.moveTo(cStartPoint[0], cStartPoint[1]);
+
+ function drawSin() {
+ if (!ctx) {
+ return;
+ }
+ ctx.beginPath();
+ ctx.save();
+
+ const sinStack = [];
+ for (let i = xOffset; i <= xOffset + axisLength; i += 20 / axisLength) {
+ const x = sp + (xOffset + i) / unit;
+ const y = Math.sin(x) * currRange;
+ const dx = i;
+ const dy = 2 * cR * (1 - currData) + (radius - cR) - unit * y;
+
+ ctx.lineTo(dx, dy);
+ sinStack.push([dx, dy]);
+ }
+
+ const startPoint = sinStack.shift() as number[];
+
+ ctx.lineTo(xOffset + axisLength, canvasHeight);
+ ctx.lineTo(xOffset, canvasHeight);
+ ctx.lineTo(startPoint[0], startPoint[1]);
+
+ const gradient = ctx.createLinearGradient(0, 0, 0, canvasHeight);
+ gradient.addColorStop(0, '#ffffff');
+ gradient.addColorStop(1, color);
+ ctx.fillStyle = gradient;
+ ctx.fill();
+ ctx.restore();
+ }
+
+ function render() {
+ if (!ctx) {
+ return;
+ }
+ ctx.clearRect(0, 0, canvasWidth, canvasHeight);
+ if (circleLock && type !== 'update') {
+ if (arcStack.length) {
+ const temp = arcStack.shift() as number[];
+ ctx.lineTo(temp[0], temp[1]);
+ ctx.stroke();
+ } else {
+ circleLock = false;
+ ctx.lineTo(cStartPoint[0], cStartPoint[1]);
+ ctx.stroke();
+ arcStack = [];
+
+ ctx.globalCompositeOperation = 'destination-over';
+ ctx.beginPath();
+ ctx.lineWidth = lineWidth;
+ ctx.arc(radius, radius, bR, 0, 2 * Math.PI, true);
+
+ ctx.beginPath();
+ ctx.save();
+ ctx.arc(radius, radius, radius - 3 * lineWidth, 0, 2 * Math.PI, true);
+
+ ctx.restore();
+ ctx.clip();
+ ctx.fillStyle = color;
+ }
+ } else {
+ if (data >= 0.85) {
+ if (currRange > range / 4) {
+ const t = range * 0.01;
+ currRange -= t;
+ }
+ } else if (data <= 0.1) {
+ if (currRange < range * 1.5) {
+ const t = range * 0.01;
+ currRange += t;
+ }
+ } else {
+ if (currRange <= range) {
+ const t = range * 0.01;
+ currRange += t;
+ }
+ if (currRange >= range) {
+ const t = range * 0.01;
+ currRange -= t;
+ }
+ }
+ if (data - currData > 0) {
+ currData += waveupsp;
+ }
+ if (data - currData < 0) {
+ currData -= waveupsp;
+ }
+
+ sp += 0.07;
+ drawSin();
+ }
+ self.timer = requestAnimationFrame(render);
+ }
+ render();
+ }
+ render() {
+ const { radio } = this.state;
+ const { percent, title, height = 1 } = this.props;
+ return (
+ (this.root = n)}
+ style={{ transform: `scale(${radio})` }}
+ >
+
+
+
+ {title && {title}}
+
{percent}%
+
+
+ );
+ }
+}
+
+export default autoHeight()(WaterWave);
diff --git a/src/pages/analysis/components/Charts/autoHeight.tsx b/src/pages/analysis/components/Charts/autoHeight.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..e7d14e07a6be59b9125bcc910c07ba9b162b5409
--- /dev/null
+++ b/src/pages/analysis/components/Charts/autoHeight.tsx
@@ -0,0 +1,75 @@
+import React from 'react';
+
+export type IReactComponent =
+ | React.StatelessComponent
+ | React.ComponentClass
+ | React.ClassicComponentClass
;
+
+function computeHeight(node: HTMLDivElement) {
+ node.style.height = '100%';
+ const totalHeight = parseInt(getComputedStyle(node).height + '', 10);
+ const padding =
+ parseInt(getComputedStyle(node).paddingTop + '', 10) +
+ parseInt(getComputedStyle(node).paddingBottom + '', 10);
+ return totalHeight - padding;
+}
+
+function getAutoHeight(n: HTMLDivElement) {
+ if (!n) {
+ return 0;
+ }
+
+ const node = n;
+
+ let height = computeHeight(node);
+ const parentNode = node.parentNode as HTMLDivElement;
+ if (parentNode) {
+ height = computeHeight(parentNode);
+ }
+
+ return height;
+}
+
+interface IAutoHeightProps {
+ height?: number;
+}
+
+function autoHeight() {
+ return function
(
+ WrappedComponent: React.ComponentClass
| React.SFC
,
+ ): React.ComponentClass
{
+ class AutoHeightComponent extends React.Component
{
+ state = {
+ computedHeight: 0,
+ };
+ root!: HTMLDivElement;
+ componentDidMount() {
+ const { height } = this.props;
+ if (!height) {
+ let h = getAutoHeight(this.root);
+ // eslint-disable-next-line
+ this.setState({ computedHeight: h });
+ if (h < 1) {
+ h = getAutoHeight(this.root);
+ this.setState({ computedHeight: h });
+ }
+ }
+ }
+ handleRoot = (node: HTMLDivElement) => {
+ this.root = node;
+ };
+ render() {
+ const { height } = this.props;
+ const { computedHeight } = this.state;
+ const h = height || computedHeight;
+ return (
+
+ {h > 0 && }
+
+ );
+ }
+ }
+ return AutoHeightComponent;
+ };
+}
+export default autoHeight;
diff --git a/src/pages/analysis/components/Charts/bizcharts.d.ts b/src/pages/analysis/components/Charts/bizcharts.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..0815ffeeffcacd0ac9710977ab3d4419d078491c
--- /dev/null
+++ b/src/pages/analysis/components/Charts/bizcharts.d.ts
@@ -0,0 +1,3 @@
+import * as BizChart from 'bizcharts';
+
+export = BizChart;
diff --git a/src/pages/analysis/components/Charts/bizcharts.tsx b/src/pages/analysis/components/Charts/bizcharts.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..e08db8d6d2dca240451bdf6ab8a30be077a3fd9d
--- /dev/null
+++ b/src/pages/analysis/components/Charts/bizcharts.tsx
@@ -0,0 +1,3 @@
+import * as BizChart from 'bizcharts';
+
+export default BizChart;
diff --git a/src/pages/analysis/components/Charts/index.less b/src/pages/analysis/components/Charts/index.less
new file mode 100644
index 0000000000000000000000000000000000000000..190428bc80d7cd7f6f22d51fd48fa37b2d44eb10
--- /dev/null
+++ b/src/pages/analysis/components/Charts/index.less
@@ -0,0 +1,19 @@
+.miniChart {
+ position: relative;
+ width: 100%;
+ .chartContent {
+ position: absolute;
+ bottom: -28px;
+ width: 100%;
+ > div {
+ margin: 0 -5px;
+ overflow: hidden;
+ }
+ }
+ .chartLoading {
+ position: absolute;
+ top: 16px;
+ left: 50%;
+ margin-left: -7px;
+ }
+}
diff --git a/src/pages/analysis/components/Charts/index.tsx b/src/pages/analysis/components/Charts/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..5e0815f18ef573549dfc4c13de317568d02db80c
--- /dev/null
+++ b/src/pages/analysis/components/Charts/index.tsx
@@ -0,0 +1,45 @@
+import numeral from 'numeral';
+import ChartCard from './ChartCard';
+import Field from './Field';
+import Bar from './Bar';
+import Pie from './Pie';
+import Gauge from './Gauge';
+import MiniArea from './MiniArea';
+import MiniBar from './MiniBar';
+import MiniProgress from './MiniProgress';
+import WaterWave from './WaterWave';
+import TagCloud from './TagCloud';
+import TimelineChart from './TimelineChart';
+
+const yuan = (val: number | string) => `¥ ${numeral(val).format('0,0')}`;
+
+const Charts = {
+ yuan,
+ Bar,
+ Pie,
+ Gauge,
+ MiniBar,
+ MiniArea,
+ MiniProgress,
+ ChartCard,
+ Field,
+ WaterWave,
+ TagCloud,
+ TimelineChart,
+};
+
+export {
+ Charts as default,
+ yuan,
+ Bar,
+ Pie,
+ Gauge,
+ MiniBar,
+ MiniArea,
+ MiniProgress,
+ ChartCard,
+ Field,
+ WaterWave,
+ TagCloud,
+ TimelineChart,
+};
diff --git a/src/pages/analysis/components/IntroduceRow.tsx b/src/pages/analysis/components/IntroduceRow.tsx
new file mode 100755
index 0000000000000000000000000000000000000000..ee2cf44dc016a8e3d89bdcee4b79296cdbd8ee4b
--- /dev/null
+++ b/src/pages/analysis/components/IntroduceRow.tsx
@@ -0,0 +1,162 @@
+import React from 'react';
+import { Row, Col, Icon, Tooltip } from 'antd';
+import { FormattedMessage } from 'umi-plugin-react/locale';
+import Charts from './Charts';
+import numeral from 'numeral';
+import styles from '../style.less';
+import Yuan from '../utils/Yuan';
+import Trend from './Trend';
+import { IVisitData } from '../data.d';
+const { ChartCard, MiniArea, MiniBar, MiniProgress, Field } = Charts;
+
+const topColResponsiveProps = {
+ xs: 24,
+ sm: 12,
+ md: 12,
+ lg: 12,
+ xl: 6,
+ style: { marginBottom: 24 },
+};
+
+const IntroduceRow = ({ loading, visitData }: { loading: boolean; visitData: IVisitData[] }) => {
+ return (
+
+
+
+ }
+ action={
+
+ }
+ >
+
+
+ }
+ loading={loading}
+ total={() => 126560}
+ footer={
+
+ }
+ value={`¥${numeral(12423).format('0,0')}`}
+ />
+ }
+ contentHeight={46}
+ >
+
+
+ 12%
+
+
+
+ 11%
+
+
+
+
+
+ }
+ action={
+
+ }
+ >
+
+
+ }
+ total={numeral(8846).format('0,0')}
+ footer={
+
+ }
+ value={numeral(1234).format('0,0')}
+ />
+ }
+ contentHeight={46}
+ >
+
+
+
+
+ }
+ action={
+
+ }
+ >
+
+
+ }
+ total={numeral(6560).format('0,0')}
+ footer={
+
+ }
+ value="60%"
+ />
+ }
+ contentHeight={46}
+ >
+
+
+
+
+
+ }
+ action={
+
+ }
+ >
+
+
+ }
+ total="78%"
+ footer={
+
+
+
+ 12%
+
+
+
+ 11%
+
+
+ }
+ contentHeight={46}
+ >
+
+
+
+
+ );
+};
+
+export default IntroduceRow;
diff --git a/src/pages/analysis/components/NumberInfo/index.less b/src/pages/analysis/components/NumberInfo/index.less
new file mode 100644
index 0000000000000000000000000000000000000000..4a77288cc29d4bc24aa9ee660461bd44cb049897
--- /dev/null
+++ b/src/pages/analysis/components/NumberInfo/index.less
@@ -0,0 +1,68 @@
+@import '~antd/lib/style/themes/default.less';
+
+.numberInfo {
+ .suffix {
+ margin-left: 4px;
+ color: @text-color;
+ font-size: 16px;
+ font-style: normal;
+ }
+ .numberInfoTitle {
+ margin-bottom: 16px;
+ color: @text-color;
+ font-size: @font-size-lg;
+ transition: all 0.3s;
+ }
+ .numberInfoSubTitle {
+ height: 22px;
+ overflow: hidden;
+ color: @text-color-secondary;
+ font-size: @font-size-base;
+ line-height: 22px;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ word-break: break-all;
+ }
+ .numberInfoValue {
+ margin-top: 4px;
+ overflow: hidden;
+ font-size: 0;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ word-break: break-all;
+ & > span {
+ display: inline-block;
+ height: 32px;
+ margin-right: 32px;
+ color: @heading-color;
+ font-size: 24px;
+ line-height: 32px;
+ }
+ .subTotal {
+ margin-right: 0;
+ color: @text-color-secondary;
+ font-size: @font-size-lg;
+ vertical-align: top;
+ i {
+ margin-left: 4px;
+ font-size: 12px;
+ transform: scale(0.82);
+ }
+ :global {
+ .anticon-caret-up {
+ color: @red-6;
+ }
+ .anticon-caret-down {
+ color: @green-6;
+ }
+ }
+ }
+ }
+}
+.numberInfolight {
+ .numberInfoValue {
+ & > span {
+ color: @text-color;
+ }
+ }
+}
diff --git a/src/pages/analysis/components/NumberInfo/index.tsx b/src/pages/analysis/components/NumberInfo/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..a8df6e6d94efbd2dc579d315b28ba64cb4d20859
--- /dev/null
+++ b/src/pages/analysis/components/NumberInfo/index.tsx
@@ -0,0 +1,61 @@
+import React from 'react';
+import { Icon } from 'antd';
+import classNames from 'classnames';
+import styles from './index.less';
+export interface NumberInfoProps {
+ title?: React.ReactNode | string;
+ subTitle?: React.ReactNode | string;
+ total?: React.ReactNode | string;
+ status?: 'up' | 'down';
+ theme?: string;
+ gap?: number;
+ subTotal?: number;
+ suffix?: string;
+ style?: React.CSSProperties;
+}
+const NumberInfo: React.SFC = ({
+ theme,
+ title,
+ subTitle,
+ total,
+ subTotal,
+ status,
+ suffix,
+ gap,
+ ...rest
+}) => (
+
+ {title && (
+
+ {title}
+
+ )}
+ {subTitle && (
+
+ {subTitle}
+
+ )}
+
+
+ {total}
+ {suffix && {suffix}}
+
+ {(status || subTotal) && (
+
+ {subTotal}
+ {status && }
+
+ )}
+
+
+);
+
+export default NumberInfo;
diff --git a/src/pages/analysis/components/OfflineData.tsx b/src/pages/analysis/components/OfflineData.tsx
new file mode 100755
index 0000000000000000000000000000000000000000..64864bdc20955cfb39083cbdccb68c1b532ca613
--- /dev/null
+++ b/src/pages/analysis/components/OfflineData.tsx
@@ -0,0 +1,82 @@
+import React from 'react';
+import { Card, Tabs, Row, Col } from 'antd';
+import { formatMessage, FormattedMessage } from 'umi-plugin-react/locale';
+import Charts from './Charts';
+import styles from '../style.less';
+import NumberInfo from './NumberInfo';
+import { IOfflineData, IOfflineChartData } from '../data';
+const { TimelineChart, Pie } = Charts;
+
+const CustomTab = ({
+ data,
+ currentTabKey: currentKey,
+}: {
+ data: IOfflineData;
+ currentTabKey: string;
+}) => {
+ return (
+
+
+
+ }
+ gap={2}
+ total={`${data.cvr * 100}%`}
+ theme={currentKey !== data.name ? 'light' : undefined}
+ />
+
+
+
+
+
+ );
+};
+
+const { TabPane } = Tabs;
+
+const OfflineData = ({
+ activeKey,
+ loading,
+ offlineData,
+ offlineChartData,
+ handleTabChange,
+}: {
+ activeKey: string;
+ loading: boolean;
+ offlineData: IOfflineData[];
+ offlineChartData: IOfflineChartData[];
+ handleTabChange: (activeKey: string) => void;
+}) => (
+
+
+ {offlineData.map(shop => (
+ } key={shop.name}>
+
+
+
+
+ ))}
+
+
+);
+
+export default OfflineData;
diff --git a/src/pages/analysis/components/PageLoading/index.tsx b/src/pages/analysis/components/PageLoading/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..77c0f165f9d20b4f974e754efb9cf08606c41a49
--- /dev/null
+++ b/src/pages/analysis/components/PageLoading/index.tsx
@@ -0,0 +1,10 @@
+import React from 'react';
+import { Spin } from 'antd';
+
+// loading components from code split
+// https://umijs.org/plugin/umi-plugin-react.html#dynamicimport
+export default () => (
+
+
+
+);
diff --git a/src/pages/analysis/components/ProportionSales.tsx b/src/pages/analysis/components/ProportionSales.tsx
new file mode 100755
index 0000000000000000000000000000000000000000..b82888ef23fe2a1b29eafc6302bb25af9c436412
--- /dev/null
+++ b/src/pages/analysis/components/ProportionSales.tsx
@@ -0,0 +1,79 @@
+import React from 'react';
+import { Card, Radio } from 'antd';
+import Charts from './Charts';
+import { FormattedMessage } from 'umi-plugin-react/locale';
+import styles from '../style.less';
+import Yuan from '../utils/Yuan';
+import { RadioChangeEvent } from 'antd/lib/radio';
+import { ISalesData } from '../data';
+
+const { Pie } = Charts;
+
+const ProportionSales = ({
+ dropdownGroup,
+ salesType,
+ loading,
+ salesPieData,
+ handleChangeSalesType,
+}: {
+ loading: boolean;
+ dropdownGroup: React.ReactNode;
+ salesType: 'all' | 'online' | 'stores';
+ salesPieData: ISalesData[];
+ handleChangeSalesType?: (e: RadioChangeEvent) => void;
+}) => {
+ return (
+
+ }
+ bodyStyle={{ padding: 24 }}
+ extra={
+
+ {dropdownGroup}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+ style={{ marginTop: 24 }}
+ >
+
+
+
+
+
}
+ total={() =>
{salesPieData.reduce((pre, now) => now.y + pre, 0)}}
+ data={salesPieData}
+ valueFormat={value =>
{value}}
+ height={248}
+ lineWidth={4}
+ />
+
+
+ );
+};
+
+export default ProportionSales;
diff --git a/src/pages/analysis/components/SalesCard.tsx b/src/pages/analysis/components/SalesCard.tsx
new file mode 100755
index 0000000000000000000000000000000000000000..05be982318ecf6df1b8483617ed881f7772712f8
--- /dev/null
+++ b/src/pages/analysis/components/SalesCard.tsx
@@ -0,0 +1,162 @@
+import React from 'react';
+import { Row, Col, Card, Tabs, DatePicker } from 'antd';
+import { FormattedMessage, formatMessage } from 'umi-plugin-react/locale';
+import numeral from 'numeral';
+import Charts from './Charts';
+import { RangePickerValue } from 'antd/lib/date-picker/interface';
+import { ISalesData } from '../data';
+import styles from '../style.less';
+
+const { Bar } = Charts;
+
+const { RangePicker } = DatePicker;
+const { TabPane } = Tabs;
+
+const rankingListData: { title: string; total: number }[] = [];
+for (let i = 0; i < 7; i += 1) {
+ rankingListData.push({
+ title: formatMessage({ id: 'analysis.analysis.test' }, { no: i }),
+ total: 323234,
+ });
+}
+
+const SalesCard = ({
+ rangePickerValue,
+ salesData,
+ isActive,
+ handleRangePickerChange,
+ loading,
+ selectDate,
+}: {
+ rangePickerValue: RangePickerValue;
+ isActive: (key: 'today' | 'week' | 'month' | 'year') => string;
+ salesData: ISalesData[];
+ loading: boolean;
+ handleRangePickerChange: (dates: RangePickerValue, dateStrings: [string, string]) => void;
+ selectDate: (key: 'today' | 'week' | 'month' | 'year') => void;
+}) => (
+
+
+ }
+ size="large"
+ tabBarStyle={{ marginBottom: 24 }}
+ >
+ }
+ key="sales"
+ >
+
+
+
+
+ }
+ data={salesData}
+ />
+
+
+
+
+
+
+
+
+ {rankingListData.map((item, i) => (
+ -
+
+ {i + 1}
+
+
+ {item.title}
+
+
+ {numeral(item.total).format('0,0')}
+
+
+ ))}
+
+
+
+
+
+ }
+ key="views"
+ >
+
+
+
+
+ }
+ data={salesData}
+ />
+
+
+
+
+
+
+
+
+ {rankingListData.map((item, i) => (
+ -
+
+ {i + 1}
+
+
+ {item.title}
+
+ {numeral(item.total).format('0,0')}
+
+ ))}
+
+
+
+
+
+
+
+
+);
+
+export default SalesCard;
diff --git a/src/pages/analysis/components/TopSearch.tsx b/src/pages/analysis/components/TopSearch.tsx
new file mode 100755
index 0000000000000000000000000000000000000000..717b6e35345a63eb8eb757272480135c3091a00f
--- /dev/null
+++ b/src/pages/analysis/components/TopSearch.tsx
@@ -0,0 +1,128 @@
+import React from 'react';
+import { Row, Col, Table, Tooltip, Card, Icon } from 'antd';
+import { FormattedMessage } from 'umi-plugin-react/locale';
+import Charts from './Charts';
+import Trend from './Trend';
+import NumberInfo from './NumberInfo';
+import numeral from 'numeral';
+import styles from '../style.less';
+import { ISearchData, IVisitData2 } from '../data';
+
+const { MiniArea } = Charts;
+
+const columns = [
+ {
+ title: ,
+ dataIndex: 'index',
+ key: 'index',
+ },
+ {
+ title: ,
+ dataIndex: 'keyword',
+ key: 'keyword',
+ render: (text: React.ReactNode) => {text},
+ },
+ {
+ title: ,
+ dataIndex: 'count',
+ key: 'count',
+ sorter: (a: { count: number }, b: { count: number }) => a.count - b.count,
+ className: styles.alignRight,
+ },
+ {
+ title: ,
+ dataIndex: 'range',
+ key: 'range',
+ sorter: (a: { range: number }, b: { range: number }) => a.range - b.range,
+ render: (text: React.ReactNode, record: { status: number }) => (
+
+ {text}%
+
+ ),
+ },
+];
+
+const TopSearch = ({
+ loading,
+ visitData2,
+ searchData,
+ dropdownGroup,
+}: {
+ loading: boolean;
+ visitData2: IVisitData2[];
+ dropdownGroup: React.ReactNode;
+ searchData: ISearchData[];
+}) => (
+
+ }
+ extra={dropdownGroup}
+ style={{ marginTop: 24 }}
+ >
+
+
+
+
+
+ }
+ >
+
+
+
+ }
+ gap={8}
+ total={numeral(12321).format('0,0')}
+ status="up"
+ subTotal={17.1}
+ />
+
+
+
+
+
+
+ }
+ >
+
+
+
+ }
+ total={2.7}
+ status="down"
+ subTotal={26.2}
+ gap={8}
+ />
+
+
+
+
+ rowKey={record => record.index}
+ size="small"
+ columns={columns}
+ dataSource={searchData}
+ pagination={{
+ style: { marginBottom: 0 },
+ pageSize: 5,
+ }}
+ />
+
+);
+
+export default TopSearch;
diff --git a/src/pages/analysis/components/Trend/index.less b/src/pages/analysis/components/Trend/index.less
new file mode 100644
index 0000000000000000000000000000000000000000..13618838afcd46f1fc0e724097a0af938ca6f7b3
--- /dev/null
+++ b/src/pages/analysis/components/Trend/index.less
@@ -0,0 +1,37 @@
+@import '~antd/lib/style/themes/default.less';
+
+.trendItem {
+ display: inline-block;
+ font-size: @font-size-base;
+ line-height: 22px;
+
+ .up,
+ .down {
+ position: relative;
+ top: 1px;
+ margin-left: 4px;
+ i {
+ font-size: 12px;
+ transform: scale(0.83);
+ }
+ }
+ .up {
+ color: @red-6;
+ }
+ .down {
+ top: -1px;
+ color: @green-6;
+ }
+
+ &.trendItemGrey .up,
+ &.trendItemGrey .down {
+ color: @text-color;
+ }
+
+ &.reverseColor .up {
+ color: @green-6;
+ }
+ &.reverseColor .down {
+ color: @red-6;
+ }
+}
diff --git a/src/pages/analysis/components/Trend/index.tsx b/src/pages/analysis/components/Trend/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..ee8df20ea51db0e14fe78c12ecf622a6f2772b96
--- /dev/null
+++ b/src/pages/analysis/components/Trend/index.tsx
@@ -0,0 +1,42 @@
+import React from 'react';
+import { Icon } from 'antd';
+import classNames from 'classnames';
+import styles from './index.less';
+
+export interface ITrendProps {
+ colorful?: boolean;
+ flag: 'up' | 'down';
+ style?: React.CSSProperties;
+ reverseColor?: boolean;
+ className?: string;
+}
+
+const Trend: React.SFC = ({
+ colorful = true,
+ reverseColor = false,
+ flag,
+ children,
+ className,
+ ...rest
+}) => {
+ const classString = classNames(
+ styles.trendItem,
+ {
+ [styles.trendItemGrey]: !colorful,
+ [styles.reverseColor]: reverseColor && colorful,
+ },
+ className,
+ );
+ return (
+
+ {children}
+ {flag && (
+
+
+
+ )}
+
+ );
+};
+
+export default Trend;
diff --git a/src/pages/analysis/data.d.ts b/src/pages/analysis/data.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..20df2e20dc6eeeff67414b3f6f50d8bfaeb7d62e
--- /dev/null
+++ b/src/pages/analysis/data.d.ts
@@ -0,0 +1,67 @@
+export interface IVisitData {
+ x: string;
+ y: number;
+}
+
+export interface IVisitData2 {
+ x: string;
+ y: number;
+}
+
+export interface ISalesData {
+ x: string;
+ y: number;
+}
+
+export interface ISearchData {
+ index: number;
+ keyword: string;
+ count: number;
+ range: number;
+ status: number;
+}
+
+export interface IOfflineData {
+ name: string;
+ cvr: number;
+}
+
+export interface IOfflineChartData {
+ x: any;
+ y1: number;
+ y2: number;
+}
+
+export interface ISalesTypeData {
+ x: string;
+ y: number;
+}
+
+export interface ISalesTypeDataOnline {
+ x: string;
+ y: number;
+}
+
+export interface ISalesTypeDataOffline {
+ x: string;
+ y: number;
+}
+
+export interface IRadarData {
+ name: string;
+ label: string;
+ value: number;
+}
+
+export interface IAnalysisData {
+ visitData: IVisitData[];
+ visitData2: IVisitData2[];
+ salesData: ISalesData[];
+ searchData: ISearchData[];
+ offlineData: IOfflineData[];
+ offlineChartData: IOfflineChartData[];
+ salesTypeData: ISalesTypeData[];
+ salesTypeDataOnline: ISalesTypeDataOnline[];
+ salesTypeDataOffline: ISalesTypeDataOffline[];
+ radarData: IRadarData[];
+}
diff --git a/src/pages/analysis/index.tsx b/src/pages/analysis/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..fa6716d5af0ec752e0c54969ccf835312db72868
--- /dev/null
+++ b/src/pages/analysis/index.tsx
@@ -0,0 +1,210 @@
+import React, { Component, Suspense } from 'react';
+import { connect } from 'dva';
+import { Row, Col, Icon, Menu, Dropdown } from 'antd';
+import { RangePickerValue } from 'antd/lib/date-picker/interface';
+import { getTimeDistance } from './utils/utils';
+import styles from './style.less';
+import PageLoading from './components/PageLoading';
+import { Dispatch } from 'redux';
+import { IAnalysisData } from './data.d';
+import { RadioChangeEvent } from 'antd/lib/radio';
+import { GridContent } from '@ant-design/pro-layout';
+
+const IntroduceRow = React.lazy(() => import('./components/IntroduceRow'));
+const SalesCard = React.lazy(() => import('./components/SalesCard'));
+const TopSearch = React.lazy(() => import('./components/TopSearch'));
+const ProportionSales = React.lazy(() => import('./components/ProportionSales'));
+const OfflineData = React.lazy(() => import('./components/OfflineData'));
+
+interface AnalysisProps {
+ analysis: IAnalysisData;
+ dispatch: Dispatch;
+ loading: boolean;
+}
+
+interface AnalysisState {
+ salesType: 'all' | 'online' | 'stores';
+ currentTabKey: string;
+ rangePickerValue: RangePickerValue;
+}
+
+@connect(
+ ({
+ analysis,
+ loading,
+ }: {
+ analysis: any;
+ loading: {
+ effects: { [key: string]: boolean };
+ };
+ }) => ({
+ analysis,
+ loading: loading.effects['analysis/fetch'],
+ }),
+)
+class Analysis extends Component {
+ state: AnalysisState = {
+ salesType: 'all',
+ currentTabKey: '',
+ rangePickerValue: getTimeDistance('year'),
+ };
+ reqRef!: number;
+ timeoutId!: number;
+ componentDidMount() {
+ const { dispatch } = this.props;
+ this.reqRef = requestAnimationFrame(() => {
+ dispatch({
+ type: 'analysis/fetch',
+ });
+ });
+ }
+
+ componentWillUnmount() {
+ const { dispatch } = this.props;
+ dispatch({
+ type: 'analysis/clear',
+ });
+ cancelAnimationFrame(this.reqRef);
+ clearTimeout(this.timeoutId);
+ }
+
+ handleChangeSalesType = (e: RadioChangeEvent) => {
+ this.setState({
+ salesType: e.target.value,
+ });
+ };
+
+ handleTabChange = (key: string) => {
+ this.setState({
+ currentTabKey: key,
+ });
+ };
+
+ handleRangePickerChange = (rangePickerValue: RangePickerValue) => {
+ const { dispatch } = this.props;
+ this.setState({
+ rangePickerValue,
+ });
+
+ dispatch({
+ type: 'analysis/fetchSalesData',
+ });
+ };
+
+ selectDate = (type: 'today' | 'week' | 'month' | 'year') => {
+ const { dispatch } = this.props;
+ this.setState({
+ rangePickerValue: getTimeDistance(type),
+ });
+
+ dispatch({
+ type: 'analysis/fetchSalesData',
+ });
+ };
+
+ isActive = (type: 'today' | 'week' | 'month' | 'year') => {
+ const { rangePickerValue } = this.state;
+ const value = getTimeDistance(type);
+ if (!rangePickerValue[0] || !rangePickerValue[1]) {
+ return '';
+ }
+ if (
+ rangePickerValue[0].isSame(value[0], 'day') &&
+ rangePickerValue[1].isSame(value[1], 'day')
+ ) {
+ return styles.currentDate;
+ }
+ return '';
+ };
+
+ render() {
+ const { rangePickerValue, salesType, currentTabKey } = this.state;
+ const { analysis, loading } = this.props;
+ const {
+ visitData,
+ visitData2,
+ salesData,
+ searchData,
+ offlineData,
+ offlineChartData,
+ salesTypeData,
+ salesTypeDataOnline,
+ salesTypeDataOffline,
+ } = analysis;
+ let salesPieData;
+ if (salesType === 'all') {
+ salesPieData = salesTypeData;
+ } else {
+ salesPieData = salesType === 'online' ? salesTypeDataOnline : salesTypeDataOffline;
+ }
+ const menu = (
+
+ );
+
+ const dropdownGroup = (
+
+
+
+
+
+ );
+
+ const activeKey = currentTabKey || (offlineData[0] && offlineData[0].name);
+ return (
+
+
+ }>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+export default Analysis;
diff --git a/src/pages/analysis/locales/en-US.ts b/src/pages/analysis/locales/en-US.ts
new file mode 100644
index 0000000000000000000000000000000000000000..a0a03f22105a642ceea239c470fc72bd7b634687
--- /dev/null
+++ b/src/pages/analysis/locales/en-US.ts
@@ -0,0 +1,34 @@
+export default {
+ 'analysis.analysis.test': 'Gongzhuan No.{no} shop',
+ 'analysis.analysis.introduce': 'Introduce',
+ 'analysis.analysis.total-sales': 'Total Sales',
+ 'analysis.analysis.day-sales': 'Daily Sales',
+ 'analysis.analysis.visits': 'Visits',
+ 'analysis.analysis.visits-trend': 'Visits Trend',
+ 'analysis.analysis.visits-ranking': 'Visits Ranking',
+ 'analysis.analysis.day-visits': 'Daily Visits',
+ 'analysis.analysis.week': 'WoW Change',
+ 'analysis.analysis.day': 'DoD Change',
+ 'analysis.analysis.payments': 'Payments',
+ 'analysis.analysis.conversion-rate': 'Conversion Rate',
+ 'analysis.analysis.operational-effect': 'Operational Effect',
+ 'analysis.analysis.sales-trend': 'Stores Sales Trend',
+ 'analysis.analysis.sales-ranking': 'Sales Ranking',
+ 'analysis.analysis.all-year': 'All Year',
+ 'analysis.analysis.all-month': 'All Month',
+ 'analysis.analysis.all-week': 'All Week',
+ 'analysis.analysis.all-day': 'All day',
+ 'analysis.analysis.search-users': 'Search Users',
+ 'analysis.analysis.per-capita-search': 'Per Capita Search',
+ 'analysis.analysis.online-top-search': 'Online Top Search',
+ 'analysis.analysis.the-proportion-of-sales': 'The Proportion Of Sales',
+ 'analysis.channel.all': 'ALL',
+ 'analysis.channel.online': 'Online',
+ 'analysis.channel.stores': 'Stores',
+ 'analysis.analysis.sales': 'Sales',
+ 'analysis.analysis.traffic': 'Traffic',
+ 'analysis.table.rank': 'Rank',
+ 'analysis.table.search-keyword': 'Keyword',
+ 'analysis.table.users': 'Users',
+ 'analysis.table.weekly-range': 'Weekly Range',
+};
diff --git a/src/pages/analysis/locales/pt-BR.ts b/src/pages/analysis/locales/pt-BR.ts
new file mode 100644
index 0000000000000000000000000000000000000000..7be015713ba9fc34fc103a641ca5d5d8c89785ea
--- /dev/null
+++ b/src/pages/analysis/locales/pt-BR.ts
@@ -0,0 +1,34 @@
+export default {
+ 'analysis.analysis.test': 'Gongzhuan No.{no} shop',
+ 'analysis.analysis.introduce': 'Introduzir',
+ 'analysis.analysis.total-sales': 'Vendas Totais',
+ 'analysis.analysis.day-sales': 'Vendas do Dia',
+ 'analysis.analysis.visits': 'Visitas',
+ 'analysis.analysis.visits-trend': 'Tendência de Visitas',
+ 'analysis.analysis.visits-ranking': 'Ranking de Visitas',
+ 'analysis.analysis.day-visits': 'Visitas do Dia',
+ 'analysis.analysis.week': 'Taxa Semanal',
+ 'analysis.analysis.day': 'Taxa Diária',
+ 'analysis.analysis.payments': 'Pagamentos',
+ 'analysis.analysis.conversion-rate': 'Taxa de Conversão',
+ 'analysis.analysis.operational-effect': 'Efeito Operacional',
+ 'analysis.analysis.sales-trend': 'Tendência de Vendas das Lojas',
+ 'analysis.analysis.sales-ranking': 'Ranking de Vendas',
+ 'analysis.$2': 'Todo ano',
+ 'analysis.analysis.all-month': 'Todo mês',
+ 'analysis.analysis.all-week': 'Toda semana',
+ 'analysis.analysis.all-day': 'Todo dia',
+ 'analysis.analysis.search-users': 'Pesquisa de Usuários',
+ 'analysis.analysis.per-capita-search': 'Busca Per Capta',
+ 'analysis.analysis.online-top-search': 'Mais Buscadas Online',
+ 'analysis.analysis.the-proportion-of-sales': 'The Proportion Of Sales',
+ 'analysis.channel.all': 'Tudo',
+ 'analysis.channel.online': 'Online',
+ 'analysis.channel.stores': 'Lojas',
+ 'analysis.analysis.sales': 'Vendas',
+ 'analysis.analysis.traffic': 'Tráfego',
+ 'analysis.table.rank': 'Rank',
+ 'analysis.table.search-keyword': 'Palavra chave',
+ 'analysis.table.users': 'Usuários',
+ 'analysis.table.weekly-range': 'Faixa Semanal',
+};
diff --git a/src/pages/analysis/locales/zh-CN.ts b/src/pages/analysis/locales/zh-CN.ts
new file mode 100644
index 0000000000000000000000000000000000000000..9308f9d5564fb1e5e2a717e5d507e468a1ae62e1
--- /dev/null
+++ b/src/pages/analysis/locales/zh-CN.ts
@@ -0,0 +1,34 @@
+export default {
+ 'analysis.analysis.test': '工专路 {no} 号店',
+ 'analysis.analysis.introduce': '指标说明',
+ 'analysis.analysis.total-sales': '总销售额',
+ 'analysis.analysis.day-sales': '日销售额',
+ 'analysis.analysis.visits': '访问量',
+ 'analysis.analysis.visits-trend': '访问量趋势',
+ 'analysis.analysis.visits-ranking': '门店访问量排名',
+ 'analysis.analysis.day-visits': '日访问量',
+ 'analysis.analysis.week': '周同比',
+ 'analysis.analysis.day': '日同比',
+ 'analysis.analysis.payments': '支付笔数',
+ 'analysis.analysis.conversion-rate': '转化率',
+ 'analysis.analysis.operational-effect': '运营活动效果',
+ 'analysis.analysis.sales-trend': '销售趋势',
+ 'analysis.analysis.sales-ranking': '门店销售额排名',
+ 'analysis.analysis.all-year': '全年',
+ 'analysis.analysis.all-month': '本月',
+ 'analysis.analysis.all-week': '本周',
+ 'analysis.analysis.all-day': '今日',
+ 'analysis.analysis.search-users': '搜索用户数',
+ 'analysis.analysis.per-capita-search': '人均搜索次数',
+ 'analysis.analysis.online-top-search': '线上热门搜索',
+ 'analysis.analysis.the-proportion-of-sales': '销售额类别占比',
+ 'analysis.channel.all': '全部渠道',
+ 'analysis.channel.online': '线上',
+ 'analysis.channel.stores': '门店',
+ 'analysis.analysis.sales': '销售额',
+ 'analysis.analysis.traffic': '客流量',
+ 'analysis.table.rank': '排名',
+ 'analysis.table.search-keyword': '搜索关键词',
+ 'analysis.table.users': '用户数',
+ 'analysis.table.weekly-range': '周涨幅',
+};
diff --git a/src/pages/analysis/locales/zh-TW.ts b/src/pages/analysis/locales/zh-TW.ts
new file mode 100644
index 0000000000000000000000000000000000000000..82a4d428924199404fa5622aac2b674c89404707
--- /dev/null
+++ b/src/pages/analysis/locales/zh-TW.ts
@@ -0,0 +1,34 @@
+export default {
+ 'analysis.analysis.test': '工專路 {no} 號店',
+ 'analysis.analysis.introduce': '指標說明',
+ 'analysis.analysis.total-sales': '總銷售額',
+ 'analysis.analysis.day-sales': '日銷售額',
+ 'analysis.analysis.visits': '訪問量',
+ 'analysis.analysis.visits-trend': '訪問量趨勢',
+ 'analysis.analysis.visits-ranking': '門店訪問量排名',
+ 'analysis.analysis.day-visits': '日訪問量',
+ 'analysis.analysis.week': '周同比',
+ 'analysis.analysis.day': '日同比',
+ 'analysis.analysis.payments': '支付筆數',
+ 'analysis.analysis.conversion-rate': '轉化率',
+ 'analysis.analysis.operational-effect': '運營活動效果',
+ 'analysis.analysis.sales-trend': '銷售趨勢',
+ 'analysis.analysis.sales-ranking': '門店銷售額排名',
+ 'analysis.analysis.all-year': '全年',
+ 'analysis.analysis.all-month': '本月',
+ 'analysis.analysis.all-week': '本周',
+ 'analysis.analysis.all-day': '今日',
+ 'analysis.analysis.search-users': '搜索用戶數',
+ 'analysis.analysis.per-capita-search': '人均搜索次數',
+ 'analysis.analysis.online-top-search': '線上熱門搜索',
+ 'analysis.analysis.the-proportion-of-sales': '銷售額類別占比',
+ 'analysis.channel.all': '全部渠道',
+ 'analysis.channel.online': '線上',
+ 'analysis.channel.stores': '門店',
+ 'analysis.analysis.sales': '銷售額',
+ 'analysis.analysis.traffic': '客流量',
+ 'analysis.table.rank': '排名',
+ 'analysis.table.search-keyword': '搜索關鍵詞',
+ 'analysis.table.users': '用戶數',
+ 'analysis.table.weekly-range': '周漲幅',
+};
diff --git a/src/pages/analysis/model.tsx b/src/pages/analysis/model.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..5706013c7e35aa8f685befcbed806e83f97e7dcc
--- /dev/null
+++ b/src/pages/analysis/model.tsx
@@ -0,0 +1,84 @@
+import { fakeChartData } from './service';
+import { IAnalysisData } from './data';
+import { Reducer } from 'redux';
+import { EffectsCommandMap } from 'dva';
+import { AnyAction } from 'redux';
+
+export type Effect = (
+ action: AnyAction,
+ effects: EffectsCommandMap & { select: (func: (state: IAnalysisData) => T) => T },
+) => void;
+
+export interface ModelType {
+ namespace: string;
+ state: IAnalysisData;
+ effects: {
+ fetch: Effect;
+ fetchSalesData: Effect;
+ };
+ reducers: {
+ save: Reducer;
+ clear: Reducer;
+ };
+}
+
+const Model: ModelType = {
+ namespace: 'analysis',
+
+ state: {
+ visitData: [],
+ visitData2: [],
+ salesData: [],
+ searchData: [],
+ offlineData: [],
+ offlineChartData: [],
+ salesTypeData: [],
+ salesTypeDataOnline: [],
+ salesTypeDataOffline: [],
+ radarData: [],
+ },
+
+ effects: {
+ *fetch(_, { call, put }) {
+ const response = yield call(fakeChartData);
+ yield put({
+ type: 'save',
+ payload: response,
+ });
+ },
+ *fetchSalesData(_, { call, put }) {
+ const response = yield call(fakeChartData);
+ yield put({
+ type: 'save',
+ payload: {
+ salesData: response.salesData,
+ },
+ });
+ },
+ },
+
+ reducers: {
+ save(state, { payload }) {
+ return {
+ ...state,
+ ...payload,
+ };
+ },
+ clear() {
+ return {
+ visitData: [],
+ visitData2: [],
+ salesData: [],
+ searchData: [],
+ offlineData: [],
+ offlineChartData: [],
+ salesTypeData: [],
+ salesTypeDataOnline: [],
+ salesTypeDataOffline: [],
+ radarData: [],
+ };
+ },
+ },
+};
+
+export default Model;
diff --git a/src/pages/analysis/service.tsx b/src/pages/analysis/service.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..64744b9d05f57b1a81a174157dc7cacfbc0e285f
--- /dev/null
+++ b/src/pages/analysis/service.tsx
@@ -0,0 +1,5 @@
+import request from 'umi-request';
+
+export async function fakeChartData() {
+ return request('/api/analysis/fake_chart_data');
+}
diff --git a/src/pages/analysis/style.less b/src/pages/analysis/style.less
new file mode 100644
index 0000000000000000000000000000000000000000..e1e898e594abec60264826e6fee472d71dfa68eb
--- /dev/null
+++ b/src/pages/analysis/style.less
@@ -0,0 +1,179 @@
+@import '~antd/lib/style/themes/default.less';
+@import './utils/utils.less';
+
+.iconGroup {
+ i {
+ margin-left: 16px;
+ color: @text-color-secondary;
+ cursor: pointer;
+ transition: color 0.32s;
+ &:hover {
+ color: @text-color;
+ }
+ }
+}
+
+.rankingList {
+ margin: 25px 0 0;
+ padding: 0;
+ list-style: none;
+ li {
+ .clearfix();
+
+ display: flex;
+ align-items: center;
+ margin-top: 16px;
+ span {
+ color: @text-color;
+ font-size: 14px;
+ line-height: 22px;
+ }
+ .rankingItemNumber {
+ display: inline-block;
+ width: 20px;
+ height: 20px;
+ margin-top: 1.5px;
+ margin-right: 16px;
+ font-weight: 600;
+ font-size: 12px;
+ line-height: 20px;
+ text-align: center;
+ background-color: @background-color-base;
+ border-radius: 20px;
+ &.active {
+ color: #fff;
+ background-color: #314659;
+ }
+ }
+ .rankingItemTitle {
+ flex: 1;
+ margin-right: 8px;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ }
+ }
+}
+
+.salesExtra {
+ display: inline-block;
+ margin-right: 24px;
+ a {
+ margin-left: 24px;
+ color: @text-color;
+ &:hover {
+ color: @primary-color;
+ }
+ &.currentDate {
+ color: @primary-color;
+ }
+ }
+}
+
+.salesCard {
+ .salesBar {
+ padding: 0 0 32px 32px;
+ }
+ .salesRank {
+ padding: 0 32px 32px 72px;
+ }
+ :global {
+ .ant-tabs-bar {
+ padding-left: 16px;
+ .ant-tabs-nav .ant-tabs-tab {
+ padding-top: 16px;
+ padding-bottom: 14px;
+ line-height: 24px;
+ }
+ }
+ .ant-tabs-extra-content {
+ padding-right: 24px;
+ line-height: 55px;
+ }
+ .ant-card-head {
+ position: relative;
+ }
+ .ant-card-head-title {
+ align-items: normal;
+ }
+ }
+}
+
+.salesCardExtra {
+ height: inherit;
+}
+
+.salesTypeRadio {
+ position: absolute;
+ right: 54px;
+ bottom: 12px;
+}
+
+.offlineCard {
+ :global {
+ .ant-tabs-ink-bar {
+ bottom: auto;
+ }
+ .ant-tabs-bar {
+ border-bottom: none;
+ }
+ .ant-tabs-nav-container-scrolling {
+ padding-right: 40px;
+ padding-left: 40px;
+ }
+ .ant-tabs-tab-prev-icon::before {
+ position: relative;
+ left: 6px;
+ }
+ .ant-tabs-tab-next-icon::before {
+ position: relative;
+ right: 6px;
+ }
+ .ant-tabs-tab-active h4 {
+ color: @primary-color;
+ }
+ }
+}
+
+.trendText {
+ margin-left: 8px;
+ color: @heading-color;
+}
+
+@media screen and (max-width: @screen-lg) {
+ .salesExtra {
+ display: none;
+ }
+
+ .rankingList {
+ li {
+ span:first-child {
+ margin-right: 8px;
+ }
+ }
+ }
+}
+
+@media screen and (max-width: @screen-md) {
+ .rankingTitle {
+ margin-top: 16px;
+ }
+
+ .salesCard .salesBar {
+ padding: 16px;
+ }
+}
+
+@media screen and (max-width: @screen-sm) {
+ .salesExtraWrap {
+ display: none;
+ }
+
+ .salesCard {
+ :global {
+ .ant-tabs-content {
+ padding-top: 30px;
+ }
+ }
+ }
+}
diff --git a/src/pages/analysis/utils/Yuan.tsx b/src/pages/analysis/utils/Yuan.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..eafefd41526f7313d6baea44faebafe2cb27f16b
--- /dev/null
+++ b/src/pages/analysis/utils/Yuan.tsx
@@ -0,0 +1,33 @@
+import React from 'react';
+import { yuan } from '../components/Charts';
+/**
+ * 减少使用 dangerouslySetInnerHTML
+ */
+export default class Yuan extends React.Component<{
+ children: React.ReactText;
+}> {
+ main: HTMLSpanElement | undefined | null;
+ componentDidMount() {
+ this.renderToHtml();
+ }
+
+ componentDidUpdate() {
+ this.renderToHtml();
+ }
+ renderToHtml = () => {
+ const { children } = this.props;
+ if (this.main) {
+ this.main.innerHTML = yuan(children);
+ }
+ };
+
+ render() {
+ return (
+ {
+ this.main = ref;
+ }}
+ />
+ );
+ }
+}
diff --git a/src/pages/analysis/utils/utils.less b/src/pages/analysis/utils/utils.less
new file mode 100644
index 0000000000000000000000000000000000000000..de1aa64222b6f14328d3a9e3c262ac5a31cce5af
--- /dev/null
+++ b/src/pages/analysis/utils/utils.less
@@ -0,0 +1,50 @@
+.textOverflow() {
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ word-break: break-all;
+}
+
+.textOverflowMulti(@line: 3, @bg: #fff) {
+ position: relative;
+ max-height: @line * 1.5em;
+ margin-right: -1em;
+ padding-right: 1em;
+ overflow: hidden;
+ line-height: 1.5em;
+ text-align: justify;
+ &::before {
+ position: absolute;
+ right: 14px;
+ bottom: 0;
+ padding: 0 1px;
+ background: @bg;
+ content: '...';
+ }
+ &::after {
+ position: absolute;
+ right: 14px;
+ width: 1em;
+ height: 1em;
+ margin-top: 0.2em;
+ background: white;
+ content: '';
+ }
+}
+
+// mixins for clearfix
+// ------------------------
+.clearfix() {
+ zoom: 1;
+ &::before,
+ &::after {
+ display: table;
+ content: ' ';
+ }
+ &::after {
+ clear: both;
+ height: 0;
+ font-size: 0;
+ visibility: hidden;
+ }
+}
diff --git a/src/pages/analysis/utils/utils.ts b/src/pages/analysis/utils/utils.ts
new file mode 100644
index 0000000000000000000000000000000000000000..7cdc266a523fe1bce8592fa49a719dda57ca54a5
--- /dev/null
+++ b/src/pages/analysis/utils/utils.ts
@@ -0,0 +1,53 @@
+import moment from 'moment';
+import { RangePickerValue } from 'antd/lib/date-picker/interface';
+
+export function fixedZero(val: number) {
+ return val * 1 < 10 ? `0${val}` : val;
+}
+
+export function getTimeDistance(type: 'today' | 'week' | 'month' | 'year'): RangePickerValue {
+ const now = new Date();
+ const oneDay = 1000 * 60 * 60 * 24;
+
+ if (type === 'today') {
+ now.setHours(0);
+ now.setMinutes(0);
+ now.setSeconds(0);
+ return [moment(now), moment(now.getTime() + (oneDay - 1000))];
+ }
+
+ if (type === 'week') {
+ let day = now.getDay();
+ now.setHours(0);
+ now.setMinutes(0);
+ now.setSeconds(0);
+
+ if (day === 0) {
+ day = 6;
+ } else {
+ day -= 1;
+ }
+
+ const beginTime = now.getTime() - day * oneDay;
+
+ return [moment(beginTime), moment(beginTime + (7 * oneDay - 1000))];
+ }
+
+ if (type === 'month') {
+ const year = now.getFullYear();
+ const month = now.getMonth();
+ const nextDate = moment(now).add(1, 'months');
+ const nextYear = nextDate.year();
+ const nextMonth = nextDate.month();
+
+ return [
+ moment(`${year}-${fixedZero(month + 1)}-01 00:00:00`),
+ moment(moment(`${nextYear}-${fixedZero(nextMonth + 1)}-01 00:00:00`).valueOf() - 1000),
+ ];
+ }
+
+ return [
+ moment(`${now.getFullYear()}-01-01 00:00:00`),
+ moment(`${now.getFullYear()}-12-31 23:59:59`),
+ ];
+}
diff --git a/src/utils/utils.less b/src/utils/utils.less
index 7be54ba58bca8f59650472bf77d1ae599165dda1..de1aa64222b6f14328d3a9e3c262ac5a31cce5af 100644
--- a/src/utils/utils.less
+++ b/src/utils/utils.less
@@ -38,8 +38,8 @@
zoom: 1;
&::before,
&::after {
- content: ' ';
display: table;
+ content: ' ';
}
&::after {
clear: both;