Commit 37644852 authored by 陈帅's avatar 陈帅

remove default block

parent dd59a08b
......@@ -4,12 +4,10 @@ import slash from 'slash2';
import { IPlugin, IConfig } from 'umi-types';
import defaultSettings from './defaultSettings';
import webpackPlugin from './plugin.config';
const { pwa, primaryColor } = defaultSettings;
// preview.pro.ant.design only do not use in your production ;
const { pwa, primaryColor } = defaultSettings; // preview.pro.ant.design only do not use in your production ;
// preview.pro.ant.design 专用环境变量,请不要在你的项目中使用它。
const { ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION, TEST, NODE_ENV } = process.env;
const { ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION, TEST, NODE_ENV } = process.env;
const plugins: IPlugin[] = [
[
'umi-plugin-react',
......@@ -59,10 +57,9 @@ const plugins: IPlugin[] = [
autoAddMenu: true,
},
],
];
// 针对 preview.pro.ant.design 的 GA 统计代码
]; // 针对 preview.pro.ant.design 的 GA 统计代码
// preview.pro.ant.design only do not use in your production ; preview.pro.ant.design 专用环境变量,请不要在你的项目中使用它。
if (ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION === 'site') {
plugins.push([
'umi-plugin-ga',
......@@ -84,7 +81,6 @@ const uglifyJSOptions =
},
}
: {};
export default {
// add for transfer to umi
plugins,
......@@ -107,9 +103,9 @@ export default {
routes: [
{
path: '/',
name: 'Analysis',
icon: 'dashboard',
component: './analysis',
name: 'welcome',
icon: 'smile',
component: './Welcome',
},
],
},
......@@ -147,7 +143,9 @@ export default {
) {
return localName;
}
const match = context.resourcePath.match(/src(.*)/);
if (match && match[1]) {
const antdProPath = match[1].replace('.less', '');
const arr = slash(antdProPath)
......@@ -156,6 +154,7 @@ export default {
.map((a: string) => a.toLowerCase());
return `antd-pro${arr.join('-')}-${localName}`.replace(/--/g, '-');
}
return localName;
},
},
......
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,
};
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 styles from '../index.less';
import autoHeight from '../autoHeight';
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 = {
height: 0,
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: propsHeight = 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,
}),
];
const { height: stateHeight } = this.state;
const height = propsHeight || stateHeight;
return (
<div className={styles.chart} style={{ height }} ref={this.handleRoot}>
<div ref={this.handleRef}>
{title && <h4 style={{ marginBottom: 20 }}>{title}</h4>}
<Chart
scale={scale}
height={title ? height - 41 : height}
forceFit={forceFit}
data={data}
padding={padding || 'auto'}
>
<Axis
name="x"
title={false}
label={autoHideXLabels ? false : {}}
tickLine={autoHideXLabels ? false : {}}
/>
<Axis name="y" min={0} />
<Tooltip showTitle={false} crosshairs={false} />
<Geom type="interval" position="x*y" color={color} tooltip={tooltip} />
</Chart>
</div>
</div>
);
}
}
export default autoHeight()(Bar);
@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;
}
}
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 = <div className={styles.total}>{total()}</div>;
break;
default:
totalDom = <div className={styles.total}>{total}</div>;
}
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<IChartCardProps> {
renderContent = () => {
const { contentHeight, title, avatar, action, total, footer, children, loading } = this.props;
if (loading) {
return false;
}
return (
<div className={styles.chartCard}>
<div
className={classNames(styles.chartTop, {
[styles.chartTopMargin]: !children && !footer,
})}
>
<div className={styles.avatar}>{avatar}</div>
<div className={styles.metaWrap}>
<div className={styles.meta}>
<span className={styles.title}>{title}</span>
<span className={styles.action}>{action}</span>
</div>
{renderTotal(total)}
</div>
</div>
{children && (
<div className={styles.content} style={{ height: contentHeight || 'auto' }}>
<div className={contentHeight && styles.contentFixed}>{children}</div>
</div>
)}
{footer && (
<div
className={classNames(styles.footer, {
[styles.footerMargin]: !children,
})}
>
{footer}
</div>
)}
</div>
);
};
render() {
const {
loading = false,
contentHeight,
title,
avatar,
action,
total,
footer,
children,
...rest
} = this.props;
return (
<Card loading={loading} bodyStyle={{ padding: '20px 24px 8px 24px' }} {...rest}>
{this.renderContent()}
</Card>
);
}
}
export default ChartCard;
@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;
}
}
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<IFieldProps> = ({ label, value, ...rest }) => (
<div className={styles.field} {...rest}>
<span className={styles.label}>{label}</span>
<span className={styles.number}>{value}</span>
</div>
);
export default Field;
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<IGaugeProps> {
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 = () => `
<div style="width: 300px;text-align: center;font-size: 12px!important;">
<p style="font-size: 14px; color: rgba(0,0,0,0.43);margin: 0;">${title}</p>
<p style="font-size: 24px;color: rgba(0,0,0,0.85);margin: 0;">
${(data[0].value * 10).toFixed(2)}%
</p>
</div>`;
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 (
<Chart height={height} data={data} scale={cols} padding={[-16, 0, 16, 0]} forceFit={forceFit}>
<Coord type="polar" startAngle={-1.25 * Math.PI} endAngle={0.25 * Math.PI} radius={0.8} />
<Axis name="1" line={undefined} />
<Axis
line={undefined}
tickLine={undefined}
subTickLine={undefined}
name="value"
zIndex={2}
label={{
offset: -12,
formatter,
textStyle: textStyle,
}}
/>
<Guide>
<Line
start={[3, 0.905]}
end={[3, 0.85]}
lineStyle={{
stroke: color,
lineDash: undefined,
lineWidth: 2,
}}
/>
<Line
start={[5, 0.905]}
end={[5, 0.85]}
lineStyle={{
stroke: color,
lineDash: undefined,
lineWidth: 3,
}}
/>
<Line
start={[7, 0.905]}
end={[7, 0.85]}
lineStyle={{
stroke: color,
lineDash: undefined,
lineWidth: 3,
}}
/>
<Arc
start={[0, 0.965]}
end={[10, 0.965]}
style={{
stroke: bgColor,
lineWidth: 10,
}}
/>
<Arc
start={[0, 0.965]}
end={[data[0].value, 0.965]}
style={{
stroke: color,
lineWidth: 10,
}}
/>
<Html position={['50%', '95%']} html={renderHtml()} />
</Guide>
<Geom
line={false}
type="point"
position="value*1"
shape="pointer"
color={color}
active={false}
/>
</Chart>
);
}
}
export default autoHeight()(Gauge);
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<IMiniAreaProps> {
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 (
<div className={styles.miniChart} style={{ height }}>
<div className={styles.chartContent}>
{height > 0 && (
<Chart
animate={animate}
scale={scaleProps}
height={chartHeight}
forceFit={forceFit}
data={data}
padding={padding}
>
<Axis
key="axis-x"
name="x"
label={false}
line={false}
tickLine={false}
grid={false}
{...xAxis}
/>
<Axis
key="axis-y"
name="y"
label={false}
line={false}
tickLine={false}
grid={false}
{...yAxis}
/>
<Tooltip showTitle={false} crosshairs={false} />
<Geom
type="area"
position="x*y"
color={color}
tooltip={tooltip}
shape="smooth"
style={{
fillOpacity: 1,
}}
/>
{line ? (
<Geom
type="line"
position="x*y"
shape="smooth"
color={borderColor}
size={borderWidth}
tooltip={false}
/>
) : (
<span style={{ display: 'none' }} />
)}
</Chart>
)}
</div>
</div>
);
}
}
export default autoHeight()(MiniArea);
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<IMiniBarProps> {
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 (
<div className={styles.miniChart} style={{ height }}>
<div className={styles.chartContent}>
<Chart
scale={scale}
height={chartHeight}
forceFit={forceFit}
data={data}
padding={padding}
>
<Tooltip showTitle={false} crosshairs={false} />
<Geom type="interval" position="x*y" color={color} tooltip={tooltip} />
</Chart>
</div>
</div>
);
}
}
export default autoHeight()(MiniBar);
@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;
}
}
}
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<IMiniProgressProps> = ({
targetLabel,
target,
color = 'rgb(19, 194, 194)',
strokeWidth,
percent,
}) => {
return (
<div className={styles.miniProgress}>
<Tooltip title={targetLabel}>
<div className={styles.target} style={{ left: target ? `${target}%` : undefined }}>
<span style={{ backgroundColor: color || undefined }} />
<span style={{ backgroundColor: color || undefined }} />
</div>
</Tooltip>
<div className={styles.progressWrap}>
<div
className={styles.progress}
style={{
backgroundColor: color || undefined,
width: percent ? `${percent}%` : undefined,
height: strokeWidth || undefined,
}}
/>
</div>
</div>
);
};
export default MiniProgress;
@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;
}
}
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 styles from './index.less';
import autoHeight from '../autoHeight';
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<IPieProps, IPieState> {
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 (
<div ref={this.handleRoot} className={pieClassName} style={style}>
<ReactFitText maxFontSize={25}>
<div className={styles.chart}>
<Chart
scale={scale}
height={height}
forceFit={forceFit}
data={dv}
padding={padding}
animate={animate}
onGetG2Instance={this.getG2Instance}
>
{!!tooltip && <Tooltip showTitle={false} />}
<Coord type="theta" innerRadius={inner} />
<Geom
style={{ lineWidth, stroke: '#fff' }}
tooltip={tooltip ? tooltipFormat : undefined}
type="intervalStack"
position="percent"
color={['x', percent || percent === 0 ? formatColor : defaultColors] as any}
selected={selected}
/>
</Chart>
{(subTitle || total) && (
<div className={styles.total}>
{subTitle && <h4 className="pie-sub-title">{subTitle}</h4>}
{/* eslint-disable-next-line */}
{total && (
<div className="pie-stat">{typeof total === 'function' ? total() : total}</div>
)}
</div>
)}
</div>
</ReactFitText>
{hasLegend && (
<ul className={styles.legend}>
{legendData.map((item, i) => (
<li key={item.x} onClick={() => this.handleLegendClick(item, i)}>
<span
className={styles.dot}
style={{
backgroundColor: !item.checked ? '#aaa' : item.color,
}}
/>
<span className={styles.legendTitle}>{item.x}</span>
<Divider type="vertical" />
<span className={styles.percent}>
{`${(Number.isNaN(item.percent) ? 0 : item.percent * 100).toFixed(2)}%`}
</span>
<span className={styles.value}>{valueFormat ? valueFormat(item.y) : item.y}</span>
</li>
))}
</ul>
)}
</div>
);
}
}
export default autoHeight()(Pie);
.tagCloud {
overflow: hidden;
canvas {
transform-origin: 0 0;
}
}
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<ITagCloudProps, ITagCloudState> {
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 (
<div
className={classNames(styles.tagCloud, className)}
style={{ width: '100%', height }}
ref={this.saveRootRef}
>
{dv && (
<Chart
width={width}
height={stateHeight}
data={dv}
padding={0}
scale={{
x: { nice: false },
y: { nice: false },
}}
>
<Tooltip showTitle={false} />
<Coord reflect="y" />
<Geom
type="point"
position="x*y"
color="text"
shape="cloud"
tooltip={[
'text*value',
function trans(text, value) {
return { name: text, value };
},
]}
/>
</Chart>
)}
</div>
);
}
}
export default autoHeight()(TagCloud);
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<ITimelineChartProps> {
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 = () => (
<Slider
padding={[0, padding[1] + 20, 0, padding[3]]}
width="auto"
height={26}
xAxis="x"
yAxis="y1"
scales={{ x: timeScale }}
data={data}
start={ds.state.start}
end={ds.state.end}
backgroundChart={{ type: 'line' }}
onChange={({ startValue, endValue }: { startValue: string; endValue: string }) => {
ds.setState('start', startValue);
ds.setState('end', endValue);
}}
/>
);
return (
<div className={styles.timelineChart} style={{ height: height + 30 }}>
<div>
{title && <h4>{title}</h4>}
<Chart height={height} padding={padding} data={dv} scale={cols} forceFit>
<Axis name="x" />
<Tooltip />
<Legend name="key" position="top" />
<Geom type="line" position="x*value" size={borderWidth} color="key" />
</Chart>
<div style={{ marginRight: -20 }}>
<SliderGen />
</div>
</div>
</div>
);
}
}
export default autoHeight()(TimelineChart);
@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;
}
}
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<IWaterWaveProps> {
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 (
<div
className={styles.waterWave}
ref={n => (this.root = n)}
style={{ transform: `scale(${radio})` }}
>
<div style={{ width: height, height, overflow: 'hidden' }}>
<canvas
className={styles.waterWaveCanvasWrapper}
ref={n => (this.node = n)}
width={height * 2}
height={height * 2}
/>
</div>
<div className={styles.text} style={{ width: height }}>
{title && <span>{title}</span>}
<h4>{percent}%</h4>
</div>
</div>
);
}
}
export default autoHeight()(WaterWave);
import React from 'react';
export type IReactComponent<P = any> =
| React.StatelessComponent<P>
| React.ComponentClass<P>
| React.ClassicComponentClass<P>;
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<P extends IAutoHeightProps>(
WrappedComponent: React.ComponentClass<P> | React.SFC<P>,
): React.ComponentClass<P> {
class AutoHeightComponent extends React.Component<P & IAutoHeightProps> {
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 (
<div ref={this.handleRoot}>
{h > 0 && <WrappedComponent {...this.props} height={h} />}
</div>
);
}
}
return AutoHeightComponent;
};
}
export default autoHeight;
import * as BizChart from 'bizcharts';
export = BizChart;
import * as BizChart from 'bizcharts';
export default BizChart;
.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;
}
}
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,
};
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 (
<Row gutter={24}>
<Col {...topColResponsiveProps}>
<ChartCard
bordered={false}
title={
<FormattedMessage id="analysis.analysis.total-sales" defaultMessage="Total Sales" />
}
action={
<Tooltip
title={
<FormattedMessage id="analysis.analysis.introduce" defaultMessage="Introduce" />
}
>
<Icon type="info-circle-o" />
</Tooltip>
}
loading={loading}
total={() => <Yuan>126560</Yuan>}
footer={
<Field
label={
<FormattedMessage id="analysis.analysis.day-sales" defaultMessage="Daily Sales" />
}
value={`¥${numeral(12423).format('0,0')}`}
/>
}
contentHeight={46}
>
<Trend flag="up" style={{ marginRight: 16 }}>
<FormattedMessage id="analysis.analysis.week" defaultMessage="Weekly Changes" />
<span className={styles.trendText}>12%</span>
</Trend>
<Trend flag="down">
<FormattedMessage id="analysis.analysis.day" defaultMessage="Daily Changes" />
<span className={styles.trendText}>11%</span>
</Trend>
</ChartCard>
</Col>
<Col {...topColResponsiveProps}>
<ChartCard
bordered={false}
loading={loading}
title={<FormattedMessage id="analysis.analysis.visits" defaultMessage="Visits" />}
action={
<Tooltip
title={
<FormattedMessage id="analysis.analysis.introduce" defaultMessage="Introduce" />
}
>
<Icon type="info-circle-o" />
</Tooltip>
}
total={numeral(8846).format('0,0')}
footer={
<Field
label={
<FormattedMessage id="analysis.analysis.day-visits" defaultMessage="Daily Visits" />
}
value={numeral(1234).format('0,0')}
/>
}
contentHeight={46}
>
<MiniArea color="#975FE4" data={visitData} />
</ChartCard>
</Col>
<Col {...topColResponsiveProps}>
<ChartCard
bordered={false}
loading={loading}
title={<FormattedMessage id="analysis.analysis.payments" defaultMessage="Payments" />}
action={
<Tooltip
title={
<FormattedMessage id="analysis.analysis.introduce" defaultMessage="Introduce" />
}
>
<Icon type="info-circle-o" />
</Tooltip>
}
total={numeral(6560).format('0,0')}
footer={
<Field
label={
<FormattedMessage
id="analysis.analysis.conversion-rate"
defaultMessage="Conversion Rate"
/>
}
value="60%"
/>
}
contentHeight={46}
>
<MiniBar data={visitData} />
</ChartCard>
</Col>
<Col {...topColResponsiveProps}>
<ChartCard
loading={loading}
bordered={false}
title={
<FormattedMessage
id="analysis.analysis.operational-effect"
defaultMessage="Operational Effect"
/>
}
action={
<Tooltip
title={
<FormattedMessage id="analysis.analysis.introduce" defaultMessage="Introduce" />
}
>
<Icon type="info-circle-o" />
</Tooltip>
}
total="78%"
footer={
<div style={{ whiteSpace: 'nowrap', overflow: 'hidden' }}>
<Trend flag="up" style={{ marginRight: 16 }}>
<FormattedMessage id="analysis.analysis.week" defaultMessage="Weekly Changes" />
<span className={styles.trendText}>12%</span>
</Trend>
<Trend flag="down">
<FormattedMessage id="analysis.analysis.day" defaultMessage="Weekly Changes" />
<span className={styles.trendText}>11%</span>
</Trend>
</div>
}
contentHeight={46}
>
<MiniProgress percent={78} strokeWidth={8} target={80} color="#13C2C2" />
</ChartCard>
</Col>
</Row>
);
};
export default IntroduceRow;
@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;
}
}
}
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<NumberInfoProps> = ({
theme,
title,
subTitle,
total,
subTotal,
status,
suffix,
gap,
...rest
}) => (
<div
className={classNames(styles.numberInfo, {
[styles[`numberInfo${theme}`]]: theme,
})}
{...rest}
>
{title && (
<div className={styles.numberInfoTitle} title={typeof title === 'string' ? title : ''}>
{title}
</div>
)}
{subTitle && (
<div
className={styles.numberInfoSubTitle}
title={typeof subTitle === 'string' ? subTitle : ''}
>
{subTitle}
</div>
)}
<div className={styles.numberInfoValue} style={gap ? { marginTop: gap } : {}}>
<span>
{total}
{suffix && <em className={styles.suffix}>{suffix}</em>}
</span>
{(status || subTotal) && (
<span className={styles.subTotal}>
{subTotal}
{status && <Icon type={`caret-${status}`} />}
</span>
)}
</div>
</div>
);
export default NumberInfo;
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 (
<Row gutter={8} style={{ width: 138, margin: '8px 0' }}>
<Col span={12}>
<NumberInfo
title={data.name}
subTitle={
<FormattedMessage
id="analysis.analysis.conversion-rate"
defaultMessage="Conversion Rate"
/>
}
gap={2}
total={`${data.cvr * 100}%`}
theme={currentKey !== data.name ? 'light' : undefined}
/>
</Col>
<Col span={12} style={{ paddingTop: 36 }}>
<Pie
animate={false}
inner={0.55}
tooltip={false}
margin={[0, 0, 0, 0]}
percent={data.cvr * 100}
height={64}
/>
</Col>
</Row>
);
};
const { TabPane } = Tabs;
const OfflineData = ({
activeKey,
loading,
offlineData,
offlineChartData,
handleTabChange,
}: {
activeKey: string;
loading: boolean;
offlineData: IOfflineData[];
offlineChartData: IOfflineChartData[];
handleTabChange: (activeKey: string) => void;
}) => (
<Card loading={loading} className={styles.offlineCard} bordered={false} style={{ marginTop: 32 }}>
<Tabs activeKey={activeKey} onChange={handleTabChange}>
{offlineData.map(shop => (
<TabPane tab={<CustomTab data={shop} currentTabKey={activeKey} />} key={shop.name}>
<div style={{ padding: '0 24px' }}>
<TimelineChart
height={400}
data={offlineChartData}
titleMap={{
y1: formatMessage({ id: 'analysis.analysis.traffic' }),
y2: formatMessage({ id: 'analysis.analysis.payments' }),
}}
/>
</div>
</TabPane>
))}
</Tabs>
</Card>
);
export default OfflineData;
import React from 'react';
import { Spin } from 'antd';
// loading components from code split
// https://umijs.org/plugin/umi-plugin-react.html#dynamicimport
export default () => (
<div style={{ paddingTop: 100, textAlign: 'center' }}>
<Spin size="large" />
</div>
);
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 (
<Card
loading={loading}
className={styles.salesCard}
bordered={false}
title={
<FormattedMessage
id="analysis.analysis.the-proportion-of-sales"
defaultMessage="The Proportion of Sales"
/>
}
bodyStyle={{ padding: 24 }}
extra={
<div className={styles.salesCardExtra}>
{dropdownGroup}
<div className={styles.salesTypeRadio}>
<Radio.Group value={salesType} onChange={handleChangeSalesType}>
<Radio.Button value="all">
<FormattedMessage id="analysis.channel.all" defaultMessage="ALL" />
</Radio.Button>
<Radio.Button value="online">
<FormattedMessage id="analysis.channel.online" defaultMessage="Online" />
</Radio.Button>
<Radio.Button value="stores">
<FormattedMessage id="analysis.channel.stores" defaultMessage="Stores" />
</Radio.Button>
</Radio.Group>
</div>
</div>
}
style={{ marginTop: 24 }}
>
<div
style={{
minHeight: 380,
}}
>
<h4 style={{ marginTop: 8, marginBottom: 32 }}>
<FormattedMessage id="analysis.analysis.sales" defaultMessage="Sales" />
</h4>
<Pie
hasLegend
subTitle={<FormattedMessage id="analysis.analysis.sales" defaultMessage="Sales" />}
total={() => <Yuan>{salesPieData.reduce((pre, now) => now.y + pre, 0)}</Yuan>}
data={salesPieData}
valueFormat={value => <Yuan>{value}</Yuan>}
height={248}
lineWidth={4}
/>
</div>
</Card>
);
};
export default ProportionSales;
import React 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;
}) => (
<Card loading={loading} bordered={false} bodyStyle={{ padding: 0 }}>
<div className={styles.salesCard}>
<Tabs
tabBarExtraContent={
<div className={styles.salesExtraWrap}>
<div className={styles.salesExtra}>
<a className={isActive('today')} onClick={() => selectDate('today')}>
<FormattedMessage id="analysis.analysis.all-day" defaultMessage="All Day" />
</a>
<a className={isActive('week')} onClick={() => selectDate('week')}>
<FormattedMessage id="analysis.analysis.all-week" defaultMessage="All Week" />
</a>
<a className={isActive('month')} onClick={() => selectDate('month')}>
<FormattedMessage id="analysis.analysis.all-month" defaultMessage="All Month" />
</a>
<a className={isActive('year')} onClick={() => selectDate('year')}>
<FormattedMessage id="analysis.analysis.all-year" defaultMessage="All Year" />
</a>
</div>
<RangePicker
value={rangePickerValue}
onChange={handleRangePickerChange}
style={{ width: 256 }}
/>
</div>
}
size="large"
tabBarStyle={{ marginBottom: 24 }}
>
<TabPane
tab={<FormattedMessage id="analysis.analysis.sales" defaultMessage="Sales" />}
key="sales"
>
<Row>
<Col xl={16} lg={12} md={12} sm={24} xs={24}>
<div className={styles.salesBar}>
<Bar
height={295}
title={
<FormattedMessage
id="analysis.analysis.sales-trend"
defaultMessage="Sales Trend"
/>
}
data={salesData}
/>
</div>
</Col>
<Col xl={8} lg={12} md={12} sm={24} xs={24}>
<div className={styles.salesRank}>
<h4 className={styles.rankingTitle}>
<FormattedMessage
id="analysis.analysis.sales-ranking"
defaultMessage="Sales Ranking"
/>
</h4>
<ul className={styles.rankingList}>
{rankingListData.map((item, i) => (
<li key={item.title}>
<span className={`${styles.rankingItemNumber} ${i < 3 ? styles.active : ''}`}>
{i + 1}
</span>
<span className={styles.rankingItemTitle} title={item.title}>
{item.title}
</span>
<span className={styles.rankingItemValue}>
{numeral(item.total).format('0,0')}
</span>
</li>
))}
</ul>
</div>
</Col>
</Row>
</TabPane>
<TabPane
tab={<FormattedMessage id="analysis.analysis.visits" defaultMessage="Visits" />}
key="views"
>
<Row>
<Col xl={16} lg={12} md={12} sm={24} xs={24}>
<div className={styles.salesBar}>
<Bar
height={292}
title={
<FormattedMessage
id="analysis.analysis.visits-trend"
defaultMessage="Visits Trend"
/>
}
data={salesData}
/>
</div>
</Col>
<Col xl={8} lg={12} md={12} sm={24} xs={24}>
<div className={styles.salesRank}>
<h4 className={styles.rankingTitle}>
<FormattedMessage
id="analysis.analysis.visits-ranking"
defaultMessage="Visits Ranking"
/>
</h4>
<ul className={styles.rankingList}>
{rankingListData.map((item, i) => (
<li key={item.title}>
<span className={`${styles.rankingItemNumber} ${i < 3 ? styles.active : ''}`}>
{i + 1}
</span>
<span className={styles.rankingItemTitle} title={item.title}>
{item.title}
</span>
<span>{numeral(item.total).format('0,0')}</span>
</li>
))}
</ul>
</div>
</Col>
</Row>
</TabPane>
</Tabs>
</div>
</Card>
);
export default SalesCard;
import React 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: <FormattedMessage id="analysis.table.rank" defaultMessage="Rank" />,
dataIndex: 'index',
key: 'index',
},
{
title: <FormattedMessage id="analysis.table.search-keyword" defaultMessage="Search keyword" />,
dataIndex: 'keyword',
key: 'keyword',
render: (text: React.ReactNode) => <a href="/">{text}</a>,
},
{
title: <FormattedMessage id="analysis.table.users" defaultMessage="Users" />,
dataIndex: 'count',
key: 'count',
sorter: (a: { count: number }, b: { count: number }) => a.count - b.count,
className: styles.alignRight,
},
{
title: <FormattedMessage id="analysis.table.weekly-range" defaultMessage="Weekly Range" />,
dataIndex: 'range',
key: 'range',
sorter: (a: { range: number }, b: { range: number }) => a.range - b.range,
render: (text: React.ReactNode, record: { status: number }) => (
<Trend flag={record.status === 1 ? 'down' : 'up'}>
<span style={{ marginRight: 4 }}>{text}%</span>
</Trend>
),
},
];
const TopSearch = ({
loading,
visitData2,
searchData,
dropdownGroup,
}: {
loading: boolean;
visitData2: IVisitData2[];
dropdownGroup: React.ReactNode;
searchData: ISearchData[];
}) => (
<Card
loading={loading}
bordered={false}
title={
<FormattedMessage
id="analysis.analysis.online-top-search"
defaultMessage="Online Top Search"
/>
}
extra={dropdownGroup}
style={{ marginTop: 24 }}
>
<Row gutter={68}>
<Col sm={12} xs={24} style={{ marginBottom: 24 }}>
<NumberInfo
subTitle={
<span>
<FormattedMessage id="analysis.analysis.search-users" defaultMessage="search users" />
<Tooltip
title={
<FormattedMessage id="analysis.analysis.introduce" defaultMessage="introduce" />
}
>
<Icon style={{ marginLeft: 8 }} type="info-circle-o" />
</Tooltip>
</span>
}
gap={8}
total={numeral(12321).format('0,0')}
status="up"
subTotal={17.1}
/>
<MiniArea line height={45} data={visitData2} />
</Col>
<Col sm={12} xs={24} style={{ marginBottom: 24 }}>
<NumberInfo
subTitle={
<span>
<FormattedMessage
id="analysis.analysis.per-capita-search"
defaultMessage="Per Capita Search"
/>
<Tooltip
title={
<FormattedMessage id="analysis.analysis.introduce" defaultMessage="introduce" />
}
>
<Icon style={{ marginLeft: 8 }} type="info-circle-o" />
</Tooltip>
</span>
}
total={2.7}
status="down"
subTotal={26.2}
gap={8}
/>
<MiniArea line height={45} data={visitData2} />
</Col>
</Row>
<Table<any>
rowKey={record => record.index}
size="small"
columns={columns}
dataSource={searchData}
pagination={{
style: { marginBottom: 0 },
pageSize: 5,
}}
/>
</Card>
);
export default TopSearch;
@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;
}
}
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<ITrendProps> = ({
colorful = true,
reverseColor = false,
flag,
children,
className,
...rest
}) => {
const classString = classNames(
styles.trendItem,
{
[styles.trendItemGrey]: !colorful,
[styles.reverseColor]: reverseColor && colorful,
},
className,
);
return (
<div {...rest} className={classString} title={typeof children === 'string' ? children : ''}>
<span>{children}</span>
{flag && (
<span className={styles[flag]}>
<Icon type={`caret-${flag}`} />
</span>
)}
</div>
);
};
export default Trend;
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[];
}
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<any>;
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<AnalysisProps, AnalysisState> {
state: AnalysisState = {
salesType: 'all',
currentTabKey: '',
rangePickerValue: getTimeDistance('year'),
};
reqRef!: number;
timeoutId!: number;
componentDidMount() {
const { dispatch } = this.props;
this.reqRef = requestAnimationFrame(() => {
dispatch({
type: 'analysis/fetch',
});
});
setTimeout(() => {
this.setState({
loading: false,
});
}, 2000);
}
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 = (
<Menu>
<Menu.Item>操作一</Menu.Item>
<Menu.Item>操作二</Menu.Item>
</Menu>
);
const dropdownGroup = (
<span className={styles.iconGroup}>
<Dropdown overlay={menu} placement="bottomRight">
<Icon type="ellipsis" />
</Dropdown>
</span>
);
const activeKey = currentTabKey || (offlineData[0] && offlineData[0].name);
return (
<GridContent>
<React.Fragment>
<Suspense fallback={<PageLoading />}>
<IntroduceRow loading={loading} visitData={visitData} />
</Suspense>
<Suspense fallback={null}>
<SalesCard
rangePickerValue={rangePickerValue}
salesData={salesData}
isActive={this.isActive}
handleRangePickerChange={this.handleRangePickerChange}
loading={loading}
selectDate={this.selectDate}
/>
</Suspense>
<Row gutter={24}>
<Col xl={12} lg={24} md={24} sm={24} xs={24}>
<Suspense fallback={null}>
<TopSearch
loading={loading}
visitData2={visitData2}
searchData={searchData}
dropdownGroup={dropdownGroup}
/>
</Suspense>
</Col>
<Col xl={12} lg={24} md={24} sm={24} xs={24}>
<Suspense fallback={null}>
<ProportionSales
dropdownGroup={dropdownGroup}
salesType={salesType}
loading={loading}
salesPieData={salesPieData}
handleChangeSalesType={this.handleChangeSalesType}
/>
</Suspense>
</Col>
</Row>
<Suspense fallback={null}>
<OfflineData
activeKey={activeKey}
loading={loading}
offlineData={offlineData}
offlineChartData={offlineChartData}
handleTabChange={this.handleTabChange}
/>
</Suspense>
</React.Fragment>
</GridContent>
);
}
}
export default Analysis;
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',
};
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',
};
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': '周涨幅',
};
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': '周漲幅',
};
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: <T>(func: (state: IAnalysisData) => T) => T },
) => void;
export interface ModelType {
namespace: string;
state: IAnalysisData;
effects: {
fetch: Effect;
fetchSalesData: Effect;
};
reducers: {
save: Reducer<IAnalysisData>;
clear: Reducer<IAnalysisData>;
};
}
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;
import request from 'umi-request';
export async function fakeChartData() {
return request('/api/analysis/fake_chart_data');
}
@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;
}
}
}
.twoColLayout {
.salesCard {
height: calc(100% - 24px);
}
}
.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;
}
}
}
}
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 (
<span
ref={ref => {
this.main = ref;
}}
/>
);
}
}
.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;
}
}
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`),
];
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment