diff --git a/components/index.js b/components/index.js
index eec918bc2..a2838e293 100644
--- a/components/index.js
+++ b/components/index.js
@@ -95,6 +95,8 @@ import { default as Slider } from './slider';
import { default as Spin } from './spin';
+import { default as Statistic } from './statistic';
+
import { default as Steps } from './steps';
import { default as Switch } from './switch';
@@ -172,6 +174,7 @@ const components = [
Select,
Slider,
Spin,
+ Statistic,
Steps,
Switch,
Table,
@@ -254,6 +257,7 @@ export {
Select,
Slider,
Spin,
+ Statistic,
Steps,
Switch,
Table,
diff --git a/components/statistic/Countdown.jsx b/components/statistic/Countdown.jsx
new file mode 100644
index 000000000..c4216252f
--- /dev/null
+++ b/components/statistic/Countdown.jsx
@@ -0,0 +1,107 @@
+import * as moment from 'moment';
+import interopDefault from '../_util/interopDefault';
+import { cloneElement } from '../_util/vnode';
+import { initDefaultProps } from '../_util/props-util';
+
+import Statistic, { StatisticProps } from './Statistic';
+import { formatCountdown } from './utils';
+
+const REFRESH_INTERVAL = 1000 / 30;
+
+function getTime(value) {
+ return interopDefault(moment)(value).valueOf();
+}
+
+export default {
+ name: 'AStatisticCountdown',
+ props: initDefaultProps(StatisticProps, {
+ prefixCls: 'ant-statistic',
+ format: 'HH:mm:ss',
+ }),
+
+ data() {
+ return {
+ uniKey: 0,
+ };
+ },
+
+ countdownId: undefined,
+
+ mounted() {
+ this.$nextTick(() => {
+ this.syncTimer();
+ });
+ },
+
+ updated() {
+ this.$nextTick(() => {
+ this.syncTimer();
+ });
+ },
+
+ beforeDestroy() {
+ this.stopTimer();
+ },
+
+ methods: {
+ syncTimer() {
+ const { value } = this.$props;
+ const timestamp = getTime(value);
+ if (timestamp >= Date.now()) {
+ this.startTimer();
+ } else {
+ this.stopTimer();
+ }
+ },
+
+ startTimer() {
+ if (this.countdownId) {
+ return;
+ }
+ this.countdownId = window.setInterval(() => {
+ this.uniKey++;
+ }, REFRESH_INTERVAL);
+ },
+
+ stopTimer() {
+ const { value } = this.$props;
+ if (this.countdownId) {
+ clearInterval(this.countdownId);
+ this.countdownId = undefined;
+
+ const timestamp = getTime(value);
+ if (timestamp < Date.now()) {
+ this.$emit('finish');
+ }
+ }
+ },
+
+ formatCountdown(value, config) {
+ const { format } = this.$props;
+ return formatCountdown(value, { ...config, format });
+ },
+
+ // Countdown do not need display the timestamp
+ valueRenderHtml: node =>
+ cloneElement(node, {
+ props: {
+ title: undefined,
+ },
+ }),
+ },
+
+ render() {
+ return (
+
+ );
+ },
+};
diff --git a/components/statistic/Number.jsx b/components/statistic/Number.jsx
new file mode 100644
index 000000000..f09aa39f9
--- /dev/null
+++ b/components/statistic/Number.jsx
@@ -0,0 +1,58 @@
+import padEnd from 'lodash/padEnd';
+
+export default {
+ name: 'AStatisticNumber',
+ functional: true,
+ render(h, context) {
+ const {
+ value,
+ formatter,
+ precision,
+ decimalSeparator,
+ groupSeparator = '',
+ prefixCls,
+ } = context.props;
+
+ let valueNode;
+
+ if (typeof formatter === 'function') {
+ // Customize formatter
+ valueNode = formatter(value);
+ } else {
+ // Internal formatter
+ const val = String(value);
+ const cells = val.match(/^(-?)(\d*)(\.(\d+))?$/);
+ // Process if illegal number
+ if (!cells) {
+ valueNode = val;
+ } else {
+ const negative = cells[1];
+ let int = cells[2] || '0';
+ let decimal = cells[4] || '';
+
+ int = int.replace(/\B(?=(\d{3})+(?!\d))/g, groupSeparator);
+ if (typeof precision === 'number') {
+ decimal = padEnd(decimal, precision, '0').slice(0, precision);
+ }
+
+ if (decimal) {
+ decimal = `${decimalSeparator}${decimal}`;
+ }
+
+ valueNode = [
+
+ {negative}
+ {int}
+ ,
+ decimal && (
+
+ {decimal}
+
+ ),
+ ];
+ }
+ }
+
+ return {valueNode};
+ },
+};
diff --git a/components/statistic/Statistic.jsx b/components/statistic/Statistic.jsx
new file mode 100644
index 000000000..aa096b96c
--- /dev/null
+++ b/components/statistic/Statistic.jsx
@@ -0,0 +1,53 @@
+import PropTypes from '../_util/vue-types';
+import { getComponentFromProp, initDefaultProps } from '../_util/props-util';
+
+import StatisticNumber from './Number';
+
+export const StatisticProps = {
+ prefixCls: PropTypes.string,
+ decimalSeparator: PropTypes.string,
+ groupSeparator: PropTypes.string,
+ format: PropTypes.string,
+ value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+ valueStyle: PropTypes.any,
+ valueRender: PropTypes.any,
+ formatter: PropTypes.any,
+ precision: PropTypes.number,
+ prefix: PropTypes.any,
+ suffix: PropTypes.any,
+ title: PropTypes.any,
+};
+
+export default {
+ name: 'AStatistic',
+ props: initDefaultProps(StatisticProps, {
+ prefixCls: 'ant-statistic',
+ decimalSeparator: '.',
+ groupSeparator: ',',
+ }),
+
+ render() {
+ const { prefixCls, value = 0, valueStyle, valueRender } = this.$props;
+ const title = getComponentFromProp(this, 'title');
+ let prefix = getComponentFromProp(this, 'prefix');
+ let suffix = getComponentFromProp(this, 'suffix');
+ const formatter = getComponentFromProp(this, 'formatter');
+ let valueNode = (
+
+ );
+ if (valueRender) {
+ valueNode = valueRender(valueNode);
+ }
+
+ return (
+
+ {title &&
{title}
}
+
+ {prefix && {prefix}}
+ {valueNode}
+ {suffix && {suffix}}
+
+
+ );
+ },
+};
diff --git a/components/statistic/__tests__/__snapshots__/demo.test.js.snap b/components/statistic/__tests__/__snapshots__/demo.test.js.snap
new file mode 100644
index 000000000..fd7925d08
--- /dev/null
+++ b/components/statistic/__tests__/__snapshots__/demo.test.js.snap
@@ -0,0 +1,57 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders ./components/statistic/demo/basic.md correctly 1`] = `
+
+
+
Active Users
+
112,893
+
+
+
Account Balance (CNY)
+
112,893.00
+
+
+`;
+
+exports[`renders ./components/statistic/demo/card.md correctly 1`] = `
+
+
+
+
+
+
Unmerged
+
1,234,567,890 / 100
+
+
+
+
+`;
+
+exports[`renders ./components/statistic/demo/countdown.md correctly 1`] = `
+
+
+
+
Million Seconds
+
48:00:30:000
+
+
+`;
+
+exports[`renders ./components/statistic/demo/unit.md correctly 1`] = `
+
+`;
diff --git a/components/statistic/__tests__/demo.test.js b/components/statistic/__tests__/demo.test.js
new file mode 100644
index 000000000..2cc48661e
--- /dev/null
+++ b/components/statistic/__tests__/demo.test.js
@@ -0,0 +1,3 @@
+import demoTest from '../../../tests/shared/demoTest';
+
+demoTest('statistic');
diff --git a/components/statistic/demo/basic.md b/components/statistic/demo/basic.md
new file mode 100644
index 000000000..037138b87
--- /dev/null
+++ b/components/statistic/demo/basic.md
@@ -0,0 +1,18 @@
+
+#### 基本
+简单展示
+
+
+
+#### Basic
+Simplest Usage.
+
+
+```html
+
+
+
+```
diff --git a/components/statistic/demo/card.md b/components/statistic/demo/card.md
new file mode 100644
index 000000000..80e1323ac
--- /dev/null
+++ b/components/statistic/demo/card.md
@@ -0,0 +1,32 @@
+
+#### 在卡片中使用
+在卡片中展示统计数值。
+
+
+
+#### In Card
+Display statistic data in Card.
+
+
+```html
+
+
+
+```
diff --git a/components/statistic/demo/countdown.md b/components/statistic/demo/countdown.md
new file mode 100644
index 000000000..832a8ec95
--- /dev/null
+++ b/components/statistic/demo/countdown.md
@@ -0,0 +1,41 @@
+
+#### 倒计时
+倒计时组件。
+
+
+
+#### Countdown
+Countdown component.
+
+
+```html
+
+
+
+
+```
\ No newline at end of file
diff --git a/components/statistic/demo/index.vue b/components/statistic/demo/index.vue
new file mode 100644
index 000000000..7370bff9c
--- /dev/null
+++ b/components/statistic/demo/index.vue
@@ -0,0 +1,50 @@
+
diff --git a/components/statistic/demo/unit.md b/components/statistic/demo/unit.md
new file mode 100644
index 000000000..336a98086
--- /dev/null
+++ b/components/statistic/demo/unit.md
@@ -0,0 +1,33 @@
+
+#### 单位
+通过前缀和后缀添加单位。
+
+
+
+#### Unit
+Add unit through `prefix` and `suffix`.
+
+
+```html
+
+
+
+```
diff --git a/components/statistic/index.en-US.md b/components/statistic/index.en-US.md
new file mode 100644
index 000000000..9db6901c0
--- /dev/null
+++ b/components/statistic/index.en-US.md
@@ -0,0 +1,26 @@
+## API
+
+#### Statistic
+
+| Property | Description | Type | Default |
+| -------- | ----------- | ---- | ------- |
+| decimalSeparator | decimal separator | string | . |
+| formatter | customize value display logic | (h) => VNode | - |
+| groupSeparator | group separator | string | , |
+| precision | precision of input value | number | - |
+| prefix | prefix node of value | string \| VNode | - |
+| suffix | suffix node of value | string \| VNode | - |
+| title | Display title | string \| VNode | - |
+| value | Display value | string \| number | - |
+
+
+#### Statistic.Countdown
+
+| Property | Description | Type | Default |
+| -------- | ----------- | ---- | ------- |
+| format | Format as [moment](http://momentjs.com/) | string | 'HH:mm:ss' |
+| onFinish | Trigger when time's up | () => void | - |
+| prefix | prefix node of value | string \| VNode | - |
+| suffix | suffix node of value | string \| VNode | - |
+| title | Display title | string \| VNode | - |
+| value | Set target countdown time | number \| moment | - |
\ No newline at end of file
diff --git a/components/statistic/index.js b/components/statistic/index.js
new file mode 100644
index 000000000..5a13e5d6a
--- /dev/null
+++ b/components/statistic/index.js
@@ -0,0 +1,11 @@
+import Statistic from './Statistic';
+import Countdown from './Countdown';
+
+Statistic.Countdown = Countdown;
+/* istanbul ignore next */
+Statistic.install = function(Vue) {
+ Vue.component(Statistic.name, Statistic);
+ Vue.component(Statistic.Countdown.name, Statistic.Countdown);
+};
+
+export default Statistic;
diff --git a/components/statistic/index.zh-CN.md b/components/statistic/index.zh-CN.md
new file mode 100644
index 000000000..af4cb1905
--- /dev/null
+++ b/components/statistic/index.zh-CN.md
@@ -0,0 +1,25 @@
+## API
+
+#### Statistic
+
+| 参数 | 说明 | 类型 | 默认值 |
+| -------- | ----------- | ---- | ------- |
+| decimalSeparator | 设置小数点 | string | . |
+| formatter | 自定义数值展示 | (h) => VNode | - |
+| groupSeparator | 设置千分位标识符 | string | , |
+| precision | 数值精度 | number | - |
+| prefix | 设置数值的前缀 | string \| VNode | - |
+| suffix | 设置数值的后缀 | string \| VNode | - |
+| title | 数值的标题 | string \| VNode | - |
+| value | 数值内容 | string \| number | - |
+
+#### Statistic.Countdown
+
+| 参数 | 说明 | 类型 | 默认值 |
+| -------- | ----------- | ---- | ------- |
+| format | 格式化倒计时展示,参考 [moment](http://momentjs.com/) | string | 'HH:mm:ss' |
+| onFinish | 倒计时完成时触发 | () => void | - |
+| prefix | 设置数值的前缀 | string \| VNode | - |
+| suffix | 设置数值的后缀 | string \| VNode | - |
+| title | 数值的标题 | string \| VNode | - |
+| value | 数值内容 | number \| moment | - |
diff --git a/components/statistic/style/index.js b/components/statistic/style/index.js
new file mode 100644
index 000000000..3a3ab0de5
--- /dev/null
+++ b/components/statistic/style/index.js
@@ -0,0 +1,2 @@
+import '../../style/index.less';
+import './index.less';
diff --git a/components/statistic/style/index.less b/components/statistic/style/index.less
new file mode 100644
index 000000000..240535822
--- /dev/null
+++ b/components/statistic/style/index.less
@@ -0,0 +1,23 @@
+@import '../../style/themes/default';
+@import '../../style/mixins/index';
+
+@statistic-prefix-cls: ~'@{ant-prefix}-statistic';
+
+.@{statistic-prefix-cls} {
+ .reset-component;
+ display: inline-block;
+ white-space: nowrap;
+ overflow: hidden;
+ vertical-align: middle;
+
+ &-content-value,
+ &-content-prefix {
+ font-size: 1.7em;
+ }
+ &-content-value-decimal {
+ font-size: 0.7em;
+ }
+ &-content-suffix {
+ font-size: 1.1em;
+ }
+}
diff --git a/components/statistic/utils.js b/components/statistic/utils.js
new file mode 100644
index 000000000..0a6efb40c
--- /dev/null
+++ b/components/statistic/utils.js
@@ -0,0 +1,39 @@
+import * as moment from 'moment';
+import padStart from 'lodash/padStart';
+
+import interopDefault from '../_util/interopDefault';
+
+// Countdown
+const timeUnits = [
+ ['Y', 1000 * 60 * 60 * 24 * 365], // years
+ ['M', 1000 * 60 * 60 * 24 * 30], // months
+ ['D', 1000 * 60 * 60 * 24], // days
+ ['H', 1000 * 60 * 60], // hours
+ ['m', 1000 * 60], // minutes
+ ['s', 1000], // seconds
+ ['S', 1], // million seconds
+];
+
+function formatTimeStr(duration, format) {
+ let leftDuration = duration;
+
+ return timeUnits.reduce((current, [name, unit]) => {
+ if (current.indexOf(name) !== -1) {
+ const value = Math.floor(leftDuration / unit);
+ leftDuration -= value * unit;
+ return current.replace(new RegExp(`${name}+`, 'g'), match => {
+ const len = match.length;
+ return padStart(value.toString(), len, '0');
+ });
+ }
+ return current;
+ }, format);
+}
+
+export function formatCountdown(value, config) {
+ const { format = '' } = config;
+ const target = interopDefault(moment)(value).valueOf();
+ const current = interopDefault(moment)().valueOf();
+ const diff = Math.max(target - current, 0);
+ return formatTimeStr(diff, format);
+}
diff --git a/components/style.js b/components/style.js
index f5abe4eb1..6f23f4989 100644
--- a/components/style.js
+++ b/components/style.js
@@ -54,3 +54,4 @@ import './skeleton/style';
import './comment/style';
import './config-provider/style';
import './empty/style';
+import './statistic/style';
diff --git a/site/components.js b/site/components.js
index 8a1fc55f7..e600a89a7 100644
--- a/site/components.js
+++ b/site/components.js
@@ -40,6 +40,7 @@ import {
Select,
Slider,
Spin,
+ Statistic,
Steps,
Switch,
Table,
@@ -110,6 +111,7 @@ Vue.use(Row);
Vue.use(Select);
Vue.use(Slider);
Vue.use(Spin);
+Vue.use(Statistic);
Vue.use(Steps);
Vue.use(Switch);
Vue.use(Table);
diff --git a/site/demo.js b/site/demo.js
index e249f8c96..654774385 100644
--- a/site/demo.js
+++ b/site/demo.js
@@ -352,4 +352,10 @@ export default {
title: 'Skeleton',
subtitle: '骨架屏',
},
+ statistic: {
+ category: 'Components',
+ subtitle: '统计数值',
+ type: 'Data Display',
+ title: 'Statistic',
+ },
};
diff --git a/site/demoRoutes.js b/site/demoRoutes.js
index de03a43fa..b23c6d993 100644
--- a/site/demoRoutes.js
+++ b/site/demoRoutes.js
@@ -7,6 +7,14 @@ export default [
path: 'avatar-cn',
component: () => import('../components/avatar/demo/index.vue'),
},
+ {
+ path: 'statistic',
+ component: () => import('../components/statistic/demo/index.vue'),
+ },
+ {
+ path: 'statistic-cn',
+ component: () => import('../components/statistic/demo/index.vue'),
+ },
{
path: 'badge',
component: () => import('../components/badge/demo/index.vue'),
diff --git a/tests/__snapshots__/index.test.js.snap b/tests/__snapshots__/index.test.js.snap
index f467b8871..691385b27 100644
--- a/tests/__snapshots__/index.test.js.snap
+++ b/tests/__snapshots__/index.test.js.snap
@@ -44,6 +44,7 @@ Array [
"Select",
"Slider",
"Spin",
+ "Statistic",
"Steps",
"Switch",
"Table",
diff --git a/types/ant-design-vue.d.ts b/types/ant-design-vue.d.ts
index e9639b70c..90b83859a 100644
--- a/types/ant-design-vue.d.ts
+++ b/types/ant-design-vue.d.ts
@@ -46,6 +46,7 @@ import { Select } from './select/select';
import { Skeleton } from './skeleton';
import { Slider } from './slider';
import { Spin } from './spin';
+import { Statistic } from './statistic';
import { Steps } from './steps/steps';
import { Switch } from './switch';
import { Table } from './table/table';
@@ -110,6 +111,7 @@ export {
Select,
Slider,
Spin,
+ Statistic,
Steps,
Switch,
Table,
diff --git a/types/statistic.ts b/types/statistic.ts
new file mode 100644
index 000000000..7d32593fa
--- /dev/null
+++ b/types/statistic.ts
@@ -0,0 +1,51 @@
+// Project: https://github.com/vueComponent/ant-design-vue
+// Definitions by: akki-jat
+// Definitions: https://github.com/vueComponent/ant-design-vue/types
+
+import { AntdComponent } from './component';
+
+export declare class Statistic extends AntdComponent {
+ /**
+ * the Icon type for an icon statistic, see Icon Component
+ * @type string
+ */
+ icon: string;
+
+ /**
+ * the shape of statistic
+ * @default 'circle'
+ * @type string
+ */
+ shape: 'circle' | 'square';
+
+ /**
+ * the size of the statistic
+ * @default 'default'
+ * @type number | string
+ */
+ size: 'small' | 'large' | 'default' | number;
+
+ /**
+ * the address of the image for an image statistic
+ * @type string
+ */
+ src: string;
+
+ /**
+ * a list of sources to use for different screen resolutions
+ * @type string
+ */
+ srcSet: string;
+
+ /**
+ * This attribute defines the alternative text describing the image
+ * @type string
+ */
+ alt: string;
+
+ /**
+ * handler when img load error,return false to prevent default fallback behavior
+ * @type
+ */
+ loadError: () => boolean;
+}