48 changed files with 13 additions and 3813 deletions
@ -1,197 +0,0 @@ |
|||||
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, |
|
||||
}; |
|
||||
@ -1,135 +0,0 @@ |
|||||
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); |
|
||||
@ -1,75 +0,0 @@ |
|||||
@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; |
|
||||
} |
|
||||
} |
|
||||
@ -1,98 +0,0 @@ |
|||||
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; |
|
||||
@ -1,17 +0,0 @@ |
|||||
@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; |
|
||||
} |
|
||||
} |
|
||||
@ -1,18 +0,0 @@ |
|||||
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; |
|
||||
@ -1,177 +0,0 @@ |
|||||
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); |
|
||||
@ -1,133 +0,0 @@ |
|||||
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); |
|
||||
@ -1,61 +0,0 @@ |
|||||
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); |
|
||||
@ -1,37 +0,0 @@ |
|||||
@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; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,43 +0,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; |
|
||||
@ -1,94 +0,0 @@ |
|||||
@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; |
|
||||
} |
|
||||
} |
|
||||
@ -1,306 +0,0 @@ |
|||||
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); |
|
||||
@ -1,6 +0,0 @@ |
|||||
.tagCloud { |
|
||||
overflow: hidden; |
|
||||
canvas { |
|
||||
transform-origin: 0 0; |
|
||||
} |
|
||||
} |
|
||||
@ -1,206 +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); |
|
||||
@ -1,3 +0,0 @@ |
|||||
.timelineChart { |
|
||||
background: #fff; |
|
||||
} |
|
||||
@ -1,133 +0,0 @@ |
|||||
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); |
|
||||
@ -1,28 +0,0 @@ |
|||||
@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; |
|
||||
} |
|
||||
} |
|
||||
@ -1,230 +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); |
|
||||
@ -1,75 +0,0 @@ |
|||||
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; |
|
||||
@ -1,3 +0,0 @@ |
|||||
import * as BizChart from 'bizcharts'; |
|
||||
|
|
||||
export = BizChart; |
|
||||
@ -1,3 +0,0 @@ |
|||||
import * as BizChart from 'bizcharts'; |
|
||||
|
|
||||
export default BizChart; |
|
||||
@ -1,19 +0,0 @@ |
|||||
.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; |
|
||||
} |
|
||||
} |
|
||||
@ -1,45 +0,0 @@ |
|||||
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, |
|
||||
}; |
|
||||
@ -1,162 +0,0 @@ |
|||||
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; |
|
||||
@ -1,68 +0,0 @@ |
|||||
@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; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,61 +0,0 @@ |
|||||
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; |
|
||||
@ -1,82 +0,0 @@ |
|||||
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; |
|
||||
@ -1,10 +0,0 @@ |
|||||
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> |
|
||||
); |
|
||||
@ -1,79 +0,0 @@ |
|||||
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; |
|
||||
@ -1,162 +0,0 @@ |
|||||
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; |
|
||||
@ -1,128 +0,0 @@ |
|||||
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; |
|
||||
@ -1,37 +0,0 @@ |
|||||
@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; |
|
||||
} |
|
||||
} |
|
||||
@ -1,42 +0,0 @@ |
|||||
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; |
|
||||
@ -1,67 +0,0 @@ |
|||||
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[]; |
|
||||
} |
|
||||
@ -1,215 +0,0 @@ |
|||||
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; |
|
||||
@ -1,34 +0,0 @@ |
|||||
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', |
|
||||
}; |
|
||||
@ -1,34 +0,0 @@ |
|||||
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', |
|
||||
}; |
|
||||
@ -1,34 +0,0 @@ |
|||||
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': '周涨幅', |
|
||||
}; |
|
||||
@ -1,34 +0,0 @@ |
|||||
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': '周漲幅', |
|
||||
}; |
|
||||
@ -1,84 +0,0 @@ |
|||||
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; |
|
||||
@ -1,5 +0,0 @@ |
|||||
import request from 'umi-request'; |
|
||||
|
|
||||
export async function fakeChartData() { |
|
||||
return request('/api/analysis/fake_chart_data'); |
|
||||
} |
|
||||
@ -1,185 +0,0 @@ |
|||||
@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; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,33 +0,0 @@ |
|||||
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; |
|
||||
}} |
|
||||
/> |
|
||||
); |
|
||||
} |
|
||||
} |
|
||||
@ -1,50 +0,0 @@ |
|||||
.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; |
|
||||
} |
|
||||
} |
|
||||
@ -1,53 +0,0 @@ |
|||||
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`), |
|
||||
]; |
|
||||
} |
|
||||
Loading…
Reference in new issue