You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@skywalking.apache.org by ha...@apache.org on 2017/12/18 08:04:11 UTC
[incubator-skywalking-ui] 02/02: Finished dashboard demo
This is an automated email from the ASF dual-hosted git repository.
hanahmily pushed a commit to branch feature/5.0.0
in repository https://gitbox.apache.org/repos/asf/incubator-skywalking-ui.git
commit 381f40bfb9499e32b0aaa0d3e64cdf71df78c148
Author: gaohongtao <ha...@gmail.com>
AuthorDate: Mon Dec 18 16:01:43 2017 +0800
Finished dashboard demo
---
src/main/frontend/public/alert.svg | 1 +
src/main/frontend/public/app.svg | 1 +
src/main/frontend/public/database.svg | 1 +
src/main/frontend/public/redis.svg | 1 +
src/main/frontend/public/service.svg | 1 +
.../frontend/src/components/Charts/Bar/index.d.ts | 14 ++
.../frontend/src/components/Charts/Bar/index.js | 151 ++++++++++++
.../src/components/Charts/ChartCard/index.d.ts | 11 +
.../src/components/Charts/ChartCard/index.js | 60 +++++
.../src/components/Charts/ChartCard/index.less | 74 ++++++
.../src/components/Charts/Field/index.d.ts | 7 +
.../frontend/src/components/Charts/Field/index.js | 12 +
.../src/components/Charts/Field/index.less | 16 ++
.../src/components/Charts/Gauge/index.d.ts | 10 +
.../frontend/src/components/Charts/Gauge/index.js | 202 ++++++++++++++++
.../src/components/Charts/MiniArea/index.d.ts | 29 +++
.../src/components/Charts/MiniArea/index.js | 125 ++++++++++
.../src/components/Charts/MiniBar/index.d.ts | 11 +
.../src/components/Charts/MiniBar/index.js | 87 +++++++
.../src/components/Charts/MiniProgress/index.d.ts | 12 +
.../src/components/Charts/MiniProgress/index.js | 30 +++
.../src/components/Charts/MiniProgress/index.less | 35 +++
.../frontend/src/components/Charts/Pie/index.d.ts | 20 ++
.../frontend/src/components/Charts/Pie/index.js | 260 +++++++++++++++++++++
.../frontend/src/components/Charts/Pie/index.less | 94 ++++++++
.../src/components/Charts/Radar/index.d.ts | 14 ++
.../frontend/src/components/Charts/Radar/index.js | 189 +++++++++++++++
.../src/components/Charts/Radar/index.less | 46 ++++
.../src/components/Charts/TagCloud/index.d.ts | 10 +
.../src/components/Charts/TagCloud/index.js | 170 ++++++++++++++
.../src/components/Charts/TagCloud/index.less | 6 +
.../src/components/Charts/TimelineChart/index.d.ts | 15 ++
.../src/components/Charts/TimelineChart/index.js | 125 ++++++++++
.../src/components/Charts/TimelineChart/index.less | 3 +
.../src/components/Charts/WaterWave/index.d.ts | 9 +
.../src/components/Charts/WaterWave/index.js | 200 ++++++++++++++++
.../src/components/Charts/WaterWave/index.less | 28 +++
.../frontend/src/components/Charts/demo/bar.md | 26 +++
.../src/components/Charts/demo/chart-card.md | 65 ++++++
.../frontend/src/components/Charts/demo/gauge.md | 18 ++
.../src/components/Charts/demo/mini-area.md | 28 +++
.../src/components/Charts/demo/mini-bar.md | 28 +++
.../src/components/Charts/demo/mini-pie.md | 16 ++
.../src/components/Charts/demo/mini-progress.md | 12 +
.../frontend/src/components/Charts/demo/mix.md | 83 +++++++
.../frontend/src/components/Charts/demo/pie.md | 47 ++++
.../frontend/src/components/Charts/demo/radar.md | 64 +++++
.../src/components/Charts/demo/tag-cloud.md | 25 ++
.../src/components/Charts/demo/timeline-chart.md | 27 +++
.../src/components/Charts/demo/waterwave.md | 20 ++
src/main/frontend/src/components/Charts/equal.js | 17 ++
src/main/frontend/src/components/Charts/index.d.ts | 17 ++
src/main/frontend/src/components/Charts/index.js | 31 +++
src/main/frontend/src/components/Charts/index.less | 19 ++
src/main/frontend/src/components/Charts/index.md | 132 +++++++++++
.../frontend/src/components/Trend/demo/basic.md | 17 ++
src/main/frontend/src/components/Trend/index.d.ts | 8 +
src/main/frontend/src/components/Trend/index.js | 22 ++
src/main/frontend/src/components/Trend/index.less | 30 +++
src/main/frontend/src/components/Trend/index.md | 21 ++
src/main/frontend/src/layouts/BasicLayout.js | 3 -
.../frontend/src/routes/Dashboard/Dashboard.js | 235 ++++++++++++++++++-
.../frontend/src/routes/Dashboard/Dashboard.less | 5 +
63 files changed, 3092 insertions(+), 4 deletions(-)
diff --git a/src/main/frontend/public/alert.svg b/src/main/frontend/public/alert.svg
new file mode 100644
index 0000000..fc9270b
--- /dev/null
+++ b/src/main/frontend/public/alert.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M611.702 44.963l263.203 149.544c60.979 35.06 98.35 99.414 98.154 168.892v299.056c0.037 69.658-37.691 134.044-98.987 168.902l-262.37 149.36c-61.333 34.828-136.864 34.828-198.176 0l-263.61-149.55c-61.285-34.859-99.008-99.246-98 [...]
\ No newline at end of file
diff --git a/src/main/frontend/public/app.svg b/src/main/frontend/public/app.svg
new file mode 100644
index 0000000..4116c21
--- /dev/null
+++ b/src/main/frontend/public/app.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M357.885 49.289h285.276c173.87 0 316.906 143.086 316.906 317.854v285.276c0 173.831-143.086 316.925-316.906 316.925h-285.276c-174.749 0-317.854-143.086-317.854-316.925v-285.276c0-174.749 143.086-317.854 317.854-317.854z" fill= [...]
\ No newline at end of file
diff --git a/src/main/frontend/public/database.svg b/src/main/frontend/public/database.svg
new file mode 100644
index 0000000..7fc1526
--- /dev/null
+++ b/src/main/frontend/public/database.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M896.483256 191.467753v64.081481c0 70.584592-172.214247 128.160915-384.484791 128.160915-212.267475 0-384.483768-57.577347-384.483768-128.160915v-64.081481c0-70.583568 172.216293-128.160915 384.483768-128.160915 212.270545 0 [...]
\ No newline at end of file
diff --git a/src/main/frontend/public/redis.svg b/src/main/frontend/public/redis.svg
new file mode 100644
index 0000000..e634317
--- /dev/null
+++ b/src/main/frontend/public/redis.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M583.66976 285.16864l-229.83168 23.296a2.19136 2.19136 0 0 0-1.84832 1.78176 2.14016 2.14016 0 0 0 1.28 2.27328l179.12832 73.07776c0.28672 0.07168 0.49664 0.14336 0.77824 0.14336 0.77312 0 1.4848-0.42496 1.83808-1.13664l50.65 [...]
\ No newline at end of file
diff --git a/src/main/frontend/public/service.svg b/src/main/frontend/public/service.svg
new file mode 100644
index 0000000..8d2daf7
--- /dev/null
+++ b/src/main/frontend/public/service.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M151.3048309167421 870.0138201689984c199.20612239587413 199.20612239587422 522.1842157706417 199.20612239587433 721.3924594868594-0.002121320343348998 199.2068295026554-199.20682950265532 199.20682950265544-522.1849228774227 [...]
\ No newline at end of file
diff --git a/src/main/frontend/src/components/Charts/Bar/index.d.ts b/src/main/frontend/src/components/Charts/Bar/index.d.ts
new file mode 100644
index 0000000..fd2d05d
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/Bar/index.d.ts
@@ -0,0 +1,14 @@
+import * as React from "react";
+export interface BarProps {
+ title: React.ReactNode;
+ color?: string;
+ margin?: [number, number, number, number];
+ height: number;
+ data: Array<{
+ x: string;
+ y: number;
+ }>;
+ autoLabel?: boolean;
+}
+
+export default class Bar extends React.Component<BarProps, any> {}
diff --git a/src/main/frontend/src/components/Charts/Bar/index.js b/src/main/frontend/src/components/Charts/Bar/index.js
new file mode 100644
index 0000000..ef834d1
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/Bar/index.js
@@ -0,0 +1,151 @@
+import React, { PureComponent } from 'react';
+import G2 from 'g2';
+import Debounce from 'lodash-decorators/debounce';
+import Bind from 'lodash-decorators/bind';
+import equal from '../equal';
+import styles from '../index.less';
+
+class Bar extends PureComponent {
+ state = {
+ autoHideXLabels: false,
+ }
+
+ componentDidMount() {
+ this.renderChart(this.props.data);
+
+ window.addEventListener('resize', this.resize);
+ }
+
+ componentWillReceiveProps(nextProps) {
+ if (!equal(this.props, nextProps)) {
+ this.renderChart(nextProps.data);
+ }
+ }
+
+ componentWillUnmount() {
+ window.removeEventListener('resize', this.resize);
+ if (this.chart) {
+ this.chart.destroy();
+ }
+ this.resize.cancel();
+ }
+
+ @Bind()
+ @Debounce(200)
+ resize() {
+ if (!this.node) {
+ return;
+ }
+ const canvasWidth = this.node.parentNode.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,
+ });
+ this.renderChart(data);
+ }
+ } else if (autoHideXLabels) {
+ this.setState({
+ autoHideXLabels: false,
+ });
+ this.renderChart(data);
+ }
+ }
+
+ handleRef = (n) => {
+ this.node = n;
+ }
+
+ renderChart(data) {
+ const { autoHideXLabels } = this.state;
+ const {
+ height = 0,
+ fit = true,
+ color = 'rgba(24, 144, 255, 0.85)',
+ margin = [32, 0, (autoHideXLabels ? 8 : 32), 40],
+ } = this.props;
+
+
+ if (!data || (data && data.length < 1)) {
+ return;
+ }
+
+ // clean
+ this.node.innerHTML = '';
+
+ const { Frame } = G2;
+ const frame = new Frame(data);
+
+ const chart = new G2.Chart({
+ container: this.node,
+ forceFit: fit,
+ height: height - 22,
+ legend: null,
+ plotCfg: {
+ margin,
+ },
+ });
+
+ if (autoHideXLabels) {
+ chart.axis('x', {
+ title: false,
+ tickLine: false,
+ labels: false,
+ });
+ } else {
+ chart.axis('x', {
+ title: false,
+ });
+ }
+ chart.axis('y', {
+ title: false,
+ line: false,
+ tickLine: false,
+ });
+
+ chart.source(frame, {
+ x: {
+ type: 'cat',
+ },
+ y: {
+ min: 0,
+ },
+ });
+
+ chart.tooltip({
+ title: null,
+ crosshairs: false,
+ map: {
+ name: 'x',
+ },
+ });
+ chart.interval().position('x*y').color(color).style({
+ fillOpacity: 1,
+ });
+ chart.render();
+
+ this.chart = chart;
+ }
+
+ render() {
+ const { height, title } = this.props;
+
+ return (
+ <div className={styles.chart} style={{ height }}>
+ <div>
+ { title && <h4>{title}</h4>}
+ <div ref={this.handleRef} />
+ </div>
+ </div>
+ );
+ }
+}
+
+export default Bar;
diff --git a/src/main/frontend/src/components/Charts/ChartCard/index.d.ts b/src/main/frontend/src/components/Charts/ChartCard/index.d.ts
new file mode 100644
index 0000000..21d2be3
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/ChartCard/index.d.ts
@@ -0,0 +1,11 @@
+import * as React from "react";
+export interface ChartCardProps {
+ title: React.ReactNode;
+ action?: React.ReactNode;
+ total?: React.ReactNode | number;
+ footer?: React.ReactNode;
+ contentHeight?: number;
+ avatar?: React.ReactNode;
+}
+
+export default class ChartCard extends React.Component<ChartCardProps, any> {}
diff --git a/src/main/frontend/src/components/Charts/ChartCard/index.js b/src/main/frontend/src/components/Charts/ChartCard/index.js
new file mode 100644
index 0000000..a472682
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/ChartCard/index.js
@@ -0,0 +1,60 @@
+import React from 'react';
+import { Card, Spin } from 'antd';
+import classNames from 'classnames';
+
+import styles from './index.less';
+
+const ChartCard = ({
+ loading = false, contentHeight, title, avatar, action, total, footer, children, ...rest
+}) => {
+ const content = (
+ <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>
+ {
+ // eslint-disable-next-line
+ (total !== undefined) && (<div className={styles.total} dangerouslySetInnerHTML={{ __html: 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>
+ );
+
+ return (
+ <Card
+ bodyStyle={{ padding: '20px 24px 8px 24px' }}
+ {...rest}
+ >
+ {<Spin spinning={loading}>{content}</Spin>}
+ </Card>
+ );
+};
+
+export default ChartCard;
diff --git a/src/main/frontend/src/components/Charts/ChartCard/index.less b/src/main/frontend/src/components/Charts/ChartCard/index.less
new file mode 100644
index 0000000..bdc573b
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/ChartCard/index.less
@@ -0,0 +1,74 @@
+@import "~antd/lib/style/themes/default.less";
+
+.chartCard {
+ position: relative;
+ .chartTop {
+ position: relative;
+ overflow: hidden;
+ width: 100%;
+ }
+ .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 {
+ color: @text-color-secondary;
+ font-size: @font-size-base;
+ line-height: 22px;
+ height: 22px;
+ }
+ .action {
+ cursor: pointer;
+ position: absolute;
+ top: 0;
+ right: 0;
+ }
+ .total {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ word-break: break-all;
+ white-space: nowrap;
+ color: @heading-color;
+ margin-top: 4px;
+ margin-bottom: 0;
+ font-size: 30px;
+ line-height: 38px;
+ height: 38px;
+ }
+ .content {
+ margin-bottom: 12px;
+ position: relative;
+ width: 100%;
+ }
+ .contentFixed {
+ position: absolute;
+ left: 0;
+ bottom: 0;
+ width: 100%;
+ }
+ .footer {
+ border-top: 1px solid @border-color-split;
+ padding-top: 9px;
+ margin-top: 8px;
+ & > * {
+ position: relative;
+ }
+ }
+ .footerMargin {
+ margin-top: 20px;
+ }
+}
diff --git a/src/main/frontend/src/components/Charts/Field/index.d.ts b/src/main/frontend/src/components/Charts/Field/index.d.ts
new file mode 100644
index 0000000..7fa1328
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/Field/index.d.ts
@@ -0,0 +1,7 @@
+import * as React from "react";
+export interface FieldProps {
+ label: React.ReactNode;
+ value: React.ReactNode;
+}
+
+export default class Field extends React.Component<FieldProps, any> {}
diff --git a/src/main/frontend/src/components/Charts/Field/index.js b/src/main/frontend/src/components/Charts/Field/index.js
new file mode 100644
index 0000000..0f9ace2
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/Field/index.js
@@ -0,0 +1,12 @@
+import React from 'react';
+
+import styles from './index.less';
+
+const Field = ({ label, value, ...rest }) => (
+ <div className={styles.field} {...rest}>
+ <span>{label}</span>
+ <span>{value}</span>
+ </div>
+);
+
+export default Field;
diff --git a/src/main/frontend/src/components/Charts/Field/index.less b/src/main/frontend/src/components/Charts/Field/index.less
new file mode 100644
index 0000000..2848f9d
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/Field/index.less
@@ -0,0 +1,16 @@
+@import "~antd/lib/style/themes/default.less";
+
+.field {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ margin: 0;
+ span {
+ font-size: @font-size-base;
+ line-height: 22px;
+ }
+ span:last-child {
+ margin-left: 8px;
+ color: @heading-color;
+ }
+}
diff --git a/src/main/frontend/src/components/Charts/Gauge/index.d.ts b/src/main/frontend/src/components/Charts/Gauge/index.d.ts
new file mode 100644
index 0000000..7f196ae
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/Gauge/index.d.ts
@@ -0,0 +1,10 @@
+import * as React from "react";
+export interface GaugeProps {
+ title: React.ReactNode;
+ color?: string;
+ height: number;
+ bgColor?: number;
+ percent: number;
+}
+
+export default class Gauge extends React.Component<GaugeProps, any> {}
diff --git a/src/main/frontend/src/components/Charts/Gauge/index.js b/src/main/frontend/src/components/Charts/Gauge/index.js
new file mode 100644
index 0000000..cba4202
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/Gauge/index.js
@@ -0,0 +1,202 @@
+import React, { PureComponent } from 'react';
+import G2 from 'g2';
+import equal from '../equal';
+
+const { Shape } = G2;
+
+const primaryColor = '#2F9CFF';
+const backgroundColor = '#F0F2F5';
+
+/* eslint no-underscore-dangle: 0 */
+class Gauge extends PureComponent {
+ componentDidMount() {
+ setTimeout(() => {
+ this.renderChart();
+ }, 10);
+ }
+
+ componentWillReceiveProps(nextProps) {
+ if (!equal(this.props, nextProps)) {
+ setTimeout(() => {
+ this.renderChart(nextProps);
+ }, 10);
+ }
+ }
+
+ componentWillUnmount() {
+ if (this.chart) {
+ this.chart.destroy();
+ }
+ }
+
+ handleRef = (n) => {
+ this.node = n;
+ }
+
+ initChart(nextProps) {
+ const { title, color = primaryColor } = nextProps || this.props;
+
+ Shape.registShape('point', 'dashBoard', {
+ drawShape(cfg, group) {
+ const originPoint = cfg.points[0];
+ const point = this.parsePoint({ x: originPoint.x, y: 0.4 });
+
+ const center = this.parsePoint({
+ x: 0,
+ y: 0,
+ });
+
+ const shape = group.addShape('polygon', {
+ attrs: {
+ points: [
+ [center.x, center.y],
+ [point.x + 8, point.y],
+ [point.x + 8, point.y - 2],
+ [center.x, center.y - 2],
+ ],
+ radius: 2,
+ lineWidth: 2,
+ arrow: false,
+ fill: color,
+ },
+ });
+
+ group.addShape('Marker', {
+ attrs: {
+ symbol: 'circle',
+ lineWidth: 2,
+ fill: color,
+ radius: 8,
+ x: center.x,
+ y: center.y,
+ },
+ });
+ group.addShape('Marker', {
+ attrs: {
+ symbol: 'circle',
+ lineWidth: 2,
+ fill: '#fff',
+ radius: 5,
+ x: center.x,
+ y: center.y,
+ },
+ });
+
+ const { origin } = cfg;
+ group.addShape('text', {
+ attrs: {
+ x: center.x,
+ y: center.y + 80,
+ text: `${origin._origin.value}%`,
+ textAlign: 'center',
+ fontSize: 24,
+ fill: 'rgba(0, 0, 0, 0.85)',
+ },
+ });
+ group.addShape('text', {
+ attrs: {
+ x: center.x,
+ y: center.y + 45,
+ text: title,
+ textAlign: 'center',
+ fontSize: 14,
+ fill: 'rgba(0, 0, 0, 0.43)',
+ },
+ });
+
+ return shape;
+ },
+ });
+ }
+
+ renderChart(nextProps) {
+ const {
+ height, color = primaryColor, bgColor = backgroundColor, title, percent, format,
+ } = nextProps || this.props;
+ const data = [{ name: title, value: percent }];
+
+ if (this.chart) {
+ this.chart.clear();
+ }
+ if (this.node) {
+ this.node.innerHTML = '';
+ }
+
+ this.initChart(nextProps);
+
+ const chart = new G2.Chart({
+ container: this.node,
+ forceFit: true,
+ height,
+ animate: false,
+ plotCfg: {
+ margin: [10, 10, 30, 10],
+ },
+ });
+
+ chart.source(data);
+
+ chart.tooltip(false);
+
+ chart.coord('gauge', {
+ startAngle: -1.2 * Math.PI,
+ endAngle: 0.20 * Math.PI,
+ });
+ chart.col('value', {
+ type: 'linear',
+ nice: true,
+ min: 0,
+ max: 100,
+ tickCount: 6,
+ });
+ chart.axis('value', {
+ subTick: false,
+ tickLine: {
+ stroke: color,
+ lineWidth: 2,
+ value: -14,
+ },
+ labelOffset: -12,
+ formatter: format,
+ });
+ chart.point().position('value').shape('dashBoard');
+ draw(data);
+
+ /* eslint no-shadow: 0 */
+ function draw(data) {
+ const val = data[0].value;
+ const lineWidth = 12;
+ chart.guide().clear();
+
+ chart.guide().arc(() => {
+ return [0, 0.95];
+ }, () => {
+ return [val, 0.95];
+ }, {
+ stroke: color,
+ lineWidth,
+ });
+
+ chart.guide().arc(() => {
+ return [val, 0.95];
+ }, (arg) => {
+ return [arg.max, 0.95];
+ }, {
+ stroke: bgColor,
+ lineWidth,
+ });
+
+ chart.changeData(data);
+ }
+
+ this.chart = chart;
+ }
+
+ render() {
+ return (
+ <div ref={this.handleRef} />
+ );
+ }
+}
+
+export default Gauge;
diff --git a/src/main/frontend/src/components/Charts/MiniArea/index.d.ts b/src/main/frontend/src/components/Charts/MiniArea/index.d.ts
new file mode 100644
index 0000000..d2f67e4
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/MiniArea/index.d.ts
@@ -0,0 +1,29 @@
+import * as React from "react";
+
+// g2已经更新到3.0
+// 不带的写了
+
+export interface Axis {
+ title: any;
+ line: any;
+ gridAlign: any;
+ labels: any;
+ tickLine: any;
+ grid: any;
+}
+
+export interface MiniAreaProps {
+ color?: string;
+ height: number;
+ borderColor?: string;
+ line?: boolean;
+ animate?: boolean;
+ xAxis?: Axis;
+ yAxis?: Axis;
+ data: Array<{
+ x: number;
+ y: number;
+ }>;
+}
+
+export default class MiniArea extends React.Component<MiniAreaProps, any> {}
diff --git a/src/main/frontend/src/components/Charts/MiniArea/index.js b/src/main/frontend/src/components/Charts/MiniArea/index.js
new file mode 100644
index 0000000..65f0969
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/MiniArea/index.js
@@ -0,0 +1,125 @@
+import React, { PureComponent } from 'react';
+import G2 from 'g2';
+import equal from '../equal';
+import styles from '../index.less';
+
+class MiniArea extends PureComponent {
+ static defaultProps = {
+ borderColor: '#1890FF',
+ color: 'rgba(24, 144, 255, 0.2)',
+ };
+
+ componentDidMount() {
+ this.renderChart(this.props.data);
+ }
+
+ componentWillReceiveProps(nextProps) {
+ if (!equal(this.props, nextProps)) {
+ this.renderChart(nextProps.data);
+ }
+ }
+
+ componentWillUnmount() {
+ if (this.chart) {
+ this.chart.destroy();
+ }
+ }
+
+ handleRef = (n) => {
+ this.node = n;
+ }
+
+ renderChart(data) {
+ const {
+ height = 0, fit = true, color, borderWidth = 2, line, xAxis, yAxis, animate = true,
+ } = this.props;
+ const borderColor = this.props.borderColor || color;
+
+ if (!data || (data && data.length < 1)) {
+ return;
+ }
+
+ // clean
+ this.node.innerHTML = '';
+
+ const chart = new G2.Chart({
+ container: this.node,
+ forceFit: fit,
+ height: height + 54,
+ animate,
+ plotCfg: {
+ margin: [36, 5, 30, 5],
+ },
+ legend: null,
+ });
+
+ if (!xAxis && !yAxis) {
+ chart.axis(false);
+ }
+
+ if (xAxis) {
+ chart.axis('x', xAxis);
+ } else {
+ chart.axis('x', false);
+ }
+
+ if (yAxis) {
+ chart.axis('y', yAxis);
+ } else {
+ chart.axis('y', false);
+ }
+
+ const dataConfig = {
+ x: {
+ type: 'cat',
+ range: [0, 1],
+ ...xAxis,
+ },
+ y: {
+ min: 0,
+ ...yAxis,
+ },
+ };
+
+ chart.tooltip({
+ title: null,
+ crosshairs: false,
+ map: {
+ title: null,
+ name: 'x',
+ value: 'y',
+ },
+ });
+
+ const view = chart.createView();
+ view.source(data, dataConfig);
+
+ view.area().position('x*y').color(color).shape('smooth')
+ .style({ fillOpacity: 1 });
+
+ if (line) {
+ const view2 = chart.createView();
+ view2.source(data, dataConfig);
+ view2.line().position('x*y').color(borderColor).size(borderWidth)
+ .shape('smooth');
+ view2.tooltip(false);
+ }
+ chart.render();
+
+ this.chart = chart;
+ }
+
+ render() {
+ const { height } = this.props;
+
+ return (
+ <div className={styles.miniChart} style={{ height }}>
+ <div className={styles.chartContent}>
+ <div ref={this.handleRef} />
+ </div>
+ </div>
+ );
+ }
+}
+
+export default MiniArea;
diff --git a/src/main/frontend/src/components/Charts/MiniBar/index.d.ts b/src/main/frontend/src/components/Charts/MiniBar/index.d.ts
new file mode 100644
index 0000000..09bd761
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/MiniBar/index.d.ts
@@ -0,0 +1,11 @@
+import * as React from "react";
+export interface MiniBarProps {
+ color?: string;
+ height: number;
+ data: Array<{
+ x: number;
+ y: number;
+ }>;
+}
+
+export default class MiniBar extends React.Component<MiniBarProps, any> {}
diff --git a/src/main/frontend/src/components/Charts/MiniBar/index.js b/src/main/frontend/src/components/Charts/MiniBar/index.js
new file mode 100644
index 0000000..991571b
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/MiniBar/index.js
@@ -0,0 +1,87 @@
+import React, { PureComponent } from 'react';
+import G2 from 'g2';
+import equal from '../equal';
+import styles from '../index.less';
+
+class MiniBar extends PureComponent {
+ componentDidMount() {
+ this.renderChart(this.props.data);
+ }
+
+ componentWillReceiveProps(nextProps) {
+ if (!equal(this.props, nextProps)) {
+ this.renderChart(nextProps.data);
+ }
+ }
+
+ componentWillUnmount() {
+ if (this.chart) {
+ this.chart.destroy();
+ }
+ }
+
+ handleRef = (n) => {
+ this.node = n;
+ }
+
+ renderChart(data) {
+ const { height = 0, fit = true, color = '#1890FF' } = this.props;
+
+ if (!data || (data && data.length < 1)) {
+ return;
+ }
+
+ // clean
+ this.node.innerHTML = '';
+
+ const { Frame } = G2;
+ const frame = new Frame(data);
+
+ const chart = new G2.Chart({
+ container: this.node,
+ forceFit: fit,
+ height: height + 54,
+ plotCfg: {
+ margin: [36, 5, 30, 5],
+ },
+ legend: null,
+ });
+
+ chart.axis(false);
+
+ chart.source(frame, {
+ x: {
+ type: 'cat',
+ },
+ y: {
+ min: 0,
+ },
+ });
+
+ chart.tooltip({
+ title: null,
+ crosshairs: false,
+ map: {
+ name: 'x',
+ },
+ });
+ chart.interval().position('x*y').color(color);
+ chart.render();
+
+ this.chart = chart;
+ }
+
+ render() {
+ const { height } = this.props;
+
+ return (
+ <div className={styles.miniChart} style={{ height }}>
+ <div className={styles.chartContent}>
+ <div ref={this.handleRef} />
+ </div>
+ </div>
+ );
+ }
+}
+
+export default MiniBar;
diff --git a/src/main/frontend/src/components/Charts/MiniProgress/index.d.ts b/src/main/frontend/src/components/Charts/MiniProgress/index.d.ts
new file mode 100644
index 0000000..a80b935
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/MiniProgress/index.d.ts
@@ -0,0 +1,12 @@
+import * as React from "react";
+export interface MiniProgressProps {
+ target: number;
+ color?: string;
+ strokeWidth?: number;
+ percent?: number;
+}
+
+export default class MiniProgress extends React.Component<
+ MiniProgressProps,
+ any
+> {}
diff --git a/src/main/frontend/src/components/Charts/MiniProgress/index.js b/src/main/frontend/src/components/Charts/MiniProgress/index.js
new file mode 100644
index 0000000..08fe9b5
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/MiniProgress/index.js
@@ -0,0 +1,30 @@
+import React from 'react';
+import { Tooltip } from 'antd';
+
+import styles from './index.less';
+
+const MiniProgress = ({ target, color = 'rgb(19, 194, 194)', strokeWidth, percent }) => (
+ <div className={styles.miniProgress}>
+ <Tooltip title={`目标值: ${target}%`}>
+ <div
+ className={styles.target}
+ style={{ left: (target ? `${target}%` : null) }}
+ >
+ <span style={{ backgroundColor: (color || null) }} />
+ <span style={{ backgroundColor: (color || null) }} />
+ </div>
+ </Tooltip>
+ <div className={styles.progressWrap}>
+ <div
+ className={styles.progress}
+ style={{
+ backgroundColor: (color || null),
+ width: (percent ? `${percent}%` : null),
+ height: (strokeWidth || null),
+ }}
+ />
+ </div>
+ </div>
+);
+
+export default MiniProgress;
diff --git a/src/main/frontend/src/components/Charts/MiniProgress/index.less b/src/main/frontend/src/components/Charts/MiniProgress/index.less
new file mode 100644
index 0000000..06823be
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/MiniProgress/index.less
@@ -0,0 +1,35 @@
+@import "~antd/lib/style/themes/default.less";
+
+.miniProgress {
+ padding: 5px 0;
+ position: relative;
+ width: 100%;
+ .progressWrap {
+ background-color: @background-color-base;
+ position: relative;
+ }
+ .progress {
+ transition: all .4s cubic-bezier(.08, .82, .17, 1) 0s;
+ border-radius: 1px 0 0 1px;
+ background-color: @primary-color;
+ width: 0;
+ height: 100%;
+ }
+ .target {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ span {
+ border-radius: 100px;
+ position: absolute;
+ top: 0;
+ left: 0;
+ height: 4px;
+ width: 2px;
+ }
+ span:last-child {
+ top: auto;
+ bottom: 0;
+ }
+ }
+}
diff --git a/src/main/frontend/src/components/Charts/Pie/index.d.ts b/src/main/frontend/src/components/Charts/Pie/index.d.ts
new file mode 100644
index 0000000..44a465d
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/Pie/index.d.ts
@@ -0,0 +1,20 @@
+import * as React from "react";
+export interface PieProps {
+ animate?: boolean;
+ color?: string;
+ height: number;
+ hasLegend?: boolean;
+ margin?: [number, number, number, number];
+ percent?: number;
+ data?: Array<{
+ x: string;
+ y: number;
+ }>;
+ total?: string;
+ title?: React.ReactNode;
+ tooltip?: boolean;
+ valueFormat?: (value: string) => string;
+ subTitle?: React.ReactNode;
+}
+
+export default class Pie extends React.Component<PieProps, any> {}
diff --git a/src/main/frontend/src/components/Charts/Pie/index.js b/src/main/frontend/src/components/Charts/Pie/index.js
new file mode 100644
index 0000000..74cd229
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/Pie/index.js
@@ -0,0 +1,260 @@
+import React, { Component } from 'react';
+import G2 from 'g2';
+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 equal from '../equal';
+import styles from './index.less';
+
+/* eslint react/no-danger:0 */
+class Pie extends Component {
+ state = {
+ legendData: [],
+ legendBlock: true,
+ };
+
+ componentDidMount() {
+ this.renderChart();
+ this.resize();
+ window.addEventListener('resize', this.resize);
+ }
+
+ componentWillReceiveProps(nextProps) {
+ if (!equal(this.props, nextProps)) {
+ this.renderChart(nextProps.data);
+ }
+ }
+
+ componentWillUnmount() {
+ window.removeEventListener('resize', this.resize);
+ if (this.chart) {
+ this.chart.destroy();
+ }
+ this.resize.cancel();
+ }
+
+ @Bind()
+ @Debounce(300)
+ resize() {
+ const { hasLegend } = this.props;
+ if (!hasLegend || !this.root) {
+ window.removeEventListener('resize', this.resize);
+ return;
+ }
+ if (this.root.parentNode.clientWidth <= 380) {
+ if (!this.state.legendBlock) {
+ this.setState({
+ legendBlock: true,
+ }, () => {
+ this.renderChart();
+ });
+ }
+ } else if (this.state.legendBlock) {
+ this.setState({
+ legendBlock: false,
+ }, () => {
+ this.renderChart();
+ });
+ }
+ }
+
+ handleRef = (n) => {
+ this.node = n;
+ }
+
+ handleRoot = (n) => {
+ this.root = n;
+ }
+
+ handleLegendClick = (item, i) => {
+ const newItem = item;
+ newItem.checked = !newItem.checked;
+
+ const { legendData } = this.state;
+ legendData[i] = newItem;
+
+ if (this.chart) {
+ const filterItem = legendData.filter(l => l.checked).map(l => l.x);
+ this.chart.filter('x', filterItem);
+ this.chart.repaint();
+ }
+
+ this.setState({
+ legendData,
+ });
+ }
+
+ renderChart(d) {
+ let data = d || this.props.data;
+
+ const {
+ height = 0,
+ hasLegend,
+ fit = true,
+ margin = [12, 0, 12, 0], percent, color,
+ inner = 0.75,
+ animate = true,
+ colors,
+ lineWidth = 0,
+ } = this.props;
+
+ const defaultColors = colors;
+
+ let selected = this.props.selected || true;
+ let tooltip = this.props.tooltips || true;
+
+ let formatColor;
+ if (percent) {
+ selected = false;
+ tooltip = false;
+ formatColor = (value) => {
+ if (value === '占比') {
+ return color || 'rgba(24, 144, 255, 0.85)';
+ } else {
+ return '#F0F2F5';
+ }
+ };
+
+ /* eslint no-param-reassign: */
+ data = [
+ {
+ x: '占比',
+ y: parseFloat(percent),
+ },
+ {
+ x: '反比',
+ y: 100 - parseFloat(percent),
+ },
+ ];
+ }
+
+ if (!data || (data && data.length < 1)) {
+ return;
+ }
+
+ // clean
+ this.node.innerHTML = '';
+
+ const { Stat } = G2;
+
+ const chart = new G2.Chart({
+ container: this.node,
+ forceFit: fit,
+ height,
+ plotCfg: {
+ margin,
+ },
+ animate,
+ });
+
+ if (!tooltip) {
+ chart.tooltip(false);
+ } else {
+ chart.tooltip({
+ title: null,
+ });
+ }
+
+ chart.axis(false);
+ chart.legend(false);
+
+ chart.source(data, {
+ x: {
+ type: 'cat',
+ range: [0, 1],
+ },
+ y: {
+ min: 0,
+ },
+ });
+
+ chart.coord('theta', {
+ inner,
+ });
+
+ chart
+ .intervalStack()
+ .position(Stat.summary.percent('y'))
+ .style({ lineWidth, stroke: '#fff' })
+ .color('x', percent ? formatColor : defaultColors)
+ .selected(selected);
+
+ chart.render();
+
+ this.chart = chart;
+
+ let legendData = [];
+ if (hasLegend) {
+ const geom = chart.getGeoms()[0]; // 获取所有的图形
+ const items = geom.getData(); // 获取图形对应的数据
+ legendData = items.map((item) => {
+ /* eslint no-underscore-dangle:0 */
+ const origin = item._origin;
+ origin.color = item.color;
+ origin.checked = true;
+ return origin;
+ });
+ }
+
+ this.setState({
+ legendData,
+ });
+ }
+
+ render() {
+ const { valueFormat, subTitle, total, hasLegend, className, style } = this.props;
+ const { legendData, legendBlock } = this.state;
+ const pieClassName = classNames(styles.pie, className, {
+ [styles.hasLegend]: !!hasLegend,
+ [styles.legendBlock]: legendBlock,
+ });
+
+ return (
+ <div ref={this.handleRoot} className={pieClassName} style={style}>
+ <ReactFitText maxFontSize={25}>
+ <div className={styles.chart}>
+ <div ref={this.handleRef} style={{ fontSize: 0 }} />
+ {
+ (subTitle || total) && (
+ <div className={styles.total}>
+ {subTitle && <h4 className="pie-sub-title">{subTitle}</h4>}
+ {
+ // eslint-disable-next-line
+ total && <div className="pie-stat" dangerouslySetInnerHTML={{ __html: total }} />
+ }
+ </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}>{`${(item['..percent'] * 100).toFixed(2)}%`}</span>
+ <span
+ className={styles.value}
+ dangerouslySetInnerHTML={{
+ __html: valueFormat ? valueFormat(item.y) : item.y,
+ }}
+ />
+ </li>
+ ))
+ }
+ </ul>
+ )
+ }
+ </div>
+ );
+ }
+}
+
+export default Pie;
diff --git a/src/main/frontend/src/components/Charts/Pie/index.less b/src/main/frontend/src/components/Charts/Pie/index.less
new file mode 100644
index 0000000..9478739
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/Pie/index.less
@@ -0,0 +1,94 @@
+@import "~antd/lib/style/themes/default.less";
+
+.pie {
+ position: relative;
+ .chart {
+ position: relative;
+ }
+ &.hasLegend .chart {
+ width: ~"calc(100% - 240px)";
+ }
+ .legend {
+ position: absolute;
+ right: 0;
+ min-width: 200px;
+ top: 50%;
+ transform: translateY(-50%);
+ margin: 0 20px;
+ list-style: none;
+ padding: 0;
+ li {
+ cursor: pointer;
+ margin-bottom: 16px;
+ height: 22px;
+ line-height: 22px;
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+ }
+ .dot {
+ border-radius: 8px;
+ display: inline-block;
+ margin-right: 8px;
+ position: relative;
+ top: -1px;
+ height: 8px;
+ width: 8px;
+ }
+ .line {
+ background-color: @border-color-split;
+ display: inline-block;
+ margin-right: 8px;
+ width: 1px;
+ height: 16px;
+ }
+ .legendTitle {
+ color: @text-color;
+ }
+ .percent {
+ color: @text-color-secondary;
+ }
+ .value {
+ position: absolute;
+ right: 0;
+ }
+ .title {
+ margin-bottom: 8px;
+ }
+ .total {
+ position: absolute;
+ left: 50%;
+ top: 50%;
+ text-align: center;
+ height: 62px;
+ transform: translate(-50%, -50%);
+ & > h4 {
+ color: @text-color-secondary;
+ font-size: 14px;
+ line-height: 22px;
+ height: 22px;
+ margin-bottom: 8px;
+ font-weight: normal;
+ }
+ & > p {
+ color: @heading-color;
+ display: block;
+ font-size: 1.2em;
+ height: 32px;
+ line-height: 32px;
+ white-space: nowrap;
+ }
+ }
+}
+
+.legendBlock {
+ &.hasLegend .chart {
+ width: 100%;
+ margin: 0 0 32px 0;
+ }
+ .legend {
+ position: relative;
+ transform: none;
+ }
+}
diff --git a/src/main/frontend/src/components/Charts/Radar/index.d.ts b/src/main/frontend/src/components/Charts/Radar/index.d.ts
new file mode 100644
index 0000000..fa85978
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/Radar/index.d.ts
@@ -0,0 +1,14 @@
+import * as React from "react";
+export interface RadarProps {
+ title?: React.ReactNode;
+ height: number;
+ margin?: [number, number, number, number];
+ hasLegend?: boolean;
+ data: Array<{
+ name: string;
+ label: string;
+ value: string;
+ }>;
+}
+
+export default class Radar extends React.Component<RadarProps, any> {}
diff --git a/src/main/frontend/src/components/Charts/Radar/index.js b/src/main/frontend/src/components/Charts/Radar/index.js
new file mode 100644
index 0000000..8f338bc
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/Radar/index.js
@@ -0,0 +1,189 @@
+import React, { PureComponent } from 'react';
+import G2 from 'g2';
+import { Row, Col } from 'antd';
+import equal from '../equal';
+import styles from './index.less';
+
+/* eslint react/no-danger:0 */
+class Radar extends PureComponent {
+ state = {
+ legendData: [],
+ }
+
+ componentDidMount() {
+ this.renderChart(this.props.data);
+ }
+
+ componentWillReceiveProps(nextProps) {
+ if (!equal(this.props, nextProps)) {
+ this.renderChart(nextProps.data);
+ }
+ }
+
+ componentWillUnmount() {
+ if (this.chart) {
+ this.chart.destroy();
+ }
+ }
+
+ handleRef = (n) => {
+ this.node = n;
+ }
+
+ handleLegendClick = (item, i) => {
+ const newItem = item;
+ newItem.checked = !newItem.checked;
+
+ const { legendData } = this.state;
+ legendData[i] = newItem;
+
+ if (this.chart) {
+ const filterItem = legendData.filter(l => l.checked).map(l => l.name);
+ this.chart.filter('name', filterItem);
+ this.chart.repaint();
+ }
+
+ this.setState({
+ legendData,
+ });
+ }
+
+ renderChart(data) {
+ const { height = 0,
+ hasLegend = true,
+ fit = true,
+ tickCount = 4,
+ margin = [24, 30, 16, 30] } = this.props;
+
+ const colors = [
+ '#1890FF', '#FACC14', '#2FC25B', '#8543E0', '#F04864', '#13C2C2', '#fa8c16', '#a0d911',
+ ];
+
+ if (!data || (data && data.length < 1)) {
+ return;
+ }
+
+ // clean
+ this.node.innerHTML = '';
+
+ const chart = new G2.Chart({
+ container: this.node,
+ forceFit: fit,
+ height: height - (hasLegend ? 80 : 22),
+ plotCfg: {
+ margin,
+ },
+ });
+
+ this.chart = chart;
+
+ chart.source(data, {
+ value: {
+ min: 0,
+ tickCount,
+ },
+ });
+
+ chart.coord('polar');
+ chart.legend(false);
+
+ chart.axis('label', {
+ line: null,
+ labelOffset: 8,
+ labels: {
+ label: {
+ fill: 'rgba(0, 0, 0, .65)',
+ },
+ },
+ grid: {
+ line: {
+ stroke: '#e9e9e9',
+ lineWidth: 1,
+ lineDash: [0, 0],
+ },
+ },
+ });
+
+ chart.axis('value', {
+ grid: {
+ type: 'polygon',
+ line: {
+ stroke: '#e9e9e9',
+ lineWidth: 1,
+ lineDash: [0, 0],
+ },
+ },
+ labels: {
+ label: {
+ fill: 'rgba(0, 0, 0, .65)',
+ },
+ },
+ });
+
+ chart.line().position('label*value').color('name', colors);
+ chart.point().position('label*value').color('name', colors).shape('circle')
+ .size(3);
+
+ chart.render();
+
+ if (hasLegend) {
+ const geom = chart.getGeoms()[0]; // 获取所有的图形
+ const items = geom.getData(); // 获取图形对应的数据
+ const legendData = items.map((item) => {
+ /* eslint no-underscore-dangle:0 */
+ const origin = item._origin;
+ const result = {
+ name: origin[0].name,
+ color: item.color,
+ checked: true,
+ value: origin.reduce((p, n) => p + n.value, 0),
+ };
+
+ return result;
+ });
+
+ this.setState({
+ legendData,
+ });
+ }
+ }
+
+ render() {
+ const { height, title, hasLegend } = this.props;
+ const { legendData } = this.state;
+
+ return (
+ <div className={styles.radar} style={{ height }}>
+ <div>
+ {title && <h4>{title}</h4>}
+ <div ref={this.handleRef} />
+ {
+ hasLegend && (
+ <Row className={styles.legend}>
+ {
+ legendData.map((item, i) => (
+ <Col
+ span={(24 / legendData.length)}
+ key={item.name}
+ onClick={() => this.handleLegendClick(item, i)}
+ >
+ <div className={styles.legendItem}>
+ <p>
+ <span className={styles.dot} style={{ backgroundColor: !item.checked ? '#aaa' : item.color }} />
+ <span>{item.name}</span>
+ </p>
+ <h6>{item.value}</h6>
+ </div>
+ </Col>
+ ))
+ }
+ </Row>
+ )
+ }
+ </div>
+ </div>
+ );
+ }
+}
+
+export default Radar;
diff --git a/src/main/frontend/src/components/Charts/Radar/index.less b/src/main/frontend/src/components/Charts/Radar/index.less
new file mode 100644
index 0000000..378db9c
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/Radar/index.less
@@ -0,0 +1,46 @@
+@import "~antd/lib/style/themes/default.less";
+
+.radar {
+ .legend {
+ margin-top: 16px;
+ .legendItem {
+ position: relative;
+ text-align: center;
+ cursor: pointer;
+ color: @text-color-secondary;
+ line-height: 22px;
+ p {
+ margin: 0;
+ }
+ h6 {
+ color: @heading-color;
+ padding-left: 16px;
+ font-size: 24px;
+ line-height: 32px;
+ margin-top: 4px;
+ margin-bottom: 0;
+ }
+ &:after {
+ background-color: @border-color-split;
+ position: absolute;
+ top: 8px;
+ right: 0;
+ height: 40px;
+ width: 1px;
+ content: '';
+ }
+ }
+ > :last-child .legendItem:after {
+ display: none;
+ }
+ .dot {
+ border-radius: 6px;
+ display: inline-block;
+ margin-right: 6px;
+ position: relative;
+ top: -1px;
+ height: 6px;
+ width: 6px;
+ }
+ }
+}
diff --git a/src/main/frontend/src/components/Charts/TagCloud/index.d.ts b/src/main/frontend/src/components/Charts/TagCloud/index.d.ts
new file mode 100644
index 0000000..e783213
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/TagCloud/index.d.ts
@@ -0,0 +1,10 @@
+import * as React from "react";
+export interface TagCloudProps {
+ data: Array<{
+ name: string;
+ value: number;
+ }>;
+ height: number;
+}
+
+export default class TagCloud extends React.Component<TagCloudProps, any> {}
diff --git a/src/main/frontend/src/components/Charts/TagCloud/index.js b/src/main/frontend/src/components/Charts/TagCloud/index.js
new file mode 100644
index 0000000..d3f0d70
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/TagCloud/index.js
@@ -0,0 +1,170 @@
+import React, { PureComponent } from 'react';
+import classNames from 'classnames';
+import G2 from 'g2';
+import Cloud from 'g-cloud';
+import Debounce from 'lodash-decorators/debounce';
+import Bind from 'lodash-decorators/bind';
+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';
+
+class TagCloud extends PureComponent {
+ componentDidMount() {
+ this.initTagCloud();
+ this.renderChart();
+
+ window.addEventListener('resize', this.resize);
+ }
+
+ componentWillReceiveProps(nextProps) {
+ if (this.props.data !== nextProps.data) {
+ this.renderChart(nextProps.data);
+ }
+ }
+
+ componentWillUnmount() {
+ window.removeEventListener('resize', this.resize);
+ this.renderChart.cancel();
+ }
+
+ resize = () => {
+ this.renderChart();
+ }
+
+ initTagCloud = () => {
+ const { Util, Shape } = G2;
+
+ function getTextAttrs(cfg) {
+ const textAttrs = Util.mix(true, {}, {
+ fillOpacity: cfg.opacity,
+ fontSize: cfg.size,
+ rotate: cfg.origin._origin.rotate,
+ // rotate: cfg.origin._origin.rotate,
+ text: cfg.origin._origin.text,
+ textAlign: 'center',
+ fill: cfg.color,
+ textBaseline: 'Alphabetic',
+ }, cfg.style);
+ return textAttrs;
+ }
+
+ // 给point注册一个词云的shape
+ Shape.registShape('point', 'cloud', {
+ drawShape(cfg, container) {
+ cfg.points = this.parsePoints(cfg.points);
+ const attrs = getTextAttrs(cfg);
+ const shape = container.addShape('text', {
+ attrs: Util.mix(attrs, {
+ x: cfg.points[0].x,
+ y: cfg.points[0].y,
+ }),
+ });
+ return shape;
+ },
+ });
+ }
+
+ saveRootRef = (node) => {
+ this.root = node;
+ }
+
+ saveNodeRef = (node) => {
+ this.node = node;
+ }
+
+ @Bind()
+ @Debounce(500)
+ renderChart(newData) {
+ const data = newData || this.props.data;
+ if (!data || data.length < 1) {
+ return;
+ }
+
+ const colors = ['#1890FF', '#41D9C7', '#2FC25B', '#FACC14', '#9AE65C'];
+
+ const height = this.props.height * 4;
+ let width = 0;
+ if (this.root) {
+ width = this.root.offsetWidth * 4;
+ }
+
+ data.sort((a, b) => b.value - a.value);
+
+ const max = data[0].value;
+ const min = data[data.length - 1].value;
+
+ // 构造一个词云布局对象
+ const layout = new Cloud({
+ words: data,
+ width,
+ height,
+
+ rotate: () => 0,
+
+ // 设定文字大小配置函数(默认为12-24px的随机大小)
+ size: words => (((words.value - min) / (max - min)) * 50) + 30,
+
+ // 设定文字内容
+ text: words => words.name,
+ });
+
+ layout.image(imgUrl, (imageCloud) => {
+ // clean
+ if (this.node) {
+ this.node.innerHTML = '';
+ }
+
+ // 执行词云布局函数,并在回调函数中调用G2对结果进行绘制
+ imageCloud.exec((texts) => {
+ const chart = new G2.Chart({
+ container: this.node,
+ width,
+ height,
+ plotCfg: {
+ margin: 0,
+ },
+ });
+
+ chart.legend(false);
+ chart.axis(false);
+ chart.tooltip(false);
+
+ chart.source(texts);
+
+ // 将词云坐标系调整为G2的坐标系
+ chart.coord().reflect();
+
+ chart
+ .point()
+ .position('x*y')
+ .color('text', colors)
+ .size('size', size => size)
+ .shape('cloud')
+ .style({
+ fontStyle: texts[0].style,
+ fontFamily: texts[0].font,
+ fontWeight: texts[0].weight,
+ });
+
+ chart.render();
+ });
+ });
+ }
+
+ render() {
+ return (
+ <div
+ className={classNames(styles.tagCloud, this.props.className)}
+ ref={this.saveRootRef}
+ style={{ width: '100%' }}
+ >
+ <div ref={this.saveNodeRef} style={{ height: this.props.height }} />
+ </div>
+ );
+ }
+}
+
+export default TagCloud;
diff --git a/src/main/frontend/src/components/Charts/TagCloud/index.less b/src/main/frontend/src/components/Charts/TagCloud/index.less
new file mode 100644
index 0000000..96b1006
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/TagCloud/index.less
@@ -0,0 +1,6 @@
+.tagCloud {
+ canvas {
+ transform: scale(0.25);
+ transform-origin: 0 0;
+ }
+}
diff --git a/src/main/frontend/src/components/Charts/TimelineChart/index.d.ts b/src/main/frontend/src/components/Charts/TimelineChart/index.d.ts
new file mode 100644
index 0000000..5ea76a1
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/TimelineChart/index.d.ts
@@ -0,0 +1,15 @@
+import * as React from "react";
+export interface TimelineChartProps {
+ data: Array<{
+ x: string;
+ y1: string;
+ y2: string;
+ }>;
+ titleMap: { y1: string; y2: string };
+ height?: number;
+}
+
+export default class TimelineChart extends React.Component<
+ TimelineChartProps,
+ any
+> {}
diff --git a/src/main/frontend/src/components/Charts/TimelineChart/index.js b/src/main/frontend/src/components/Charts/TimelineChart/index.js
new file mode 100644
index 0000000..65048b8
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/TimelineChart/index.js
@@ -0,0 +1,125 @@
+import React, { Component } from 'react';
+import G2 from 'g2';
+import Slider from 'g2-plugin-slider';
+import styles from './index.less';
+
+class TimelineChart extends Component {
+ componentDidMount() {
+ this.renderChart(this.props.data);
+ }
+
+ componentWillReceiveProps(nextProps) {
+ if (nextProps.data !== this.props.data) {
+ this.renderChart(nextProps.data);
+ }
+ }
+
+ componentWillUnmount() {
+ if (this.chart) {
+ this.chart.destroy();
+ }
+ if (this.slider) {
+ this.slider.destroy();
+ }
+ }
+
+ sliderId = `timeline-chart-slider-${Math.random() * 1000}`
+
+ handleRef = (n) => {
+ this.node = n;
+ }
+
+ renderChart(data) {
+ const { height = 400, margin = [60, 20, 40, 40], titleMap, borderWidth = 2 } = this.props;
+
+ if (!data || (data && data.length < 1)) {
+ return;
+ }
+
+ // clean
+ if (this.sliderId) {
+ document.getElementById(this.sliderId).innerHTML = '';
+ }
+ this.node.innerHTML = '';
+
+ const chart = new G2.Chart({
+ container: this.node,
+ forceFit: true,
+ height,
+ plotCfg: {
+ margin,
+ },
+ });
+
+ chart.axis('x', {
+ title: false,
+ });
+ chart.axis('y1', {
+ title: false,
+ });
+ chart.axis('y2', false);
+
+ chart.legend({
+ mode: false,
+ position: 'top',
+ });
+
+ 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);
+ }
+
+ chart.source(data, {
+ x: {
+ type: 'timeCat',
+ tickCount: 16,
+ mask: 'HH:MM',
+ range: [0, 1],
+ },
+ y1: {
+ alias: titleMap.y1,
+ max,
+ min: 0,
+ },
+ y2: {
+ alias: titleMap.y2,
+ max,
+ min: 0,
+ },
+ });
+
+ chart.line().position('x*y1').color('#1890FF').size(borderWidth);
+ chart.line().position('x*y2').color('#2FC25B').size(borderWidth);
+
+ this.chart = chart;
+
+ /* eslint new-cap:0 */
+ const slider = new Slider({
+ domId: this.sliderId,
+ height: 26,
+ xDim: 'x',
+ yDim: 'y1',
+ charts: [chart],
+ });
+ slider.render();
+
+ this.slider = slider;
+ }
+
+ render() {
+ const { height, title } = this.props;
+
+ return (
+ <div className={styles.timelineChart} style={{ height }}>
+ <div>
+ { title && <h4>{title}</h4>}
+ <div ref={this.handleRef} />
+ <div id={this.sliderId} />
+ </div>
+ </div>
+ );
+ }
+}
+
+export default TimelineChart;
diff --git a/src/main/frontend/src/components/Charts/TimelineChart/index.less b/src/main/frontend/src/components/Charts/TimelineChart/index.less
new file mode 100644
index 0000000..1751975
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/TimelineChart/index.less
@@ -0,0 +1,3 @@
+.timelineChart {
+ background: #fff;
+}
diff --git a/src/main/frontend/src/components/Charts/WaterWave/index.d.ts b/src/main/frontend/src/components/Charts/WaterWave/index.d.ts
new file mode 100644
index 0000000..0fefbea
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/WaterWave/index.d.ts
@@ -0,0 +1,9 @@
+import * as React from "react";
+export interface WaterWaveProps {
+ title: React.ReactNode;
+ color?: string;
+ height: number;
+ percent: number;
+}
+
+export default class WaterWave extends React.Component<WaterWaveProps, any> {}
diff --git a/src/main/frontend/src/components/Charts/WaterWave/index.js b/src/main/frontend/src/components/Charts/WaterWave/index.js
new file mode 100644
index 0000000..a9eece6
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/WaterWave/index.js
@@ -0,0 +1,200 @@
+import React, { PureComponent } from 'react';
+import styles from './index.less';
+
+/* eslint no-return-assign: 0 */
+// riddle: https://riddle.alibaba-inc.com/riddles/2d9a4b90
+
+class WaterWave extends PureComponent {
+ static defaultProps = {
+ height: 160,
+ }
+ state = {
+ radio: 1,
+ }
+
+ componentDidMount() {
+ this.renderChart();
+ this.resize();
+
+ window.addEventListener('resize', this.resize);
+ }
+
+ componentWillUnmount() {
+ cancelAnimationFrame(this.timer);
+ if (this.node) {
+ this.node.innerHTML = '';
+ }
+ window.removeEventListener('resize', this.resize);
+ }
+
+ resize = () => {
+ const { height } = this.props;
+ const { offsetWidth } = this.root.parentNode;
+ this.setState({
+ radio: offsetWidth < height ? offsetWidth / height : 1,
+ });
+ }
+
+ renderChart() {
+ const { percent, color = '#1890FF' } = this.props;
+ const data = percent / 100;
+ const self = this;
+
+ if (!this.node || !data) {
+ return;
+ }
+
+ const canvas = this.node;
+ const ctx = canvas.getContext('2d');
+
+ 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 = [];
+ 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();
+ ctx.strokeStyle = color;
+ ctx.moveTo(cStartPoint[0], cStartPoint[1]);
+
+ function drawSin() {
+ 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();
+
+ 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, '#1890FF');
+ ctx.fillStyle = gradient;
+ ctx.fill();
+ ctx.restore();
+ }
+
+ function render() {
+ ctx.clearRect(0, 0, canvasWidth, canvasHeight);
+ if (circleLock) {
+ if (arcStack.length) {
+ const temp = arcStack.shift();
+ ctx.lineTo(temp[0], temp[1]);
+ ctx.stroke();
+ } else {
+ circleLock = false;
+ ctx.lineTo(cStartPoint[0], cStartPoint[1]);
+ ctx.stroke();
+ arcStack = null;
+
+ ctx.globalCompositeOperation = 'destination-over';
+ ctx.beginPath();
+ ctx.lineWidth = lineWidth;
+ ctx.arc(radius, radius, bR, 0, 2 * Math.PI, 1);
+
+ ctx.beginPath();
+ ctx.save();
+ ctx.arc(radius, radius, radius - (3 * lineWidth), 0, 2 * Math.PI, 1);
+
+ ctx.restore();
+ ctx.clip();
+ ctx.fillStyle = '#1890FF';
+ }
+ } 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 } = 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 WaterWave;
diff --git a/src/main/frontend/src/components/Charts/WaterWave/index.less b/src/main/frontend/src/components/Charts/WaterWave/index.less
new file mode 100644
index 0000000..d185ca3
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/WaterWave/index.less
@@ -0,0 +1,28 @@
+@import "~antd/lib/style/themes/default.less";
+
+.waterWave {
+ display: inline-block;
+ position: relative;
+ transform-origin: left;
+ .text {
+ position: absolute;
+ left: 0;
+ top: 32px;
+ text-align: center;
+ width: 100%;
+ span {
+ color: @text-color-secondary;
+ font-size: 14px;
+ line-height: 22px;
+ }
+ h4 {
+ color: @heading-color;
+ line-height: 32px;
+ font-size: 24px;
+ }
+ }
+ .waterWaveCanvasWrapper {
+ transform: scale(.5);
+ transform-origin: 0 0;
+ }
+}
diff --git a/src/main/frontend/src/components/Charts/demo/bar.md b/src/main/frontend/src/components/Charts/demo/bar.md
new file mode 100644
index 0000000..955f44e
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/demo/bar.md
@@ -0,0 +1,26 @@
+---
+order: 4
+title: 柱状图
+---
+
+通过设置 `x`,`y` 属性,可以快速的构建出一个漂亮的柱状图,各种纬度的关系则是通过自定义的数据展现。
+
+````jsx
+import { Bar } from 'ant-design-pro/lib/Charts';
+
+const salesData = [];
+for (let i = 0; i < 12; i += 1) {
+ salesData.push({
+ x: `${i + 1}月`,
+ y: Math.floor(Math.random() * 1000) + 200,
+ });
+}
+
+ReactDOM.render(
+ <Bar
+ height={200}
+ title="销售额趋势"
+ data={salesData}
+ />
+, mountNode);
+````
diff --git a/src/main/frontend/src/components/Charts/demo/chart-card.md b/src/main/frontend/src/components/Charts/demo/chart-card.md
new file mode 100644
index 0000000..5120479
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/demo/chart-card.md
@@ -0,0 +1,65 @@
+---
+order: 1
+title: 图表卡片
+---
+
+用于展示图表的卡片容器,可以方便的配合其它图表套件展示丰富信息。
+
+````jsx
+import { ChartCard, yuan, Field } from 'ant-design-pro/lib/Charts';
+import Trend from 'ant-design-pro/lib/Trend';
+import { Row, Col, Icon, Tooltip } from 'antd';
+import numeral from 'numeral';
+
+ReactDOM.render(
+ <Row>
+ <Col span={24}>
+ <ChartCard
+ title="销售额"
+ action={<Tooltip title="指标说明"><Icon type="info-circle-o" /></Tooltip>}
+ total={yuan(126560)}
+ footer={<Field label="日均销售额" value={numeral(12423).format('0,0')} />}
+ contentHeight={46}
+ >
+ <span>
+ 周同比
+ <Trend flag="up" style={{ marginLeft: 8, color: 'rgba(0,0,0,.85)' }}>12%</Trend>
+ </span>
+ <span style={{ marginLeft: 16 }}>
+ 日环比
+ <Trend flag="down" style={{ marginLeft: 8, color: 'rgba(0,0,0,.85)' }}>11%</Trend>
+ </span>
+ </ChartCard>
+ </Col>
+ <Col span={24} style={{ marginTop: 24 }}>
+ <ChartCard
+ title="移动指标"
+ avatar={
+ <img
+ style={{ width: 56, height: 56 }}
+ src="https://gw.alipayobjects.com/zos/rmsportal/sfjbOqnsXXJgNCjCzDBL.png"
+ alt="indicator"
+ />
+ }
+ action={<Tooltip title="指标说明"><Icon type="info-circle-o" /></Tooltip>}
+ total={yuan(126560)}
+ footer={<Field label="日均销售额" value={numeral(12423).format('0,0')} />}
+ />
+ </Col>
+ <Col span={24} style={{ marginTop: 24 }}>
+ <ChartCard
+ title="移动指标"
+ avatar={(
+ <img
+ alt="indicator"
+ style={{ width: 56, height: 56 }}
+ src="https://gw.alipayobjects.com/zos/rmsportal/dURIMkkrRFpPgTuzkwnB.png"
+ />
+ )}
+ action={<Tooltip title="指标说明"><Icon type="info-circle-o" /></Tooltip>}
+ total={yuan(126560)}
+ />
+ </Col>
+ </Row>
+, mountNode);
+````
diff --git a/src/main/frontend/src/components/Charts/demo/gauge.md b/src/main/frontend/src/components/Charts/demo/gauge.md
new file mode 100644
index 0000000..f53465d
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/demo/gauge.md
@@ -0,0 +1,18 @@
+---
+order: 7
+title: 仪表盘
+---
+
+仪表盘是一种进度展示方式,可以更直观的展示当前的进展情况,通常也可表示占比。
+
+````jsx
+import { Gauge } from 'ant-design-pro/lib/Charts';
+
+ReactDOM.render(
+ <Gauge
+ title="核销率"
+ height={164}
+ percent={87}
+ />
+, mountNode);
+````
diff --git a/src/main/frontend/src/components/Charts/demo/mini-area.md b/src/main/frontend/src/components/Charts/demo/mini-area.md
new file mode 100644
index 0000000..2b9bfb4
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/demo/mini-area.md
@@ -0,0 +1,28 @@
+---
+order: 2
+col: 2
+title: 迷你区域图
+---
+
+````jsx
+import { MiniArea } from 'ant-design-pro/lib/Charts';
+import moment from 'moment';
+
+const visitData = [];
+const beginDay = new Date().getTime();
+for (let i = 0; i < 20; i += 1) {
+ visitData.push({
+ x: moment(new Date(beginDay + (1000 * 60 * 60 * 24 * i))).format('YYYY-MM-DD'),
+ y: Math.floor(Math.random() * 100) + 10,
+ });
+}
+
+ReactDOM.render(
+ <MiniArea
+ line
+ color="#cceafe"
+ height={45}
+ data={visitData}
+ />
+, mountNode);
+````
diff --git a/src/main/frontend/src/components/Charts/demo/mini-bar.md b/src/main/frontend/src/components/Charts/demo/mini-bar.md
new file mode 100644
index 0000000..fef301b
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/demo/mini-bar.md
@@ -0,0 +1,28 @@
+---
+order: 2
+col: 2
+title: 迷你柱状图
+---
+
+迷你柱状图更适合展示简单的区间数据,简洁的表现方式可以很好的减少大数据量的视觉展现压力。
+
+````jsx
+import { MiniBar } from 'ant-design-pro/lib/Charts';
+import moment from 'moment';
+
+const visitData = [];
+const beginDay = new Date().getTime();
+for (let i = 0; i < 20; i += 1) {
+ visitData.push({
+ x: moment(new Date(beginDay + (1000 * 60 * 60 * 24 * i))).format('YYYY-MM-DD'),
+ y: Math.floor(Math.random() * 100) + 10,
+ });
+}
+
+ReactDOM.render(
+ <MiniBar
+ height={45}
+ data={visitData}
+ />
+, mountNode);
+````
diff --git a/src/main/frontend/src/components/Charts/demo/mini-pie.md b/src/main/frontend/src/components/Charts/demo/mini-pie.md
new file mode 100644
index 0000000..9b1abf0
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/demo/mini-pie.md
@@ -0,0 +1,16 @@
+---
+order: 6
+title: 迷你饼状图
+---
+
+通过简化 `Pie` 属性的设置,可以快速的实现极简的饼状图,可配合 `ChartCard` 组合展
+现更多业务场景。
+
+```jsx
+import { Pie } from 'ant-design-pro/lib/Charts';
+
+ReactDOM.render(
+ <Pie percent={28} subTitle="中式快餐" total="28%" height={140} />,
+ mountNode
+);
+```
diff --git a/src/main/frontend/src/components/Charts/demo/mini-progress.md b/src/main/frontend/src/components/Charts/demo/mini-progress.md
new file mode 100644
index 0000000..6308a8f
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/demo/mini-progress.md
@@ -0,0 +1,12 @@
+---
+order: 3
+title: 迷你进度条
+---
+
+````jsx
+import { MiniProgress } from 'ant-design-pro/lib/Charts';
+
+ReactDOM.render(
+ <MiniProgress percent={78} strokeWidth={8} target={80} />
+, mountNode);
+````
diff --git a/src/main/frontend/src/components/Charts/demo/mix.md b/src/main/frontend/src/components/Charts/demo/mix.md
new file mode 100644
index 0000000..0c158e5
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/demo/mix.md
@@ -0,0 +1,83 @@
+---
+order: 0
+title: 图表套件组合展示
+---
+
+利用 Ant Design Pro 提供的图表套件,可以灵活组合符合设计规范的图表来满足复杂的业务需求。
+
+````jsx
+import { ChartCard, Field, MiniArea, MiniBar, MiniProgress } from 'ant-design-pro/lib/Charts';
+import Trend from 'ant-design-pro/lib/Trend';
+import NumberInfo from 'ant-design-pro/lib/NumberInfo';
+import { Row, Col, Icon, Tooltip } from 'antd';
+import numeral from 'numeral';
+import moment from 'moment';
+
+const visitData = [];
+const beginDay = new Date().getTime();
+for (let i = 0; i < 20; i += 1) {
+ visitData.push({
+ x: moment(new Date(beginDay + (1000 * 60 * 60 * 24 * i))).format('YYYY-MM-DD'),
+ y: Math.floor(Math.random() * 100) + 10,
+ });
+}
+
+ReactDOM.render(
+ <Row>
+ <Col span={24}>
+ <ChartCard
+ title="搜索用户数量"
+ contentHeight={134}
+ >
+ <NumberInfo
+ subTitle={<span>本周访问</span>}
+ total={numeral(12321).format('0,0')}
+ status="up"
+ subTotal={17.1}
+ />
+ <MiniArea
+ line
+ height={45}
+ data={visitData}
+ />
+ </ChartCard>
+ </Col>
+ <Col span={24} style={{ marginTop: 24 }}>
+ <ChartCard
+ title="访问量"
+ action={<Tooltip title="指标说明"><Icon type="info-circle-o" /></Tooltip>}
+ total={numeral(8846).format('0,0')}
+ footer={<Field label="日访问量" value={numeral(1234).format('0,0')} />}
+ contentHeight={46}
+ >
+ <MiniBar
+ height={46}
+ data={visitData}
+ />
+ </ChartCard>
+ </Col>
+ <Col span={24} style={{ marginTop: 24 }}>
+ <ChartCard
+ title="线上购物转化率"
+ action={<Tooltip title="指标说明"><Icon type="info-circle-o" /></Tooltip>}
+ total="78%"
+ footer={
+ <div>
+ <span>
+ 周同比
+ <Trend flag="up" style={{ marginLeft: 8, color: 'rgba(0,0,0,.85)' }}>12%</Trend>
+ </span>
+ <span style={{ marginLeft: 16 }}>
+ 日环比
+ <Trend flag="down" style={{ marginLeft: 8, color: 'rgba(0,0,0,.85)' }}>11%</Trend>
+ </span>
+ </div>
+ }
+ contentHeight={46}
+ >
+ <MiniProgress percent={78} strokeWidth={8} target={80} />
+ </ChartCard>
+ </Col>
+ </Row>
+, mountNode);
+````
diff --git a/src/main/frontend/src/components/Charts/demo/pie.md b/src/main/frontend/src/components/Charts/demo/pie.md
new file mode 100644
index 0000000..2929f2a
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/demo/pie.md
@@ -0,0 +1,47 @@
+---
+order: 5
+title: 饼状图
+---
+
+````jsx
+import { Pie, yuan } from 'ant-design-pro/lib/Charts';
+
+const salesPieData = [
+ {
+ x: '家用电器',
+ y: 4544,
+ },
+ {
+ x: '食用酒水',
+ y: 3321,
+ },
+ {
+ x: '个护健康',
+ y: 3113,
+ },
+ {
+ x: '服饰箱包',
+ y: 2341,
+ },
+ {
+ x: '母婴产品',
+ y: 1231,
+ },
+ {
+ x: '其他',
+ y: 1231,
+ },
+];
+
+ReactDOM.render(
+ <Pie
+ hasLegend
+ title="销售额"
+ subTitle="销售额"
+ total={yuan(salesPieData.reduce((pre, now) => now.y + pre, 0))}
+ data={salesPieData}
+ valueFormat={val => yuan(val)}
+ height={294}
+ />
+, mountNode);
+````
diff --git a/src/main/frontend/src/components/Charts/demo/radar.md b/src/main/frontend/src/components/Charts/demo/radar.md
new file mode 100644
index 0000000..584344a
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/demo/radar.md
@@ -0,0 +1,64 @@
+---
+order: 7
+title: 雷达图
+---
+
+````jsx
+import { Radar, ChartCard } from 'ant-design-pro/lib/Charts';
+
+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 = [];
+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],
+ });
+ }
+ });
+});
+
+ReactDOM.render(
+ <ChartCard title="数据比例">
+ <Radar
+ hasLegend
+ height={286}
+ data={radarData}
+ />
+ </ChartCard>
+, mountNode);
+````
diff --git a/src/main/frontend/src/components/Charts/demo/tag-cloud.md b/src/main/frontend/src/components/Charts/demo/tag-cloud.md
new file mode 100644
index 0000000..c66f6fe
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/demo/tag-cloud.md
@@ -0,0 +1,25 @@
+---
+order: 9
+title: 标签云
+---
+
+标签云是一套相关的标签以及与此相应的权重展示方式,一般典型的标签云有 30 至 150 个标签,而权重影响使用的字体大小或其他视觉效果。
+
+````jsx
+import { TagCloud } from 'ant-design-pro/lib/Charts';
+
+const tags = [];
+for (let i = 0; i < 50; i += 1) {
+ tags.push({
+ name: `TagClout-Title-${i}`,
+ value: Math.floor((Math.random() * 50)) + 20,
+ });
+}
+
+ReactDOM.render(
+ <TagCloud
+ data={tags}
+ height={200}
+ />
+, mountNode);
+````
diff --git a/src/main/frontend/src/components/Charts/demo/timeline-chart.md b/src/main/frontend/src/components/Charts/demo/timeline-chart.md
new file mode 100644
index 0000000..60773b5
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/demo/timeline-chart.md
@@ -0,0 +1,27 @@
+---
+order: 9
+title: 带有时间轴的图表
+---
+
+使用 `TimelineChart` 组件可以实现带有时间轴的柱状图展现,而其中的 `x` 属性,则是时间值的指向,默认最多支持同时展现两个指标,分别是 `y1` 和 `y2`。
+
+````jsx
+import { TimelineChart } from 'ant-design-pro/lib/Charts';
+
+const chartData = [];
+for (let i = 0; i < 20; i += 1) {
+ chartData.push({
+ x: (new Date().getTime()) + (1000 * 60 * 30 * i),
+ y1: Math.floor(Math.random() * 100) + 1000,
+ y2: Math.floor(Math.random() * 100) + 10,
+ });
+}
+
+ReactDOM.render(
+ <TimelineChart
+ height={200}
+ data={chartData}
+ titleMap={{ y1: '客流量', y2: '支付笔数' }}
+ />
+, mountNode);
+````
diff --git a/src/main/frontend/src/components/Charts/demo/waterwave.md b/src/main/frontend/src/components/Charts/demo/waterwave.md
new file mode 100644
index 0000000..74d290f
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/demo/waterwave.md
@@ -0,0 +1,20 @@
+---
+order: 8
+title: 水波图
+---
+
+水波图是一种比例的展示方式,可以更直观的展示关键值的占比。
+
+````jsx
+import { WaterWave } from 'ant-design-pro/lib/Charts';
+
+ReactDOM.render(
+ <div style={{ textAlign: 'center' }}>
+ <WaterWave
+ height={161}
+ title="补贴资金剩余"
+ percent={34}
+ />
+ </div>
+, mountNode);
+````
diff --git a/src/main/frontend/src/components/Charts/equal.js b/src/main/frontend/src/components/Charts/equal.js
new file mode 100644
index 0000000..ff3a4c7
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/equal.js
@@ -0,0 +1,17 @@
+/* eslint eqeqeq: 0 */
+
+function equal(old, target) {
+ let r = true;
+ for (const prop in old) {
+ if (typeof old[prop] === 'function' && typeof target[prop] === 'function') {
+ if (old[prop].toString() != target[prop].toString()) {
+ r = false;
+ }
+ } else if (old[prop] != target[prop]) {
+ r = false;
+ }
+ }
+ return r;
+}
+
+export default equal;
diff --git a/src/main/frontend/src/components/Charts/index.d.ts b/src/main/frontend/src/components/Charts/index.d.ts
new file mode 100644
index 0000000..e47b947
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/index.d.ts
@@ -0,0 +1,17 @@
+export { default as numeral } from "numeral";
+export { default as ChartCard } from "./ChartCard";
+export { default as Bar } from "./Bar";
+export { default as Pie } from "./Pie";
+export { default as Radar } from "./Radar";
+export { default as Gauge } from "./Gauge";
+export { default as MiniArea } from "./MiniArea";
+export { default as MiniBar } from "./MiniBar";
+export { default as MiniProgress } from "./MiniProgress";
+export { default as Field } from "./Field";
+export { default as WaterWave } from "./WaterWave";
+export { default as TagCloud } from "./TagCloud";
+export { default as TimelineChart } from "./TimelineChart";
+
+declare const yuan: (value: number | string) => string;
+
+export { yuan };
diff --git a/src/main/frontend/src/components/Charts/index.js b/src/main/frontend/src/components/Charts/index.js
new file mode 100644
index 0000000..cea9949
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/index.js
@@ -0,0 +1,31 @@
+import numeral from 'numeral';
+import ChartCard from './ChartCard';
+import Bar from './Bar';
+import Pie from './Pie';
+import Radar from './Radar';
+import Gauge from './Gauge';
+import MiniArea from './MiniArea';
+import MiniBar from './MiniBar';
+import MiniProgress from './MiniProgress';
+import Field from './Field';
+import WaterWave from './WaterWave';
+import TagCloud from './TagCloud';
+import TimelineChart from './TimelineChart';
+
+const yuan = val => `¥ ${numeral(val).format('0,0')}`;
+
+export default {
+ yuan,
+ Bar,
+ Pie,
+ Gauge,
+ Radar,
+ MiniBar,
+ MiniArea,
+ MiniProgress,
+ ChartCard,
+ Field,
+ WaterWave,
+ TagCloud,
+ TimelineChart,
+};
diff --git a/src/main/frontend/src/components/Charts/index.less b/src/main/frontend/src/components/Charts/index.less
new file mode 100644
index 0000000..52f97c4
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/index.less
@@ -0,0 +1,19 @@
+.miniChart {
+ position: relative;
+ width: 100%;
+ .chartContent {
+ position: absolute;
+ bottom: -34px;
+ width: 100%;
+ & > div {
+ margin: 0 -5px;
+ overflow: hidden;
+ }
+ }
+ .chartLoading {
+ position: absolute;
+ top: 16px;
+ left: 50%;
+ margin-left: -7px;
+ }
+}
diff --git a/src/main/frontend/src/components/Charts/index.md b/src/main/frontend/src/components/Charts/index.md
new file mode 100644
index 0000000..218f4eb
--- /dev/null
+++ b/src/main/frontend/src/components/Charts/index.md
@@ -0,0 +1,132 @@
+---
+title:
+ en-US: Charts
+ zh-CN: Charts
+subtitle: 图表
+order: 2
+cols: 2
+---
+
+Ant Design Pro 提供的业务中常用的图表类型,都是基于 [G2](https://antv.alipay.com/g2/doc/index.html) 按照 Ant Design 图表规范封装,需要注意的是 Ant Design Pro 的图表组件以套件形式提供,可以任意组合实现复杂的业务需求。
+
+因为结合了 Ant Design 的标准设计,本着极简的设计思想以及开箱即用的理念,简化了大量 API 配置,所以如果需要灵活定制图表,可以参考 Ant Design Pro 图表实现,自行基于 [G2](https://antv.alipay.com/g2/doc/index.html) 封装图表组件使用。
+
+## API
+
+### ChartCard
+
+| 参数 | 说明 | 类型 | 默认值 |
+|----------|------------------------------------------|-------------|-------|
+| title | 卡片标题 | ReactNode\|string | - |
+| action | 卡片操作 | ReactNode | - |
+| total | 数据总量 | ReactNode \| number | - |
+| footer | 卡片底部 | ReactNode | - |
+| contentHeight | 内容区域高度 | number | - |
+| avatar | 右侧图标 | React.ReactNode | - |
+### MiniBar
+
+| 参数 | 说明 | 类型 | 默认值 |
+|----------|------------------------------------------|-------------|-------|
+| color | 图表颜色 | string | `#1890FF` |
+| height | 图表高度 | number | - |
+| data | 数据 | array<{x, y}> | - |
+
+### MiniArea
+
+| 参数 | 说明 | 类型 | 默认值 |
+|----------|------------------------------------------|-------------|-------|
+| color | 图表颜色 | string | `rgba(24, 144, 255, 0.2)` |
+| borderColor | 图表边颜色 | string | `#1890FF` |
+| height | 图表高度 | number | - |
+| line | 是否显示描边 | boolean | false |
+| animate | 是否显示动画 | boolean | true |
+| xAxis | [x 轴配置](http://antvis.github.io/g2/doc/tutorial/start/axis.html) | object | - |
+| yAxis | [y 轴配置](http://antvis.github.io/g2/doc/tutorial/start/axis.html) | object | - |
+| data | 数据 | array<{x, y}> | - |
+
+### MiniProgress
+
+| 参数 | 说明 | 类型 | 默认值 |
+|----------|------------------------------------------|-------------|-------|
+| target | 目标比例 | number | - |
+| color | 进度条颜色 | string | - |
+| strokeWidth | 进度条高度 | number | - |
+| percent | 进度比例 | number | - |
+
+### Bar
+
+| 参数 | 说明 | 类型 | 默认值 |
+|----------|------------------------------------------|-------------|-------|
+| title | 图表标题 | ReactNode\|string | - |
+| color | 图表颜色 | string | `rgba(24, 144, 255, 0.85)` |
+| margin | 图表内部间距 | array | \[32, 0, 32, 40\] |
+| height | 图表高度 | number | - |
+| data | 数据 | array<{x, y}> | - |
+| autoLabel | 在宽度不足时,自动隐藏 x 轴的 label | boolean | `true` |
+
+### Pie
+
+| 参数 | 说明 | 类型 | 默认值 |
+|----------|------------------------------------------|-------------|-------|
+| animate | 是否显示动画 | boolean | true |
+| color | 图表颜色 | string | `rgba(24, 144, 255, 0.85)` |
+| height | 图表高度 | number | - |
+| hasLegend | 是否显示 legend | boolean | `false` |
+| margin | 图表内部间距 | array | \[24, 0, 24, 0\] |
+| percent | 占比 | number | - |
+| tooltip | 是否显示 tooltip | boolean | true |
+| valueFormat | 显示值的格式化函数 | function | - |
+| title | 图表标题 | ReactNode|string | - |
+| subTitle | 图表子标题 | ReactNode|string | - |
+| total | 图标中央的总数 | string | - |
+
+### Radar
+
+| 参数 | 说明 | 类型 | 默认值 |
+|----------|------------------------------------------|-------------|-------|
+| title | 图表标题 | ReactNode\|string | - |
+| height | 图表高度 | number | - |
+| hasLegend | 是否显示 legend | boolean | `false` |
+| margin | 图表内部间距 | array | \[24, 30, 16, 30\] |
+| data | 图标数据 | array<{name,label,value}> | - |
+
+### Gauge
+
+| 参数 | 说明 | 类型 | 默认值 |
+|----------|------------------------------------------|-------------|-------|
+| title | 图表标题 | ReactNode\|string | - |
+| height | 图表高度 | number | - |
+| color | 图表颜色 | string | `#2F9CFF` |
+| bgColor | 图表背景颜色 | string | `#F0F2F5` |
+| percent | 进度比例 | number | - |
+
+### WaterWave
+
+| 参数 | 说明 | 类型 | 默认值 |
+|----------|------------------------------------------|-------------|-------|
+| title | 图表标题 | ReactNode\|string | - |
+| height | 图表高度 | number | - |
+| color | 图表颜色 | string | `#1890FF` |
+| percent | 进度比例 | number | - |
+
+### TagCloud
+
+| 参数 | 说明 | 类型 | 默认值 |
+|----------|------------------------------------------|-------------|-------|
+| data | 标题 | Array<name, value\> | - |
+| height | 高度值 | number | - |
+
+### TimelineChart
+
+| 参数 | 说明 | 类型 | 默认值 |
+|----------|------------------------------------------|-------------|-------|
+| data | 标题 | Array<x, y1, y2\> | - |
+| titleMap | 指标别名 | Object{y1: '客流量', y2: '支付笔数'} | - |
+| height | 高度值 | number | 400 |
+
+### Field
+
+| 参数 | 说明 | 类型 | 默认值 |
+|----------|------------------------------------------|-------------|-------|
+| label | 标题 | ReactNode\|string | - |
+| value | 值 | ReactNode\|string | - |
diff --git a/src/main/frontend/src/components/Trend/demo/basic.md b/src/main/frontend/src/components/Trend/demo/basic.md
new file mode 100644
index 0000000..82afcda
--- /dev/null
+++ b/src/main/frontend/src/components/Trend/demo/basic.md
@@ -0,0 +1,17 @@
+---
+order: 0
+title: 演示
+---
+
+在数值背后添加一个小图标来标识涨跌情况。
+
+````jsx
+import Trend from 'ant-design-pro/lib/Trend';
+
+ReactDOM.render(
+ <div>
+ <Trend flag="up">12%</Trend>
+ <Trend flag="down" style={{ marginLeft: 8 }}>11%</Trend>
+ </div>
+, mountNode);
+````
diff --git a/src/main/frontend/src/components/Trend/index.d.ts b/src/main/frontend/src/components/Trend/index.d.ts
new file mode 100644
index 0000000..698a49d
--- /dev/null
+++ b/src/main/frontend/src/components/Trend/index.d.ts
@@ -0,0 +1,8 @@
+import * as React from "react";
+
+export interface TrendProps {
+ colorful?: boolean;
+ flag: "up" | "down";
+}
+
+export default class Trend extends React.Component<TrendProps, any> {}
diff --git a/src/main/frontend/src/components/Trend/index.js b/src/main/frontend/src/components/Trend/index.js
new file mode 100644
index 0000000..2cbaad4
--- /dev/null
+++ b/src/main/frontend/src/components/Trend/index.js
@@ -0,0 +1,22 @@
+import React from 'react';
+import { Icon } from 'antd';
+import classNames from 'classnames';
+import styles from './index.less';
+
+const Trend = ({ colorful = true, flag, children, className, ...rest }) => {
+ const classString = classNames(styles.trendItem, {
+ [styles.trendItemGrey]: !colorful,
+ }, className);
+ return (
+ <div
+ {...rest}
+ className={classString}
+ title={typeof children === 'string' ? children : ''}
+ >
+ <span className={styles.value}>{children}</span>
+ {flag && <span className={styles[flag]}><Icon type={`caret-${flag}`} /></span>}
+ </div>
+ );
+};
+
+export default Trend;
diff --git a/src/main/frontend/src/components/Trend/index.less b/src/main/frontend/src/components/Trend/index.less
new file mode 100644
index 0000000..48695c9
--- /dev/null
+++ b/src/main/frontend/src/components/Trend/index.less
@@ -0,0 +1,30 @@
+@import "~antd/lib/style/themes/default.less";
+
+.trendItem {
+ display: inline-block;
+ font-size: @font-size-base;
+ line-height: 22px;
+
+ .up,
+ .down {
+ margin-left: 4px;
+ position: relative;
+ top: 1px;
+ i {
+ font-size: 12px;
+ transform: scale(0.83);
+ }
+ }
+ .up {
+ color: @red-6;
+ }
+ .down {
+ color: @green-6;
+ top: -1px;
+ }
+
+ &.trendItemGrey .up,
+ &.trendItemGrey .down {
+ color: @text-color;
+ }
+}
diff --git a/src/main/frontend/src/components/Trend/index.md b/src/main/frontend/src/components/Trend/index.md
new file mode 100644
index 0000000..683ed61
--- /dev/null
+++ b/src/main/frontend/src/components/Trend/index.md
@@ -0,0 +1,21 @@
+---
+title:
+ en-US: Trend
+ zh-CN: Trend
+subtitle: 趋势标记
+cols: 1
+order: 14
+---
+
+趋势符号,标记上升和下降趋势。通常用绿色代表“好”,红色代表“不好”,股票涨跌场景除外。
+
+## API
+
+```html
+<Trend flag="up">50%</Trend>
+```
+
+| 参数 | 说明 | 类型 | 默认值 |
+|----------|------------------------------------------|-------------|-------|
+| colorful | 是否彩色标记 | Boolean | true |
+| flag | 上升下降标识:`up|down` | string | - |
diff --git a/src/main/frontend/src/layouts/BasicLayout.js b/src/main/frontend/src/layouts/BasicLayout.js
index 6eab185..457b2cb 100644
--- a/src/main/frontend/src/layouts/BasicLayout.js
+++ b/src/main/frontend/src/layouts/BasicLayout.js
@@ -66,9 +66,6 @@ class BasicLayout extends React.PureComponent {
return { location, breadcrumbNameMap };
}
componentDidMount() {
- this.props.dispatch({
- type: 'user/fetchCurrent',
- });
}
componentWillUnmount() {
clearTimeout(this.resizeTimeout);
diff --git a/src/main/frontend/src/routes/Dashboard/Dashboard.js b/src/main/frontend/src/routes/Dashboard/Dashboard.js
index 353eca8..9a5dbae 100644
--- a/src/main/frontend/src/routes/Dashboard/Dashboard.js
+++ b/src/main/frontend/src/routes/Dashboard/Dashboard.js
@@ -1,13 +1,246 @@
import React, { PureComponent } from 'react';
import { connect } from 'dva';
+import { Row, Col, Icon, Tooltip, Card, Table } from 'antd';
+import moment from 'moment';
+import numeral from 'numeral';
+import {
+ ChartCard, Pie, MiniArea, MiniBar, MiniProgress, Field,
+} from '../../components/Charts';
+import Trend from '../../components/Trend';
+
+import styles from './Dashboard.less';
@connect(state => ({
dashboard: state.dashboard,
}))
export default class Dashboard extends PureComponent {
render() {
+ const visitData = [];
+ 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 databasePieData = [
+ {
+ x: 'MySQL',
+ y: 10,
+ },
+ {
+ x: 'Oracle',
+ y: 7,
+ },
+ {
+ x: 'SQLServer',
+ y: 3,
+ },
+ ];
+ const tableColumns = [{
+ title: 'Time',
+ dataIndex: 'time',
+ key: 'time',
+ }, {
+ title: 'Name',
+ dataIndex: 'name',
+ key: 'name',
+ }, {
+ title: 'Duration',
+ dataIndex: 'duration',
+ key: 'duration',
+ }];
+
+ const slowServiceData = [{
+ key: '1',
+ name: 'ServiceA',
+ time: '2017/12/11 19:22:32',
+ duration: '5000ms',
+ }, {
+ key: '1',
+ name: 'ServiceA',
+ time: '2017/12/11 19:22:32',
+ duration: '5000ms',
+ }, {
+ key: '1',
+ name: 'ServiceA',
+ time: '2017/12/11 19:22:32',
+ duration: '5000ms',
+ }, {
+ key: '1',
+ name: 'ServiceA',
+ time: '2017/12/11 19:22:32',
+ duration: '5000ms',
+ }, {
+ key: '1',
+ name: 'ServiceA',
+ time: '2017/12/11 19:22:32',
+ duration: '5000ms',
+ }];
+
+ const applicationThroughputColumns = [{
+ title: 'Name',
+ dataIndex: 'name',
+ key: 'name',
+ }, {
+ title: 'Tps',
+ dataIndex: 'tps',
+ key: 'tps',
+ }];
+
+ const applicationThroughputData = [{
+ key: '1',
+ name: 'App1',
+ tps: '500',
+ }, {
+ key: '1',
+ name: 'App1',
+ tps: '500',
+ }, {
+ key: '1',
+ name: 'App1',
+ tps: '500',
+ }, {
+ key: '1',
+ name: 'App1',
+ tps: '500',
+ }, {
+ key: '1',
+ name: 'App1',
+ tps: '500',
+ }];
+
+ const topColResponsiveProps = {
+ xs: 24,
+ sm: 12,
+ md: 12,
+ lg: 6,
+ xl: 6,
+ style: { marginBottom: 24 },
+ };
+ const middleColResponsiveProps = {
+ xs: 24,
+ sm: 24,
+ md: 24,
+ lg: 8,
+ xl: 8,
+ style: { marginBottom: 24, marginTop: 24 },
+ };
return (
- <div>test</div>
+ <div>
+ <Row gutter={24}>
+ <Col {...topColResponsiveProps}>
+ <ChartCard
+ title="Total Application"
+ avatar={<img style={{ width: 56, height: 56 }} src="app.svg" alt="app" />}
+ action={<Tooltip title="Tip"><Icon type="info-circle-o" /></Tooltip>}
+ total={25}
+ />
+ </Col>
+ <Col {...topColResponsiveProps}>
+ <ChartCard
+ title="Total Service"
+ avatar={<img style={{ width: 56, height: 56 }} src="service.svg" alt="service" />}
+ action={<Tooltip title="Tip"><Icon type="info-circle-o" /></Tooltip>}
+ total={525}
+ />
+ </Col>
+ <Col {...topColResponsiveProps}>
+ <ChartCard
+ title="Total Database"
+ avatar={<img style={{ width: 56, height: 56 }} src="database.svg" alt="database" />}
+ action={<Tooltip title="Tip"><Icon type="info-circle-o" /></Tooltip>}
+ total={18}
+ />
+ </Col>
+ <Col {...topColResponsiveProps}>
+ <ChartCard
+ title="Total Cache"
+ avatar={<img style={{ width: 56, height: 56 }} src="redis.svg" alt="redis" />}
+ action={<Tooltip title="Tip"><Icon type="info-circle-o" /></Tooltip>}
+ total={5}
+ />
+ </Col>
+ </Row>
+ <Card
+ bordered={false}
+ bodyStyle={{ padding: 0 }}
+ >
+ <div Style="height: 400px">Topoloy</div>
+ </Card>
+ <Row gutter={24}>
+ <Col xs={24} sm={24} md={24} lg={24} xl={24} style={{ marginTop: 24 }}>
+ <ChartCard
+ title="Avg Application Alert"
+ avatar={<img style={{ width: 56, height: 56 }} src="alert.svg" alt="app" />}
+ action={<Tooltip title="Tip"><Icon type="info-circle-o" /></Tooltip>}
+ total="5%"
+ footer={<div><Field label="Max" value="10%" /> <Field label="Min" value="2%" /></div>}
+ >
+ <MiniArea
+ color="#D87093"
+ borderColor="#B22222"
+ line="true"
+ height={196}
+ data={visitData}
+ yAxis={{
+ formatter(val) {
+ return `${val} %`;
+ },
+ }}
+ />
+ </ChartCard>
+ </Col>
+ </Row>
+ <Row gutter={24}>
+ <Col {...middleColResponsiveProps}>
+ <Card
+ bordered={false}
+ bodyStyle={{ padding: 0 }}
+ >
+ <Pie
+ hasLegend
+ title="Database"
+ subTitle="Total"
+ total={databasePieData.reduce((pre, now) => now.y + pre, 0)}
+ data={databasePieData}
+ height={300}
+ lineWidth={4}
+ />
+ </Card>
+ </Col>
+ <Col {...middleColResponsiveProps}>
+ <Card
+ title="Slow Service"
+ bordered={false}
+ bodyStyle={{ padding: 0 }}
+ >
+ <Table
+ columns={tableColumns}
+ dataSource={slowServiceData}
+ pagination={{
+ style: { marginBottom: 0 },
+ pageSize: 5,
+ }}
+ />
+ </Card>
+ </Col>
+ <Col {...middleColResponsiveProps}>
+ <Card
+ title="Application Throughput"
+ bordered={false}
+ bodyStyle={{ padding: 0 }}
+ >
+ <Table
+ columns={applicationThroughputColumns}
+ dataSource={applicationThroughputData}
+ />
+ </Card>
+ </Col>
+ </Row>
+ </div>
);
}
}
diff --git a/src/main/frontend/src/routes/Dashboard/Dashboard.less b/src/main/frontend/src/routes/Dashboard/Dashboard.less
index e52dd30..8e13f67 100644
--- a/src/main/frontend/src/routes/Dashboard/Dashboard.less
+++ b/src/main/frontend/src/routes/Dashboard/Dashboard.less
@@ -21,3 +21,8 @@
height: auto;
}
}
+
+.trendText {
+ margin-left: 8px;
+ color: @heading-color;
+}
--
To stop receiving notification emails like this one, please contact
"commits@skywalking.apache.org" <co...@skywalking.apache.org>.