From 14d4b7745de78f54cc52d00264ba69e64a4f1023 Mon Sep 17 00:00:00 2001 From: "mingyan.yu" Date: Sat, 25 May 2019 17:18:04 +0800 Subject: [PATCH] feat: add statistic --- components/index.js | 4 + components/statistic/Countdown.jsx | 107 ++++++++++++++++++ components/statistic/Number.jsx | 58 ++++++++++ components/statistic/Statistic.jsx | 53 +++++++++ .../__tests__/__snapshots__/demo.test.js.snap | 57 ++++++++++ components/statistic/__tests__/demo.test.js | 3 + components/statistic/demo/basic.md | 18 +++ components/statistic/demo/card.md | 32 ++++++ components/statistic/demo/countdown.md | 41 +++++++ components/statistic/demo/index.vue | 50 ++++++++ components/statistic/demo/unit.md | 33 ++++++ components/statistic/index.en-US.md | 26 +++++ components/statistic/index.js | 11 ++ components/statistic/index.zh-CN.md | 25 ++++ components/statistic/style/index.js | 2 + components/statistic/style/index.less | 23 ++++ components/statistic/utils.js | 39 +++++++ components/style.js | 1 + site/components.js | 2 + site/demo.js | 6 + site/demoRoutes.js | 8 ++ tests/__snapshots__/index.test.js.snap | 1 + types/ant-design-vue.d.ts | 2 + types/statistic.ts | 51 +++++++++ 24 files changed, 653 insertions(+) create mode 100644 components/statistic/Countdown.jsx create mode 100644 components/statistic/Number.jsx create mode 100644 components/statistic/Statistic.jsx create mode 100644 components/statistic/__tests__/__snapshots__/demo.test.js.snap create mode 100644 components/statistic/__tests__/demo.test.js create mode 100644 components/statistic/demo/basic.md create mode 100644 components/statistic/demo/card.md create mode 100644 components/statistic/demo/countdown.md create mode 100644 components/statistic/demo/index.vue create mode 100644 components/statistic/demo/unit.md create mode 100644 components/statistic/index.en-US.md create mode 100644 components/statistic/index.js create mode 100644 components/statistic/index.zh-CN.md create mode 100644 components/statistic/style/index.js create mode 100644 components/statistic/style/index.less create mode 100644 components/statistic/utils.js create mode 100644 types/statistic.ts 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`] = ` +
+
+
+
+
Feedback
+
1,128
+
+
+
Unmerged
+
1,234,567,890 / 100
+
+
+
+
+`; + +exports[`renders ./components/statistic/demo/countdown.md correctly 1`] = ` +
+
+
Countdown
+
48:00:30
+
+
+
Million Seconds
+
48:00:30:000
+
+
+`; + +exports[`renders ./components/statistic/demo/unit.md correctly 1`] = ` +
+
+
Feedback
+
11.28%
+
+
+
Unmerged
+
78 / 100
+
+
+`; 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; +}